diff --git a/.dockerignore b/.dockerignore index 3a0ec49f78..0adca0b324 100644 --- a/.dockerignore +++ b/.dockerignore @@ -23,3 +23,5 @@ yarn-error.log .rnd /.ssh .ignition.json +.env.dusk.local +docker/coolify-realtime/node_modules diff --git a/.env.dusk.ci b/.env.dusk.ci new file mode 100644 index 0000000000..9660de7b48 --- /dev/null +++ b/.env.dusk.ci @@ -0,0 +1,15 @@ +APP_ENV=production +APP_NAME="Coolify Staging" +APP_ID=development +APP_KEY= +APP_URL=http://localhost +APP_PORT=8000 +SSH_MUX_ENABLED=true + +# PostgreSQL Database Configuration +DB_DATABASE=coolify +DB_USERNAME=coolify +DB_PASSWORD=password +DB_HOST=localhost +DB_PORT=5432 + diff --git a/.env.windows-docker-desktop.example b/.env.windows-docker-desktop.example index 02a5a41742..b067b4c5c0 100644 --- a/.env.windows-docker-desktop.example +++ b/.env.windows-docker-desktop.example @@ -4,6 +4,7 @@ APP_ID=coolify-windows-docker-desktop APP_NAME=Coolify APP_KEY=base64:ssTlCmrIE/q7whnKMvT6DwURikg69COzGsAwFVROm80= +DB_USERNAME=coolify DB_PASSWORD=coolify REDIS_PASSWORD=coolify diff --git a/.github/workflows/browser-tests.yml b/.github/workflows/browser-tests.yml new file mode 100644 index 0000000000..b06c9e97cf --- /dev/null +++ b/.github/workflows/browser-tests.yml @@ -0,0 +1,65 @@ +name: Dusk +on: + push: + branches: [ "not-existing" ] +jobs: + dusk: + runs-on: ubuntu-latest + + services: + redis: + image: redis + env: + REDIS_HOST: localhost + REDIS_PORT: 6379 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v4 + - name: Set up PostgreSQL + run: | + sudo systemctl start postgresql + sudo -u postgres psql -c "CREATE DATABASE coolify;" + sudo -u postgres psql -c "CREATE USER coolify WITH PASSWORD 'password';" + sudo -u postgres psql -c "ALTER ROLE coolify SET client_encoding TO 'utf8';" + sudo -u postgres psql -c "ALTER ROLE coolify SET default_transaction_isolation TO 'read committed';" + sudo -u postgres psql -c "ALTER ROLE coolify SET timezone TO 'UTC';" + sudo -u postgres psql -c "GRANT ALL PRIVILEGES ON DATABASE coolify TO coolify;" + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + - name: Copy .env + run: cp .env.dusk.ci .env + - name: Install Dependencies + run: composer install --no-progress --prefer-dist --optimize-autoloader + - name: Generate key + run: php artisan key:generate + - name: Install Chrome binaries + run: php artisan dusk:chrome-driver --detect + - name: Start Chrome Driver + run: ./vendor/laravel/dusk/bin/chromedriver-linux --port=4444 & + - name: Build assets + run: npm install && npm run build + - name: Run Laravel Server + run: php artisan serve --no-reload & + - name: Execute tests + run: php artisan dusk + - name: Upload Screenshots + if: failure() + uses: actions/upload-artifact@v4 + with: + name: screenshots + path: tests/Browser/screenshots + - name: Upload Console Logs + if: failure() + uses: actions/upload-artifact@v4 + with: + name: console + path: tests/Browser/console diff --git a/.github/workflows/lock-closed-issues-discussions-and-prs.yml b/.github/workflows/chore-lock-closed-issues-discussions-and-prs.yml similarity index 100% rename from .github/workflows/lock-closed-issues-discussions-and-prs.yml rename to .github/workflows/chore-lock-closed-issues-discussions-and-prs.yml diff --git a/.github/workflows/manage-stale-issues-and-prs.yml b/.github/workflows/chore-manage-stale-issues-and-prs.yml similarity index 100% rename from .github/workflows/manage-stale-issues-and-prs.yml rename to .github/workflows/chore-manage-stale-issues-and-prs.yml diff --git a/.github/workflows/remove-labels-and-assignees-on-close.yml b/.github/workflows/chore-remove-labels-and-assignees-on-close.yml similarity index 100% rename from .github/workflows/remove-labels-and-assignees-on-close.yml rename to .github/workflows/chore-remove-labels-and-assignees-on-close.yml diff --git a/.github/workflows/coolify-helper-next.yml b/.github/workflows/coolify-helper-next.yml index 4add8516ee..4354294b14 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 a9e8a5dd0d..6d852a2b3e 100644 --- a/.github/workflows/coolify-helper.yml +++ b/.github/workflows/coolify-helper.yml @@ -1,4 +1,4 @@ -name: Coolify Helper Image (v4) +name: Coolify Helper Image on: push: @@ -8,7 +8,8 @@ on: - docker/coolify-helper/Dockerfile env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-helper" jobs: @@ -19,25 +20,36 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} labels: | coolify.managed=true aarch64: @@ -47,25 +59,36 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-helper/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 labels: | coolify.managed=true merge-manifest: @@ -75,25 +98,43 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | - echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.helper.version' versions.json)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.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.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.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/coolify-production-build.yml b/.github/workflows/coolify-production-build.yml new file mode 100644 index 0000000000..531466cdce --- /dev/null +++ b/.github/workflows/coolify-production-build.yml @@ -0,0 +1,139 @@ +name: Production Build (v4) + +on: + push: + branches: ["main"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/* + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + + aarch64: + runs-on: [self-hosted, arm64] + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT] + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Get Version + id: version + run: | + echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app php:8.2-alpine3.16 php bootstrap/getVersion.php)"|xargs >> $GITHUB_OUTPUT + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime-next.yml b/.github/workflows/coolify-realtime-next.yml index 33e048627d..7e937d17af 100644 --- a/.github/workflows/coolify-realtime-next.yml +++ b/.github/workflows/coolify-realtime-next.yml @@ -1,17 +1,18 @@ -name: Coolify Realtime Development (v4) +name: Coolify Realtime Development on: push: branches: [ "next" ] paths: - - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml - docker/coolify-realtime/Dockerfile - docker/coolify-realtime/terminal-server.js - docker/coolify-realtime/package.json - docker/coolify-realtime/soketi-entrypoint.sh env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-realtime" jobs: @@ -22,27 +23,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next labels: | coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -50,27 +63,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 labels: | coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -78,26 +103,44 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:next + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-next \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:next + - uses: sarisia/actions-status-discord@v1 if: always() with: - webhook: ${{ secrets.DISCORD_WEBHOOK_PROD_RELEASE_CHANNEL }} + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-realtime.yml b/.github/workflows/coolify-realtime.yml index 30910ae0bd..97bfd52ebc 100644 --- a/.github/workflows/coolify-realtime.yml +++ b/.github/workflows/coolify-realtime.yml @@ -1,4 +1,4 @@ -name: Coolify Realtime (v4) +name: Coolify Realtime on: push: @@ -11,7 +11,8 @@ on: - docker/coolify-realtime/soketi-entrypoint.sh env: - REGISTRY: ghcr.io + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io IMAGE_NAME: "coollabsio/coolify-realtime" jobs: @@ -22,27 +23,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/amd64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} labels: | coolify.managed=true + aarch64: runs-on: [ self-hosted, arm64 ] permissions: @@ -50,27 +63,39 @@ jobs: packages: write steps: - uses: actions/checkout@v4 - - name: Login to ghcr.io + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Build image and push to registry - uses: docker/build-push-action@v5 + + - name: Build and Push Image + uses: docker/build-push-action@v6 with: - no-cache: true context: . file: docker/coolify-realtime/Dockerfile platforms: linux/aarch64 push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 labels: | coolify.managed=true + merge-manifest: runs-on: ubuntu-latest permissions: @@ -78,25 +103,43 @@ jobs: packages: write needs: [ amd64, aarch64 ] steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} uses: docker/login-action@v3 with: - registry: ${{ env.REGISTRY }} + registry: ${{ env.GITHUB_REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + - name: Get Version id: version run: | echo "VERSION=$(docker run --rm -v "$(pwd):/app" -w /app ghcr.io/jqlang/jq:latest '.coolify.realtime.version' versions.json)"|xargs >> $GITHUB_OUTPUT - - name: Create & publish manifest + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }} \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:latest + - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.github/workflows/coolify-staging-build.yml b/.github/workflows/coolify-staging-build.yml new file mode 100644 index 0000000000..1aafc2f0bf --- /dev/null +++ b/.github/workflows/coolify-staging-build.yml @@ -0,0 +1,125 @@ +name: Staging Build + +on: + push: + branches-ignore: ["main", "v3"] + paths-ignore: + - .github/workflows/coolify-helper.yml + - .github/workflows/coolify-helper-next.yml + - .github/workflows/coolify-realtime.yml + - .github/workflows/coolify-realtime-next.yml + - docker/coolify-helper/Dockerfile + - docker/coolify-realtime/Dockerfile + - docker/testing-host/Dockerfile + - templates/* + +env: + GITHUB_REGISTRY: ghcr.io + DOCKER_REGISTRY: docker.io + IMAGE_NAME: "coollabsio/coolify" + +jobs: + amd64: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/amd64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + aarch64: + runs-on: [self-hosted, arm64] + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and Push Image + uses: docker/build-push-action@v6 + with: + context: . + file: docker/prod/Dockerfile + platforms: linux/aarch64 + push: true + tags: | + ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 + + merge-manifest: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + needs: [amd64, aarch64] + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + + - name: Login to ${{ env.GITHUB_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.GITHUB_REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Login to ${{ env.DOCKER_REGISTRY }} + uses: docker/login-action@v3 + with: + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Create & publish manifest on ${{ env.GITHUB_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.GITHUB_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - name: Create & publish manifest on ${{ env.DOCKER_REGISTRY }} + run: | + docker buildx imagetools create \ + --append ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 \ + --tag ${{ env.DOCKER_REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + + - uses: sarisia/actions-status-discord@v1 + if: always() + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/coolify-testing-host.yml b/.github/workflows/coolify-testing-host.yml index 5fdc329914..95a228114a 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 268b885aca..0000000000 --- a/.github/workflows/development-build.yml +++ /dev/null @@ -1,79 +0,0 @@ -name: Development Build (v4) - -on: - push: - branches-ignore: ["main", "v3"] - paths-ignore: - - .github/workflows/coolify-helper.yml - - docker/coolify-helper/Dockerfile - -env: - REGISTRY: ghcr.io - IMAGE_NAME: "coollabsio/coolify" - -jobs: - amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/amd64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - aarch64: - runs-on: [self-hosted, arm64] - permissions: - contents: read - packages: write - steps: - - uses: actions/checkout@v4 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build image and push to registry - uses: docker/build-push-action@v5 - with: - context: . - file: docker/prod/Dockerfile - platforms: linux/aarch64 - push: true - tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 - merge-manifest: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - needs: [amd64, aarch64] - steps: - - name: Checkout - uses: actions/checkout@v4 - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - name: Login to ghcr.io - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create & publish manifest - run: | - docker buildx imagetools create --append ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}-aarch64 --tag ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} - - uses: sarisia/actions-status-discord@v1 - if: always() - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_DEV_RELEASE_CHANNEL }} diff --git a/.github/workflows/production-build.yml b/.github/workflows/production-build.yml deleted file mode 100644 index c78c865bfe..0000000000 --- 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 09504afeeb..dd6b141b90 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ _ide_helper_models.php /.ssh scripts/load-test/* .ignition.json +.env.dusk.local +docker/coolify-realtime/node_modules diff --git a/app/Actions/Application/GenerateConfig.php b/app/Actions/Application/GenerateConfig.php index 69365f9213..991146b482 100644 --- a/app/Actions/Application/GenerateConfig.php +++ b/app/Actions/Application/GenerateConfig.php @@ -11,7 +11,6 @@ class GenerateConfig public function handle(Application $application, bool $is_json = false) { - ray()->clearAll(); return $application->generateConfig(is_json: $is_json); } } diff --git a/app/Actions/Application/StopApplication.php b/app/Actions/Application/StopApplication.php index 61005845be..cab7e45f0e 100644 --- a/app/Actions/Application/StopApplication.php +++ b/app/Actions/Application/StopApplication.php @@ -17,7 +17,6 @@ public function handle(Application $application, bool $previewDeployments = fals if (! $server->isFunctional()) { return 'Server is not functional'; } - ray('Stopping application: '.$application->name); if ($server->isSwarm()) { instant_remote_process(["docker stack rm {$application->uuid}"], $server); @@ -36,8 +35,6 @@ public function handle(Application $application, bool $previewDeployments = fals CleanupDocker::dispatch($server, true); } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Actions/Application/StopApplicationOneServer.php b/app/Actions/Application/StopApplicationOneServer.php index da8c700fe1..b13b10efd6 100644 --- a/app/Actions/Application/StopApplicationOneServer.php +++ b/app/Actions/Application/StopApplicationOneServer.php @@ -32,8 +32,6 @@ public function handle(Application $application, Server $server) } } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Actions/CoolifyTask/PrepareCoolifyTask.php b/app/Actions/CoolifyTask/PrepareCoolifyTask.php index 686b60780e..6676b79376 100644 --- a/app/Actions/CoolifyTask/PrepareCoolifyTask.php +++ b/app/Actions/CoolifyTask/PrepareCoolifyTask.php @@ -48,7 +48,6 @@ public function __invoke(): Activity call_event_data: $this->remoteProcessArgs->call_event_data, ); if ($this->remoteProcessArgs->type === ActivityTypes::COMMAND->value) { - ray('Dispatching a high priority job'); dispatch($job)->onQueue('high'); } else { dispatch($job); diff --git a/app/Actions/CoolifyTask/RunRemoteProcess.php b/app/Actions/CoolifyTask/RunRemoteProcess.php index c691f52c0b..981b81378e 100644 --- a/app/Actions/CoolifyTask/RunRemoteProcess.php +++ b/app/Actions/CoolifyTask/RunRemoteProcess.php @@ -9,6 +9,7 @@ use App\Models\Server; use Illuminate\Process\ProcessResult; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Process; use Spatie\Activitylog\Models\Activity; @@ -39,7 +40,6 @@ class RunRemoteProcess */ public function __construct(Activity $activity, bool $hide_from_output = false, bool $ignore_errors = false, $call_event_on_finish = null, $call_event_data = null) { - if ($activity->getExtraProperty('type') !== ActivityTypes::INLINE->value && $activity->getExtraProperty('type') !== ActivityTypes::COMMAND->value) { throw new \RuntimeException('Incompatible Activity to run a remote command.'); } @@ -125,7 +125,7 @@ public function __invoke(): ProcessResult ])); } } catch (\Throwable $e) { - ray($e); + Log::error('Error calling event: '.$e->getMessage()); } } diff --git a/app/Actions/Database/StartClickhouse.php b/app/Actions/Database/StartClickhouse.php index 6d0063749e..13667e8297 100644 --- a/app/Actions/Database/StartClickhouse.php +++ b/app/Actions/Database/StartClickhouse.php @@ -51,6 +51,8 @@ public function handle(StandaloneClickhouse $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => "clickhouse-client --password {$this->database->clickhouse_admin_password} --query 'SELECT 1'", @@ -97,8 +99,8 @@ public function handle(StandaloneClickhouse $database) } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartDatabase.php b/app/Actions/Database/StartDatabase.php index 323c52ff93..869a885212 100644 --- a/app/Actions/Database/StartDatabase.php +++ b/app/Actions/Database/StartDatabase.php @@ -23,33 +23,33 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St return 'Server is not functional'; } switch ($database->getMorphClass()) { - case 'App\Models\StandalonePostgresql': + case \App\Models\StandalonePostgresql::class: $activity = StartPostgresql::run($database); break; - case 'App\Models\StandaloneRedis': + case \App\Models\StandaloneRedis::class: $activity = StartRedis::run($database); break; - case 'App\Models\StandaloneMongodb': + case \App\Models\StandaloneMongodb::class: $activity = StartMongodb::run($database); break; - case 'App\Models\StandaloneMysql': + case \App\Models\StandaloneMysql::class: $activity = StartMysql::run($database); break; - case 'App\Models\StandaloneMariadb': + case \App\Models\StandaloneMariadb::class: $activity = StartMariadb::run($database); break; - case 'App\Models\StandaloneKeydb': + case \App\Models\StandaloneKeydb::class: $activity = StartKeydb::run($database); break; - case 'App\Models\StandaloneDragonfly': + case \App\Models\StandaloneDragonfly::class: $activity = StartDragonfly::run($database); break; - case 'App\Models\StandaloneClickhouse': + case \App\Models\StandaloneClickhouse::class: $activity = StartClickhouse::run($database); break; } if ($database->is_public && $database->public_port) { - StartDatabaseProxy::dispatch($database); + StartDatabaseProxy::dispatch($database)->onQueue('high'); } return $activity; diff --git a/app/Actions/Database/StartDatabaseProxy.php b/app/Actions/Database/StartDatabaseProxy.php index a514c51b49..d7a3bc697a 100644 --- a/app/Actions/Database/StartDatabaseProxy.php +++ b/app/Actions/Database/StartDatabaseProxy.php @@ -26,7 +26,7 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $server = data_get($database, 'destination.server'); $containerName = data_get($database, 'uuid'); $proxyContainerName = "{$database->uuid}-proxy"; - if ($database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $databaseType = $database->databaseType(); // $connectPredefined = data_get($database, 'service.connect_to_docker_network'); $network = $database->service->uuid; @@ -34,54 +34,54 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St $proxyContainerName = "{$database->service->uuid}-proxy"; switch ($databaseType) { case 'standalone-mariadb': - $type = 'App\Models\StandaloneMariadb'; + $type = \App\Models\StandaloneMariadb::class; $containerName = "mariadb-{$database->service->uuid}"; break; case 'standalone-mongodb': - $type = 'App\Models\StandaloneMongodb'; + $type = \App\Models\StandaloneMongodb::class; $containerName = "mongodb-{$database->service->uuid}"; break; case 'standalone-mysql': - $type = 'App\Models\StandaloneMysql'; + $type = \App\Models\StandaloneMysql::class; $containerName = "mysql-{$database->service->uuid}"; break; case 'standalone-postgresql': - $type = 'App\Models\StandalonePostgresql'; + $type = \App\Models\StandalonePostgresql::class; $containerName = "postgresql-{$database->service->uuid}"; break; case 'standalone-redis': - $type = 'App\Models\StandaloneRedis'; + $type = \App\Models\StandaloneRedis::class; $containerName = "redis-{$database->service->uuid}"; break; case 'standalone-keydb': - $type = 'App\Models\StandaloneKeydb'; + $type = \App\Models\StandaloneKeydb::class; $containerName = "keydb-{$database->service->uuid}"; break; case 'standalone-dragonfly': - $type = 'App\Models\StandaloneDragonfly'; + $type = \App\Models\StandaloneDragonfly::class; $containerName = "dragonfly-{$database->service->uuid}"; break; case 'standalone-clickhouse': - $type = 'App\Models\StandaloneClickhouse'; + $type = \App\Models\StandaloneClickhouse::class; $containerName = "clickhouse-{$database->service->uuid}"; break; } } - if ($type === 'App\Models\StandaloneRedis') { + if ($type === \App\Models\StandaloneRedis::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandalonePostgresql') { + } elseif ($type === \App\Models\StandalonePostgresql::class) { $internalPort = 5432; - } elseif ($type === 'App\Models\StandaloneMongodb') { + } elseif ($type === \App\Models\StandaloneMongodb::class) { $internalPort = 27017; - } elseif ($type === 'App\Models\StandaloneMysql') { + } elseif ($type === \App\Models\StandaloneMysql::class) { $internalPort = 3306; - } elseif ($type === 'App\Models\StandaloneMariadb') { + } elseif ($type === \App\Models\StandaloneMariadb::class) { $internalPort = 3306; - } elseif ($type === 'App\Models\StandaloneKeydb') { + } elseif ($type === \App\Models\StandaloneKeydb::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandaloneDragonfly') { + } elseif ($type === \App\Models\StandaloneDragonfly::class) { $internalPort = 6379; - } elseif ($type === 'App\Models\StandaloneClickhouse') { + } elseif ($type === \App\Models\StandaloneClickhouse::class) { $internalPort = 9000; } $configuration_dir = database_proxy_dir($database->uuid); diff --git a/app/Actions/Database/StartDragonfly.php b/app/Actions/Database/StartDragonfly.php index 3ee46a2e10..c72714e1cd 100644 --- a/app/Actions/Database/StartDragonfly.php +++ b/app/Actions/Database/StartDragonfly.php @@ -48,6 +48,8 @@ public function handle(StandaloneDragonfly $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => "redis-cli -a {$this->database->dragonfly_password} ping", @@ -94,8 +96,8 @@ public function handle(StandaloneDragonfly $database) } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartKeydb.php b/app/Actions/Database/StartKeydb.php index a11452a682..bd98258ab6 100644 --- a/app/Actions/Database/StartKeydb.php +++ b/app/Actions/Database/StartKeydb.php @@ -50,6 +50,8 @@ public function handle(StandaloneKeydb $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => "keydb-cli --pass {$this->database->keydb_password} ping", @@ -105,8 +107,8 @@ public function handle(StandaloneKeydb $database) } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); $this->commands[] = "echo '{$docker_compose_base64}' | base64 -d | tee $this->configuration_dir/docker-compose.yml > /dev/null"; diff --git a/app/Actions/Database/StartMariadb.php b/app/Actions/Database/StartMariadb.php index a5630f734a..696dd7ff4b 100644 --- a/app/Actions/Database/StartMariadb.php +++ b/app/Actions/Database/StartMariadb.php @@ -45,6 +45,8 @@ public function handle(StandaloneMariadb $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => ['CMD', 'healthcheck.sh', '--connect', '--innodb_initialized'], @@ -99,8 +101,8 @@ public function handle(StandaloneMariadb $database) } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartMongodb.php b/app/Actions/Database/StartMongodb.php index 5bff194d50..26a0f82d0a 100644 --- a/app/Actions/Database/StartMongodb.php +++ b/app/Actions/Database/StartMongodb.php @@ -25,6 +25,10 @@ public function handle(StandaloneMongodb $database) $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; + if (isDev()) { + $this->configuration_dir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/databases/'.$container_name; + } + $this->commands = [ "echo 'Starting {$database->name}.'", "mkdir -p $this->configuration_dir", @@ -49,6 +53,8 @@ public function handle(StandaloneMongodb $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => [ @@ -115,8 +121,8 @@ public function handle(StandaloneMongodb $database) ]; // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartMysql.php b/app/Actions/Database/StartMysql.php index cc4203580c..a3694648f9 100644 --- a/app/Actions/Database/StartMysql.php +++ b/app/Actions/Database/StartMysql.php @@ -45,6 +45,8 @@ public function handle(StandaloneMysql $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => ['CMD', 'mysqladmin', 'ping', '-h', 'localhost', '-u', 'root', "-p{$this->database->mysql_root_password}"], @@ -99,8 +101,8 @@ public function handle(StandaloneMysql $database) } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartPostgresql.php b/app/Actions/Database/StartPostgresql.php index 2a8e5476c9..f5e85087ff 100644 --- a/app/Actions/Database/StartPostgresql.php +++ b/app/Actions/Database/StartPostgresql.php @@ -49,6 +49,8 @@ public function handle(StandalonePostgresql $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => [ @@ -120,8 +122,8 @@ public function handle(StandalonePostgresql $database) ]; } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); diff --git a/app/Actions/Database/StartRedis.php b/app/Actions/Database/StartRedis.php index eeddab924a..7a2d2b34d5 100644 --- a/app/Actions/Database/StartRedis.php +++ b/app/Actions/Database/StartRedis.php @@ -21,8 +21,6 @@ public function handle(StandaloneRedis $database) { $this->database = $database; - $startCommand = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; - $container_name = $this->database->uuid; $this->configuration_dir = database_configuration_dir().'/'.$container_name; @@ -37,6 +35,8 @@ public function handle(StandaloneRedis $database) $environment_variables = $this->generate_environment_variables(); $this->add_custom_redis(); + $startCommand = $this->buildStartCommand(); + $docker_compose = [ 'services' => [ $container_name => [ @@ -50,6 +50,8 @@ public function handle(StandaloneRedis $database) ], 'labels' => [ 'coolify.managed' => 'true', + 'coolify.type' => 'database', + 'coolify.databaseId' => $this->database->id, ], 'healthcheck' => [ 'test' => [ @@ -105,12 +107,11 @@ public function handle(StandaloneRedis $database) 'target' => '/usr/local/etc/redis/redis.conf', 'read_only' => true, ]; - $docker_compose['services'][$container_name]['command'] = "redis-server /usr/local/etc/redis/redis.conf --requirepass {$this->database->redis_password} --appendonly yes"; } // Add custom docker run options - $docker_run_options = convert_docker_run_to_compose($this->database->custom_docker_run_options); - $docker_compose = generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); + $docker_run_options = convertDockerRunToCompose($this->database->custom_docker_run_options); + $docker_compose = generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $this->database->destination->network); $docker_compose = Yaml::dump($docker_compose, 10); $docker_compose_base64 = base64_encode($docker_compose); @@ -160,12 +161,26 @@ private function generate_local_persistent_volumes_only_volume_names() private function generate_environment_variables() { $environment_variables = collect(); + foreach ($this->database->runtime_environment_variables as $env) { - $environment_variables->push("$env->key=$env->real_value"); - } + if ($env->is_shared) { + $environment_variables->push("$env->key=$env->real_value"); - if ($environment_variables->filter(fn ($env) => str($env)->contains('REDIS_PASSWORD'))->isEmpty()) { - $environment_variables->push("REDIS_PASSWORD={$this->database->redis_password}"); + if ($env->key === 'REDIS_PASSWORD') { + $this->database->update(['redis_password' => $env->real_value]); + } + + if ($env->key === 'REDIS_USERNAME') { + $this->database->update(['redis_username' => $env->real_value]); + } + } else { + if ($env->key === 'REDIS_PASSWORD') { + $env->update(['value' => $this->database->redis_password]); + } elseif ($env->key === 'REDIS_USERNAME') { + $env->update(['value' => $this->database->redis_username]); + } + $environment_variables->push("$env->key=$env->real_value"); + } } add_coolify_default_environment_variables($this->database, $environment_variables, $environment_variables); @@ -173,6 +188,27 @@ private function generate_environment_variables() return $environment_variables->all(); } + private function buildStartCommand(): string + { + $hasRedisConf = ! is_null($this->database->redis_conf) && ! empty($this->database->redis_conf); + $redisConfPath = '/usr/local/etc/redis/redis.conf'; + + if ($hasRedisConf) { + $confContent = $this->database->redis_conf; + $hasRequirePass = str_contains($confContent, 'requirepass'); + + if ($hasRequirePass) { + $command = "redis-server $redisConfPath"; + } else { + $command = "redis-server $redisConfPath --requirepass {$this->database->redis_password}"; + } + } else { + $command = "redis-server --requirepass {$this->database->redis_password} --appendonly yes"; + } + + return $command; + } + private function add_custom_redis() { if (is_null($this->database->redis_conf) || empty($this->database->redis_conf)) { diff --git a/app/Actions/Database/StopDatabaseProxy.php b/app/Actions/Database/StopDatabaseProxy.php index b2092e2ef4..0a166d24ae 100644 --- a/app/Actions/Database/StopDatabaseProxy.php +++ b/app/Actions/Database/StopDatabaseProxy.php @@ -2,7 +2,7 @@ namespace App\Actions\Database; -use App\Events\DatabaseStatusChanged; +use App\Events\DatabaseProxyStopped; use App\Models\ServiceDatabase; use App\Models\StandaloneClickhouse; use App\Models\StandaloneDragonfly; @@ -22,12 +22,16 @@ public function handle(StandaloneRedis|StandalonePostgresql|StandaloneMongodb|St { $server = data_get($database, 'destination.server'); $uuid = $database->uuid; - if ($database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($database->getMorphClass() === \App\Models\ServiceDatabase::class) { $uuid = $database->service->uuid; $server = data_get($database, 'service.server'); } instant_remote_process(["docker rm -f {$uuid}-proxy"], $server); + + $database->is_public = false; $database->save(); - DatabaseStatusChanged::dispatch(); + + DatabaseProxyStopped::dispatch(); + } } diff --git a/app/Actions/Docker/GetContainersStatus.php b/app/Actions/Docker/GetContainersStatus.php index ed563eaae9..a08056837f 100644 --- a/app/Actions/Docker/GetContainersStatus.php +++ b/app/Actions/Docker/GetContainersStatus.php @@ -3,14 +3,11 @@ namespace App\Actions\Docker; use App\Actions\Database\StartDatabaseProxy; -use App\Actions\Proxy\CheckProxy; -use App\Actions\Proxy\StartProxy; use App\Actions\Shared\ComplexStatusCheck; use App\Models\ApplicationPreview; use App\Models\Server; use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; -use App\Notifications\Container\ContainerStopped; use Illuminate\Support\Arr; use Illuminate\Support\Collection; use Lorisleiva\Actions\Concerns\AsAction; @@ -33,7 +30,7 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti $this->containerReplicates = $containerReplicates; $this->server = $server; if (! $this->server->isFunctional()) { - return 'Server is not ready.'; + return 'Server is not functional.'; } $this->applications = $this->server->applications(); $skip_these_applications = collect([]); @@ -49,323 +46,8 @@ public function handle(Server $server, ?Collection $containers = null, ?Collecti $this->applications = $this->applications->filter(function ($value, $key) use ($skip_these_applications) { return ! $skip_these_applications->pluck('id')->contains($value->id); }); - $this->old_way(); - // if ($this->server->isSwarm()) { - // $this->old_way(); - // } else { - // if (!$this->server->is_metrics_enabled) { - // $this->old_way(); - // return; - // } - // $sentinel_found = instant_remote_process(["docker inspect coolify-sentinel"], $this->server, false); - // $sentinel_found = json_decode($sentinel_found, true); - // $status = data_get($sentinel_found, '0.State.Status', 'exited'); - // if ($status === 'running') { - // ray('Checking with Sentinel'); - // $this->sentinel(); - // } else { - // ray('Checking the Old way'); - // $this->old_way(); - // } - // } - } - - // private function sentinel() - // { - // try { - // $this->containers = $this->server->getContainersWithSentinel(); - // if ($this->containers->count() === 0) { - // return; - // } - // $databases = $this->server->databases(); - // $services = $this->server->services()->get(); - // $previews = $this->server->previews(); - // $foundApplications = []; - // $foundApplicationPreviews = []; - // $foundDatabases = []; - // $foundServices = []; - - // foreach ($this->containers as $container) { - // $labels = Arr::undot(data_get($container, 'labels')); - // $containerStatus = data_get($container, 'state'); - // $containerHealth = data_get($container, 'health_status', 'unhealthy'); - // $containerStatus = "$containerStatus ($containerHealth)"; - // $applicationId = data_get($labels, 'coolify.applicationId'); - // if ($applicationId) { - // $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - // if ($pullRequestId) { - // if (str($applicationId)->contains('-')) { - // $applicationId = str($applicationId)->before('-'); - // } - // $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - // if ($preview) { - // $foundApplicationPreviews[] = $preview->id; - // $statusFromDb = $preview->status; - // if ($statusFromDb !== $containerStatus) { - // $preview->update(['status' => $containerStatus]); - // } - // } else { - // //Notify user that this container should not be there. - // } - // } else { - // $application = $this->applications->where('id', $applicationId)->first(); - // if ($application) { - // $foundApplications[] = $application->id; - // $statusFromDb = $application->status; - // if ($statusFromDb !== $containerStatus) { - // $application->update(['status' => $containerStatus]); - // } - // } else { - // //Notify user that this container should not be there. - // } - // } - // } else { - // $uuid = data_get($labels, 'com.docker.compose.service'); - // $type = data_get($labels, 'coolify.type'); - // if ($uuid) { - // if ($type === 'service') { - // $database_id = data_get($labels, 'coolify.service.subId'); - // if ($database_id) { - // $service_db = ServiceDatabase::where('id', $database_id)->first(); - // if ($service_db) { - // $uuid = $service_db->service->uuid; - // $isPublic = data_get($service_db, 'is_public'); - // if ($isPublic) { - // $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - // if ($this->server->isSwarm()) { - // // TODO: fix this with sentinel - // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - // } else { - // return data_get($value, 'name') === "$uuid-proxy"; - // } - // })->first(); - // if (! $foundTcpProxy) { - // StartDatabaseProxy::run($service_db); - // // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - // } - // } - // } - // } - // } else { - // $database = $databases->where('uuid', $uuid)->first(); - // if ($database) { - // $isPublic = data_get($database, 'is_public'); - // $foundDatabases[] = $database->id; - // $statusFromDb = $database->status; - // if ($statusFromDb !== $containerStatus) { - // $database->update(['status' => $containerStatus]); - // } - // if ($isPublic) { - // $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - // if ($this->server->isSwarm()) { - // // TODO: fix this with sentinel - // return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - // } else { - // return data_get($value, 'name') === "$uuid-proxy"; - // } - // })->first(); - // if (! $foundTcpProxy) { - // StartDatabaseProxy::run($database); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - // } - // } - // } else { - // // Notify user that this container should not be there. - // } - // } - // } - // if (data_get($container, 'name') === 'coolify-db') { - // $foundDatabases[] = 0; - // } - // } - // $serviceLabelId = data_get($labels, 'coolify.serviceId'); - // if ($serviceLabelId) { - // $subType = data_get($labels, 'coolify.service.subType'); - // $subId = data_get($labels, 'coolify.service.subId'); - // $service = $services->where('id', $serviceLabelId)->first(); - // if (! $service) { - // continue; - // } - // if ($subType === 'application') { - // $service = $service->applications()->where('id', $subId)->first(); - // } else { - // $service = $service->databases()->where('id', $subId)->first(); - // } - // if ($service) { - // $foundServices[] = "$service->id-$service->name"; - // $statusFromDb = $service->status; - // if ($statusFromDb !== $containerStatus) { - // // ray('Updating status: ' . $containerStatus); - // $service->update(['status' => $containerStatus]); - // } - // } - // } - // } - // $exitedServices = collect([]); - // foreach ($services as $service) { - // $apps = $service->applications()->get(); - // $dbs = $service->databases()->get(); - // foreach ($apps as $app) { - // if (in_array("$app->id-$app->name", $foundServices)) { - // continue; - // } else { - // $exitedServices->push($app); - // } - // } - // foreach ($dbs as $db) { - // if (in_array("$db->id-$db->name", $foundServices)) { - // continue; - // } else { - // $exitedServices->push($db); - // } - // } - // } - // $exitedServices = $exitedServices->unique('id'); - // foreach ($exitedServices as $exitedService) { - // if (str($exitedService->status)->startsWith('exited')) { - // continue; - // } - // $name = data_get($exitedService, 'name'); - // $fqdn = data_get($exitedService, 'fqdn'); - // if ($name) { - // if ($fqdn) { - // $containerName = "$name, available at $fqdn"; - // } else { - // $containerName = $name; - // } - // } else { - // if ($fqdn) { - // $containerName = $fqdn; - // } else { - // $containerName = null; - // } - // } - // $projectUuid = data_get($service, 'environment.project.uuid'); - // $serviceUuid = data_get($service, 'uuid'); - // $environmentName = data_get($service, 'environment.name'); - - // if ($projectUuid && $serviceUuid && $environmentName) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - // } else { - // $url = null; - // } - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // $exitedService->update(['status' => 'exited']); - // } - - // $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - // foreach ($notRunningApplications as $applicationId) { - // $application = $this->applications->where('id', $applicationId)->first(); - // if (str($application->status)->startsWith('exited')) { - // continue; - // } - // $application->update(['status' => 'exited']); - - // $name = data_get($application, 'name'); - // $fqdn = data_get($application, 'fqdn'); - - // $containerName = $name ? "$name ($fqdn)" : $fqdn; - - // $projectUuid = data_get($application, 'environment.project.uuid'); - // $applicationUuid = data_get($application, 'uuid'); - // $environment = data_get($application, 'environment.name'); - - // if ($projectUuid && $applicationUuid && $environment) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - // } else { - // $url = null; - // } - - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // } - // $notRunningApplicationPreviews = $previews->pluck('id')->diff($foundApplicationPreviews); - // foreach ($notRunningApplicationPreviews as $previewId) { - // $preview = $previews->where('id', $previewId)->first(); - // if (str($preview->status)->startsWith('exited')) { - // continue; - // } - // $preview->update(['status' => 'exited']); - - // $name = data_get($preview, 'name'); - // $fqdn = data_get($preview, 'fqdn'); - - // $containerName = $name ? "$name ($fqdn)" : $fqdn; - - // $projectUuid = data_get($preview, 'application.environment.project.uuid'); - // $environmentName = data_get($preview, 'application.environment.name'); - // $applicationUuid = data_get($preview, 'application.uuid'); - - // if ($projectUuid && $applicationUuid && $environmentName) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - // } else { - // $url = null; - // } - - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // } - // $notRunningDatabases = $databases->pluck('id')->diff($foundDatabases); - // foreach ($notRunningDatabases as $database) { - // $database = $databases->where('id', $database)->first(); - // if (str($database->status)->startsWith('exited')) { - // continue; - // } - // $database->update(['status' => 'exited']); - - // $name = data_get($database, 'name'); - // $fqdn = data_get($database, 'fqdn'); - - // $containerName = $name; - - // $projectUuid = data_get($database, 'environment.project.uuid'); - // $environmentName = data_get($database, 'environment.name'); - // $databaseUuid = data_get($database, 'uuid'); - - // if ($projectUuid && $databaseUuid && $environmentName) { - // $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - // } else { - // $url = null; - // } - // // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - // } - - // // Check if proxy is running - // $this->server->proxyType(); - // $foundProxyContainer = $this->containers->filter(function ($value, $key) { - // if ($this->server->isSwarm()) { - // // TODO: fix this with sentinel - // return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - // } else { - // return data_get($value, 'name') === 'coolify-proxy'; - // } - // })->first(); - // if (! $foundProxyContainer) { - // try { - // $shouldStart = CheckProxy::run($this->server); - // if ($shouldStart) { - // StartProxy::run($this->server, false); - // $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - // } - // } catch (\Throwable $e) { - // ray($e); - // } - // } else { - // $this->server->proxy->status = data_get($foundProxyContainer, 'state'); - // $this->server->save(); - // $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - // instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - // } - // } catch (\Exception $e) { - // // send_internal_notification("ContainerStatusJob failed on ({$this->server->id}) with: " . $e->getMessage()); - // ray($e->getMessage()); - - // return handleError($e); - // } - // } - - private function old_way() - { if ($this->containers === null) { - ['containers' => $this->containers,'containerReplicates' => $this->containerReplicates] = $this->server->getContainers(); + ['containers' => $this->containers, 'containerReplicates' => $this->containerReplicates] = $this->server->getContainers(); } if (is_null($this->containers)) { @@ -425,6 +107,8 @@ private function old_way() $statusFromDb = $preview->status; if ($statusFromDb !== $containerStatus) { $preview->update(['status' => $containerStatus]); + } else { + $preview->update(['last_online_at' => now()]); } } else { //Notify user that this container should not be there. @@ -436,6 +120,8 @@ private function old_way() $statusFromDb = $application->status; if ($statusFromDb !== $containerStatus) { $application->update(['status' => $containerStatus]); + } else { + $application->update(['last_online_at' => now()]); } } else { //Notify user that this container should not be there. @@ -478,7 +164,10 @@ private function old_way() $statusFromDb = $database->status; if ($statusFromDb !== $containerStatus) { $database->update(['status' => $containerStatus]); + } else { + $database->update(['last_online_at' => now()]); } + if ($isPublic) { $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { if ($this->server->isSwarm()) { @@ -489,7 +178,7 @@ private function old_way() })->first(); if (! $foundTcpProxy) { StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); } } } else { @@ -520,6 +209,8 @@ private function old_way() if ($statusFromDb !== $containerStatus) { // ray('Updating status: ' . $containerStatus); $service->update(['status' => $containerStatus]); + } else { + $service->update(['last_online_at' => now()]); } } } @@ -650,32 +341,5 @@ private function old_way() } // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); } - - if (! $this->server->proxySet() || $this->server->proxy->force_stop) { - return; - } - $foundProxyContainer = $this->containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); - } - } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } } } diff --git a/app/Actions/Fortify/CreateNewUser.php b/app/Actions/Fortify/CreateNewUser.php index 4817571622..ea2befd3a8 100644 --- a/app/Actions/Fortify/CreateNewUser.php +++ b/app/Actions/Fortify/CreateNewUser.php @@ -6,12 +6,11 @@ use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\CreatesNewUsers; class CreateNewUser implements CreatesNewUsers { - use PasswordValidationRules; - /** * Validate and create a newly registered user. * @@ -32,7 +31,7 @@ public function create(array $input): User 'max:255', Rule::unique(User::class), ], - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); if (User::count() == 0) { @@ -41,7 +40,7 @@ public function create(array $input): User $user = User::create([ 'id' => 0, 'name' => $input['name'], - 'email' => $input['email'], + 'email' => strtolower($input['email']), 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); @@ -53,7 +52,7 @@ public function create(array $input): User } else { $user = User::create([ 'name' => $input['name'], - 'email' => $input['email'], + 'email' => strtolower($input['email']), 'password' => Hash::make($input['password']), ]); $team = $user->teams()->first(); diff --git a/app/Actions/Fortify/PasswordValidationRules.php b/app/Actions/Fortify/PasswordValidationRules.php deleted file mode 100644 index 92fcc75325..0000000000 --- a/app/Actions/Fortify/PasswordValidationRules.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - protected function passwordRules(): array - { - return ['required', 'string', new Password, 'confirmed']; - } -} diff --git a/app/Actions/Fortify/ResetUserPassword.php b/app/Actions/Fortify/ResetUserPassword.php index 7a57c5037b..d3727a52cd 100644 --- a/app/Actions/Fortify/ResetUserPassword.php +++ b/app/Actions/Fortify/ResetUserPassword.php @@ -5,12 +5,11 @@ use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\ResetsUserPasswords; class ResetUserPassword implements ResetsUserPasswords { - use PasswordValidationRules; - /** * Validate and reset the user's forgotten password. * @@ -19,7 +18,7 @@ class ResetUserPassword implements ResetsUserPasswords public function reset(User $user, array $input): void { Validator::make($input, [ - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ])->validate(); $user->forceFill([ diff --git a/app/Actions/Fortify/UpdateUserPassword.php b/app/Actions/Fortify/UpdateUserPassword.php index 7005639052..0c51ec56db 100644 --- a/app/Actions/Fortify/UpdateUserPassword.php +++ b/app/Actions/Fortify/UpdateUserPassword.php @@ -5,12 +5,11 @@ use App\Models\User; use Illuminate\Support\Facades\Hash; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\Rules\Password; use Laravel\Fortify\Contracts\UpdatesUserPasswords; class UpdateUserPassword implements UpdatesUserPasswords { - use PasswordValidationRules; - /** * Validate and update the user's password. * @@ -20,7 +19,7 @@ public function update(User $user, array $input): void { Validator::make($input, [ 'current_password' => ['required', 'string', 'current_password:web'], - 'password' => $this->passwordRules(), + 'password' => ['required', Password::defaults(), 'confirmed'], ], [ 'current_password.current_password' => __('The provided password does not match your current password.'), ])->validateWithBag('updatePassword'); diff --git a/app/Actions/License/CheckResaleLicense.php b/app/Actions/License/CheckResaleLicense.php index 55af1a8c02..26a1ff7bf9 100644 --- a/app/Actions/License/CheckResaleLicense.php +++ b/app/Actions/License/CheckResaleLicense.php @@ -25,8 +25,6 @@ public function handle() // } $base_url = config('coolify.license_url'); $instance_id = config('app.id'); - - ray("Checking license key against $base_url/lemon/validate"); $data = Http::withHeaders([ 'Accept' => 'application/json', ])->get("$base_url/lemon/validate", [ @@ -34,7 +32,6 @@ public function handle() 'instance_id' => $instance_id, ])->json(); if (data_get($data, 'valid') === true && data_get($data, 'license_key.status') === 'active') { - ray('Valid & active license key'); $settings->update([ 'is_resale_license_active' => true, ]); @@ -48,7 +45,6 @@ public function handle() 'instance_id' => $instance_id, ])->json(); if (data_get($data, 'activated') === true) { - ray('Activated license key'); $settings->update([ 'is_resale_license_active' => true, ]); @@ -60,7 +56,6 @@ public function handle() } throw new \Exception('Cannot activate license key.'); } catch (\Throwable $e) { - ray($e); $settings->update([ 'resale_license' => null, 'is_resale_license_active' => false, diff --git a/app/Actions/Proxy/CheckProxy.php b/app/Actions/Proxy/CheckProxy.php index 03a0beddf7..51303d87a1 100644 --- a/app/Actions/Proxy/CheckProxy.php +++ b/app/Actions/Proxy/CheckProxy.php @@ -4,6 +4,7 @@ use App\Enums\ProxyTypes; use App\Models\Server; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; use Symfony\Component\Yaml\Yaml; @@ -88,7 +89,7 @@ public function handle(Server $server, $fromUI = false): bool $portsToCheck = []; } } catch (\Exception $e) { - ray($e->getMessage()); + Log::error('Error checking proxy: '.$e->getMessage()); } if (count($portsToCheck) === 0) { return false; diff --git a/app/Actions/Proxy/StartProxy.php b/app/Actions/Proxy/StartProxy.php index f20c10123e..7c93720cbf 100644 --- a/app/Actions/Proxy/StartProxy.php +++ b/app/Actions/Proxy/StartProxy.php @@ -13,67 +13,60 @@ class StartProxy public function handle(Server $server, bool $async = true, bool $force = false): string|Activity { - try { - $proxyType = $server->proxyType(); - if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { - return 'OK'; - } - $commands = collect([]); - $proxy_path = $server->proxyPath(); - $configuration = CheckConfiguration::run($server); - if (! $configuration) { - throw new \Exception('Configuration is not synced'); - } - SaveConfiguration::run($server, $configuration); - $docker_compose_yml_base64 = base64_encode($configuration); - $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); - $server->save(); - if ($server->isSwarm()) { - $commands = $commands->merge([ - "mkdir -p $proxy_path/dynamic", - "cd $proxy_path", - "echo 'Creating required Docker Compose file.'", - "echo 'Starting coolify-proxy.'", - 'docker stack deploy -c docker-compose.yml coolify-proxy', - "echo 'Successfully started coolify-proxy.'", - ]); - } else { - $caddfile = 'import /dynamic/*.caddy'; - $commands = $commands->merge([ - "mkdir -p $proxy_path/dynamic", - "cd $proxy_path", - "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", - "echo 'Creating required Docker Compose file.'", - "echo 'Pulling docker image.'", - 'docker compose pull', - 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then', - " echo 'Stopping and removing existing coolify-proxy.'", - ' docker rm -f coolify-proxy || true', - " echo 'Successfully stopped and removed existing coolify-proxy.'", - 'fi', - "echo 'Starting coolify-proxy.'", - 'docker compose up -d --remove-orphans', - "echo 'Successfully started coolify-proxy.'", - ]); - $commands = $commands->merge(connectProxyToNetworks($server)); - } - - if ($async) { - $activity = remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); + $proxyType = $server->proxyType(); + if ((is_null($proxyType) || $proxyType === 'NONE' || $server->proxy->force_stop || $server->isBuildServer()) && $force === false) { + return 'OK'; + } + $commands = collect([]); + $proxy_path = $server->proxyPath(); + $configuration = CheckConfiguration::run($server); + if (! $configuration) { + throw new \Exception('Configuration is not synced'); + } + SaveConfiguration::run($server, $configuration); + $docker_compose_yml_base64 = base64_encode($configuration); + $server->proxy->last_applied_settings = str($docker_compose_yml_base64)->pipe('md5')->value(); + $server->save(); + if ($server->isSwarm()) { + $commands = $commands->merge([ + "mkdir -p $proxy_path/dynamic", + "cd $proxy_path", + "echo 'Creating required Docker Compose file.'", + "echo 'Starting coolify-proxy.'", + 'docker stack deploy -c docker-compose.yml coolify-proxy', + "echo 'Successfully started coolify-proxy.'", + ]); + } else { + $caddfile = 'import /dynamic/*.caddy'; + $commands = $commands->merge([ + "mkdir -p $proxy_path/dynamic", + "cd $proxy_path", + "echo '$caddfile' > $proxy_path/dynamic/Caddyfile", + "echo 'Creating required Docker Compose file.'", + "echo 'Pulling docker image.'", + 'docker compose pull', + 'if docker ps -a --format "{{.Names}}" | grep -q "^coolify-proxy$"; then', + " echo 'Stopping and removing existing coolify-proxy.'", + ' docker rm -f coolify-proxy || true', + " echo 'Successfully stopped and removed existing coolify-proxy.'", + 'fi', + "echo 'Starting coolify-proxy.'", + 'docker compose up -d --remove-orphans', + "echo 'Successfully started coolify-proxy.'", + ]); + $commands = $commands->merge(connectProxyToNetworks($server)); + } - return $activity; - } else { - instant_remote_process($commands, $server); - $server->proxy->set('status', 'running'); - $server->proxy->set('type', $proxyType); - $server->save(); - ProxyStarted::dispatch($server); + if ($async) { + return remote_process($commands, $server, callEventOnFinish: 'ProxyStarted', callEventData: $server); + } else { + instant_remote_process($commands, $server); + $server->proxy->set('status', 'running'); + $server->proxy->set('type', $proxyType); + $server->save(); + ProxyStarted::dispatch($server); - return 'OK'; - } - } catch (\Throwable $e) { - ray($e); - throw $e; + return 'OK'; } } } diff --git a/app/Actions/Server/ConfigureCloudflared.php b/app/Actions/Server/ConfigureCloudflared.php index 0d36e88630..fc04e67a45 100644 --- a/app/Actions/Server/ConfigureCloudflared.php +++ b/app/Actions/Server/ConfigureCloudflared.php @@ -40,7 +40,6 @@ public function handle(Server $server, string $cloudflare_token) ]); instant_remote_process($commands, $server); } catch (\Throwable $e) { - ray($e); $server->settings->is_cloudflare_tunnel = false; $server->settings->save(); throw $e; @@ -51,7 +50,6 @@ public function handle(Server $server, string $cloudflare_token) 'rm -fr /tmp/cloudflared', ]); instant_remote_process($commands, $server); - } } } diff --git a/app/Actions/Server/DeleteServer.php b/app/Actions/Server/DeleteServer.php new file mode 100644 index 0000000000..15c892e755 --- /dev/null +++ b/app/Actions/Server/DeleteServer.php @@ -0,0 +1,17 @@ +forceDelete(); + } +} diff --git a/app/Actions/Server/InstallDocker.php b/app/Actions/Server/InstallDocker.php index f671f2d2a2..ba6c23ffce 100644 --- a/app/Actions/Server/InstallDocker.php +++ b/app/Actions/Server/InstallDocker.php @@ -12,12 +12,11 @@ class InstallDocker public function handle(Server $server) { + $dockerVersion = config('constants.docker_install_version'); $supported_os_type = $server->validateOS(); if (! $supported_os_type) { throw new \Exception('Server OS type is not supported for automated installation. Please install Docker manually before continuing: documentation.'); } - ray('Installing Docker on server: '.$server->name.' ('.$server->ip.')'.' with OS type: '.$supported_os_type); - $dockerVersion = '24.0'; $config = base64_encode('{ "log-driver": "json-file", "log-opts": { diff --git a/app/Actions/Server/ResourcesCheck.php b/app/Actions/Server/ResourcesCheck.php new file mode 100644 index 0000000000..e6b90ba380 --- /dev/null +++ b/app/Actions/Server/ResourcesCheck.php @@ -0,0 +1,41 @@ +subSeconds($seconds))->update(['status' => 'exited']); + ServiceApplication::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + ServiceDatabase::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandalonePostgresql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneRedis::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMongodb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMysql::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneMariadb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneKeydb::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneDragonfly::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + StandaloneClickhouse::where('last_online_at', '<', now()->subSeconds($seconds))->update(['status' => 'exited']); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Actions/Server/RestartContainer.php b/app/Actions/Server/RestartContainer.php new file mode 100644 index 0000000000..63361d8b71 --- /dev/null +++ b/app/Actions/Server/RestartContainer.php @@ -0,0 +1,16 @@ +restartContainer($containerName); + } +} diff --git a/app/Actions/Server/RunCommand.php b/app/Actions/Server/RunCommand.php index fce862eb08..254c78587c 100644 --- a/app/Actions/Server/RunCommand.php +++ b/app/Actions/Server/RunCommand.php @@ -12,8 +12,6 @@ class RunCommand public function handle(Server $server, $command) { - $activity = remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value); - - return $activity; + return remote_process(command: [$command], server: $server, ignore_errors: true, type: ActivityTypes::COMMAND->value); } } diff --git a/app/Actions/Server/ServerCheck.php b/app/Actions/Server/ServerCheck.php new file mode 100644 index 0000000000..1dae03fd90 --- /dev/null +++ b/app/Actions/Server/ServerCheck.php @@ -0,0 +1,269 @@ +server = $server; + try { + if ($this->server->isFunctional() === false) { + return 'Server is not functional.'; + } + + if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { + + if (isset($data)) { + $data = collect($data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + $containerReplicates = null; + $this->isSentinel = true; + + } else { + ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); + // ServerStorageCheckJob::dispatch($this->server); + } + + if (is_null($this->containers)) { + return 'No containers found.'; + } + + if (isset($containerReplicates)) { + foreach ($containerReplicates as $containerReplica) { + $name = data_get($containerReplica, 'Name'); + $this->containers = $this->containers->map(function ($container) use ($name, $containerReplica) { + if (data_get($container, 'Spec.Name') === $name) { + $replicas = data_get($containerReplica, 'Replicas'); + $running = str($replicas)->explode('/')[0]; + $total = str($replicas)->explode('/')[1]; + if ($running === $total) { + data_set($container, 'State.Status', 'running'); + data_set($container, 'State.Health.Status', 'healthy'); + } else { + data_set($container, 'State.Status', 'starting'); + data_set($container, 'State.Health.Status', 'unhealthy'); + } + } + + return $container; + }); + } + } + $this->checkContainers(); + + if ($this->server->isSentinelEnabled() && $this->isSentinel === false) { + CheckAndStartSentinelJob::dispatch($this->server); + } + + if ($this->server->isLogDrainEnabled()) { + $this->checkLogDrainContainer(); + } + + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + } + } catch (\Throwable $e) { + return handleError($e); + } + } + + private function checkLogDrainContainer() + { + $foundLogDrainContainer = $this->containers->filter(function ($value, $key) { + return data_get($value, 'Name') === '/coolify-log-drain'; + })->first(); + if ($foundLogDrainContainer) { + $status = data_get($foundLogDrainContainer, 'State.Status'); + if ($status !== 'running') { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } else { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } + + private function checkContainers() + { + foreach ($this->containers as $container) { + if ($this->isSentinel) { + $labels = Arr::undot(data_get($container, 'labels')); + } else { + if ($this->server->isSwarm()) { + $labels = Arr::undot(data_get($container, 'Spec.Labels')); + } else { + $labels = Arr::undot(data_get($container, 'Config.Labels')); + } + + } + $managed = data_get($labels, 'coolify.managed'); + if (! $managed) { + continue; + } + $uuid = data_get($labels, 'coolify.name'); + if (! $uuid) { + $uuid = data_get($labels, 'com.docker.compose.service'); + } + + if ($this->isSentinel) { + $containerStatus = data_get($container, 'state'); + $containerHealth = data_get($container, 'health_status'); + } else { + $containerStatus = data_get($container, 'State.Status'); + $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); + } + $containerStatus = "$containerStatus ($containerHealth)"; + + $applicationId = data_get($labels, 'coolify.applicationId'); + $serviceId = data_get($labels, 'coolify.serviceId'); + $databaseId = data_get($labels, 'coolify.databaseId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId'); + + if ($applicationId) { + // Application + if ($pullRequestId != 0) { + if (str($applicationId)->contains('-')) { + $applicationId = str($applicationId)->before('-'); + } + $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); + if ($preview) { + $preview->update(['status' => $containerStatus]); + } + } else { + $application = Application::where('id', $applicationId)->first(); + if ($application) { + $application->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + } + } + } elseif (isset($serviceId)) { + // Service + $subType = data_get($labels, 'coolify.service.subType'); + $subId = data_get($labels, 'coolify.service.subId'); + $service = Service::where('id', $serviceId)->first(); + if (! $service) { + continue; + } + if ($subType === 'application') { + $service = ServiceApplication::where('id', $subId)->first(); + } else { + $service = ServiceDatabase::where('id', $subId)->first(); + } + if ($service) { + $service->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + if ($subType === 'database') { + $isPublic = data_get($service, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->isSentinel) { + return data_get($value, 'name') === $uuid.'-proxy'; + } else { + + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + return data_get($value, 'Name') === "/$uuid-proxy"; + } + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($service); + } + } + } + } + } else { + // Database + if (is_null($this->databases)) { + $this->databases = $this->server->databases(); + } + $database = $this->databases->where('uuid', $uuid)->first(); + if ($database) { + $database->update([ + 'status' => $containerStatus, + 'last_online_at' => now(), + ]); + + $isPublic = data_get($database, 'is_public'); + if ($isPublic) { + $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { + if ($this->isSentinel) { + return data_get($value, 'name') === $uuid.'-proxy'; + } else { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; + } else { + + return data_get($value, 'Name') === "/$uuid-proxy"; + } + } + })->first(); + if (! $foundTcpProxy) { + StartDatabaseProxy::run($database); + // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } + } + } + } + } + } +} diff --git a/app/Actions/Server/InstallLogDrain.php b/app/Actions/Server/StartLogDrain.php similarity index 96% rename from app/Actions/Server/InstallLogDrain.php rename to app/Actions/Server/StartLogDrain.php index 9b67412115..0e8036cd96 100644 --- a/app/Actions/Server/InstallLogDrain.php +++ b/app/Actions/Server/StartLogDrain.php @@ -5,7 +5,7 @@ use App\Models\Server; use Lorisleiva\Actions\Concerns\AsAction; -class InstallLogDrain +class StartLogDrain { use AsAction; @@ -13,12 +13,16 @@ public function handle(Server $server) { if ($server->settings->is_logdrain_newrelic_enabled) { $type = 'newrelic'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_highlight_enabled) { $type = 'highlight'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_axiom_enabled) { $type = 'axiom'; + StopLogDrain::run($server); } elseif ($server->settings->is_logdrain_custom_enabled) { $type = 'custom'; + StopLogDrain::run($server); } else { $type = 'none'; } @@ -151,6 +155,8 @@ public function handle(Server $server) - ./parsers.conf:/parsers.conf ports: - 127.0.0.1:24224:24224 + labels: + - coolify.managed=true restart: unless-stopped '); $readme = base64_encode('# New Relic Log Drain @@ -202,10 +208,8 @@ public function handle(Server $server) throw new \Exception('Unknown log drain type.'); } $restart_command = [ - "echo 'Stopping old Fluent Bit'", - "cd $config_path && docker compose down --remove-orphans || true", "echo 'Starting Fluent Bit'", - "cd $config_path && docker compose up -d --remove-orphans", + "cd $config_path && docker compose up -d", ]; $command = array_merge($command, $add_envs_command, $restart_command); diff --git a/app/Actions/Server/StartSentinel.php b/app/Actions/Server/StartSentinel.php index b79bc8f672..587ac4a8db 100644 --- a/app/Actions/Server/StartSentinel.php +++ b/app/Actions/Server/StartSentinel.php @@ -9,18 +9,57 @@ class StartSentinel { use AsAction; - public function handle(Server $server, $version = 'latest', bool $restart = false) + public function handle(Server $server, bool $restart = false, ?string $latestVersion = null) { + if ($server->isSwarm() || $server->isBuildServer()) { + return; + } if ($restart) { StopSentinel::run($server); } - $metrics_history = $server->settings->metrics_history_days; - $refresh_rate = $server->settings->metrics_refresh_rate_seconds; - $token = $server->settings->metrics_token; + $version = $latestVersion ?? get_latest_sentinel_version(); + $metricsHistory = data_get($server, 'settings.sentinel_metrics_history_days'); + $refreshRate = data_get($server, 'settings.sentinel_metrics_refresh_rate_seconds'); + $pushInterval = data_get($server, 'settings.sentinel_push_interval_seconds'); + $token = data_get($server, 'settings.sentinel_token'); + $endpoint = data_get($server, 'settings.sentinel_custom_url'); + $debug = data_get($server, 'settings.is_sentinel_debug_enabled'); + $mountDir = '/data/coolify/sentinel'; + $image = "ghcr.io/coollabsio/sentinel:$version"; + if (! $endpoint) { + throw new \Exception('You should set FQDN in Instance Settings.'); + } + $environments = [ + 'TOKEN' => $token, + 'DEBUG' => $debug ? 'true' : 'false', + 'PUSH_ENDPOINT' => $endpoint, + 'PUSH_INTERVAL_SECONDS' => $pushInterval, + 'COLLECTOR_ENABLED' => $server->isMetricsEnabled() ? 'true' : 'false', + 'COLLECTOR_REFRESH_RATE_SECONDS' => $refreshRate, + 'COLLECTOR_RETENTION_PERIOD_DAYS' => $metricsHistory, + ]; + $labels = [ + 'coolify.managed' => 'true', + ]; + if (isDev()) { + // data_set($environments, 'DEBUG', 'true'); + // $image = 'sentinel'; + $mountDir = '/var/lib/docker/volumes/coolify_dev_coolify_data/_data/sentinel'; + } + $dockerEnvironments = '-e "'.implode('" -e "', array_map(fn ($key, $value) => "$key=$value", array_keys($environments), $environments)).'"'; + $dockerLabels = implode(' ', array_map(fn ($key, $value) => "$key=$value", array_keys($labels), $labels)); + $dockerCommand = "docker run -d $dockerEnvironments --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v $mountDir:/app/db --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 --add-host=host.docker.internal:host-gateway --label $dockerLabels $image"; + instant_remote_process([ - "docker run --rm --pull always -d -e \"TOKEN={$token}\" -e \"SCHEDULER=true\" -e \"METRICS_HISTORY={$metrics_history}\" -e \"REFRESH_RATE={$refresh_rate}\" --name coolify-sentinel -v /var/run/docker.sock:/var/run/docker.sock -v /data/coolify/metrics:/app/metrics -v /data/coolify/logs:/app/logs --pid host --health-cmd \"curl --fail http://127.0.0.1:8888/api/health || exit 1\" --health-interval 10s --health-retries 3 ghcr.io/coollabsio/sentinel:$version", - 'chown -R 9999:root /data/coolify/metrics /data/coolify/logs', - 'chmod -R 700 /data/coolify/metrics /data/coolify/logs', - ], $server, true); + 'docker rm -f coolify-sentinel || true', + "mkdir -p $mountDir", + $dockerCommand, + "chown -R 9999:root $mountDir", + "chmod -R 700 $mountDir", + ], $server); + + $server->settings->is_sentinel_enabled = true; + $server->settings->save(); + $server->sentinelHeartbeat(); } } diff --git a/app/Actions/Server/StopLogDrain.php b/app/Actions/Server/StopLogDrain.php index a5bce94a57..96c2466dec 100644 --- a/app/Actions/Server/StopLogDrain.php +++ b/app/Actions/Server/StopLogDrain.php @@ -12,7 +12,7 @@ class StopLogDrain public function handle(Server $server) { try { - return instant_remote_process(['docker rm -f coolify-log-drain || true'], $server); + return instant_remote_process(['docker rm -f coolify-log-drain'], $server, false); } catch (\Throwable $e) { return handleError($e); } diff --git a/app/Actions/Server/StopSentinel.php b/app/Actions/Server/StopSentinel.php index 21ffca3bd7..aecb96c87c 100644 --- a/app/Actions/Server/StopSentinel.php +++ b/app/Actions/Server/StopSentinel.php @@ -12,5 +12,6 @@ class StopSentinel public function handle(Server $server) { instant_remote_process(['docker rm -f coolify-sentinel'], $server, false); + $server->sentinelHeartbeat(isReset: true); } } diff --git a/app/Actions/Server/UpdateCoolify.php b/app/Actions/Server/UpdateCoolify.php index 30664df263..3185c22b74 100644 --- a/app/Actions/Server/UpdateCoolify.php +++ b/app/Actions/Server/UpdateCoolify.php @@ -18,32 +18,28 @@ class UpdateCoolify public function handle($manual_update = false) { - try { - $settings = instanceSettings(); - $this->server = Server::find(0); - if (! $this->server) { + $settings = instanceSettings(); + $this->server = Server::find(0); + if (! $this->server) { + return; + } + CleanupDocker::dispatch($this->server)->onQueue('high'); + $this->latestVersion = get_latest_version_of_coolify(); + $this->currentVersion = config('version'); + if (! $manual_update) { + if (! $settings->is_auto_update_enabled) { return; } - CleanupDocker::dispatch($this->server)->onQueue('high'); - $this->latestVersion = get_latest_version_of_coolify(); - $this->currentVersion = config('version'); - if (! $manual_update) { - if (! $settings->is_auto_update_enabled) { - return; - } - if ($this->latestVersion === $this->currentVersion) { - return; - } - if (version_compare($this->latestVersion, $this->currentVersion, '<')) { - return; - } + if ($this->latestVersion === $this->currentVersion) { + return; + } + if (version_compare($this->latestVersion, $this->currentVersion, '<')) { + return; } - $this->update(); - $settings->new_version_available = false; - $settings->save(); - } catch (\Throwable $e) { - throw $e; } + $this->update(); + $settings->new_version_available = false; + $settings->save(); } private function update() diff --git a/app/Actions/Service/DeleteService.php b/app/Actions/Service/DeleteService.php index f28e5490e5..9b87454dac 100644 --- a/app/Actions/Service/DeleteService.php +++ b/app/Actions/Service/DeleteService.php @@ -4,6 +4,7 @@ use App\Actions\Server\CleanupDocker; use App\Models\Service; +use Illuminate\Support\Facades\Log; use Lorisleiva\Actions\Concerns\AsAction; class DeleteService @@ -39,8 +40,8 @@ public function handle(Service $service, bool $deleteConfigurations, bool $delet if (! empty($commands)) { foreach ($commands as $command) { $result = instant_remote_process([$command], $server, false); - if ($result !== 0) { - ray("Failed to execute: $command"); + if ($result !== null && $result !== 0) { + Log::error('Error deleting volumes: '.$result); } } } diff --git a/app/Actions/Service/StartService.php b/app/Actions/Service/StartService.php index 06d2e0efb3..82de066d7b 100644 --- a/app/Actions/Service/StartService.php +++ b/app/Actions/Service/StartService.php @@ -12,7 +12,6 @@ class StartService public function handle(Service $service) { - ray('Starting service: '.$service->name); $service->saveComposeConfigs(); $commands[] = 'cd '.$service->workdir(); $commands[] = "echo 'Saved configuration files to {$service->workdir()}.'"; @@ -34,8 +33,7 @@ public function handle(Service $service) $commands[] = "docker network connect --alias {$serviceName}-{$service->uuid} $network {$serviceName}-{$service->uuid} >/dev/null 2>&1 || true"; } } - $activity = remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); - return $activity; + return remote_process($commands, $service->server, type_uuid: $service->uuid, callEventOnFinish: 'ServiceStatusChanged'); } } diff --git a/app/Actions/Service/StopService.php b/app/Actions/Service/StopService.php index 5c7bbc2aa9..046d94ced4 100644 --- a/app/Actions/Service/StopService.php +++ b/app/Actions/Service/StopService.php @@ -28,8 +28,6 @@ public function handle(Service $service, bool $isDeleteOperation = false, bool $ } } } catch (\Exception $e) { - ray($e->getMessage()); - return $e->getMessage(); } } diff --git a/app/Console/Commands/CleanupDatabase.php b/app/Console/Commands/CleanupDatabase.php index 6f130626b1..a0adc8b36b 100644 --- a/app/Console/Commands/CleanupDatabase.php +++ b/app/Console/Commands/CleanupDatabase.php @@ -7,7 +7,7 @@ class CleanupDatabase extends Command { - protected $signature = 'cleanup:database {--yes}'; + protected $signature = 'cleanup:database {--yes} {--keep-days=}'; protected $description = 'Cleanup database'; @@ -20,9 +20,9 @@ public function handle() } if (isCloud()) { // Later on we can increase this to 180 days or dynamically set - $keep_days = 60; + $keep_days = $this->option('keep-days') ?? 60; } else { - $keep_days = 60; + $keep_days = $this->option('keep-days') ?? 60; } echo "Keep days: $keep_days\n"; // Cleanup failed jobs table @@ -64,6 +64,5 @@ public function handle() if ($this->option('yes')) { $webhooks->delete(); } - } } diff --git a/app/Console/Commands/CleanupRedis.php b/app/Console/Commands/CleanupRedis.php index ed0740d34f..5fc2b4e617 100644 --- a/app/Console/Commands/CleanupRedis.php +++ b/app/Console/Commands/CleanupRedis.php @@ -26,6 +26,5 @@ public function handle() collect($queueOverlaps)->each(function ($key) { Redis::connection()->del($key); }); - } } diff --git a/app/Console/Commands/CleanupStuckedResources.php b/app/Console/Commands/CleanupStuckedResources.php index 66c25ec27a..9d36ce9b82 100644 --- a/app/Console/Commands/CleanupStuckedResources.php +++ b/app/Console/Commands/CleanupStuckedResources.php @@ -30,14 +30,12 @@ class CleanupStuckedResources extends Command public function handle() { - ray('Running cleanup stucked resources.'); echo "Running cleanup stucked resources.\n"; $this->cleanup_stucked_resources(); } private function cleanup_stucked_resources() { - try { $servers = Server::all()->filter(function ($server) { return $server->isFunctional(); diff --git a/app/Console/Commands/CloudCleanupSubscriptions.php b/app/Console/Commands/CloudCleanupSubscriptions.php index d220aa00b6..8bb420ab86 100644 --- a/app/Console/Commands/CloudCleanupSubscriptions.php +++ b/app/Console/Commands/CloudCleanupSubscriptions.php @@ -19,7 +19,6 @@ public function handle() return; } - ray()->clearAll(); $this->info('Cleaning up subcriptions teams'); $stripe = new \Stripe\StripeClient(config('subscription.stripe_api_key')); @@ -74,7 +73,6 @@ public function handle() } } } - } catch (\Exception $e) { $this->error($e->getMessage()); @@ -96,6 +94,5 @@ private function disableServers(Team $team) ]); } } - } } diff --git a/app/Console/Commands/Dev.php b/app/Console/Commands/Dev.php index 20a2667c32..f5f1233fe8 100644 --- a/app/Console/Commands/Dev.php +++ b/app/Console/Commands/Dev.php @@ -25,7 +25,6 @@ public function handle() return; } - } public function generateOpenApi() diff --git a/app/Console/Commands/Emails.php b/app/Console/Commands/Emails.php index 36722564c5..cda4ca84f1 100644 --- a/app/Console/Commands/Emails.php +++ b/app/Console/Commands/Emails.php @@ -15,7 +15,6 @@ use App\Notifications\Application\StatusChanged; use App\Notifications\Database\BackupFailed; use App\Notifications\Database\BackupSuccess; -use App\Notifications\Database\DailyBackup; use App\Notifications\Test; use Exception; use Illuminate\Console\Command; @@ -121,28 +120,10 @@ public function handle() $this->mail = (new Test)->toMail(); $this->sendEmail(); break; - case 'database-backup-statuses-daily': - $scheduled_backups = ScheduledDatabaseBackup::all(); - $databases = collect(); - foreach ($scheduled_backups as $scheduled_backup) { - $last_days_backups = $scheduled_backup->get_last_days_backup_status(); - if ($last_days_backups->isEmpty()) { - continue; - } - $failed = $last_days_backups->where('status', 'failed'); - $database = $scheduled_backup->database; - $databases->put($database->name, [ - 'failed_count' => $failed->count(), - ]); - } - $this->mail = (new DailyBackup($databases))->toMail(); - $this->sendEmail(); - break; case 'application-deployment-success-daily': $applications = Application::all(); foreach ($applications as $application) { $deployments = $application->get_last_days_deployments(); - ray($deployments); if ($deployments->isEmpty()) { continue; } diff --git a/app/Console/Commands/Init.php b/app/Console/Commands/Init.php index ad7bff86da..c802fb1169 100644 --- a/app/Console/Commands/Init.php +++ b/app/Console/Commands/Init.php @@ -10,6 +10,7 @@ use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; +use App\Models\User; use Illuminate\Console\Command; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; @@ -32,16 +33,16 @@ public function handle() $this->servers = Server::all(); if (isCloud()) { - } else { $this->send_alive_signal(); get_public_ips(); } // Backward compatibility - $this->disable_metrics(); + // $this->disable_metrics(); $this->replace_slash_in_environment_name(); $this->restore_coolify_db_backup(); + $this->update_user_emails(); // $this->update_traefik_labels(); if (! isCloud() || $this->option('force-cloud')) { @@ -79,17 +80,26 @@ public function handle() } } - private function disable_metrics() + // private function disable_metrics() + // { + // if (version_compare('4.0.0-beta.312', config('version'), '<=')) { + // foreach ($this->servers as $server) { + // if ($server->settings->is_metrics_enabled === true) { + // $server->settings->update(['is_metrics_enabled' => false]); + // } + // if ($server->isFunctional()) { + // StopSentinel::dispatch($server)->onQueue('high'); + // } + // } + // } + // } + + private function update_user_emails() { - if (version_compare('4.0.0-beta.312', config('version'), '<=')) { - foreach ($this->servers as $server) { - if ($server->settings->is_metrics_enabled === true) { - $server->settings->update(['is_metrics_enabled' => false]); - } - if ($server->isFunctional()) { - StopSentinel::dispatch($server); - } - } + try { + User::whereRaw('email ~ \'[A-Z]\'')->get()->each(fn (User $user) => $user->update(['email' => strtolower($user->email)])); + } catch (\Throwable $e) { + echo "Error in updating user emails: {$e->getMessage()}\n"; } } @@ -120,7 +130,6 @@ private function cleanup_unnecessary_dynamic_proxy_configuration() } catch (\Throwable $e) { echo "Error in cleaning up unnecessary dynamic proxy configuration: {$e->getMessage()}\n"; } - } } @@ -180,7 +189,7 @@ private function restore_coolify_db_backup() 'save_s3' => false, 'frequency' => '0 0 * * *', 'database_id' => $database->id, - 'database_type' => 'App\Models\StandalonePostgresql', + 'database_type' => \App\Models\StandalonePostgresql::class, 'team_id' => 0, ]); } @@ -219,7 +228,6 @@ private function cleanup_in_progress_application_deployments() } $queued_inprogress_deployments = ApplicationDeploymentQueue::whereIn('status', [ApplicationDeploymentStatus::IN_PROGRESS->value, ApplicationDeploymentStatus::QUEUED->value])->get(); foreach ($queued_inprogress_deployments as $deployment) { - ray($deployment->id, $deployment->status); echo "Cleaning up deployment: {$deployment->id}\n"; $deployment->status = ApplicationDeploymentStatus::FAILED->value; $deployment->save(); diff --git a/app/Console/Commands/NotifyDemo.php b/app/Console/Commands/NotifyDemo.php index 81333b8682..f0131b7b2a 100644 --- a/app/Console/Commands/NotifyDemo.php +++ b/app/Console/Commands/NotifyDemo.php @@ -36,8 +36,6 @@ public function handle() return; } - - ray($channel); } private function showHelp() diff --git a/app/Console/Commands/OpenApi.php b/app/Console/Commands/OpenApi.php index e8d73ef477..e248aa2c0c 100644 --- a/app/Console/Commands/OpenApi.php +++ b/app/Console/Commands/OpenApi.php @@ -21,6 +21,5 @@ public function handle() $error = preg_replace('/^\h*\v+/m', '', $error); echo $error; echo $process->output(); - } } diff --git a/app/Console/Commands/ServicesDelete.php b/app/Console/Commands/ServicesDelete.php index b5a74166a5..1e5d5808c6 100644 --- a/app/Console/Commands/ServicesDelete.php +++ b/app/Console/Commands/ServicesDelete.php @@ -96,7 +96,7 @@ private function deleteApplication() if (! $confirmed) { break; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } @@ -122,7 +122,7 @@ private function deleteDatabase() if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } @@ -148,7 +148,7 @@ private function deleteService() if (! $confirmed) { return; } - DeleteResourceJob::dispatch($toDelete); + DeleteResourceJob::dispatch($toDelete)->onQueue('high'); } } } diff --git a/app/Console/Commands/ServicesGenerate.php b/app/Console/Commands/ServicesGenerate.php index 9720e81ac9..1559e5f6df 100644 --- a/app/Console/Commands/ServicesGenerate.php +++ b/app/Console/Commands/ServicesGenerate.php @@ -3,128 +3,82 @@ namespace App\Console\Commands; use Illuminate\Console\Command; +use Illuminate\Support\Arr; use Symfony\Component\Yaml\Yaml; class ServicesGenerate extends Command { /** - * The name and signature of the console command. - * - * @var string + * {@inheritdoc} */ protected $signature = 'services:generate'; /** - * The console command description. - * - * @var string + * {@inheritdoc} */ protected $description = 'Generate service-templates.yaml based on /templates/compose directory'; - /** - * Execute the console command. - */ - public function handle() + public function handle(): int { - $files = array_diff(scandir(base_path('templates/compose')), ['.', '..']); - $files = array_filter($files, function ($file) { - return strpos($file, '.yaml') !== false; - }); - $serviceTemplatesJson = []; - foreach ($files as $file) { - $parsed = $this->process_file($file); - if ($parsed) { - $name = data_get($parsed, 'name'); - $parsed = data_forget($parsed, 'name'); - $serviceTemplatesJson[$name] = $parsed; - } - } - $serviceTemplatesJson = json_encode($serviceTemplatesJson, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + $serviceTemplatesJson = collect(glob(base_path('templates/compose/*.yaml'))) + ->mapWithKeys(function ($file): array { + $file = basename($file); + $parsed = $this->processFile($file); + + return $parsed === false ? [] : [ + Arr::pull($parsed, 'name') => $parsed, + ]; + })->toJson(JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES); + file_put_contents(base_path('templates/service-templates.json'), $serviceTemplatesJson.PHP_EOL); + + return self::SUCCESS; } - private function process_file($file) + private function processFile(string $file): false|array { - $serviceName = str($file)->before('.yaml')->value(); $content = file_get_contents(base_path("templates/compose/$file")); - // $this->info($content); - $ignore = collect(preg_grep('/^# ignore:/', explode("\n", $content)))->values(); - if ($ignore->count() > 0) { - $ignore = (bool) str($ignore[0])->after('# ignore:')->trim()->value(); - } else { - $ignore = false; - } - if ($ignore) { + + $data = collect(explode(PHP_EOL, $content))->mapWithKeys(function ($line): array { + preg_match('/^#(?.*):(?.*)$/U', $line, $m); + + return $m ? [trim($m['key']) => trim($m['value'])] : []; + }); + + if (str($data->get('ignore'))->toBoolean()) { $this->info("Ignoring $file"); - return; + return false; } + $this->info("Processing $file"); - $documentation = collect(preg_grep('/^# documentation:/', explode("\n", $content)))->values(); - if ($documentation->count() > 0) { - $documentation = str($documentation[0])->after('# documentation:')->trim()->value(); - $documentation = str($documentation)->append('?utm_source=coolify.io'); - } else { - $documentation = 'https://coolify.io/docs'; - } - $slogan = collect(preg_grep('/^# slogan:/', explode("\n", $content)))->values(); - if ($slogan->count() > 0) { - $slogan = str($slogan[0])->after('# slogan:')->trim()->value(); - } else { - $slogan = str($file)->headline()->value(); - } - $logo = collect(preg_grep('/^# logo:/', explode("\n", $content)))->values(); - if ($logo->count() > 0) { - $logo = str($logo[0])->after('# logo:')->trim()->value(); - } else { - $logo = 'svgs/coolify.png'; - } - $minversion = collect(preg_grep('/^# minversion:/', explode("\n", $content)))->values(); - if ($minversion->count() > 0) { - $minversion = str($minversion[0])->after('# minversion:')->trim()->value(); - } else { - $minversion = '0.0.0'; - } - $env_file = collect(preg_grep('/^# env_file:/', explode("\n", $content)))->values(); - if ($env_file->count() > 0) { - $env_file = str($env_file[0])->after('# env_file:')->trim()->value(); - } else { - $env_file = null; - } + $documentation = $data->get('documentation'); + $documentation = $documentation ? $documentation.'?utm_source=coolify.io' : 'https://coolify.io/docs'; - $tags = collect(preg_grep('/^# tags:/', explode("\n", $content)))->values(); - if ($tags->count() > 0) { - $tags = str($tags[0])->after('# tags:')->trim()->explode(',')->map(function ($tag) { - return str($tag)->trim()->lower()->value(); - })->values(); - } else { - $tags = null; - } - $port = collect(preg_grep('/^# port:/', explode("\n", $content)))->values(); - if ($port->count() > 0) { - $port = str($port[0])->after('# port:')->trim()->value(); - } else { - $port = null; - } $json = Yaml::parse($content); - $yaml = base64_encode(Yaml::dump($json, 10, 2)); + $compose = base64_encode(Yaml::dump($json, 10, 2)); + + $tags = str($data->get('tags'))->lower()->explode(',')->map(fn ($tag) => trim($tag))->filter(); + $tags = $tags->isEmpty() ? null : $tags->all(); + $payload = [ - 'name' => $serviceName, + 'name' => pathinfo($file, PATHINFO_FILENAME), 'documentation' => $documentation, - 'slogan' => $slogan, - 'compose' => $yaml, + 'slogan' => $data->get('slogan', str($file)->headline()), + 'compose' => $compose, 'tags' => $tags, - 'logo' => $logo, - 'minversion' => $minversion, + 'logo' => $data->get('logo', 'svgs/coolify.png'), + 'minversion' => $data->get('minversion', '0.0.0'), ]; - if ($port) { + + if ($port = $data->get('port')) { $payload['port'] = $port; } - if ($env_file) { - $env_file_content = file_get_contents(base_path("templates/compose/$env_file")); - $env_file_base64 = base64_encode($env_file_content); - $payload['envs'] = $env_file_base64; + + if ($envFile = $data->get('env_file')) { + $envFileContent = file_get_contents(base_path("templates/compose/$envFile")); + $payload['envs'] = base64_encode($envFileContent); } return $payload; diff --git a/app/Console/Commands/Weird.php b/app/Console/Commands/Weird.php new file mode 100644 index 0000000000..e471a5f961 --- /dev/null +++ b/app/Console/Commands/Weird.php @@ -0,0 +1,58 @@ +error('This command can only be run in development mode'); + + return; + } + $run = $this->option('run'); + if ($run) { + $servers = Server::all(); + foreach ($servers as $server) { + ServerCheck::dispatch($server); + } + + return; + } + $number = $this->option('number'); + for ($i = 0; $i < $number; $i++) { + $uuid = Str::uuid(); + $server = Server::create([ + 'name' => 'localhost-'.$uuid, + 'description' => 'This is a test docker container in development mode', + 'ip' => 'coolify-testing-host', + 'team_id' => 0, + 'private_key_id' => 1, + 'proxy' => [ + 'type' => ProxyTypes::NONE->value, + 'status' => ProxyStatus::EXITED->value, + ], + ]); + $server->settings->update([ + 'is_usable' => true, + 'is_reachable' => true, + ]); + } + } catch (\Exception $e) { + $this->error($e->getMessage()); + } + } +} diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php index 1430fcdd1d..3fb4de60b6 100644 --- a/app/Console/Kernel.php +++ b/app/Console/Kernel.php @@ -2,33 +2,45 @@ namespace App\Console; +use App\Jobs\CheckAndStartSentinelJob; use App\Jobs\CheckForUpdatesJob; +use App\Jobs\CheckHelperImageJob; use App\Jobs\CleanupInstanceStuffsJob; use App\Jobs\CleanupStaleMultiplexedConnections; use App\Jobs\DatabaseBackupJob; use App\Jobs\DockerCleanupJob; -use App\Jobs\PullHelperImageJob; -use App\Jobs\PullSentinelImageJob; use App\Jobs\PullTemplatesFromCDN; use App\Jobs\ScheduledTaskJob; use App\Jobs\ServerCheckJob; +use App\Jobs\ServerCleanupMux; use App\Jobs\ServerStorageCheckJob; 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 { - private $all_servers; + private $allServers; + + private InstanceSettings $settings; + + private string $updateCheckFrequency; + + private string $instanceTimezone; protected function schedule(Schedule $schedule): void { - $this->all_servers = Server::all(); - $settings = instanceSettings(); + $this->allServers = Server::where('ip', '!=', '1.2.3.4'); + + $this->settings = instanceSettings(); + $this->updateCheckFrequency = $this->settings->update_check_frequency ?: '0 * * * *'; + $this->instanceTimezone = $this->settings->instance_timezone ?: config('app.timezone'); $schedule->job(new CleanupStaleMultiplexedConnections)->hourly(); @@ -36,108 +48,118 @@ protected function schedule(Schedule $schedule): void // Instance Jobs $schedule->command('horizon:snapshot')->everyMinute(); $schedule->job(new CleanupInstanceStuffsJob)->everyMinute()->onOneServer(); + $schedule->job(new CheckHelperImageJob)->everyTenMinutes()->onOneServer(); + // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->check_scheduled_tasks($schedule); - $schedule->command('uploads:clear')->everyTwoMinutes(); + $this->checkResources($schedule); - $schedule->command('telescope:prune')->daily(); + $this->checkScheduledBackups($schedule); + $this->checkScheduledTasks($schedule); + + $schedule->command('uploads:clear')->everyTwoMinutes(); - $schedule->job(new PullHelperImageJob)->everyFiveMinutes()->onOneServer(); } else { // Instance Jobs $schedule->command('horizon:snapshot')->everyFiveMinutes(); $schedule->command('cleanup:unreachable-servers')->daily()->onOneServer(); - $schedule->job(new PullTemplatesFromCDN)->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); + $schedule->job(new PullTemplatesFromCDN)->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); $schedule->job(new CleanupInstanceStuffsJob)->everyTwoMinutes()->onOneServer(); - $this->schedule_updates($schedule); + $this->scheduleUpdates($schedule); // Server Jobs - $this->check_scheduled_backups($schedule); - $this->check_resources($schedule); - $this->pull_images($schedule); - $this->check_scheduled_tasks($schedule); + $this->checkResources($schedule); + + $this->pullImages($schedule); + + $this->checkScheduledBackups($schedule); + $this->checkScheduledTasks($schedule); $schedule->command('cleanup:database --yes')->daily(); $schedule->command('uploads:clear')->everyTwoMinutes(); } } - private function pull_images($schedule) + private function pullImages($schedule): void { - $settings = instanceSettings(); - $servers = $this->all_servers->where('settings.is_usable', true)->where('settings.is_reachable', true)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->whereRelation('settings', 'is_usable', true)->whereRelation('settings', 'is_reachable', true)->get(); foreach ($servers as $server) { if ($server->isSentinelEnabled()) { $schedule->job(function () use ($server) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $server, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - PullSentinelImageJob::dispatch($server); - } - })->cron($settings->update_check_frequency)->timezone($settings->instance_timezone)->onOneServer(); + CheckAndStartSentinelJob::dispatch($server); + })->cron($this->updateCheckFrequency)->timezone($this->instanceTimezone)->onOneServer(); } } - $schedule->job(new PullHelperImageJob) - ->cron($settings->update_check_frequency) - ->timezone($settings->instance_timezone) + $schedule->job(new CheckHelperImageJob) + ->cron($this->updateCheckFrequency) + ->timezone($this->instanceTimezone) ->onOneServer(); } - private function schedule_updates($schedule) + private function scheduleUpdates($schedule): void { - $settings = instanceSettings(); - - $updateCheckFrequency = $settings->update_check_frequency; $schedule->job(new CheckForUpdatesJob) - ->cron($updateCheckFrequency) - ->timezone($settings->instance_timezone) + ->cron($this->updateCheckFrequency) + ->timezone($this->instanceTimezone) ->onOneServer(); - if ($settings->is_auto_update_enabled) { - $autoUpdateFrequency = $settings->auto_update_frequency; + if ($this->settings->is_auto_update_enabled) { + $autoUpdateFrequency = $this->settings->auto_update_frequency; $schedule->job(new UpdateCoolifyJob) ->cron($autoUpdateFrequency) - ->timezone($settings->instance_timezone) + ->timezone($this->instanceTimezone) ->onOneServer(); } } - private function check_resources($schedule) + private function checkResources($schedule): void { if (isCloud()) { - $servers = $this->all_servers->whereNotNull('team.subscription')->where('team.subscription.stripe_trial_already_ended', false)->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->whereHas('team.subscription')->get(); $own = Team::find(0)->servers; $servers = $servers->merge($own); } else { - $servers = $this->all_servers->where('ip', '!=', '1.2.3.4'); + $servers = $this->allServers->get(); } + foreach ($servers as $server) { - $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); - // $schedule->job(new ServerStorageCheckJob($server))->everyMinute()->onOneServer(); $serverTimezone = $server->settings->server_timezone; + + // Sentinel check + $lastSentinelUpdate = $server->sentinel_updated_at; + if (Carbon::parse($lastSentinelUpdate)->isBefore(now()->subSeconds($server->waitBeforeDoingSshCheck()))) { + // Check container status every minute if Sentinel does not activated + $schedule->job(new ServerCheckJob($server))->everyMinute()->onOneServer(); + // $schedule->job(new \App\Jobs\ServerCheckNewJob($server))->everyMinute()->onOneServer(); + + // Check storage usage every 10 minutes if Sentinel does not activated + $schedule->job(new ServerStorageCheckJob($server))->everyTenMinutes()->onOneServer(); + } if ($server->settings->force_docker_cleanup) { $schedule->job(new DockerCleanupJob($server))->cron($server->settings->docker_cleanup_frequency)->timezone($serverTimezone)->onOneServer(); } else { $schedule->job(new DockerCleanupJob($server))->everyTenMinutes()->timezone($serverTimezone)->onOneServer(); } + + // Cleanup multiplexed connections every hour + $schedule->job(new ServerCleanupMux($server))->hourly()->onOneServer(); + + // Temporary solution until we have better memory management for Sentinel + if ($server->isSentinelEnabled()) { + $schedule->job(function () use ($server) { + $server->restartContainer('coolify-sentinel'); + })->daily()->onOneServer(); + } } } - private function check_scheduled_backups($schedule) + private function checkScheduledBackups($schedule): void { - $scheduled_backups = ScheduledDatabaseBackup::all(); + $scheduled_backups = ScheduledDatabaseBackup::where('enabled', true)->get(); if ($scheduled_backups->isEmpty()) { return; } foreach ($scheduled_backups as $scheduled_backup) { - if (! $scheduled_backup->enabled) { - continue; - } if (is_null(data_get($scheduled_backup, 'database'))) { - ray('database not found'); $scheduled_backup->delete(); continue; @@ -145,35 +167,30 @@ private function check_scheduled_backups($schedule) $server = $scheduled_backup->server(); - if (! $server) { + if (is_null($server)) { continue; } - $serverTimezone = $server->settings->server_timezone; if (isset(VALID_CRON_STRINGS[$scheduled_backup->frequency])) { $scheduled_backup->frequency = VALID_CRON_STRINGS[$scheduled_backup->frequency]; } $schedule->job(new DatabaseBackupJob( backup: $scheduled_backup - ))->cron($scheduled_backup->frequency)->timezone($serverTimezone)->onOneServer(); + ))->cron($scheduled_backup->frequency)->timezone($this->instanceTimezone)->onOneServer(); } } - private function check_scheduled_tasks($schedule) + private function checkScheduledTasks($schedule): void { - $scheduled_tasks = ScheduledTask::all(); + $scheduled_tasks = ScheduledTask::where('enabled', true)->get(); if ($scheduled_tasks->isEmpty()) { return; } foreach ($scheduled_tasks as $scheduled_task) { - if ($scheduled_task->enabled === false) { - continue; - } $service = $scheduled_task->service; $application = $scheduled_task->application; if (! $application && ! $service) { - ray('application/service attached to scheduled task does not exist'); $scheduled_task->delete(); continue; @@ -193,14 +210,13 @@ private function check_scheduled_tasks($schedule) if (! $server) { continue; } - $serverTimezone = $server->settings->server_timezone ?: config('app.timezone'); if (isset(VALID_CRON_STRINGS[$scheduled_task->frequency])) { $scheduled_task->frequency = VALID_CRON_STRINGS[$scheduled_task->frequency]; } $schedule->job(new ScheduledTaskJob( task: $scheduled_task - ))->cron($scheduled_task->frequency)->timezone($serverTimezone)->onOneServer(); + ))->cron($scheduled_task->frequency)->timezone($this->instanceTimezone)->onOneServer(); } } diff --git a/app/Enums/Role.php b/app/Enums/Role.php new file mode 100644 index 0000000000..a37a5076c1 --- /dev/null +++ b/app/Enums/Role.php @@ -0,0 +1,37 @@ + 1, + self::ADMIN => 2, + self::OWNER => 3, + }; + } + + public function lt(Role|string $role): bool + { + if (is_string($role)) { + $role = Role::from($role); + } + + return $this->rank() < $role->rank(); + } + + public function gt(Role|string $role): bool + { + if (is_string($role)) { + $role = Role::from($role); + } + + return $this->rank() > $role->rank(); + } +} diff --git a/app/Events/DatabaseProxyStopped.php b/app/Events/DatabaseProxyStopped.php new file mode 100644 index 0000000000..b457dc6a07 --- /dev/null +++ b/app/Events/DatabaseProxyStopped.php @@ -0,0 +1,35 @@ +currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/DatabaseStatusChanged.php b/app/Events/DatabaseStatusChanged.php index a94bc22724..913b21bc22 100644 --- a/app/Events/DatabaseStatusChanged.php +++ b/app/Events/DatabaseStatusChanged.php @@ -7,27 +7,29 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class DatabaseStatusChanged implements ShouldBroadcast { use Dispatchable, InteractsWithSockets, SerializesModels; - public ?string $userId = null; + public $userId = null; public function __construct($userId = null) { if (is_null($userId)) { - $userId = auth()->user()->id ?? null; + $userId = Auth::id() ?? null; } if (is_null($userId)) { return false; } + $this->userId = $userId; } public function broadcastOn(): ?array { - if ($this->userId) { + if (! is_null($this->userId)) { return [ new PrivateChannel("user.{$this->userId}"), ]; diff --git a/app/Events/FileStorageChanged.php b/app/Events/FileStorageChanged.php index 27fdc6b5c9..57004cf4c3 100644 --- a/app/Events/FileStorageChanged.php +++ b/app/Events/FileStorageChanged.php @@ -16,7 +16,6 @@ class FileStorageChanged implements ShouldBroadcast public function __construct($teamId = null) { - ray($teamId); if (is_null($teamId)) { throw new \Exception('Team id is null'); } diff --git a/app/Events/ScheduledTaskDone.php b/app/Events/ScheduledTaskDone.php new file mode 100644 index 0000000000..c8b5547f61 --- /dev/null +++ b/app/Events/ScheduledTaskDone.php @@ -0,0 +1,34 @@ +user()->currentTeam()->id ?? null; + } + if (is_null($teamId)) { + throw new \Exception('Team id is null'); + } + $this->teamId = $teamId; + } + + public function broadcastOn(): array + { + return [ + new PrivateChannel("team.{$this->teamId}"), + ]; + } +} diff --git a/app/Events/ServiceStatusChanged.php b/app/Events/ServiceStatusChanged.php index a86a8b02dc..3950022e10 100644 --- a/app/Events/ServiceStatusChanged.php +++ b/app/Events/ServiceStatusChanged.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Broadcasting\ShouldBroadcast; use Illuminate\Foundation\Events\Dispatchable; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Auth; class ServiceStatusChanged implements ShouldBroadcast { @@ -17,7 +18,7 @@ class ServiceStatusChanged implements ShouldBroadcast public function __construct($userId = null) { if (is_null($userId)) { - $userId = auth()->user()->id ?? null; + $userId = Auth::id() ?? null; } if (is_null($userId)) { return false; diff --git a/app/Exceptions/Handler.php b/app/Exceptions/Handler.php index 63fbfc8621..8c89bb07f9 100644 --- a/app/Exceptions/Handler.php +++ b/app/Exceptions/Handler.php @@ -84,7 +84,6 @@ function (Scope $scope) { if (str($e->getMessage())->contains('No space left on device')) { return; } - ray('reporting to sentry'); Integration::captureUnhandledException($e); }); } diff --git a/app/Http/Controllers/Api/ApplicationsController.php b/app/Http/Controllers/Api/ApplicationsController.php index 2a1f846d39..f0eeb56d8d 100644 --- a/app/Http/Controllers/Api/ApplicationsController.php +++ b/app/Http/Controllers/Api/ApplicationsController.php @@ -1203,7 +1203,7 @@ private function create_application(Request $request, $type) $service->name = "service-$service->uuid"; $service->parse(isNew: true); if ($instantDeploy) { - StartService::dispatch($service); + StartService::dispatch($service)->onQueue('high'); } return response()->json(serializeApiResponse([ @@ -1213,7 +1213,6 @@ private function create_application(Request $request, $type) } return response()->json(['message' => 'Invalid type.'], 400); - } #[OA\Get( @@ -1359,7 +1358,7 @@ public function delete_by_uuid(Request $request) deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - ); + )->onQueue('high'); return response()->json([ 'message' => 'Application deletion request queued.', @@ -1579,11 +1578,16 @@ public function update_by_uuid(Request $request) $request->offsetUnset('docker_compose_domains'); } $instantDeploy = $request->instant_deploy; + $isStatic = $request->is_static; + $useBuildServer = $request->use_build_server; - $use_build_server = $request->use_build_server; + if (isset($useBuildServer)) { + $application->settings->is_build_server_enabled = $useBuildServer; + $application->settings->save(); + } - if (isset($use_build_server)) { - $application->settings->is_build_server_enabled = $use_build_server; + if (isset($isStatic)) { + $application->settings->is_static = $isStatic; $application->settings->save(); } @@ -1687,9 +1691,8 @@ public function envs(Request $request) 'standalone_postgresql_id', 'standalone_redis_id', ]); - $env = $this->removeSensitiveData($env); - return $env; + return $this->removeSensitiveData($env); }); return response()->json($envs); @@ -1864,18 +1867,15 @@ public function update_env_by_uuid(Request $request) return response()->json($this->removeSensitiveData($env))->setStatusCode(201); } else { - return response()->json([ 'message' => 'Environment variable not found.', ], 404); - } } return response()->json([ 'message' => 'Something is not okay. Are you okay?', ], 500); - } #[OA\Patch( @@ -2220,14 +2220,12 @@ public function create_env(Request $request) return response()->json([ 'uuid' => $env->uuid, ])->setStatusCode(201); - } } return response()->json([ 'message' => 'Something went wrong.', ], 500); - } #[OA\Delete( @@ -2484,7 +2482,7 @@ public function action_stop(Request $request) if (! $application) { return response()->json(['message' => 'Application not found.'], 404); } - StopApplication::dispatch($application); + StopApplication::dispatch($application)->onQueue('high'); return response()->json( [ @@ -2575,7 +2573,6 @@ public function action_restart(Request $request) 'deployment_uuid' => $deployment_uuid->toString(), ], ); - } #[OA\Post( @@ -2741,7 +2738,6 @@ private function validateDataApplications(Request $request, Server $server) 'custom_labels' => 'The custom_labels should be base64 encoded.', ], ], 422); - } } if ($request->has('domains') && $server->isProxyShouldRun()) { diff --git a/app/Http/Controllers/Api/DatabasesController.php b/app/Http/Controllers/Api/DatabasesController.php index 65873f8187..eaa542a83a 100644 --- a/app/Http/Controllers/Api/DatabasesController.php +++ b/app/Http/Controllers/Api/DatabasesController.php @@ -471,7 +471,6 @@ public function update_by_uuid(Request $request) $request->offsetSet('mysql_conf', $mysqlConf); } break; - } $extraFields = array_diff(array_keys($request->all()), $allowedFields); if ($validator->fails() || ! empty($extraFields)) { @@ -498,15 +497,14 @@ public function update_by_uuid(Request $request) $database->update($request->all()); if ($whatToDoWithDatabaseProxy === 'start') { - StartDatabaseProxy::dispatch($database); + StartDatabaseProxy::dispatch($database)->onQueue('high'); } elseif ($whatToDoWithDatabaseProxy === 'stop') { - StopDatabaseProxy::dispatch($database); + StopDatabaseProxy::dispatch($database)->onQueue('high'); } return response()->json([ 'message' => 'Database updated.', ]); - } #[OA\Post( @@ -1153,7 +1151,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $database = create_standalone_postgresql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); $payload = [ @@ -1165,7 +1163,6 @@ public function create_database(Request $request, NewDatabaseTypes $type) } return response()->json(serializeApiResponse($payload))->setStatusCode(201); - } elseif ($type === NewDatabaseTypes::MARIADB) { $allowedFields = ['name', 'description', 'image', 'public_port', 'is_public', 'project_uuid', 'environment_name', 'server_uuid', 'destination_uuid', 'instant_deploy', 'limits_memory', 'limits_memory_swap', 'limits_memory_swappiness', 'limits_memory_reservation', 'limits_cpus', 'limits_cpuset', 'limits_cpu_shares', 'mariadb_conf', 'mariadb_root_password', 'mariadb_user', 'mariadb_password', 'mariadb_database']; $validator = customApiValidator($request->all(), [ @@ -1209,7 +1206,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $database = create_standalone_mariadb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1267,7 +1264,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $database = create_standalone_mysql($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1323,7 +1320,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $database = create_standalone_redis($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1360,7 +1357,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_dragonfly($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } return response()->json(serializeApiResponse([ @@ -1409,7 +1406,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $database = create_standalone_keydb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1445,7 +1442,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) removeUnnecessaryFieldsFromRequest($request); $database = create_standalone_clickhouse($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1503,7 +1500,7 @@ public function create_database(Request $request, NewDatabaseTypes $type) } $database = create_standalone_mongodb($environment->id, $destination->uuid, $request->all()); if ($instantDeploy) { - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); } $database->refresh(); @@ -1596,7 +1593,7 @@ public function delete_by_uuid(Request $request) deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - ); + )->onQueue('high'); return response()->json([ 'message' => 'Database deletion request queued.', @@ -1669,7 +1666,7 @@ public function action_deploy(Request $request) if (str($database->status)->contains('running')) { return response()->json(['message' => 'Database is already running.'], 400); } - StartDatabase::dispatch($database); + StartDatabase::dispatch($database)->onQueue('high'); return response()->json( [ @@ -1745,7 +1742,7 @@ public function action_stop(Request $request) if (str($database->status)->contains('stopped') || str($database->status)->contains('exited')) { return response()->json(['message' => 'Database is already stopped.'], 400); } - StopDatabase::dispatch($database); + StopDatabase::dispatch($database)->onQueue('high'); return response()->json( [ @@ -1818,7 +1815,7 @@ public function action_restart(Request $request) if (! $database) { return response()->json(['message' => 'Database not found.'], 404); } - RestartDatabase::dispatch($database); + RestartDatabase::dispatch($database)->onQueue('high'); return response()->json( [ @@ -1826,6 +1823,5 @@ public function action_restart(Request $request) ], 200 ); - } } diff --git a/app/Http/Controllers/Api/DeployController.php b/app/Http/Controllers/Api/DeployController.php index d1c8f5ea62..59b199d874 100644 --- a/app/Http/Controllers/Api/DeployController.php +++ b/app/Http/Controllers/Api/DeployController.php @@ -292,7 +292,7 @@ public function deploy_resource($resource, bool $force = false): array return ['message' => "Resource ($resource) not found.", 'deployment_uuid' => $deployment_uuid]; } switch ($resource?->getMorphClass()) { - case 'App\Models\Application': + case \App\Models\Application::class: $deployment_uuid = new Cuid2; queue_application_deployment( application: $resource, @@ -301,13 +301,13 @@ public function deploy_resource($resource, bool $force = false): array ); $message = "Application {$resource->name} deployment queued."; break; - case 'App\Models\Service': + case \App\Models\Service::class: StartService::run($resource); $message = "Service {$resource->name} started. It could take a while, be patient."; break; default: // Database resource - StartDatabase::dispatch($resource); + StartDatabase::dispatch($resource)->onQueue('high'); $resource->update([ 'started_at' => now(), ]); diff --git a/app/Http/Controllers/Api/OtherController.php b/app/Http/Controllers/Api/OtherController.php index 2414b7a420..062cc04e7a 100644 --- a/app/Http/Controllers/Api/OtherController.php +++ b/app/Http/Controllers/Api/OtherController.php @@ -160,7 +160,7 @@ public function feedback(Request $request) #[OA\Get( summary: 'Healthcheck', description: 'Healthcheck endpoint.', - path: '/healthcheck', + path: '/health', operationId: 'healthcheck', responses: [ new OA\Response( diff --git a/app/Http/Controllers/Api/ProjectController.php b/app/Http/Controllers/Api/ProjectController.php index f1958de2c2..b69028b706 100644 --- a/app/Http/Controllers/Api/ProjectController.php +++ b/app/Http/Controllers/Api/ProjectController.php @@ -356,7 +356,6 @@ public function update_project(Request $request) 'name' => $project->name, 'description' => $project->description, ])->setStatusCode(201); - } #[OA\Delete( @@ -423,7 +422,7 @@ public function delete_project(Request $request) if (! $project) { return response()->json(['message' => 'Project not found.'], 404); } - if ($project->resource_count() > 0) { + if (! $project->isEmpty()) { return response()->json(['message' => 'Project has resources, so it cannot be deleted.'], 400); } diff --git a/app/Http/Controllers/Api/ResourcesController.php b/app/Http/Controllers/Api/ResourcesController.php index 1fd5792e03..4180cef9a1 100644 --- a/app/Http/Controllers/Api/ResourcesController.php +++ b/app/Http/Controllers/Api/ResourcesController.php @@ -53,7 +53,7 @@ public function resources(Request $request) $resources = $resources->flatten(); $resources = $resources->map(function ($resource) { $payload = $resource->toArray(); - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { $payload['status'] = $resource->status(); } else { $payload['status'] = $resource->status; diff --git a/app/Http/Controllers/Api/ServersController.php b/app/Http/Controllers/Api/ServersController.php index a495155792..024ef35fad 100644 --- a/app/Http/Controllers/Api/ServersController.php +++ b/app/Http/Controllers/Api/ServersController.php @@ -2,6 +2,7 @@ namespace App\Http\Controllers\Api; +use App\Actions\Server\DeleteServer; use App\Actions\Server\ValidateServer; use App\Enums\ProxyStatus; use App\Enums\ProxyTypes; @@ -23,7 +24,7 @@ private function removeSensitiveDataFromSettings($settings) return serializeApiResponse($settings); } $settings = $settings->makeHidden([ - 'metrics_token', + 'sentinel_token', ]); return serializeApiResponse($settings); @@ -248,7 +249,6 @@ public function resources_by_server(Request $request) return $payload; }); $server = $this->removeSensitiveData($server); - ray($server); return response()->json(serializeApiResponse(data_get($server, 'resources'))); } @@ -538,7 +538,7 @@ public function create_server(Request $request) 'is_build_server' => $request->is_build_server, ]); if ($request->instant_validate) { - ValidateServer::dispatch($server); + ValidateServer::dispatch($server)->onQueue('high'); } return response()->json([ @@ -651,7 +651,7 @@ public function update_server(Request $request) ]); } if ($request->instant_validate) { - ValidateServer::dispatch($server); + ValidateServer::dispatch($server)->onQueue('high'); } return response()->json(serializeApiResponse($server))->setStatusCode(201); @@ -726,6 +726,7 @@ public function delete_server(Request $request) return response()->json(['message' => 'Server has resources, so you need to delete them before.'], 400); } $server->delete(); + DeleteServer::dispatch($server); return response()->json(['message' => 'Server deleted.']); } @@ -786,7 +787,7 @@ public function validate_server(Request $request) if (! $server) { return response()->json(['message' => 'Server not found.'], 404); } - ValidateServer::dispatch($server); + ValidateServer::dispatch($server)->onQueue('high'); return response()->json(['message' => 'Validation started.']); } diff --git a/app/Http/Controllers/Api/ServicesController.php b/app/Http/Controllers/Api/ServicesController.php index 89418517ba..bdb5612adc 100644 --- a/app/Http/Controllers/Api/ServicesController.php +++ b/app/Http/Controllers/Api/ServicesController.php @@ -342,7 +342,7 @@ public function create_service(Request $request) } $service->parse(isNew: true); if ($instantDeploy) { - StartService::dispatch($service); + StartService::dispatch($service)->onQueue('high'); } $domains = $service->applications()->get()->pluck('fqdn')->sort(); $domains = $domains->map(function ($domain) { @@ -487,7 +487,7 @@ public function delete_by_uuid(Request $request) deleteVolumes: $request->query->get('delete_volumes', true), dockerCleanup: $request->query->get('docker_cleanup', true), deleteConnectedNetworks: $request->query->get('delete_connected_networks', true) - ); + )->onQueue('high'); return response()->json([ 'message' => 'Service deletion request queued.', @@ -566,9 +566,8 @@ public function envs(Request $request) 'standalone_postgresql_id', 'standalone_redis_id', ]); - $env = $this->removeSensitiveData($env); - return $env; + return $this->removeSensitiveData($env); }); return response()->json($envs); @@ -1077,7 +1076,7 @@ public function action_deploy(Request $request) if (str($service->status())->contains('running')) { return response()->json(['message' => 'Service is already running.'], 400); } - StartService::dispatch($service); + StartService::dispatch($service)->onQueue('high'); return response()->json( [ @@ -1155,7 +1154,7 @@ public function action_stop(Request $request) if (str($service->status())->contains('stopped') || str($service->status())->contains('exited')) { return response()->json(['message' => 'Service is already stopped.'], 400); } - StopService::dispatch($service); + StopService::dispatch($service)->onQueue('high'); return response()->json( [ @@ -1230,7 +1229,7 @@ public function action_restart(Request $request) if (! $service) { return response()->json(['message' => 'Service not found.'], 404); } - RestartService::dispatch($service); + RestartService::dispatch($service)->onQueue('high'); return response()->json( [ @@ -1238,6 +1237,5 @@ public function action_restart(Request $request) ], 200 ); - } } diff --git a/app/Http/Controllers/Controller.php b/app/Http/Controllers/Controller.php index 38d9e22729..9f1e4eeb81 100644 --- a/app/Http/Controllers/Controller.php +++ b/app/Http/Controllers/Controller.php @@ -110,59 +110,54 @@ public function link() return redirect()->route('login')->with('error', 'Invalid credentials.'); } - public function accept_invitation() + public function acceptInvitation() { - try { - $resetPassword = request()->query('reset-password'); - $invitationUuid = request()->route('uuid'); - $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - $invitationValid = $invitation->isValid(); - if ($invitationValid) { - if ($resetPassword) { - $user->update([ - 'password' => Hash::make($invitationUuid), - 'force_password_reset' => true, - ]); - } - if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { - $invitation->delete(); + $resetPassword = request()->query('reset-password'); + $invitationUuid = request()->route('uuid'); - return redirect()->route('team.index'); - } - $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + $invitation = TeamInvitation::whereUuid($invitationUuid)->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + + if (Auth::id() !== $user->id) { + abort(400, 'You are not allowed to accept this invitation.'); + } + $invitationValid = $invitation->isValid(); + + if ($invitationValid) { + if ($resetPassword) { + $user->update([ + 'password' => Hash::make($invitationUuid), + 'force_password_reset' => true, + ]); + } + if ($user->teams()->where('team_id', $invitation->team->id)->exists()) { $invitation->delete(); - if (auth()->user()?->id !== $user->id) { - return redirect()->route('login'); - } - refreshSession($invitation->team); return redirect()->route('team.index'); - } else { - abort(401); } - } catch (\Throwable $e) { - ray($e->getMessage()); - throw $e; + $user->teams()->attach($invitation->team->id, ['role' => $invitation->role]); + $invitation->delete(); + + refreshSession($invitation->team); + + return redirect()->route('team.index'); + } else { + abort(400, 'Invitation expired.'); } } public function revoke_invitation() { - try { - $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); - $user = User::whereEmail($invitation->email)->firstOrFail(); - if (is_null(auth()->user())) { - return redirect()->route('login'); - } - if (auth()->user()->id !== $user->id) { - abort(401); - } - $invitation->delete(); - - return redirect()->route('team.index'); - } catch (\Throwable $e) { - throw $e; + $invitation = TeamInvitation::whereUuid(request()->route('uuid'))->firstOrFail(); + $user = User::whereEmail($invitation->email)->firstOrFail(); + if (is_null(Auth::user())) { + return redirect()->route('login'); } + if (Auth::id() !== $user->id) { + abort(401); + } + $invitation->delete(); + + return redirect()->route('team.index'); } } diff --git a/app/Http/Controllers/OauthController.php b/app/Http/Controllers/OauthController.php index 630d010459..3a3f18c9c4 100644 --- a/app/Http/Controllers/OauthController.php +++ b/app/Http/Controllers/OauthController.php @@ -35,8 +35,6 @@ public function callback(string $provider) return redirect('/'); } catch (\Exception $e) { - ray($e->getMessage()); - $errorCode = $e instanceof HttpException ? 'auth.failed' : 'auth.failed.callback'; return redirect()->route('login')->withErrors([__($errorCode)]); diff --git a/app/Http/Controllers/UploadController.php b/app/Http/Controllers/UploadController.php index 21fdd2ef81..4d34a10007 100644 --- a/app/Http/Controllers/UploadController.php +++ b/app/Http/Controllers/UploadController.php @@ -5,7 +5,6 @@ use Illuminate\Http\Request; use Illuminate\Http\UploadedFile; use Illuminate\Routing\Controller as BaseController; -use Illuminate\Support\Facades\Storage; use Pion\Laravel\ChunkUpload\Exceptions\UploadMissingFileException; use Pion\Laravel\ChunkUpload\Handler\HandlerFactory; use Pion\Laravel\ChunkUpload\Receiver\FileReceiver; diff --git a/app/Http/Controllers/Webhook/Bitbucket.php b/app/Http/Controllers/Webhook/Bitbucket.php index ef85d59e3c..8c74f95e5c 100644 --- a/app/Http/Controllers/Webhook/Bitbucket.php +++ b/app/Http/Controllers/Webhook/Bitbucket.php @@ -16,7 +16,6 @@ public function manual(Request $request) { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -55,7 +54,6 @@ public function manual(Request $request) 'message' => 'Nothing to do. No branch found in the request.', ]); } - ray('Manual webhook bitbucket push event with branch: '.$branch); } if ($x_bitbucket_event === 'pullrequest:created' || $x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { $branch = data_get($payload, 'pullrequest.destination.branch.name'); @@ -85,7 +83,6 @@ public function manual(Request $request) 'status' => 'failed', 'message' => 'Invalid signature.', ]); - ray('Invalid signature'); continue; } @@ -96,13 +93,11 @@ public function manual(Request $request) 'status' => 'failed', 'message' => 'Server is not functional.', ]); - ray('Server is not functional: '.$application->destination->server->name); continue; } if ($x_bitbucket_event === 'repo:push') { if ($application->isDeployable()) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -126,7 +121,6 @@ public function manual(Request $request) } if ($x_bitbucket_event === 'pullrequest:created') { if ($application->isPRDeployable()) { - ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $deployment_uuid = new Cuid2; $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if (! $found) { @@ -171,7 +165,6 @@ public function manual(Request $request) } } if ($x_bitbucket_event === 'pullrequest:rejected' || $x_bitbucket_event === 'pullrequest:fulfilled') { - ray('Pull request rejected'); $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); if ($found) { $found->delete(); @@ -191,12 +184,9 @@ public function manual(Request $request) } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Gitea.php b/app/Http/Controllers/Webhook/Gitea.php index e042b74c9a..cc53f20344 100644 --- a/app/Http/Controllers/Webhook/Gitea.php +++ b/app/Http/Controllers/Webhook/Gitea.php @@ -19,15 +19,12 @@ public function manual(Request $request) $return_payloads = collect([]); $x_gitea_delivery = request()->header('X-Gitea-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $gitea_delivery_found = collect($files)->filter(function ($file) use ($x_gitea_delivery) { return Str::contains($file, $x_gitea_delivery); })->first(); if ($gitea_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -67,8 +64,6 @@ public function manual(Request $request) $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray($changed_files); - ray('Manual Webhook Gitea Push Event with branch: '.$branch); } if ($x_gitea_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -77,7 +72,6 @@ public function manual(Request $request) $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook Gitea Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -99,7 +93,6 @@ public function manual(Request $request) $webhook_secret = data_get($application, 'manual_webhook_secret_gitea'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', @@ -122,7 +115,6 @@ public function manual(Request $request) if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -182,7 +174,6 @@ public function manual(Request $request) 'pull_request_html_url' => $pull_request_html_url, ]); } - } queue_application_deployment( application: $application, @@ -228,12 +219,9 @@ public function manual(Request $request) } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Github.php b/app/Http/Controllers/Webhook/Github.php index 5f3ba933bd..3683adaa85 100644 --- a/app/Http/Controllers/Webhook/Github.php +++ b/app/Http/Controllers/Webhook/Github.php @@ -25,15 +25,12 @@ public function manual(Request $request) $return_payloads = collect([]); $x_github_delivery = request()->header('X-GitHub-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { return Str::contains($file, $x_github_delivery); })->first(); if ($github_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -73,7 +70,6 @@ public function manual(Request $request) $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitHub Push Event with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -82,7 +78,6 @@ public function manual(Request $request) $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $branch) { return response('Nothing to do. No branch found in the request.'); @@ -104,7 +99,6 @@ public function manual(Request $request) $webhook_secret = data_get($application, 'manual_webhook_secret_github'); $hmac = hash_hmac('sha256', $request->getContent(), $webhook_secret); if (! hash_equals($x_hub_signature_256, $hmac) && ! isDev()) { - ray('Invalid signature'); $return_payloads->push([ 'application' => $application->name, 'status' => 'failed', @@ -127,7 +121,6 @@ public function manual(Request $request) if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -232,12 +225,9 @@ public function manual(Request $request) } } } - ray($return_payloads); return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } @@ -249,15 +239,12 @@ public function normal(Request $request) $id = null; $x_github_delivery = $request->header('X-GitHub-Delivery'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $files = Storage::disk('webhooks-during-maintenance')->files(); $github_delivery_found = collect($files)->filter(function ($file) use ($x_github_delivery) { return Str::contains($file, $x_github_delivery); })->first(); if ($github_delivery_found) { - ray('Webhook already found'); - return; } $data = [ @@ -313,7 +300,6 @@ public function normal(Request $request) $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Webhook GitHub Push Event: '.$id.' with branch: '.$branch); } if ($x_github_event === 'pull_request') { $action = data_get($payload, 'action'); @@ -322,7 +308,6 @@ public function normal(Request $request) $pull_request_html_url = data_get($payload, 'pull_request.html_url'); $branch = data_get($payload, 'pull_request.head.ref'); $base_branch = data_get($payload, 'pull_request.base.ref'); - ray('Webhook GitHub Pull Request Event: '.$id.' with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } if (! $id || ! $branch) { return response('Nothing to do. No id or branch found.'); @@ -356,7 +341,6 @@ public function normal(Request $request) if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -460,8 +444,6 @@ public function normal(Request $request) return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } @@ -505,7 +487,6 @@ public function install(Request $request) try { $installation_id = $request->get('installation_id'); if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), diff --git a/app/Http/Controllers/Webhook/Gitlab.php b/app/Http/Controllers/Webhook/Gitlab.php index ec7f51a0d7..f56711bad5 100644 --- a/app/Http/Controllers/Webhook/Gitlab.php +++ b/app/Http/Controllers/Webhook/Gitlab.php @@ -17,7 +17,6 @@ public function manual(Request $request) { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -67,7 +66,6 @@ public function manual(Request $request) $removed_files = data_get($payload, 'commits.*.removed'); $modified_files = data_get($payload, 'commits.*.modified'); $changed_files = collect($added_files)->concat($removed_files)->concat($modified_files)->unique()->flatten(); - ray('Manual Webhook GitLab Push Event with branch: '.$branch); } if ($x_gitlab_event === 'merge_request') { $action = data_get($payload, 'object_attributes.action'); @@ -84,7 +82,6 @@ public function manual(Request $request) return response($return_payloads); } - ray('Webhook GitHub Pull Request Event with branch: '.$branch.' and base branch: '.$base_branch.' and pull request id: '.$pull_request_id); } $applications = Application::where('git_repository', 'like', "%$full_name%"); if ($x_gitlab_event === 'push') { @@ -117,7 +114,6 @@ public function manual(Request $request) 'status' => 'failed', 'message' => 'Invalid signature.', ]); - ray('Invalid signature'); continue; } @@ -128,7 +124,6 @@ public function manual(Request $request) 'status' => 'failed', 'message' => 'Server is not functional', ]); - ray('Server is not functional: '.$application->destination->server->name); continue; } @@ -136,7 +131,6 @@ public function manual(Request $request) if ($application->isDeployable()) { $is_watch_path_triggered = $application->isWatchPathsTriggered($changed_files); if ($is_watch_path_triggered || is_null($application->watch_paths)) { - ray('Deploying '.$application->name.' with branch '.$branch); $deployment_uuid = new Cuid2; queue_application_deployment( application: $application, @@ -171,7 +165,6 @@ public function manual(Request $request) 'application_uuid' => $application->uuid, 'application_name' => $application->name, ]); - ray('Deployments disabled for '.$application->name); } } if ($x_gitlab_event === 'merge_request') { @@ -207,7 +200,6 @@ public function manual(Request $request) is_webhook: true, git_type: 'gitlab' ); - ray('Deploying preview for '.$application->name.' with branch '.$branch.' and base branch '.$base_branch.' and pull request id '.$pull_request_id); $return_payloads->push([ 'application' => $application->name, 'status' => 'success', @@ -219,7 +211,6 @@ public function manual(Request $request) 'status' => 'failed', 'message' => 'Preview deployments disabled', ]); - ray('Preview deployments disabled for '.$application->name); } } elseif ($action === 'closed' || $action === 'close' || $action === 'merge') { $found = ApplicationPreview::where('application_id', $application->id)->where('pull_request_id', $pull_request_id)->first(); @@ -253,8 +244,6 @@ public function manual(Request $request) return response($return_payloads); } catch (Exception $e) { - ray($e->getMessage()); - return handleError($e); } } diff --git a/app/Http/Controllers/Webhook/Stripe.php b/app/Http/Controllers/Webhook/Stripe.php index 1643225867..5d297b2428 100644 --- a/app/Http/Controllers/Webhook/Stripe.php +++ b/app/Http/Controllers/Webhook/Stripe.php @@ -13,7 +13,6 @@ use Exception; use Illuminate\Http\Request; use Illuminate\Support\Facades\Storage; -use Illuminate\Support\Sleep; use Illuminate\Support\Str; class Stripe extends Controller @@ -22,7 +21,6 @@ public function events(Request $request) { try { if (app()->isDownForMaintenance()) { - ray('Maintenance mode is on'); $epoch = now()->valueOf(); $data = [ 'attributes' => $request->attributes->all(), @@ -65,22 +63,18 @@ public function events(Request $request) $piData = $stripe->paymentIntents->retrieve($pi, []); $customerId = data_get($piData, 'customer'); $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - } - if (! $subscription) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - } if ($subscription) { $subscriptionId = data_get($subscription, 'stripe_subscription_id'); $stripe->subscriptions->cancel($subscriptionId, []); $subscription->update([ 'stripe_invoice_paid' => false, ]); + send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}"); + } else { + send_internal_notification("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}"); + + return response("Early fraud warning: subscription not found. Charge: {$charge}, id: {$id}, pi: {$pi}", 400); } - send_internal_notification("Early fraud warning created Refunded, subscription canceled. Charge: {$charge}, id: {$id}, pi: {$pi}"); break; case 'checkout.session.completed': $clientReferenceId = data_get($data, 'client_reference_id'); @@ -96,7 +90,8 @@ public function events(Request $request) $found = $team->members->where('id', $userId)->first(); if (! $found->isAdmin()) { send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); - throw new Exception("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}."); + + return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}, subscriptionid: {$subscriptionId}.", 400); } $subscription = Subscription::where('team_id', $teamId)->first(); if ($subscription) { @@ -124,13 +119,13 @@ public function events(Request $request) break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); + if ($subscription) { + $subscription->update([ + 'stripe_invoice_paid' => true, + ]); + } else { + return response("No subscription found for customer: {$customerId}", 400); } - $subscription->update([ - 'stripe_invoice_paid' => true, - ]); break; case 'invoice.payment_failed': $customerId = data_get($data, 'customer'); @@ -168,7 +163,42 @@ public function events(Request $request) } send_internal_notification('Subscription payment failed for customer: '.$customerId); break; + case 'customer.subscription.created': + $customerId = data_get($data, 'customer'); + $subscriptionId = data_get($data, 'id'); + $teamId = data_get($data, 'metadata.team_id'); + $userId = data_get($data, 'metadata.user_id'); + if (! $teamId || ! $userId) { + $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); + if ($subscription) { + return response("Subscription already exists for customer: {$customerId}", 200); + } + + return response('No team id or user id found', 400); + } + $team = Team::find($teamId); + $found = $team->members->where('id', $userId)->first(); + if (! $found->isAdmin()) { + send_internal_notification("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}."); + + return response("User {$userId} is not an admin or owner of team {$team->id}, customerid: {$customerId}.", 400); + } + $subscription = Subscription::where('team_id', $teamId)->first(); + if ($subscription) { + return response("Subscription already exists for team: {$teamId}", 200); + } else { + Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => false, + ]); + + return response('Subscription created'); + } case 'customer.subscription.updated': + $teamId = data_get($data, 'metadata.team_id'); + $userId = data_get($data, 'metadata.user_id'); $customerId = data_get($data, 'customer'); $status = data_get($data, 'status'); $subscriptionId = data_get($data, 'items.data.0.subscription'); @@ -178,32 +208,27 @@ public function events(Request $request) break; } $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - if (! $subscription) { - Sleep::for(5)->seconds(); - $subscription = Subscription::where('stripe_customer_id', $customerId)->first(); - } if (! $subscription) { if ($status === 'incomplete_expired') { - // send_internal_notification('Subscription incomplete expired for customer: '.$customerId); - return response('Subscription incomplete expired', 200); } - // send_internal_notification('No subscription found for: '.$customerId); - - return response('No subscription found', 400); + if ($teamId) { + $subscription = Subscription::create([ + 'team_id' => $teamId, + 'stripe_subscription_id' => $subscriptionId, + 'stripe_customer_id' => $customerId, + 'stripe_invoice_paid' => false, + ]); + } else { + return response('No subscription and team id found', 400); + } } - $trialEndedAlready = data_get($subscription, 'stripe_trial_already_ended'); $cancelAtPeriodEnd = data_get($data, 'cancel_at_period_end'); - $alreadyCancelAtPeriodEnd = data_get($subscription, 'stripe_cancel_at_period_end'); $feedback = data_get($data, 'cancellation_details.feedback'); $comment = data_get($data, 'cancellation_details.comment'); $lookup_key = data_get($data, 'items.data.0.price.lookup_key'); - if (str($lookup_key)->contains('ultimate') || str($lookup_key)->contains('dynamic')) { - if (str($lookup_key)->contains('dynamic')) { - $quantity = data_get($data, 'items.data.0.quantity', 2); - } else { - $quantity = data_get($data, 'items.data.0.quantity', 10); - } + if (str($lookup_key)->contains('dynamic')) { + $quantity = data_get($data, 'items.data.0.quantity', 2); $team = data_get($subscription, 'team'); if ($team) { $team->update([ @@ -222,28 +247,12 @@ public function events(Request $request) $subscription->update([ 'stripe_invoice_paid' => false, ]); - // send_internal_notification('Subscription paused or incomplete for customer: '.$customerId); } - - // Trial ended but subscribed, reactive servers - if ($trialEndedAlready && $status === 'active') { - $team = data_get($subscription, 'team'); - $team->trialEndedButSubscribed(); - } - if ($feedback) { $reason = "Cancellation feedback for {$customerId}: '".$feedback."'"; if ($comment) { $reason .= ' with comment: \''.$comment."'"; } - // send_internal_notification($reason); - } - if ($alreadyCancelAtPeriodEnd !== $cancelAtPeriodEnd) { - if ($cancelAtPeriodEnd) { - // send_internal_notification('Subscription cancelled at period end for team: ' . $subscription->team->id); - } else { - // send_internal_notification('customer.subscription.updated for customer: '.$customerId); - } } break; case 'customer.subscription.deleted': @@ -269,7 +278,7 @@ public function events(Request $request) $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $team = data_get($subscription, 'team'); if (! $team) { - throw new Exception('No team found for subscription: '.$subscription->id); + return response('No team found for subscription: '.$subscription->id, 400); } SubscriptionTrialEndsSoonJob::dispatch($team); break; @@ -278,7 +287,7 @@ public function events(Request $request) $subscription = Subscription::where('stripe_customer_id', $customerId)->firstOrFail(); $team = data_get($subscription, 'team'); if (! $team) { - throw new Exception('No team found for subscription: '.$subscription->id); + return response('No team found for subscription: '.$subscription->id, 400); } $team->trialEnded(); $subscription->update([ diff --git a/app/Http/Controllers/Webhook/Waitlist.php b/app/Http/Controllers/Webhook/Waitlist.php index ea635836c9..dec8ca72dd 100644 --- a/app/Http/Controllers/Webhook/Waitlist.php +++ b/app/Http/Controllers/Webhook/Waitlist.php @@ -13,7 +13,6 @@ public function confirm(Request $request) { $email = request()->get('email'); $confirmation_code = request()->get('confirmation_code'); - ray($email, $confirmation_code); try { $found = ModelsWaitlist::where('uuid', $confirmation_code)->where('email', $email)->first(); if ($found) { @@ -36,7 +35,6 @@ public function confirm(Request $request) return redirect()->route('dashboard'); } catch (Exception $e) { send_internal_notification('Waitlist confirmation failed: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('dashboard'); } @@ -58,7 +56,6 @@ public function cancel(Request $request) return redirect()->route('dashboard'); } catch (Exception $e) { send_internal_notification('Waitlist cancellation failed: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('dashboard'); } diff --git a/app/Http/Middleware/ApiAllowed.php b/app/Http/Middleware/ApiAllowed.php index 471e6d602e..dc6be5da3d 100644 --- a/app/Http/Middleware/ApiAllowed.php +++ b/app/Http/Middleware/ApiAllowed.php @@ -10,7 +10,6 @@ class ApiAllowed { public function handle(Request $request, Closure $next): Response { - ray()->clearAll(); if (isCloud()) { return $next($request); } diff --git a/app/Jobs/ApplicationDeploymentJob.php b/app/Jobs/ApplicationDeploymentJob.php index 9ae383a9fa..5ceed332a9 100644 --- a/app/Jobs/ApplicationDeploymentJob.php +++ b/app/Jobs/ApplicationDeploymentJob.php @@ -208,7 +208,6 @@ public function __construct(int $application_deployment_queue_id) $this->container_name = "{$this->application->settings->custom_internal_name}-pr-{$this->pull_request_id}"; } } - ray('New container name: ', $this->container_name)->green(); $this->saved_outputs = collect(); @@ -231,7 +230,7 @@ public function handle(): void $this->application_deployment_queue->update([ 'status' => ApplicationDeploymentStatus::IN_PROGRESS->value, ]); - if (! $this->server->isFunctional()) { + if ($this->server->isFunctional() === false) { $this->application_deployment_queue->addLogEntry('Server is not functional.'); $this->fail('Server is not functional.'); @@ -298,7 +297,6 @@ public function handle(): void if ($this->pull_request_id !== 0 && $this->application->is_github_based()) { ApplicationPullRequestUpdateJob::dispatch(application: $this->application, preview: $this->preview, deployment_uuid: $this->deployment_uuid, status: ProcessStatus::ERROR); } - ray($e); $this->fail($e); throw $e; } finally { @@ -389,7 +387,6 @@ private function deploy_dockerimage_buildpack() } else { $this->dockerImageTag = $this->application->docker_registry_image_tag; } - ray("echo 'Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}.'"); $this->application_deployment_queue->addLogEntry("Starting deployment of {$this->dockerImage}:{$this->dockerImageTag} to {$this->server->name}."); $this->generate_image_names(); $this->prepare_builder_image(); @@ -712,38 +709,26 @@ private function push_to_docker_registry() { $forceFail = true; if (str($this->application->docker_registry_image_name)->isEmpty()) { - ray('empty docker_registry_image_name'); - return; } if ($this->restart_only) { - ray('restart_only'); - return; } if ($this->application->build_pack === 'dockerimage') { - ray('dockerimage'); - return; } if ($this->use_build_server) { - ray('use_build_server'); $forceFail = true; } if ($this->server->isSwarm() && $this->build_pack !== 'dockerimage') { - ray('isSwarm'); $forceFail = true; } if ($this->application->additional_servers->count() > 0) { - ray('additional_servers'); $forceFail = true; } if ($this->is_this_additional_server) { - ray('this is an additional_servers, no pushy pushy'); - return; } - ray('push_to_docker_registry noww: '.$this->production_image_name); try { instant_remote_process(["docker images --format '{{json .}}' {$this->production_image_name}"], $this->server); $this->application_deployment_queue->addLogEntry('----------------------------------------'); @@ -775,7 +760,6 @@ private function push_to_docker_registry() if ($forceFail) { throw new RuntimeException($e->getMessage(), 69420); } - ray($e); } } @@ -1386,8 +1370,6 @@ private function deploy_to_additional_destinations() return; } if ($destination_ids->contains($this->destination->id)) { - ray('Same destination found in additional destinations. Skipping.'); - return; } foreach ($destination_ids as $destination_id) { @@ -1854,7 +1836,7 @@ private function generate_compose_file() } if ($this->pull_request_id === 0) { - $custom_compose = convert_docker_run_to_compose($this->application->custom_docker_run_options); + $custom_compose = convertDockerRunToCompose($this->application->custom_docker_run_options); if ((bool) $this->application->settings->is_consistent_container_name_enabled) { if (! $this->application->settings->custom_internal_name) { $docker_compose['services'][$this->application->uuid] = $docker_compose['services'][$this->container_name]; @@ -2449,7 +2431,6 @@ public function failed(Throwable $exception): void if ($this->application->build_pack !== 'dockercompose') { $code = $exception->getCode(); - ray($code); if ($code !== 69420) { // 69420 means failed to push the image to the registry, so we don't need to remove the new version as it is the currently running one if ($this->application->settings->is_consistent_container_name_enabled || str($this->application->settings->custom_internal_name)->isNotEmpty()) { diff --git a/app/Jobs/ApplicationPullRequestUpdateJob.php b/app/Jobs/ApplicationPullRequestUpdateJob.php index 6120d1cba8..2eefc4dd23 100755 --- a/app/Jobs/ApplicationPullRequestUpdateJob.php +++ b/app/Jobs/ApplicationPullRequestUpdateJob.php @@ -31,8 +31,6 @@ public function handle() { try { if ($this->application->is_public_repository()) { - ray('Public repository. Skipping comment update.'); - return; } if ($this->status === ProcessStatus::CLOSED) { @@ -53,16 +51,12 @@ public function handle() $this->body .= '[Open Build Logs]('.$this->build_logs_url.")\n\n\n"; $this->body .= 'Last updated at: '.now()->toDateTimeString().' CET'; - - ray('Updating comment', $this->body); if ($this->preview->pull_request_issue_comment_id) { $this->update_comment(); } else { $this->create_comment(); } } catch (\Throwable $e) { - ray($e); - return $e; } } @@ -73,7 +67,6 @@ private function update_comment() 'body' => $this->body, ], throwError: false); if (data_get($data, 'message') === 'Not Found') { - ray('Comment not found. Creating new one.'); $this->create_comment(); } } diff --git a/app/Jobs/CheckAndStartSentinelJob.php b/app/Jobs/CheckAndStartSentinelJob.php new file mode 100644 index 0000000000..788db89eaa --- /dev/null +++ b/app/Jobs/CheckAndStartSentinelJob.php @@ -0,0 +1,52 @@ +server, false); + $sentinelFoundJson = json_decode($sentinelFound, true); + $sentinelStatus = data_get($sentinelFoundJson, '0.State.Status', 'exited'); + if ($sentinelStatus !== 'running') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + // If sentinel is running, check if it needs an update + $runningVersion = instant_remote_process(['docker exec coolify-sentinel sh -c "curl http://127.0.0.1:8888/api/version"'], $this->server, false); + if (empty($runningVersion)) { + $runningVersion = '0.0.0'; + } + if ($latestVersion === '0.0.0' && $runningVersion === '0.0.0') { + StartSentinel::run(server: $this->server, restart: true, latestVersion: 'latest'); + + return; + } else { + if (version_compare($runningVersion, $latestVersion, '<')) { + StartSentinel::run(server: $this->server, restart: true, latestVersion: $latestVersion); + + return; + } + } + } +} diff --git a/app/Jobs/CheckHelperImageJob.php b/app/Jobs/CheckHelperImageJob.php new file mode 100644 index 0000000000..6abb8a1502 --- /dev/null +++ b/app/Jobs/CheckHelperImageJob.php @@ -0,0 +1,39 @@ +get('https://cdn.coollabs.io/coolify/versions.json'); + if ($response->successful()) { + $versions = $response->json(); + $settings = instanceSettings(); + $latest_version = data_get($versions, 'coolify.helper.version'); + $current_version = $settings->helper_version; + if (version_compare($latest_version, $current_version, '>')) { + $settings->update(['helper_version' => $latest_version]); + } + } + } catch (\Throwable $e) { + send_internal_notification('CheckHelperImageJob failed with: '.$e->getMessage()); + throw $e; + } + } +} diff --git a/app/Jobs/CheckResaleLicenseJob.php b/app/Jobs/CheckResaleLicenseJob.php index b55ae99676..7479867b63 100644 --- a/app/Jobs/CheckResaleLicenseJob.php +++ b/app/Jobs/CheckResaleLicenseJob.php @@ -22,7 +22,6 @@ public function handle(): void CheckResaleLicense::run(); } catch (\Throwable $e) { send_internal_notification('CheckResaleLicenseJob failed with: '.$e->getMessage()); - ray($e); throw $e; } } diff --git a/app/Jobs/CleanupHelperContainersJob.php b/app/Jobs/CleanupHelperContainersJob.php index b8ca8b7ed8..f185ab7812 100644 --- a/app/Jobs/CleanupHelperContainersJob.php +++ b/app/Jobs/CleanupHelperContainersJob.php @@ -20,18 +20,15 @@ public function __construct(public Server $server) {} public function handle(): void { try { - ray('Cleaning up helper containers on '.$this->server->name); $containers = instant_remote_process(['docker container ps --format \'{{json .}}\' | jq -s \'map(select(.Image | contains("ghcr.io/coollabsio/coolify-helper")))\''], $this->server, false); $containerIds = collect(json_decode($containers))->pluck('ID'); if ($containerIds->count() > 0) { foreach ($containerIds as $containerId) { - ray('Removing container '.$containerId); instant_remote_process(['docker container rm -f '.$containerId], $this->server, false); } } } catch (\Throwable $e) { send_internal_notification('CleanupHelperContainersJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); } } } diff --git a/app/Jobs/CleanupInstanceStuffsJob.php b/app/Jobs/CleanupInstanceStuffsJob.php index d9de3f6fe7..84f14ed02a 100644 --- a/app/Jobs/CleanupInstanceStuffsJob.php +++ b/app/Jobs/CleanupInstanceStuffsJob.php @@ -3,14 +3,15 @@ namespace App\Jobs; use App\Models\TeamInvitation; -use App\Models\Waitlist; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldBeUnique; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\Log; class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, ShouldQueue { @@ -18,36 +19,21 @@ class CleanupInstanceStuffsJob implements ShouldBeEncrypted, ShouldBeUnique, Sho public function __construct() {} - // public function uniqueId(): string - // { - // return $this->container_name; - // } + public function middleware(): array + { + return [(new WithoutOverlapping('cleanup-instance-stuffs'))->dontRelease()]; + } public function handle(): void { try { - // $this->cleanup_waitlist(); - } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); - } - try { - $this->cleanup_invitation_link(); + $this->cleanupInvitationLink(); } catch (\Throwable $e) { - send_internal_notification('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); - ray($e->getMessage()); - } - } - - private function cleanup_waitlist() - { - $waitlist = Waitlist::whereVerified(false)->where('created_at', '<', now()->subMinutes(config('constants.waitlist.expiration')))->get(); - foreach ($waitlist as $item) { - $item->delete(); + Log::error('CleanupInstanceStuffsJob failed with error: '.$e->getMessage()); } } - private function cleanup_invitation_link() + private function cleanupInvitationLink() { $invitation = TeamInvitation::all(); foreach ($invitation as $item) { diff --git a/app/Jobs/DatabaseBackupJob.php b/app/Jobs/DatabaseBackupJob.php index 769739d5e7..fcfe2fe3d0 100644 --- a/app/Jobs/DatabaseBackupJob.php +++ b/app/Jobs/DatabaseBackupJob.php @@ -72,7 +72,7 @@ public function handle(): void return; } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $this->database = data_get($this->backup, 'database'); $this->server = $this->database->service->server; $this->s3 = $this->backup->s3; @@ -92,11 +92,9 @@ public function handle(): void $status = str(data_get($this->database, 'status')); if (! $status->startsWith('running') && $this->database->id !== 0) { - ray('database not running'); - return; } - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $databaseType = $this->database->databaseType(); $serviceUuid = $this->database->service->uuid; $serviceName = str($this->database->service->name)->slug(); @@ -131,7 +129,6 @@ public function handle(): void if ($this->postgres_password) { $this->postgres_password = str($this->postgres_password)->after('POSTGRES_PASSWORD=')->value(); } - } elseif (str($databaseType)->contains('mysql')) { $this->container_name = "{$this->database->name}-$serviceUuid"; $this->directory_name = $serviceName.'-'.$this->container_name; @@ -222,7 +219,6 @@ public function handle(): void // Format: db1:collection1,collection2|db2:collection3,collection4 $databasesToBackup = explode('|', $databasesToBackup); $databasesToBackup = array_map('trim', $databasesToBackup); - ray($databasesToBackup); } elseif (str($databaseType)->contains('mysql')) { // Format: db1,db2,db3 $databasesToBackup = explode(',', $databasesToBackup); @@ -244,7 +240,6 @@ public function handle(): void } foreach ($databasesToBackup as $database) { $size = 0; - ray('Backing up '.$database); try { if (str($databaseType)->contains('postgres')) { $this->backup_file = "/pg-dump-$database-".Carbon::now()->timestamp.'.dmp'; @@ -377,10 +372,8 @@ private function backup_standalone_mongodb(string $databaseWithCollections): voi if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -400,16 +393,13 @@ private function backup_standalone_postgresql(string $database): void } $commands[] = $backupCommand; - ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -428,10 +418,8 @@ private function backup_standalone_mysql(string $database): void if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -445,16 +433,13 @@ private function backup_standalone_mariadb(string $database): void } else { $commands[] = "docker exec $this->container_name mariadb-dump -u root -p{$this->database->mariadb_root_password} $database > $this->backup_location"; } - ray($commands); $this->backup_output = instant_remote_process($commands, $this->server); $this->backup_output = trim($this->backup_output); if ($this->backup_output === '') { $this->backup_output = null; } - ray('Backup done for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location); } catch (\Throwable $e) { $this->add_to_backup_output($e->getMessage()); - ray('Backup failed for '.$this->container_name.' at '.$this->server->name.':'.$this->backup_location.'\n\nError:'.$e->getMessage()); throw $e; } } @@ -498,14 +483,12 @@ private function upload_to_s3(): void $bucket = $this->s3->bucket; $endpoint = $this->s3->endpoint; $this->s3->testConnection(shouldSave: true); - if (data_get($this->backup, 'database_type') === 'App\Models\ServiceDatabase') { + if (data_get($this->backup, 'database_type') === \App\Models\ServiceDatabase::class) { $network = $this->database->service->destination->network; } else { $network = $this->database->destination->network; } - $this->ensureHelperImageAvailable(); - $fullImageName = $this->getFullImageName(); if (isDev()) { @@ -538,35 +521,6 @@ private function upload_to_s3(): void } } - private function ensureHelperImageAvailable(): void - { - $fullImageName = $this->getFullImageName(); - - $imageExists = $this->checkImageExists($fullImageName); - - if (! $imageExists) { - $this->pullHelperImage($fullImageName); - } - } - - private function checkImageExists(string $fullImageName): bool - { - $result = instant_remote_process(["docker image inspect {$fullImageName} >/dev/null 2>&1 && echo 'exists' || echo 'not exists'"], $this->server, false); - - return trim($result) === 'exists'; - } - - private function pullHelperImage(string $fullImageName): void - { - try { - instant_remote_process(["docker pull {$fullImageName}"], $this->server); - } catch (\Exception $e) { - $errorMessage = 'Failed to pull helper image: '.$e->getMessage(); - $this->add_to_backup_output($errorMessage); - throw new \RuntimeException($errorMessage); - } - } - private function getFullImageName(): string { $settings = instanceSettings(); diff --git a/app/Jobs/DockerCleanupJob.php b/app/Jobs/DockerCleanupJob.php index 900bae99c3..0d7e63dd2d 100644 --- a/app/Jobs/DockerCleanupJob.php +++ b/app/Jobs/DockerCleanupJob.php @@ -10,6 +10,7 @@ use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Facades\Log; @@ -23,6 +24,11 @@ class DockerCleanupJob implements ShouldBeEncrypted, ShouldQueue public ?string $usageBefore = null; + public function middleware(): array + { + return [(new WithoutOverlapping($this->server->id))->dontRelease()]; + } + public function __construct(public Server $server, public bool $manualCleanup = false) {} public function handle(): void diff --git a/app/Jobs/GithubAppPermissionJob.php b/app/Jobs/GithubAppPermissionJob.php index 9c0a2b55b0..d483fe4c2d 100644 --- a/app/Jobs/GithubAppPermissionJob.php +++ b/app/Jobs/GithubAppPermissionJob.php @@ -42,7 +42,6 @@ public function handle() $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); } catch (\Throwable $e) { send_internal_notification('GithubAppPermissionJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/PullHelperImageJob.php b/app/Jobs/PullHelperImageJob.php index 4b208fc315..a92e44c6b9 100644 --- a/app/Jobs/PullHelperImageJob.php +++ b/app/Jobs/PullHelperImageJob.php @@ -9,7 +9,6 @@ use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Facades\Http; class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue { @@ -17,29 +16,12 @@ class PullHelperImageJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 1000; - public function __construct() {} + public function __construct(public Server $server) {} public function handle(): void { - try { - $response = Http::retry(3, 1000)->get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - $settings = instanceSettings(); - $latest_version = data_get($versions, 'coolify.helper.version'); - $current_version = $settings->helper_version; - if (version_compare($latest_version, $current_version, '>')) { - // New version available - // $helperImage = config('coolify.helper_image'); - // instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server); - $settings->update(['helper_version' => $latest_version]); - } - } - - } catch (\Throwable $e) { - send_internal_notification('PullHelperImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } + $helperImage = config('coolify.helper_image'); + $latest_version = instanceSettings()->helper_version; + instant_remote_process(["docker pull -q {$helperImage}:{$latest_version}"], $this->server, false); } } diff --git a/app/Jobs/PullSentinelImageJob.php b/app/Jobs/PullSentinelImageJob.php deleted file mode 100644 index 32f84e6d50..0000000000 --- a/app/Jobs/PullSentinelImageJob.php +++ /dev/null @@ -1,47 +0,0 @@ -server, false); - if (empty($local_version)) { - $local_version = '0.0.0'; - } - if (version_compare($local_version, $version, '<')) { - StartSentinel::run($this->server, $version, true); - - return; - } - ray('Sentinel image is up to date'); - } catch (\Throwable $e) { - // send_internal_notification('PullSentinelImageJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - throw $e; - } - } -} diff --git a/app/Jobs/PullTemplatesFromCDN.php b/app/Jobs/PullTemplatesFromCDN.php index 72c9710339..bde5e6c7ae 100644 --- a/app/Jobs/PullTemplatesFromCDN.php +++ b/app/Jobs/PullTemplatesFromCDN.php @@ -25,7 +25,6 @@ public function handle(): void if (isDev() || isCloud()) { return; } - ray('PullTemplatesAndVersions service-templates'); $response = Http::retry(3, 1000)->get(config('constants.services.official')); if ($response->successful()) { $services = $response->json(); @@ -35,7 +34,6 @@ public function handle(): void } } catch (\Throwable $e) { send_internal_notification('PullTemplatesAndVersions failed with: '.$e->getMessage()); - ray($e->getMessage()); } } } diff --git a/app/Jobs/PullVersionsFromCDN.php b/app/Jobs/PullVersionsFromCDN.php deleted file mode 100644 index 79ebad7a81..0000000000 --- a/app/Jobs/PullVersionsFromCDN.php +++ /dev/null @@ -1,39 +0,0 @@ -get('https://cdn.coollabs.io/coolify/versions.json'); - if ($response->successful()) { - $versions = $response->json(); - File::put(base_path('versions.json'), json_encode($versions, JSON_PRETTY_PRINT)); - } else { - send_internal_notification('PullTemplatesAndVersions failed with: '.$response->status().' '.$response->body()); - } - } - } catch (\Throwable $e) { - throw $e; - } - } -} diff --git a/app/Jobs/PushServerUpdateJob.php b/app/Jobs/PushServerUpdateJob.php new file mode 100644 index 0000000000..9822ca0719 --- /dev/null +++ b/app/Jobs/PushServerUpdateJob.php @@ -0,0 +1,366 @@ +containers = collect(); + $this->foundApplicationIds = collect(); + $this->foundDatabaseUuids = collect(); + $this->foundServiceApplicationIds = collect(); + $this->foundApplicationPreviewsIds = collect(); + $this->foundServiceDatabaseIds = collect(); + $this->allApplicationIds = collect(); + $this->allDatabaseUuids = collect(); + $this->allTcpProxyUuids = collect(); + $this->allServiceApplicationIds = collect(); + $this->allServiceDatabaseIds = collect(); + } + + public function handle() + { + // TODO: Swarm is not supported yet + if (! $this->data) { + throw new \Exception('No data provided'); + } + $data = collect($this->data); + + $this->server->sentinelHeartbeat(); + + $this->containers = collect(data_get($data, 'containers')); + + $filesystemUsageRoot = data_get($data, 'filesystem_usage_root.used_percentage'); + ServerStorageCheckJob::dispatch($this->server, $filesystemUsageRoot); + + if ($this->containers->isEmpty()) { + return; + } + $this->applications = $this->server->applications(); + $this->databases = $this->server->databases(); + $this->previews = $this->server->previews(); + $this->services = $this->server->services()->get(); + $this->allApplicationIds = $this->applications->filter(function ($application) { + return $application->additional_servers->count() === 0; + })->pluck('id'); + $this->allApplicationsWithAdditionalServers = $this->applications->filter(function ($application) { + return $application->additional_servers->count() > 0; + }); + $this->allApplicationPreviewsIds = $this->previews->pluck('id'); + $this->allDatabaseUuids = $this->databases->pluck('uuid'); + $this->allTcpProxyUuids = $this->databases->where('is_public', true)->pluck('uuid'); + $this->services->each(function ($service) { + $service->applications()->pluck('id')->each(function ($applicationId) { + $this->allServiceApplicationIds->push($applicationId); + }); + $service->databases()->pluck('id')->each(function ($databaseId) { + $this->allServiceDatabaseIds->push($databaseId); + }); + }); + + foreach ($this->containers as $container) { + $containerStatus = data_get($container, 'state', 'exited'); + $containerHealth = data_get($container, 'health_status', 'unhealthy'); + $containerStatus = "$containerStatus ($containerHealth)"; + $labels = collect(data_get($container, 'labels')); + $coolify_managed = $labels->has('coolify.managed'); + if ($coolify_managed) { + $name = data_get($container, 'name'); + if ($name === 'coolify-log-drain' && $this->isRunning($containerStatus)) { + $this->foundLogDrainContainer = true; + } + if ($labels->has('coolify.applicationId')) { + $applicationId = $labels->get('coolify.applicationId'); + $pullRequestId = data_get($labels, 'coolify.pullRequestId', '0'); + try { + if ($pullRequestId === '0') { + if ($this->allApplicationIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationIds->push($applicationId); + } + $this->updateApplicationStatus($applicationId, $containerStatus); + } else { + if ($this->allApplicationPreviewsIds->contains($applicationId) && $this->isRunning($containerStatus)) { + $this->foundApplicationPreviewsIds->push($applicationId); + } + $this->updateApplicationPreviewStatus($applicationId, $containerStatus); + } + } catch (\Exception $e) { + } + } elseif ($labels->has('coolify.serviceId')) { + $serviceId = $labels->get('coolify.serviceId'); + $subType = $labels->get('coolify.service.subType'); + $subId = $labels->get('coolify.service.subId'); + if ($subType === 'application' && $this->isRunning($containerStatus)) { + $this->foundServiceApplicationIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } elseif ($subType === 'database' && $this->isRunning($containerStatus)) { + $this->foundServiceDatabaseIds->push($subId); + $this->updateServiceSubStatus($serviceId, $subType, $subId, $containerStatus); + } + } else { + $uuid = $labels->get('com.docker.compose.service'); + $type = $labels->get('coolify.type'); + if ($name === 'coolify-proxy' && $this->isRunning($containerStatus)) { + $this->foundProxy = true; + } elseif ($type === 'service' && $this->isRunning($containerStatus)) { + } else { + if ($this->allDatabaseUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->foundDatabaseUuids->push($uuid); + if ($this->allTcpProxyUuids->contains($uuid) && $this->isRunning($containerStatus)) { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: true); + } else { + $this->updateDatabaseStatus($uuid, $containerStatus, tcpProxy: false); + } + } + } + } + } + } + + $this->updateProxyStatus(); + + $this->updateNotFoundApplicationStatus(); + $this->updateNotFoundApplicationPreviewStatus(); + $this->updateNotFoundDatabaseStatus(); + $this->updateNotFoundServiceStatus(); + + $this->updateAdditionalServersStatus(); + + $this->checkLogDrainContainer(); + } + + private function updateApplicationStatus(string $applicationId, string $containerStatus) + { + $application = $this->applications->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + } + + private function updateApplicationPreviewStatus(string $applicationId, string $containerStatus) + { + $application = $this->previews->where('id', $applicationId)->first(); + if (! $application) { + return; + } + $application->status = $containerStatus; + $application->save(); + } + + private function updateNotFoundApplicationStatus() + { + $notFoundApplicationIds = $this->allApplicationIds->diff($this->foundApplicationIds); + if ($notFoundApplicationIds->isNotEmpty()) { + $notFoundApplicationIds->each(function ($applicationId) { + $application = Application::find($applicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + } + }); + } + } + + private function updateNotFoundApplicationPreviewStatus() + { + $notFoundApplicationPreviewsIds = $this->allApplicationPreviewsIds->diff($this->foundApplicationPreviewsIds); + if ($notFoundApplicationPreviewsIds->isNotEmpty()) { + $notFoundApplicationPreviewsIds->each(function ($applicationPreviewId) { + $applicationPreview = ApplicationPreview::find($applicationPreviewId); + if ($applicationPreview) { + $applicationPreview->status = 'exited'; + $applicationPreview->save(); + } + }); + } + } + + private function updateProxyStatus() + { + // If proxy is not found, start it + if ($this->server->isProxyShouldRun()) { + if ($this->foundProxy === false) { + try { + if (CheckProxy::run($this->server)) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + } + } + + private function updateDatabaseStatus(string $databaseUuid, string $containerStatus, bool $tcpProxy = false) + { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if (! $database) { + return; + } + $database->status = $containerStatus; + $database->save(); + if ($this->isRunning($containerStatus) && $tcpProxy) { + $tcpProxyContainerFound = $this->containers->filter(function ($value, $key) use ($databaseUuid) { + return data_get($value, 'name') === "$databaseUuid-proxy" && data_get($value, 'state') === 'running'; + })->first(); + if (! $tcpProxyContainerFound) { + StartDatabaseProxy::dispatch($database); + $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); + } else { + } + } + } + + private function updateNotFoundDatabaseStatus() + { + $notFoundDatabaseUuids = $this->allDatabaseUuids->diff($this->foundDatabaseUuids); + if ($notFoundDatabaseUuids->isNotEmpty()) { + $notFoundDatabaseUuids->each(function ($databaseUuid) { + $database = $this->databases->where('uuid', $databaseUuid)->first(); + if ($database) { + $database->status = 'exited'; + $database->save(); + if ($database->is_public) { + StopDatabaseProxy::dispatch($database); + } + } + }); + } + } + + private function updateServiceSubStatus(string $serviceId, string $subType, string $subId, string $containerStatus) + { + $service = $this->services->where('id', $serviceId)->first(); + if (! $service) { + return; + } + if ($subType === 'application') { + $application = $service->applications()->where('id', $subId)->first(); + $application->status = $containerStatus; + $application->save(); + } elseif ($subType === 'database') { + $database = $service->databases()->where('id', $subId)->first(); + $database->status = $containerStatus; + $database->save(); + } else { + } + } + + private function updateNotFoundServiceStatus() + { + $notFoundServiceApplicationIds = $this->allServiceApplicationIds->diff($this->foundServiceApplicationIds); + $notFoundServiceDatabaseIds = $this->allServiceDatabaseIds->diff($this->foundServiceDatabaseIds); + if ($notFoundServiceApplicationIds->isNotEmpty()) { + $notFoundServiceApplicationIds->each(function ($serviceApplicationId) { + $application = ServiceApplication::find($serviceApplicationId); + if ($application) { + $application->status = 'exited'; + $application->save(); + } + }); + } + if ($notFoundServiceDatabaseIds->isNotEmpty()) { + $notFoundServiceDatabaseIds->each(function ($serviceDatabaseId) { + $database = ServiceDatabase::find($serviceDatabaseId); + if ($database) { + $database->status = 'exited'; + $database->save(); + } + }); + } + } + + private function updateAdditionalServersStatus() + { + $this->allApplicationsWithAdditionalServers->each(function ($application) { + ComplexStatusCheck::run($application); + }); + } + + private function isRunning(string $containerStatus) + { + return str($containerStatus)->contains('running'); + } + + private function checkLogDrainContainer() + { + if ($this->server->isLogDrainEnabled() && $this->foundLogDrainContainer === false) { + StartLogDrain::dispatch($this->server)->onQueue('high'); + } + } +} diff --git a/app/Jobs/ScheduledTaskJob.php b/app/Jobs/ScheduledTaskJob.php index 6850ae98a9..7bfc29af30 100644 --- a/app/Jobs/ScheduledTaskJob.php +++ b/app/Jobs/ScheduledTaskJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Events\ScheduledTaskDone; use App\Models\Application; use App\Models\ScheduledTask; use App\Models\ScheduledTaskExecution; @@ -19,7 +20,7 @@ class ScheduledTaskJob implements ShouldQueue { use Dispatchable, InteractsWithQueue, Queueable, SerializesModels; - public ?Team $team = null; + public Team $team; public Server $server; @@ -47,20 +48,16 @@ public function __construct($task) } else { throw new \RuntimeException('ScheduledTaskJob failed: No resource found.'); } - $this->team = Team::find($task->team_id); + $this->team = Team::findOrFail($task->team_id); $this->server_timezone = $this->getServerTimezone(); } private function getServerTimezone(): string { if ($this->resource instanceof Application) { - $timezone = $this->resource->destination->server->settings->server_timezone; - - return $timezone; + return $this->resource->destination->server->settings->server_timezone; } elseif ($this->resource instanceof Service) { - $timezone = $this->resource->server->settings->server_timezone; - - return $timezone; + return $this->resource->server->settings->server_timezone; } return 'UTC'; @@ -68,7 +65,6 @@ private function getServerTimezone(): string public function handle(): void { - try { $this->task_log = ScheduledTaskExecution::create([ 'scheduled_task_id' => $this->task->id, @@ -76,14 +72,14 @@ public function handle(): void $this->server = $this->resource->destination->server; - if ($this->resource->type() == 'application') { + if ($this->resource->type() === 'application') { $containers = getCurrentApplicationContainerStatus($this->server, $this->resource->id, 0); if ($containers->count() > 0) { $containers->each(function ($container) { $this->containers[] = str_replace('/', '', $container['Names']); }); } - } elseif ($this->resource->type() == 'service') { + } elseif ($this->resource->type() === 'service') { $this->resource->applications()->get()->each(function ($application) { if (str(data_get($application, 'status'))->contains('running')) { $this->containers[] = data_get($application, 'name').'-'.data_get($this->resource, 'uuid'); @@ -130,6 +126,7 @@ public function handle(): void // send_internal_notification('ScheduledTaskJob failed with: ' . $e->getMessage()); throw $e; } finally { + ScheduledTaskDone::dispatch($this->team->id); } } } diff --git a/app/Jobs/SendConfirmationForWaitlistJob.php b/app/Jobs/SendConfirmationForWaitlistJob.php index 070598e712..7af8205fc3 100755 --- a/app/Jobs/SendConfirmationForWaitlistJob.php +++ b/app/Jobs/SendConfirmationForWaitlistJob.php @@ -31,7 +31,6 @@ public function handle() send_user_an_email($mail, $this->email); } catch (\Throwable $e) { send_internal_notification("SendConfirmationForWaitlistJob failed for {$this->email} with error: ".$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SendMessageToDiscordJob.php b/app/Jobs/SendMessageToDiscordJob.php index f38cf823c7..5b406f50f8 100644 --- a/app/Jobs/SendMessageToDiscordJob.php +++ b/app/Jobs/SendMessageToDiscordJob.php @@ -2,6 +2,7 @@ namespace App\Jobs; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; @@ -29,7 +30,7 @@ class SendMessageToDiscordJob implements ShouldBeEncrypted, ShouldQueue public int $maxExceptions = 5; public function __construct( - public string $text, + public DiscordMessage $message, public string $webhookUrl ) {} @@ -38,9 +39,6 @@ public function __construct( */ public function handle(): void { - $payload = [ - 'content' => $this->text, - ]; - Http::post($this->webhookUrl, $payload); + Http::post($this->webhookUrl, $this->message->toPayload()); } } diff --git a/app/Jobs/ServerCheckJob.php b/app/Jobs/ServerCheckJob.php index 39d4aa0c02..c584f493d6 100644 --- a/app/Jobs/ServerCheckJob.php +++ b/app/Jobs/ServerCheckJob.php @@ -2,22 +2,19 @@ namespace App\Jobs; -use App\Actions\Database\StartDatabaseProxy; use App\Actions\Docker\GetContainersStatus; use App\Actions\Proxy\CheckProxy; use App\Actions\Proxy\StartProxy; -use App\Actions\Server\InstallLogDrain; -use App\Models\ApplicationPreview; +use App\Actions\Server\StartLogDrain; use App\Models\Server; -use App\Models\ServiceDatabase; use App\Notifications\Container\ContainerRestarted; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; +use Illuminate\Queue\Middleware\WithoutOverlapping; use Illuminate\Queue\SerializesModels; -use Illuminate\Support\Arr; class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue { @@ -29,17 +26,9 @@ class ServerCheckJob implements ShouldBeEncrypted, ShouldQueue public $containers; - public $applications; - - public $databases; - - public $services; - - public $previews; - - public function backoff(): int + public function middleware(): array { - return isDev() ? 1 : 3; + return [(new WithoutOverlapping($this->server->id))->dontRelease()]; } public function __construct(public Server $server) {} @@ -47,72 +36,54 @@ public function __construct(public Server $server) {} public function handle() { try { - $this->applications = $this->server->applications(); - $this->databases = $this->server->databases(); - $this->services = $this->server->services()->get(); - $this->previews = $this->server->previews(); - - $up = $this->serverStatus(); - if (! $up) { - ray('Server is not reachable.'); - - return 'Server is not reachable.'; + if ($this->server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; } - if (! $this->server->isFunctional()) { - ray('Server is not ready.'); - return 'Server is not ready.'; - } if (! $this->server->isSwarmWorker() && ! $this->server->isBuildServer()) { ['containers' => $this->containers, 'containerReplicates' => $containerReplicates] = $this->server->getContainers(); if (is_null($this->containers)) { return 'No containers found.'; } GetContainersStatus::run($this->server, $this->containers, $containerReplicates); + + if ($this->server->isSentinelEnabled()) { + CheckAndStartSentinelJob::dispatch($this->server); + } + if ($this->server->isLogDrainEnabled()) { $this->checkLogDrainContainer(); } - } - - } catch (\Throwable $e) { - ray($e->getMessage()); - - return handleError($e); - } - - } - private function serverStatus() - { - ['uptime' => $uptime] = $this->server->validateConnection(false); - if ($uptime) { - if ($this->server->unreachable_notification_sent === true) { - $this->server->update(['unreachable_notification_sent' => false]); - } - } else { - // $this->server->team?->notify(new Unreachable($this->server)); - foreach ($this->applications as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); + if ($this->server->proxySet() && ! $this->server->proxy->force_stop) { + $this->server->proxyType(); + $foundProxyContainer = $this->containers->filter(function ($value, $key) { + if ($this->server->isSwarm()) { + return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; + } else { + return data_get($value, 'Name') === '/coolify-proxy'; + } + })->first(); + if (! $foundProxyContainer) { + try { + $shouldStart = CheckProxy::run($this->server); + if ($shouldStart) { + StartProxy::run($this->server, false); + $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); + } + } catch (\Throwable $e) { + } + } else { + $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); + $this->server->save(); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } } } - - return false; + } catch (\Throwable $e) { + return handleError($e); } - - return true; - } private function checkLogDrainContainer() @@ -123,295 +94,10 @@ private function checkLogDrainContainer() if ($foundLogDrainContainer) { $status = data_get($foundLogDrainContainer, 'State.Status'); if ($status !== 'running') { - InstallLogDrain::dispatch($this->server); - } - } else { - InstallLogDrain::dispatch($this->server); - } - } - - private function containerStatus() - { - - $foundApplications = []; - $foundApplicationPreviews = []; - $foundDatabases = []; - $foundServices = []; - - foreach ($this->containers as $container) { - if ($this->server->isSwarm()) { - $labels = data_get($container, 'Spec.Labels'); - $uuid = data_get($labels, 'coolify.name'); - } else { - $labels = data_get($container, 'Config.Labels'); - } - $containerStatus = data_get($container, 'State.Status'); - $containerHealth = data_get($container, 'State.Health.Status', 'unhealthy'); - $containerStatus = "$containerStatus ($containerHealth)"; - $labels = Arr::undot(format_docker_labels_to_json($labels)); - $applicationId = data_get($labels, 'coolify.applicationId'); - if ($applicationId) { - $pullRequestId = data_get($labels, 'coolify.pullRequestId'); - if ($pullRequestId) { - if (str($applicationId)->contains('-')) { - $applicationId = str($applicationId)->before('-'); - } - $preview = ApplicationPreview::where('application_id', $applicationId)->where('pull_request_id', $pullRequestId)->first(); - if ($preview) { - $foundApplicationPreviews[] = $preview->id; - $statusFromDb = $preview->status; - if ($statusFromDb !== $containerStatus) { - $preview->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } else { - $application = $this->applications->where('id', $applicationId)->first(); - if ($application) { - $foundApplications[] = $application->id; - $statusFromDb = $application->status; - if ($statusFromDb !== $containerStatus) { - $application->update(['status' => $containerStatus]); - } - } else { - //Notify user that this container should not be there. - } - } - } else { - $uuid = data_get($labels, 'com.docker.compose.service'); - $type = data_get($labels, 'coolify.type'); - - if ($uuid) { - if ($type === 'service') { - $database_id = data_get($labels, 'coolify.service.subId'); - if ($database_id) { - $service_db = ServiceDatabase::where('id', $database_id)->first(); - if ($service_db) { - $uuid = data_get($service_db, 'service.uuid'); - if ($uuid) { - $isPublic = data_get($service_db, 'is_public'); - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($service_db); - // $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$service_db->service->name}", $this->server)); - } - } - } - } - } - } else { - $database = $this->databases->where('uuid', $uuid)->first(); - if ($database) { - $isPublic = data_get($database, 'is_public'); - $foundDatabases[] = $database->id; - $statusFromDb = $database->status; - if ($statusFromDb !== $containerStatus) { - $database->update(['status' => $containerStatus]); - } - if ($isPublic) { - $foundTcpProxy = $this->containers->filter(function ($value, $key) use ($uuid) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === "coolify-proxy_$uuid"; - } else { - return data_get($value, 'Name') === "/$uuid-proxy"; - } - })->first(); - if (! $foundTcpProxy) { - StartDatabaseProxy::run($database); - $this->server->team?->notify(new ContainerRestarted("TCP Proxy for {$database->name}", $this->server)); - } - } - } else { - // Notify user that this container should not be there. - } - } - } - if (data_get($container, 'Name') === '/coolify-db') { - $foundDatabases[] = 0; - } - } - $serviceLabelId = data_get($labels, 'coolify.serviceId'); - if ($serviceLabelId) { - $subType = data_get($labels, 'coolify.service.subType'); - $subId = data_get($labels, 'coolify.service.subId'); - $service = $this->services->where('id', $serviceLabelId)->first(); - if (! $service) { - continue; - } - if ($subType === 'application') { - $service = $service->applications()->where('id', $subId)->first(); - } else { - $service = $service->databases()->where('id', $subId)->first(); - } - if ($service) { - $foundServices[] = "$service->id-$service->name"; - $statusFromDb = $service->status; - if ($statusFromDb !== $containerStatus) { - // ray('Updating status: ' . $containerStatus); - $service->update(['status' => $containerStatus]); - } - } - } - } - $exitedServices = collect([]); - foreach ($this->services as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - if (in_array("$app->id-$app->name", $foundServices)) { - continue; - } else { - $exitedServices->push($app); - } - } - foreach ($dbs as $db) { - if (in_array("$db->id-$db->name", $foundServices)) { - continue; - } else { - $exitedServices->push($db); - } - } - } - $exitedServices = $exitedServices->unique('id'); - foreach ($exitedServices as $exitedService) { - if (str($exitedService->status)->startsWith('exited')) { - continue; - } - $name = data_get($exitedService, 'name'); - $fqdn = data_get($exitedService, 'fqdn'); - if ($name) { - if ($fqdn) { - $containerName = "$name, available at $fqdn"; - } else { - $containerName = $name; - } - } else { - if ($fqdn) { - $containerName = $fqdn; - } else { - $containerName = null; - } - } - $projectUuid = data_get($service, 'environment.project.uuid'); - $serviceUuid = data_get($service, 'uuid'); - $environmentName = data_get($service, 'environment.name'); - - if ($projectUuid && $serviceUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/service/'.$serviceUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - $exitedService->update(['status' => 'exited']); - } - - $notRunningApplications = $this->applications->pluck('id')->diff($foundApplications); - foreach ($notRunningApplications as $applicationId) { - $application = $this->applications->where('id', $applicationId)->first(); - if (str($application->status)->startsWith('exited')) { - continue; - } - $application->update(['status' => 'exited']); - - $name = data_get($application, 'name'); - $fqdn = data_get($application, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($application, 'environment.project.uuid'); - $applicationUuid = data_get($application, 'uuid'); - $environment = data_get($application, 'environment.name'); - - if ($projectUuid && $applicationUuid && $environment) { - $url = base_url().'/project/'.$projectUuid.'/'.$environment.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningApplicationPreviews = $this->previews->pluck('id')->diff($foundApplicationPreviews); - foreach ($notRunningApplicationPreviews as $previewId) { - $preview = $this->previews->where('id', $previewId)->first(); - if (str($preview->status)->startsWith('exited')) { - continue; - } - $preview->update(['status' => 'exited']); - - $name = data_get($preview, 'name'); - $fqdn = data_get($preview, 'fqdn'); - - $containerName = $name ? "$name ($fqdn)" : $fqdn; - - $projectUuid = data_get($preview, 'application.environment.project.uuid'); - $environmentName = data_get($preview, 'application.environment.name'); - $applicationUuid = data_get($preview, 'application.uuid'); - - if ($projectUuid && $applicationUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/application/'.$applicationUuid; - } else { - $url = null; - } - - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - $notRunningDatabases = $this->databases->pluck('id')->diff($foundDatabases); - foreach ($notRunningDatabases as $database) { - $database = $this->databases->where('id', $database)->first(); - if (str($database->status)->startsWith('exited')) { - continue; - } - $database->update(['status' => 'exited']); - - $name = data_get($database, 'name'); - $fqdn = data_get($database, 'fqdn'); - - $containerName = $name; - - $projectUuid = data_get($database, 'environment.project.uuid'); - $environmentName = data_get($database, 'environment.name'); - $databaseUuid = data_get($database, 'uuid'); - - if ($projectUuid && $databaseUuid && $environmentName) { - $url = base_url().'/project/'.$projectUuid.'/'.$environmentName.'/database/'.$databaseUuid; - } else { - $url = null; - } - // $this->server->team?->notify(new ContainerStopped($containerName, $this->server, $url)); - } - - // Check if proxy is running - $this->server->proxyType(); - $foundProxyContainer = $this->containers->filter(function ($value, $key) { - if ($this->server->isSwarm()) { - return data_get($value, 'Spec.Name') === 'coolify-proxy_traefik'; - } else { - return data_get($value, 'Name') === '/coolify-proxy'; - } - })->first(); - if (! $foundProxyContainer) { - try { - $shouldStart = CheckProxy::run($this->server); - if ($shouldStart) { - StartProxy::run($this->server, false); - $this->server->team?->notify(new ContainerRestarted('coolify-proxy', $this->server)); - } - } catch (\Throwable $e) { - ray($e); + StartLogDrain::dispatch($this->server)->onQueue('high'); } } else { - $this->server->proxy->status = data_get($foundProxyContainer, 'State.Status'); - $this->server->save(); - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + StartLogDrain::dispatch($this->server)->onQueue('high'); } } } diff --git a/app/Jobs/ServerCheckNewJob.php b/app/Jobs/ServerCheckNewJob.php new file mode 100644 index 0000000000..3e8e60a31d --- /dev/null +++ b/app/Jobs/ServerCheckNewJob.php @@ -0,0 +1,34 @@ +server); + ResourcesCheck::dispatch($this->server); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Jobs/ServerCleanupMux.php b/app/Jobs/ServerCleanupMux.php new file mode 100644 index 0000000000..b793c3eca6 --- /dev/null +++ b/app/Jobs/ServerCleanupMux.php @@ -0,0 +1,40 @@ +server->serverStatus() === false) { + return 'Server is not reachable or not ready.'; + } + SshMultiplexingHelper::removeMuxFile($this->server); + } catch (\Throwable $e) { + return handleError($e); + } + } +} diff --git a/app/Jobs/ServerLimitCheckJob.php b/app/Jobs/ServerLimitCheckJob.php index 1f09d5a3b5..aa82c6dade 100644 --- a/app/Jobs/ServerLimitCheckJob.php +++ b/app/Jobs/ServerLimitCheckJob.php @@ -30,11 +30,8 @@ public function handle() try { $servers = $this->team->servers; $servers_count = $servers->count(); - $limit = data_get($this->team->limits, 'serverLimit', 2); - $number_of_servers_to_disable = $servers_count - $limit; - ray('ServerLimitCheckJob', $this->team->uuid, $servers_count, $limit, $number_of_servers_to_disable); + $number_of_servers_to_disable = $servers_count - $this->team->limits; if ($number_of_servers_to_disable > 0) { - ray('Disabling servers'); $servers = $servers->sortbyDesc('created_at'); $servers_to_disable = $servers->take($number_of_servers_to_disable); $servers_to_disable->each(function ($server) { @@ -51,7 +48,6 @@ public function handle() } } catch (\Throwable $e) { send_internal_notification('ServerLimitCheckJob failed with: '.$e->getMessage()); - ray($e->getMessage()); return handleError($e); } diff --git a/app/Jobs/ServerStatusJob.php b/app/Jobs/ServerStatusJob.php deleted file mode 100644 index fcc33c8594..0000000000 --- a/app/Jobs/ServerStatusJob.php +++ /dev/null @@ -1,60 +0,0 @@ -server->isServerReady($this->tries)) { - throw new \RuntimeException('Server is not ready.'); - } - try { - if ($this->server->isFunctional()) { - $this->remove_unnecessary_coolify_yaml(); - if ($this->server->isSentinelEnabled()) { - $this->server->checkSentinel(); - } - } - } catch (\Throwable $e) { - // send_internal_notification('ServerStatusJob failed with: '.$e->getMessage()); - ray($e->getMessage()); - - return handleError($e); - } - - } - - private function remove_unnecessary_coolify_yaml() - { - // This will remote the coolify.yaml file from the server as it is not needed on cloud servers - if (isCloud() && $this->server->id !== 0) { - $file = $this->server->proxyPath().'/dynamic/coolify.yaml'; - - return instant_remote_process([ - "rm -f $file", - ], $this->server, false); - } - } -} diff --git a/app/Jobs/ServerStorageCheckJob.php b/app/Jobs/ServerStorageCheckJob.php index 376cb85325..0723ffceee 100644 --- a/app/Jobs/ServerStorageCheckJob.php +++ b/app/Jobs/ServerStorageCheckJob.php @@ -3,12 +3,14 @@ namespace App\Jobs; use App\Models\Server; +use App\Notifications\Server\HighDiskUsage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldBeEncrypted; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; +use Illuminate\Support\Facades\RateLimiter; class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue { @@ -18,42 +20,46 @@ class ServerStorageCheckJob implements ShouldBeEncrypted, ShouldQueue public $timeout = 60; - public $containers; - - public $applications; - - public $databases; - - public $services; - - public $previews; - public function backoff(): int { return isDev() ? 1 : 3; } - public function __construct(public Server $server) {} + public function __construct(public Server $server, public ?int $percentage = null) {} public function handle() { try { - if (! $this->server->isFunctional()) { - ray('Server is not ready.'); + if ($this->server->isFunctional() === false) { + return 'Server is not functional.'; + } + $team = data_get($this->server, 'team'); + $serverDiskUsageNotificationThreshold = data_get($this->server, 'settings.server_disk_usage_notification_threshold'); - return 'Server is not ready.'; + if (is_null($this->percentage)) { + $this->percentage = $this->server->storageCheck(); } - $team = $this->server->team; - $percentage = $this->server->storageCheck(); - if ($percentage > 1) { - ray('Server storage is at '.$percentage.'%'); + if (! $this->percentage) { + return 'No percentage could be retrieved.'; + } + if ($this->percentage > $serverDiskUsageNotificationThreshold) { + $executed = RateLimiter::attempt( + 'high-disk-usage:'.$this->server->id, + $maxAttempts = 0, + function () use ($team, $serverDiskUsageNotificationThreshold) { + $team->notify(new HighDiskUsage($this->server, $this->percentage, $serverDiskUsageNotificationThreshold)); + }, + $decaySeconds = 3600, + ); + + if (! $executed) { + return 'Too many messages sent!'; + } + } else { + RateLimiter::hit('high-disk-usage:'.$this->server->id, 600); } - } catch (\Throwable $e) { - ray($e->getMessage()); - return handleError($e); } - } } diff --git a/app/Jobs/SubscriptionInvoiceFailedJob.php b/app/Jobs/SubscriptionInvoiceFailedJob.php index b4ef7baa0d..aabeecef5c 100755 --- a/app/Jobs/SubscriptionInvoiceFailedJob.php +++ b/app/Jobs/SubscriptionInvoiceFailedJob.php @@ -27,14 +27,12 @@ public function handle() ]); $mail->subject('Your last payment was failed for Coolify Cloud.'); $this->team->members()->each(function ($member) use ($mail) { - ray($member); if ($member->isAdmin()) { send_user_an_email($mail, $member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionInvoiceFailedJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SubscriptionTrialEndedJob.php b/app/Jobs/SubscriptionTrialEndedJob.php index 8635b439ca..88a5e06be5 100755 --- a/app/Jobs/SubscriptionTrialEndedJob.php +++ b/app/Jobs/SubscriptionTrialEndedJob.php @@ -30,14 +30,12 @@ public function handle(): void ]); $this->team->members()->each(function ($member) use ($mail) { if ($member->isAdmin()) { - ray('Sending trial ended email to '.$member->email); send_user_an_email($mail, $member->email); send_internal_notification('Trial reminder email sent to '.$member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/SubscriptionTrialEndsSoonJob.php b/app/Jobs/SubscriptionTrialEndsSoonJob.php index 244624749c..2a76a1097e 100755 --- a/app/Jobs/SubscriptionTrialEndsSoonJob.php +++ b/app/Jobs/SubscriptionTrialEndsSoonJob.php @@ -30,14 +30,12 @@ public function handle(): void ]); $this->team->members()->each(function ($member) use ($mail) { if ($member->isAdmin()) { - ray('Sending trial ending email to '.$member->email); send_user_an_email($mail, $member->email); send_internal_notification('Trial reminder email sent to '.$member->email); } }); } catch (\Throwable $e) { send_internal_notification('SubscriptionTrialEndsSoonJob failed with: '.$e->getMessage()); - ray($e->getMessage()); throw $e; } } diff --git a/app/Jobs/UpdateCoolifyJob.php b/app/Jobs/UpdateCoolifyJob.php index 2cc705e4ac..1e5197b6f8 100644 --- a/app/Jobs/UpdateCoolifyJob.php +++ b/app/Jobs/UpdateCoolifyJob.php @@ -41,7 +41,6 @@ public function handle(): void $settings->update(['new_version_available' => false]); Log::info('Coolify update completed successfully.'); - } catch (\Throwable $e) { Log::error('UpdateCoolifyJob failed: '.$e->getMessage()); // Consider implementing a notification to administrators diff --git a/app/Listeners/MaintenanceModeDisabledNotification.php b/app/Listeners/MaintenanceModeDisabledNotification.php index c7cd1bcded..6c3ab83d8f 100644 --- a/app/Listeners/MaintenanceModeDisabledNotification.php +++ b/app/Listeners/MaintenanceModeDisabledNotification.php @@ -13,7 +13,6 @@ public function __construct() {} public function handle(EventsMaintenanceModeDisabled $event): void { - ray('Maintenance mode disabled!'); $files = Storage::disk('webhooks-during-maintenance')->files(); $files = collect($files); $files = $files->sort(); @@ -41,7 +40,6 @@ public function handle(EventsMaintenanceModeDisabled $event): void $instance = new $class; $instance->$method($request); } catch (\Throwable $th) { - ray($th); } finally { Storage::disk('webhooks-during-maintenance')->delete($file); } diff --git a/app/Listeners/MaintenanceModeEnabledNotification.php b/app/Listeners/MaintenanceModeEnabledNotification.php index b2cd8c738a..5aab248ea8 100644 --- a/app/Listeners/MaintenanceModeEnabledNotification.php +++ b/app/Listeners/MaintenanceModeEnabledNotification.php @@ -17,8 +17,5 @@ public function __construct() /** * Handle the event. */ - public function handle(EventsMaintenanceModeEnabled $event): void - { - ray('Maintenance mode enabled!'); - } + public function handle(EventsMaintenanceModeEnabled $event): void {} } diff --git a/app/Livewire/ActivityMonitor.php b/app/Livewire/ActivityMonitor.php index bd1e30088b..2e36f34ee7 100644 --- a/app/Livewire/ActivityMonitor.php +++ b/app/Livewire/ActivityMonitor.php @@ -2,7 +2,6 @@ namespace App\Livewire; -use App\Enums\ProcessStatus; use App\Models\User; use Livewire\Component; use Spatie\Activitylog\Models\Activity; diff --git a/app/Livewire/Admin/Index.php b/app/Livewire/Admin/Index.php index 26b31e5151..2579c3db20 100644 --- a/app/Livewire/Admin/Index.php +++ b/app/Livewire/Admin/Index.php @@ -3,75 +3,63 @@ namespace App\Livewire\Admin; use App\Models\User; +use Illuminate\Container\Attributes\Auth as AttributesAuth; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Livewire\Component; class Index extends Component { - public $active_subscribers = []; + public int $activeSubscribers; - public $inactive_subscribers = []; + public int $inactiveSubscribers; - public $search = ''; + public Collection $foundUsers; - public function submitSearch() - { - if ($this->search !== '') { - $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->where(function ($query) { - $query->where('name', 'like', "%{$this->search}%") - ->orWhere('email', 'like', "%{$this->search}%"); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - $this->active_subscribers = User::whereHas('teams', function ($query) { - $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->where(function ($query) { - $query->where('name', 'like', "%{$this->search}%") - ->orWhere('email', 'like', "%{$this->search}%"); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - } else { - $this->getSubscribers(); - } - } + public string $search = ''; public function mount() { if (! isCloud()) { return redirect()->route('dashboard'); } - if (auth()->user()->id !== 0) { + + if (Auth::id() !== 0) { return redirect()->route('dashboard'); } $this->getSubscribers(); } + public function submitSearch() + { + if ($this->search !== '') { + $this->foundUsers = User::where(function ($query) { + $query->where('name', 'like', "%{$this->search}%") + ->orWhere('email', 'like', "%{$this->search}%"); + })->get(); + } + } + public function getSubscribers() { - $this->inactive_subscribers = User::whereDoesntHave('teams', function ($query) { + $this->inactiveSubscribers = User::whereDoesntHave('teams', function ($query) { $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); - $this->active_subscribers = User::whereHas('teams', function ($query) { + })->count(); + $this->activeSubscribers = User::whereHas('teams', function ($query) { $query->whereRelation('subscription', 'stripe_subscription_id', '!=', null); - })->get()->filter(function ($user) { - return $user->id !== 0; - }); + })->count(); } public function switchUser(int $user_id) { - if (auth()->user()->id !== 0) { + if (AttributesAuth::id() !== 0) { return redirect()->route('dashboard'); } $user = User::find($user_id); $team_to_switch_to = $user->teams->first(); Cache::forget("team:{$user->id}"); - auth()->login($user); + Auth::login($user); refreshSession($team_to_switch_to); return redirect(request()->header('Referer')); diff --git a/app/Livewire/Boarding/Index.php b/app/Livewire/Boarding/Index.php index 52d4674ee1..c9c3092b32 100644 --- a/app/Livewire/Boarding/Index.php +++ b/app/Livewire/Boarding/Index.php @@ -73,8 +73,6 @@ public function mount() } $this->privateKeyName = generate_random_name(); $this->remoteServerName = generate_random_name(); - $this->remoteServerPort = $this->remoteServerPort; - $this->remoteServerUser = $this->remoteServerUser; if (isDev()) { $this->privateKey = '-----BEGIN OPENSSH PRIVATE KEY----- b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW @@ -87,26 +85,6 @@ public function mount() $this->remoteServerDescription = 'Created by Coolify'; $this->remoteServerHost = 'coolify-testing-host'; } - // if ($this->currentState === 'create-project') { - // $this->getProjects(); - // } - // if ($this->currentState === 'create-resource') { - // $this->selectExistingServer(); - // $this->selectExistingProject(); - // } - // if ($this->currentState === 'private-key') { - // $this->setServerType('remote'); - // } - // if ($this->currentState === 'create-server') { - // $this->selectExistingPrivateKey(); - // } - // if ($this->currentState === 'validate-server') { - // $this->selectExistingServer(); - // } - // if ($this->currentState === 'select-existing-server') { - // $this->selectExistingServer(); - // } - } public function explanation() diff --git a/app/Livewire/Dashboard.php b/app/Livewire/Dashboard.php index d18a7689e0..69ba19e401 100644 --- a/app/Livewire/Dashboard.php +++ b/app/Livewire/Dashboard.php @@ -16,28 +16,28 @@ class Dashboard extends Component public Collection $servers; - public Collection $private_keys; + public Collection $privateKeys; - public $deployments_per_server; + public array $deploymentsPerServer = []; public function mount() { - $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); + $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get(); $this->servers = Server::ownedByCurrentTeam()->get(); $this->projects = Project::ownedByCurrentTeam()->get(); - $this->get_deployments(); + $this->loadDeployments(); } - public function cleanup_queue() + public function cleanupQueue() { Artisan::queue('cleanup:deployment-queue', [ '--team-id' => currentTeam()->id, ]); } - public function get_deployments() + public function loadDeployments() { - $this->deployments_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ + $this->deploymentsPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('server_id', $this->servers->pluck('id'))->get([ 'id', 'application_id', 'application_name', diff --git a/app/Livewire/Destination/Form.php b/app/Livewire/Destination/Form.php deleted file mode 100644 index 87ae839313..0000000000 --- a/app/Livewire/Destination/Form.php +++ /dev/null @@ -1,46 +0,0 @@ - 'required', - 'destination.network' => 'required', - 'destination.server.ip' => 'required', - ]; - - protected $validationAttributes = [ - 'destination.name' => 'name', - 'destination.network' => 'network', - 'destination.server.ip' => 'IP Address/Domain', - ]; - - public function submit() - { - $this->validate(); - $this->destination->save(); - } - - public function delete() - { - try { - if ($this->destination->getMorphClass() === 'App\Models\StandaloneDocker') { - if ($this->destination->attachedTo()) { - return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); - } - instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); - instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); - } - $this->destination->delete(); - - return redirect()->route('destination.all'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } -} diff --git a/app/Livewire/Destination/Index.php b/app/Livewire/Destination/Index.php new file mode 100644 index 0000000000..a3df3fd562 --- /dev/null +++ b/app/Livewire/Destination/Index.php @@ -0,0 +1,23 @@ +servers = Server::isUsable()->get(); + } + + public function render() + { + return view('livewire.destination.index'); + } +} diff --git a/app/Livewire/Destination/New/Docker.php b/app/Livewire/Destination/New/Docker.php index 4fc938df84..f86f42e34b 100644 --- a/app/Livewire/Destination/New/Docker.php +++ b/app/Livewire/Destination/New/Docker.php @@ -3,111 +3,91 @@ namespace App\Livewire\Destination\New; use App\Models\Server; -use App\Models\StandaloneDocker as ModelsStandaloneDocker; +use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; use Visus\Cuid2\Cuid2; class Docker extends Component { - public string $name; - - public string $network; + #[Locked] + public $servers; - public ?Collection $servers = null; + #[Locked] + public Server $selectedServer; - public Server $server; - - public ?int $server_id = null; + #[Validate(['required', 'string'])] + public string $name; - public bool $is_swarm = false; + #[Validate(['required', 'string'])] + public string $network; - protected $rules = [ - 'name' => 'required|string', - 'network' => 'required|string', - 'server_id' => 'required|integer', - 'is_swarm' => 'boolean', - ]; + #[Validate(['required', 'string'])] + public string $serverId; - protected $validationAttributes = [ - 'name' => 'name', - 'network' => 'network', - 'server_id' => 'server', - 'is_swarm' => 'swarm', - ]; + #[Validate(['required', 'boolean'])] + public bool $isSwarm = false; - public function mount() + public function mount(?string $server_id = null) { - if (is_null($this->servers)) { - $this->servers = Server::isReachable()->get(); - } - if (request()->query('server_id')) { - $this->server_id = request()->query('server_id'); - } else { - if ($this->servers->count() > 0) { - $this->server_id = $this->servers->first()->id; - } - } - if (request()->query('network_name')) { - $this->network = request()->query('network_name'); + $this->network = new Cuid2; + $this->servers = Server::isUsable()->get(); + if ($server_id) { + $this->selectedServer = $this->servers->find($server_id); + $this->serverId = $this->selectedServer->id; } else { - $this->network = new Cuid2; - } - if ($this->servers->count() > 0) { - $this->name = str("{$this->servers->first()->name}-{$this->network}")->kebab(); + $this->selectedServer = $this->servers->first(); + $this->serverId = $this->selectedServer->id; } + $this->generateName(); } - public function generate_name() + public function updatedServerId() { - $this->server = Server::find($this->server_id); - $this->name = str("{$this->server->name}-{$this->network}")->kebab(); + $this->selectedServer = $this->servers->find($this->serverId); + $this->generateName(); + } + + public function generateName() + { + $name = data_get($this->selectedServer, 'name', new Cuid2); + $this->name = str("{$name}-{$this->network}")->kebab(); } public function submit() { - $this->validate(); try { - $this->server = Server::find($this->server_id); - if ($this->is_swarm) { - $found = $this->server->swarmDockers()->where('network', $this->network)->first(); + $this->validate(); + if ($this->isSwarm) { + $found = $this->selectedServer->swarmDockers()->where('network', $this->network)->first(); if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; + throw new \Exception('Network already added to this server.'); } else { $docker = SwarmDocker::create([ 'name' => $this->name, 'network' => $this->network, - 'server_id' => $this->server_id, + 'server_id' => $this->selectedServer->id, ]); } } else { - $found = $this->server->standaloneDockers()->where('network', $this->network)->first(); + $found = $this->selectedServer->standaloneDockers()->where('network', $this->network)->first(); if ($found) { - $this->dispatch('error', 'Network already added to this server.'); - - return; + throw new \Exception('Network already added to this server.'); } else { - $docker = ModelsStandaloneDocker::create([ + $docker = StandaloneDocker::create([ 'name' => $this->name, 'network' => $this->network, - 'server_id' => $this->server_id, + 'server_id' => $this->selectedServer->id, ]); } } - $this->createNetworkAndAttachToProxy(); - - return redirect()->route('destination.show', $docker->uuid); + $connectProxyToDockerNetworks = connectProxyToNetworks($this->selectedServer); + instant_remote_process($connectProxyToDockerNetworks, $this->selectedServer, false); + $this->dispatch('reloadWindow'); } catch (\Throwable $e) { return handleError($e, $this); } } - - private function createNetworkAndAttachToProxy() - { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } } diff --git a/app/Livewire/Destination/Show.php b/app/Livewire/Destination/Show.php index 5650e82ba0..5c4d6c1701 100644 --- a/app/Livewire/Destination/Show.php +++ b/app/Livewire/Destination/Show.php @@ -5,71 +5,91 @@ use App\Models\Server; use App\Models\StandaloneDocker; use App\Models\SwarmDocker; -use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { - public Server $server; + #[Locked] + public $destination; - public Collection|array $networks = []; + #[Validate(['string', 'required'])] + public string $name; - private function createNetworkAndAttachToProxy() - { - $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); - instant_remote_process($connectProxyToDockerNetworks, $this->server, false); - } + #[Validate(['string', 'required'])] + public string $network; - public function add($name) - { - if ($this->server->isSwarm()) { - $found = $this->server->swarmDockers()->where('network', $name)->first(); - if ($found) { - $this->dispatch('error', 'Network already added to this server.'); + #[Validate(['string', 'required'])] + public string $serverIp; - return; - } else { - SwarmDocker::create([ - 'name' => $this->server->name.'-'.$name, - 'network' => $this->name, - 'server_id' => $this->server->id, - ]); - } - } else { - $found = $this->server->standaloneDockers()->where('network', $name)->first(); - if ($found) { - $this->dispatch('error', 'Network already added to this server.'); + public function mount(string $destination_uuid) + { + try { + $destination = StandaloneDocker::whereUuid($destination_uuid)->first() ?? + SwarmDocker::whereUuid($destination_uuid)->firstOrFail(); - return; - } else { - StandaloneDocker::create([ - 'name' => $this->server->name.'-'.$name, - 'network' => $name, - 'server_id' => $this->server->id, - ]); + $ownedByTeam = Server::ownedByCurrentTeam()->each(function ($server) use ($destination) { + if ($server->standaloneDockers->contains($destination) || $server->swarmDockers->contains($destination)) { + $this->destination = $destination; + $this->syncData(); + } + }); + if ($ownedByTeam === false) { + return redirect()->route('destination.index'); } - $this->createNetworkAndAttachToProxy(); + $this->destination = $destination; + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function scan() + public function syncData(bool $toModel = false) { - if ($this->server->isSwarm()) { - $alreadyAddedNetworks = $this->server->swarmDockers; + if ($toModel) { + $this->validate(); + $this->destination->name = $this->name; + $this->destination->network = $this->network; + $this->destination->server->ip = $this->serverIp; + $this->destination->save(); } else { - $alreadyAddedNetworks = $this->server->standaloneDockers; + $this->name = $this->destination->name; + $this->network = $this->destination->network; + $this->serverIp = $this->destination->server->ip; } - $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false); - $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { - return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; - })->filter(function ($network) use ($alreadyAddedNetworks) { - return ! $alreadyAddedNetworks->contains('network', $network['Name']); - }); - if ($this->networks->count() === 0) { - $this->dispatch('success', 'No new networks found.'); + } - return; + public function submit() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Destination saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->dispatch('success', 'Scan done.'); + } + + public function delete() + { + try { + if ($this->destination->getMorphClass() === \App\Models\StandaloneDocker::class) { + if ($this->destination->attachedTo()) { + return $this->dispatch('error', 'You must delete all resources before deleting this destination.'); + } + instant_remote_process(["docker network disconnect {$this->destination->network} coolify-proxy"], $this->destination->server, throwError: false); + instant_remote_process(['docker network rm -f '.$this->destination->network], $this->destination->server); + } + $this->destination->delete(); + + return redirect()->route('destination.index'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.destination.show'); } } diff --git a/app/Livewire/ForcePasswordReset.php b/app/Livewire/ForcePasswordReset.php index a732ef1c96..61a2a20e95 100644 --- a/app/Livewire/ForcePasswordReset.php +++ b/app/Livewire/ForcePasswordReset.php @@ -4,6 +4,7 @@ use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; use Livewire\Component; class ForcePasswordReset extends Component @@ -16,14 +17,19 @@ class ForcePasswordReset extends Component public string $password_confirmation; - protected $rules = [ - 'email' => 'required|email', - 'password' => 'required|min:8', - 'password_confirmation' => 'required|same:password', - ]; + public function rules(): array + { + return [ + 'email' => ['required', 'email'], + 'password' => ['required', Password::defaults(), 'confirmed'], + ]; + } public function mount() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } $this->email = auth()->user()->email; } @@ -34,6 +40,10 @@ public function render() public function submit() { + if (auth()->user()->force_password_reset === false) { + return redirect()->route('dashboard'); + } + try { $this->rateLimit(10); $this->validate(); diff --git a/app/Livewire/Help.php b/app/Livewire/Help.php index 934e816614..f51527fbeb 100644 --- a/app/Livewire/Help.php +++ b/app/Livewire/Help.php @@ -5,55 +5,39 @@ use DanHarrin\LivewireRateLimiting\WithRateLimiting; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Support\Facades\Http; -use Illuminate\Support\Facades\Route; +use Livewire\Attributes\Validate; use Livewire\Component; class Help extends Component { use WithRateLimiting; + #[Validate(['required', 'min:10', 'max:1000'])] public string $description; + #[Validate(['required', 'min:3'])] public string $subject; - public ?string $path = null; - - protected $rules = [ - 'description' => 'required|min:10', - 'subject' => 'required|min:3', - ]; - - public function mount() - { - $this->path = Route::current()?->uri() ?? null; - if (isDev()) { - $this->description = "I'm having trouble with {$this->path}"; - $this->subject = "Help with {$this->path}"; - } - } - public function submit() { try { - $this->rateLimit(3, 30); $this->validate(); - $debug = "Route: {$this->path}"; + $this->rateLimit(3, 30); + + $settings = instanceSettings(); $mail = new MailMessage; $mail->view( 'emails.help', [ 'description' => $this->description, - 'debug' => $debug, ] ); $mail->subject("[HELP]: {$this->subject}"); - $settings = instanceSettings(); $type = set_transanctional_email_settings($settings); - if (! $type) { + + // Sending feedback through Cloud API + if ($type === false) { $url = 'https://app.coolify.io/api/feedback'; - if (isDev()) { - $url = 'http://localhost:80/api/feedback'; - } Http::post($url, [ 'content' => 'User: `'.auth()->user()?->email.'` with subject: `'.$this->subject.'` has the following problem: `'.$this->description.'`', ]); diff --git a/app/Livewire/NavbarDeleteTeam.php b/app/Livewire/NavbarDeleteTeam.php index 988add7c89..cc5d78f604 100644 --- a/app/Livewire/NavbarDeleteTeam.php +++ b/app/Livewire/NavbarDeleteTeam.php @@ -2,6 +2,8 @@ namespace App\Livewire; +use App\Models\InstanceSettings; +use Illuminate\Container\Attributes\Auth as AttributesAuth; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\Hash; @@ -18,17 +20,19 @@ public function mount() public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $currentTeam = currentTeam(); $currentTeam->delete(); $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === auth()->user()->id) { + if ($user->id === AttributesAuth::id()) { return; } $user->teams()->detach($currentTeam); diff --git a/app/Livewire/NewActivityMonitor.php b/app/Livewire/NewActivityMonitor.php index 10dbb9ce7d..a9334e7108 100644 --- a/app/Livewire/NewActivityMonitor.php +++ b/app/Livewire/NewActivityMonitor.php @@ -68,7 +68,6 @@ public function polling() } else { $this->dispatch($this->eventToDispatch); } - ray('Dispatched event: '.$this->eventToDispatch.' with data: '.$this->eventData); } } } diff --git a/app/Livewire/Notifications/Discord.php b/app/Livewire/Notifications/Discord.php index 65c202b7db..7a177a2278 100644 --- a/app/Livewire/Notifications/Discord.php +++ b/app/Livewire/Notifications/Discord.php @@ -4,60 +4,124 @@ use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class Discord extends Component { public Team $team; - protected $rules = [ - 'team.discord_enabled' => 'nullable|boolean', - 'team.discord_webhook_url' => 'required|url', - 'team.discord_notifications_test' => 'nullable|boolean', - 'team.discord_notifications_deployments' => 'nullable|boolean', - 'team.discord_notifications_status_changes' => 'nullable|boolean', - 'team.discord_notifications_database_backups' => 'nullable|boolean', - 'team.discord_notifications_scheduled_tasks' => 'nullable|boolean', - ]; + #[Validate(['boolean'])] + public bool $discordEnabled = false; - protected $validationAttributes = [ - 'team.discord_webhook_url' => 'Discord Webhook', - ]; + #[Validate(['url', 'nullable'])] + public ?string $discordWebhookUrl = null; + + #[Validate(['boolean'])] + public bool $discordNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsScheduledTasks = false; + + #[Validate(['boolean'])] + public bool $discordNotificationsServerDiskUsage = false; public function mount() { - $this->team = auth()->user()->currentTeam(); + try { + $this->team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->discord_enabled = $this->discordEnabled; + $this->team->discord_webhook_url = $this->discordWebhookUrl; + $this->team->discord_notifications_test = $this->discordNotificationsTest; + $this->team->discord_notifications_deployments = $this->discordNotificationsDeployments; + $this->team->discord_notifications_status_changes = $this->discordNotificationsStatusChanges; + $this->team->discord_notifications_database_backups = $this->discordNotificationsDatabaseBackups; + $this->team->discord_notifications_scheduled_tasks = $this->discordNotificationsScheduledTasks; + $this->team->discord_notifications_server_disk_usage = $this->discordNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->discordEnabled = $this->team->discord_enabled; + $this->discordWebhookUrl = $this->team->discord_webhook_url; + $this->discordNotificationsTest = $this->team->discord_notifications_test; + $this->discordNotificationsDeployments = $this->team->discord_notifications_deployments; + $this->discordNotificationsStatusChanges = $this->team->discord_notifications_status_changes; + $this->discordNotificationsDatabaseBackups = $this->team->discord_notifications_database_backups; + $this->discordNotificationsScheduledTasks = $this->team->discord_notifications_scheduled_tasks; + $this->discordNotificationsServerDiskUsage = $this->team->discord_notifications_server_disk_usage; + } + } + + public function instantSaveDiscordEnabled() + { + try { + $this->validate([ + 'discordWebhookUrl' => 'required', + ], [ + 'discordWebhookUrl.required' => 'Discord Webhook URL is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->discordEnabled = false; + + return handleError($e, $this); + } } public function instantSave() { try { - $this->submit(); + $this->syncData(true); } catch (\Throwable $e) { - ray($e->getMessage()); - $this->team->discord_enabled = false; - $this->validate(); + return handleError($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - $this->saveModel(); + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } public function sendTestNotification() { - $this->team?->notify(new Test); - $this->dispatch('success', 'Test notification sent.'); + try { + $this->team->notify(new Test); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Notifications/Email.php b/app/Livewire/Notifications/Email.php index 53673292ed..b49638b488 100644 --- a/app/Livewire/Notifications/Email.php +++ b/app/Livewire/Notifications/Email.php @@ -4,115 +4,196 @@ use App\Models\Team; use App\Notifications\Test; +use Illuminate\Support\Facades\RateLimiter; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Email extends Component { public Team $team; + #[Locked] public string $emails; - public bool $sharedEmailEnabled = false; - - protected $rules = [ - 'team.smtp_enabled' => 'nullable|boolean', - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.smtp_recipients' => 'nullable', - 'team.smtp_host' => 'required', - 'team.smtp_port' => 'required', - 'team.smtp_encryption' => 'nullable', - 'team.smtp_username' => 'nullable', - 'team.smtp_password' => 'nullable', - 'team.smtp_timeout' => 'nullable', - 'team.smtp_notifications_test' => 'nullable|boolean', - 'team.smtp_notifications_deployments' => 'nullable|boolean', - 'team.smtp_notifications_status_changes' => 'nullable|boolean', - 'team.smtp_notifications_database_backups' => 'nullable|boolean', - 'team.smtp_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.use_instance_email_settings' => 'boolean', - 'team.resend_enabled' => 'nullable|boolean', - 'team.resend_api_key' => 'nullable', - ]; - - protected $validationAttributes = [ - 'team.smtp_from_address' => 'From Address', - 'team.smtp_from_name' => 'From Name', - 'team.smtp_recipients' => 'Recipients', - 'team.smtp_host' => 'Host', - 'team.smtp_port' => 'Port', - 'team.smtp_encryption' => 'Encryption', - 'team.smtp_username' => 'Username', - 'team.smtp_password' => 'Password', - 'team.smtp_timeout' => 'Timeout', - 'team.resend_enabled' => 'Resend Enabled', - 'team.resend_api_key' => 'Resend API Key', - ]; + #[Validate(['boolean'])] + public bool $smtpEnabled = false; + + #[Validate(['boolean'])] + public bool $useInstanceEmailSettings = false; + + #[Validate(['nullable', 'email'])] + public ?string $smtpFromAddress = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpFromName = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpRecipients = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpHost = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpEncryption = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpUsername = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpPassword = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpTimeout = null; + + #[Validate(['boolean'])] + public bool $smtpNotificationsTest; + + #[Validate(['boolean'])] + public bool $smtpNotificationsDeployments; + + #[Validate(['boolean'])] + public bool $smtpNotificationsStatusChanges; + + #[Validate(['boolean'])] + public bool $smtpNotificationsDatabaseBackups; + + #[Validate(['boolean'])] + public bool $smtpNotificationsScheduledTasks; + + #[Validate(['boolean'])] + public bool $smtpNotificationsServerDiskUsage; + + #[Validate(['boolean'])] + public bool $resendEnabled; + + #[Validate(['nullable', 'string'])] + public ?string $resendApiKey = null; public function mount() { - $this->team = auth()->user()->currentTeam(); - ['sharedEmailEnabled' => $this->sharedEmailEnabled] = $this->team->limits; - $this->emails = auth()->user()->email; + try { + $this->team = auth()->user()->currentTeam(); + $this->emails = auth()->user()->email; + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } - public function submitFromFields() + public function syncData(bool $toModel = false) { - try { - $this->resetErrorBag(); - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - ]); + if ($toModel) { + $this->validate(); + $this->team->smtp_enabled = $this->smtpEnabled; + $this->team->smtp_from_address = $this->smtpFromAddress; + $this->team->smtp_from_name = $this->smtpFromName; + $this->team->smtp_host = $this->smtpHost; + $this->team->smtp_port = $this->smtpPort; + $this->team->smtp_encryption = $this->smtpEncryption; + $this->team->smtp_username = $this->smtpUsername; + $this->team->smtp_password = $this->smtpPassword; + $this->team->smtp_timeout = $this->smtpTimeout; + $this->team->smtp_recipients = $this->smtpRecipients; + $this->team->smtp_notifications_test = $this->smtpNotificationsTest; + $this->team->smtp_notifications_deployments = $this->smtpNotificationsDeployments; + $this->team->smtp_notifications_status_changes = $this->smtpNotificationsStatusChanges; + $this->team->smtp_notifications_database_backups = $this->smtpNotificationsDatabaseBackups; + $this->team->smtp_notifications_scheduled_tasks = $this->smtpNotificationsScheduledTasks; + $this->team->smtp_notifications_server_disk_usage = $this->smtpNotificationsServerDiskUsage; + $this->team->use_instance_email_settings = $this->useInstanceEmailSettings; + $this->team->resend_enabled = $this->resendEnabled; + $this->team->resend_api_key = $this->resendApiKey; $this->team->save(); refreshSession(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); + } else { + $this->smtpEnabled = $this->team->smtp_enabled; + $this->smtpFromAddress = $this->team->smtp_from_address; + $this->smtpFromName = $this->team->smtp_from_name; + $this->smtpHost = $this->team->smtp_host; + $this->smtpPort = $this->team->smtp_port; + $this->smtpEncryption = $this->team->smtp_encryption; + $this->smtpUsername = $this->team->smtp_username; + $this->smtpPassword = $this->team->smtp_password; + $this->smtpTimeout = $this->team->smtp_timeout; + $this->smtpRecipients = $this->team->smtp_recipients; + $this->smtpNotificationsTest = $this->team->smtp_notifications_test; + $this->smtpNotificationsDeployments = $this->team->smtp_notifications_deployments; + $this->smtpNotificationsStatusChanges = $this->team->smtp_notifications_status_changes; + $this->smtpNotificationsDatabaseBackups = $this->team->smtp_notifications_database_backups; + $this->smtpNotificationsScheduledTasks = $this->team->smtp_notifications_scheduled_tasks; + $this->smtpNotificationsServerDiskUsage = $this->team->smtp_notifications_server_disk_usage; + $this->useInstanceEmailSettings = $this->team->use_instance_email_settings; + $this->resendEnabled = $this->team->resend_enabled; + $this->resendApiKey = $this->team->resend_api_key; } } public function sendTestNotification() { - $this->team?->notify(new Test($this->emails)); - $this->dispatch('success', 'Test Email sent.'); + try { + $executed = RateLimiter::attempt( + 'test-email:'.$this->team->id, + $perMinute = 0, + function () { + $this->team?->notify(new Test($this->emails)); + $this->dispatch('success', 'Test Email sent.'); + }, + $decaySeconds = 10, + ); + + if (! $executed) { + throw new \Exception('Too many messages sent!'); + } + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function instantSaveInstance() { try { - if (! $this->sharedEmailEnabled) { - throw new \Exception('Not allowed to change settings. Please upgrade your subscription.'); - } - $this->team->smtp_enabled = false; - $this->team->resend_enabled = false; - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->smtpEnabled = false; + $this->resendEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function instantSaveResend() + public function instantSaveSmtpEnabled() { try { - $this->team->smtp_enabled = false; - $this->submitResend(); + $this->validate([ + 'smtpHost' => 'required', + 'smtpPort' => 'required|numeric', + ], [ + 'smtpHost.required' => 'SMTP Host is required.', + 'smtpPort.required' => 'SMTP Port is required.', + ]); + $this->resendEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { - $this->team->smtp_enabled = false; + $this->smtpEnabled = false; return handleError($e, $this); } } - public function instantSave() + public function instantSaveResend() { try { - $this->team->resend_enabled = false; - $this->submit(); + $this->validate([ + ], [ + 'resendApiKey.required' => 'Resend API Key is required.', + ]); + $this->smtpEnabled = false; + $this->saveModel(); } catch (\Throwable $e) { - $this->team->smtp_enabled = false; + $this->resendEnabled = false; return handleError($e, $this); } @@ -120,7 +201,7 @@ public function instantSave() public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } @@ -129,43 +210,8 @@ public function submit() { try { $this->resetErrorBag(); - if (! $this->team->use_instance_email_settings) { - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.smtp_host' => 'required', - 'team.smtp_port' => 'required|numeric', - 'team.smtp_encryption' => 'nullable', - 'team.smtp_username' => 'nullable', - 'team.smtp_password' => 'nullable', - 'team.smtp_timeout' => 'nullable', - ]); - } - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - $this->team->smtp_enabled = false; - - return handleError($e, $this); - } - } - - public function submitResend() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'team.smtp_from_address' => 'required|email', - 'team.smtp_from_name' => 'required', - 'team.resend_api_key' => 'required', - ]); - $this->team->save(); - refreshSession(); - $this->dispatch('success', 'Settings saved.'); + $this->saveModel(); } catch (\Throwable $e) { - $this->team->resend_enabled = false; - return handleError($e, $this); } } @@ -173,35 +219,28 @@ public function submitResend() public function copyFromInstanceSettings() { $settings = instanceSettings(); + if ($settings->smtp_enabled) { - $team = currentTeam(); - $team->update([ - 'smtp_enabled' => $settings->smtp_enabled, - 'smtp_from_address' => $settings->smtp_from_address, - 'smtp_from_name' => $settings->smtp_from_name, - 'smtp_recipients' => $settings->smtp_recipients, - 'smtp_host' => $settings->smtp_host, - 'smtp_port' => $settings->smtp_port, - 'smtp_encryption' => $settings->smtp_encryption, - 'smtp_username' => $settings->smtp_username, - 'smtp_password' => $settings->smtp_password, - 'smtp_timeout' => $settings->smtp_timeout, - ]); - refreshSession(); - $this->team = $team; - $this->dispatch('success', 'Settings saved.'); + $this->smtpEnabled = true; + $this->smtpFromAddress = $settings->smtp_from_address; + $this->smtpFromName = $settings->smtp_from_name; + $this->smtpRecipients = $settings->smtp_recipients; + $this->smtpHost = $settings->smtp_host; + $this->smtpPort = $settings->smtp_port; + $this->smtpEncryption = $settings->smtp_encryption; + $this->smtpUsername = $settings->smtp_username; + $this->smtpPassword = $settings->smtp_password; + $this->smtpTimeout = $settings->smtp_timeout; + $this->resendEnabled = false; + $this->saveModel(); return; } if ($settings->resend_enabled) { - $team = currentTeam(); - $team->update([ - 'resend_enabled' => $settings->resend_enabled, - 'resend_api_key' => $settings->resend_api_key, - ]); - refreshSession(); - $this->team = $team; - $this->dispatch('success', 'Settings saved.'); + $this->resendEnabled = true; + $this->resendApiKey = $settings->resend_api_key; + $this->smtpEnabled = false; + $this->saveModel(); return; } diff --git a/app/Livewire/Notifications/Telegram.php b/app/Livewire/Notifications/Telegram.php index e163a25e0d..15ec205778 100644 --- a/app/Livewire/Notifications/Telegram.php +++ b/app/Livewire/Notifications/Telegram.php @@ -4,67 +4,157 @@ use App\Models\Team; use App\Notifications\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class Telegram extends Component { public Team $team; - protected $rules = [ - 'team.telegram_enabled' => 'nullable|boolean', - 'team.telegram_token' => 'required|string', - 'team.telegram_chat_id' => 'required|string', - 'team.telegram_notifications_test' => 'nullable|boolean', - 'team.telegram_notifications_deployments' => 'nullable|boolean', - 'team.telegram_notifications_status_changes' => 'nullable|boolean', - 'team.telegram_notifications_database_backups' => 'nullable|boolean', - 'team.telegram_notifications_scheduled_tasks' => 'nullable|boolean', - 'team.telegram_notifications_test_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_deployments_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_status_changes_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_database_backups_message_thread_id' => 'nullable|string', - 'team.telegram_notifications_scheduled_tasks_thread_id' => 'nullable|string', - ]; - - protected $validationAttributes = [ - 'team.telegram_token' => 'Token', - 'team.telegram_chat_id' => 'Chat ID', - ]; + #[Validate(['boolean'])] + public bool $telegramEnabled = false; + + #[Validate(['nullable', 'string'])] + public ?string $telegramToken = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramChatId = null; + + #[Validate(['boolean'])] + public bool $telegramNotificationsTest = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsDeployments = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsStatusChanges = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsDatabaseBackups = false; + + #[Validate(['boolean'])] + public bool $telegramNotificationsScheduledTasks = false; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsTestMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsDeploymentsMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsStatusChangesMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsDatabaseBackupsMessageThreadId = null; + + #[Validate(['nullable', 'string'])] + public ?string $telegramNotificationsScheduledTasksThreadId = null; + + #[Validate(['boolean'])] + public bool $telegramNotificationsServerDiskUsage = false; public function mount() { - $this->team = auth()->user()->currentTeam(); + try { + $this->team = auth()->user()->currentTeam(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->team->telegram_enabled = $this->telegramEnabled; + $this->team->telegram_token = $this->telegramToken; + $this->team->telegram_chat_id = $this->telegramChatId; + $this->team->telegram_notifications_test = $this->telegramNotificationsTest; + $this->team->telegram_notifications_deployments = $this->telegramNotificationsDeployments; + $this->team->telegram_notifications_status_changes = $this->telegramNotificationsStatusChanges; + $this->team->telegram_notifications_database_backups = $this->telegramNotificationsDatabaseBackups; + $this->team->telegram_notifications_scheduled_tasks = $this->telegramNotificationsScheduledTasks; + $this->team->telegram_notifications_test_message_thread_id = $this->telegramNotificationsTestMessageThreadId; + $this->team->telegram_notifications_deployments_message_thread_id = $this->telegramNotificationsDeploymentsMessageThreadId; + $this->team->telegram_notifications_status_changes_message_thread_id = $this->telegramNotificationsStatusChangesMessageThreadId; + $this->team->telegram_notifications_database_backups_message_thread_id = $this->telegramNotificationsDatabaseBackupsMessageThreadId; + $this->team->telegram_notifications_scheduled_tasks_thread_id = $this->telegramNotificationsScheduledTasksThreadId; + $this->team->telegram_notifications_server_disk_usage = $this->telegramNotificationsServerDiskUsage; + $this->team->save(); + refreshSession(); + } else { + $this->telegramEnabled = $this->team->telegram_enabled; + $this->telegramToken = $this->team->telegram_token; + $this->telegramChatId = $this->team->telegram_chat_id; + $this->telegramNotificationsTest = $this->team->telegram_notifications_test; + $this->telegramNotificationsDeployments = $this->team->telegram_notifications_deployments; + $this->telegramNotificationsStatusChanges = $this->team->telegram_notifications_status_changes; + $this->telegramNotificationsDatabaseBackups = $this->team->telegram_notifications_database_backups; + $this->telegramNotificationsScheduledTasks = $this->team->telegram_notifications_scheduled_tasks; + $this->telegramNotificationsTestMessageThreadId = $this->team->telegram_notifications_test_message_thread_id; + $this->telegramNotificationsDeploymentsMessageThreadId = $this->team->telegram_notifications_deployments_message_thread_id; + $this->telegramNotificationsStatusChangesMessageThreadId = $this->team->telegram_notifications_status_changes_message_thread_id; + $this->telegramNotificationsDatabaseBackupsMessageThreadId = $this->team->telegram_notifications_database_backups_message_thread_id; + $this->telegramNotificationsScheduledTasksThreadId = $this->team->telegram_notifications_scheduled_tasks_thread_id; + $this->telegramNotificationsServerDiskUsage = $this->team->telegram_notifications_server_disk_usage; + } + } public function instantSave() { try { - $this->submit(); + $this->syncData(true); } catch (\Throwable $e) { - ray($e->getMessage()); - $this->team->telegram_enabled = false; - $this->validate(); + return handleError($e, $this); } } public function submit() { - $this->resetErrorBag(); - $this->validate(); - $this->saveModel(); + try { + $this->resetErrorBag(); + $this->syncData(true); + $this->saveModel(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSaveTelegramEnabled() + { + try { + $this->validate([ + 'telegramToken' => 'required', + 'telegramChatId' => 'required', + ], [ + 'telegramToken.required' => 'Telegram Token is required.', + 'telegramChatId.required' => 'Telegram Chat ID is required.', + ]); + $this->saveModel(); + } catch (\Throwable $e) { + $this->telegramEnabled = false; + + return handleError($e, $this); + } } public function saveModel() { - $this->team->save(); + $this->syncData(true); refreshSession(); $this->dispatch('success', 'Settings saved.'); } public function sendTestNotification() { - $this->team?->notify(new Test); - $this->dispatch('success', 'Test notification sent.'); + try { + $this->team->notify(new Test); + $this->dispatch('success', 'Test notification sent.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Profile/Index.php b/app/Livewire/Profile/Index.php index 3be1b05ce8..53314cd5c3 100644 --- a/app/Livewire/Profile/Index.php +++ b/app/Livewire/Profile/Index.php @@ -2,7 +2,9 @@ namespace App\Livewire\Profile; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Illuminate\Validation\Rules\Password; use Livewire\Attributes\Validate; use Livewire\Component; @@ -23,9 +25,9 @@ class Index extends Component public function mount() { - $this->userId = auth()->user()->id; - $this->name = auth()->user()->name; - $this->email = auth()->user()->email; + $this->userId = Auth::id(); + $this->name = Auth::user()->name; + $this->email = Auth::user()->email; } public function submit() @@ -34,7 +36,7 @@ public function submit() $this->validate([ 'name' => 'required', ]); - auth()->user()->update([ + Auth::user()->update([ 'name' => $this->name, ]); @@ -48,9 +50,8 @@ public function resetPassword() { try { $this->validate([ - 'current_password' => 'required', - 'new_password' => 'required|min:8', - 'new_password_confirmation' => 'required|min:8|same:new_password', + 'current_password' => ['required'], + 'new_password' => ['required', Password::defaults(), 'confirmed'], ]); if (! Hash::check($this->current_password, auth()->user()->password)) { $this->dispatch('error', 'Current password is incorrect.'); diff --git a/app/Livewire/Project/AddEmpty.php b/app/Livewire/Project/AddEmpty.php index c3353be848..fd976548ab 100644 --- a/app/Livewire/Project/AddEmpty.php +++ b/app/Livewire/Project/AddEmpty.php @@ -3,24 +3,17 @@ namespace App\Livewire\Project; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class AddEmpty extends Component { - public string $name = ''; + #[Validate(['required', 'string', 'min:3'])] + public string $name; + #[Validate(['nullable', 'string'])] public string $description = ''; - protected $rules = [ - 'name' => 'required|string|min:3', - 'description' => 'nullable|string', - ]; - - protected $validationAttributes = [ - 'name' => 'Project Name', - 'description' => 'Project Description', - ]; - public function submit() { try { @@ -34,8 +27,6 @@ public function submit() return redirect()->route('project.show', $project->uuid); } catch (\Throwable $e) { return handleError($e, $this); - } finally { - $this->name = ''; } } } diff --git a/app/Livewire/Project/AddEnvironment.php b/app/Livewire/Project/AddEnvironment.php deleted file mode 100644 index 7b2767dc6e..0000000000 --- a/app/Livewire/Project/AddEnvironment.php +++ /dev/null @@ -1,44 +0,0 @@ - 'required|string|min:3', - ]; - - protected $validationAttributes = [ - 'name' => 'Environment Name', - ]; - - public function submit() - { - try { - $this->validate(); - $environment = Environment::create([ - 'name' => $this->name, - 'project_id' => $this->project->id, - ]); - - return redirect()->route('project.resource.index', [ - 'project_uuid' => $this->project->uuid, - 'environment_name' => $environment->name, - ]); - } catch (\Throwable $e) { - handleError($e, $this); - } finally { - $this->name = ''; - } - } -} diff --git a/app/Livewire/Project/Application/Advanced.php b/app/Livewire/Project/Application/Advanced.php index a3a688f7c4..05ac25429e 100644 --- a/app/Livewire/Project/Application/Advanced.php +++ b/app/Livewire/Project/Application/Advanced.php @@ -3,120 +3,200 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use Livewire\Attributes\Validate; use Livewire\Component; class Advanced extends Component { public Application $application; - public bool $is_force_https_enabled; - - public bool $is_gzip_enabled; - - public bool $is_stripprefix_enabled; - - protected $rules = [ - 'application.settings.is_git_submodules_enabled' => 'boolean|required', - 'application.settings.is_git_lfs_enabled' => 'boolean|required', - 'application.settings.is_preview_deployments_enabled' => 'boolean|required', - 'application.settings.is_auto_deploy_enabled' => 'boolean|required', - 'is_force_https_enabled' => 'boolean|required', - 'application.settings.is_log_drain_enabled' => 'boolean|required', - 'application.settings.is_gpu_enabled' => 'boolean|required', - 'application.settings.is_build_server_enabled' => 'boolean|required', - 'application.settings.is_consistent_container_name_enabled' => 'boolean|required', - 'application.settings.custom_internal_name' => 'string|nullable', - 'application.settings.is_gzip_enabled' => 'boolean|required', - 'application.settings.is_stripprefix_enabled' => 'boolean|required', - 'application.settings.gpu_driver' => 'string|required', - 'application.settings.gpu_count' => 'string|required', - 'application.settings.gpu_device_ids' => 'string|required', - 'application.settings.gpu_options' => 'string|required', - 'application.settings.is_raw_compose_deployment_enabled' => 'boolean|required', - 'application.settings.connect_to_docker_network' => 'boolean|required', - ]; + #[Validate(['boolean'])] + public bool $isForceHttpsEnabled = false; + + #[Validate(['boolean'])] + public bool $isGitSubmodulesEnabled = false; + + #[Validate(['boolean'])] + public bool $isGitLfsEnabled = false; + + #[Validate(['boolean'])] + public bool $isPreviewDeploymentsEnabled = false; + + #[Validate(['boolean'])] + public bool $isAutoDeployEnabled = true; + + #[Validate(['boolean'])] + public bool $isLogDrainEnabled = false; + + #[Validate(['boolean'])] + public bool $isGpuEnabled = false; + + #[Validate(['string'])] + public string $gpuDriver = ''; + + #[Validate(['string', 'nullable'])] + public ?string $gpuCount = null; + + #[Validate(['string', 'nullable'])] + public ?string $gpuDeviceIds = null; + + #[Validate(['string', 'nullable'])] + public ?string $gpuOptions = null; + + #[Validate(['boolean'])] + public bool $isBuildServerEnabled = false; + + #[Validate(['boolean'])] + public bool $isConsistentContainerNameEnabled = false; + + #[Validate(['string', 'nullable'])] + public ?string $customInternalName = null; + + #[Validate(['boolean'])] + public bool $isGzipEnabled = true; + + #[Validate(['boolean'])] + public bool $isStripprefixEnabled = true; + + #[Validate(['boolean'])] + public bool $isRawComposeDeploymentEnabled = false; + + #[Validate(['boolean'])] + public bool $isConnectToDockerNetworkEnabled = false; public function mount() { - $this->is_force_https_enabled = $this->application->isForceHttpsEnabled(); - $this->is_gzip_enabled = $this->application->isGzipEnabled(); - $this->is_stripprefix_enabled = $this->application->isStripprefixEnabled(); + try { + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->application->settings->is_force_https_enabled = $this->isForceHttpsEnabled; + $this->application->settings->is_git_submodules_enabled = $this->isGitSubmodulesEnabled; + $this->application->settings->is_git_lfs_enabled = $this->isGitLfsEnabled; + $this->application->settings->is_preview_deployments_enabled = $this->isPreviewDeploymentsEnabled; + $this->application->settings->is_auto_deploy_enabled = $this->isAutoDeployEnabled; + $this->application->settings->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->application->settings->is_gpu_enabled = $this->isGpuEnabled; + $this->application->settings->gpu_driver = $this->gpuDriver; + $this->application->settings->gpu_count = $this->gpuCount; + $this->application->settings->gpu_device_ids = $this->gpuDeviceIds; + $this->application->settings->gpu_options = $this->gpuOptions; + $this->application->settings->is_build_server_enabled = $this->isBuildServerEnabled; + $this->application->settings->is_consistent_container_name_enabled = $this->isConsistentContainerNameEnabled; + $this->application->settings->custom_internal_name = $this->customInternalName; + $this->application->settings->is_gzip_enabled = $this->isGzipEnabled; + $this->application->settings->is_stripprefix_enabled = $this->isStripprefixEnabled; + $this->application->settings->is_raw_compose_deployment_enabled = $this->isRawComposeDeploymentEnabled; + $this->application->settings->connect_to_docker_network = $this->isConnectToDockerNetworkEnabled; + $this->application->settings->save(); + } else { + $this->isForceHttpsEnabled = $this->application->isForceHttpsEnabled(); + $this->isGzipEnabled = $this->application->isGzipEnabled(); + $this->isStripprefixEnabled = $this->application->isStripprefixEnabled(); + $this->isLogDrainEnabled = $this->application->isLogDrainEnabled(); + + $this->isGitSubmodulesEnabled = $this->application->settings->is_git_submodules_enabled; + $this->isGitLfsEnabled = $this->application->settings->is_git_lfs_enabled; + $this->isPreviewDeploymentsEnabled = $this->application->settings->is_preview_deployments_enabled; + $this->isAutoDeployEnabled = $this->application->settings->is_auto_deploy_enabled; + $this->isGpuEnabled = $this->application->settings->is_gpu_enabled; + $this->gpuDriver = $this->application->settings->gpu_driver; + $this->gpuCount = $this->application->settings->gpu_count; + $this->gpuDeviceIds = $this->application->settings->gpu_device_ids; + $this->gpuOptions = $this->application->settings->gpu_options; + $this->isBuildServerEnabled = $this->application->settings->is_build_server_enabled; + $this->isConsistentContainerNameEnabled = $this->application->settings->is_consistent_container_name_enabled; + $this->customInternalName = $this->application->settings->custom_internal_name; + $this->isRawComposeDeploymentEnabled = $this->application->settings->is_raw_compose_deployment_enabled; + $this->isConnectToDockerNetworkEnabled = $this->application->settings->connect_to_docker_network; + } } public function instantSave() { - if ($this->application->isLogDrainEnabled()) { - if (! $this->application->destination->server->isLogDrainEnabled()) { - $this->application->settings->is_log_drain_enabled = false; - $this->dispatch('error', 'Log drain is not enabled on this server.'); + try { + if ($this->isLogDrainEnabled) { + if (! $this->application->destination->server->isLogDrainEnabled()) { + $this->isLogDrainEnabled = false; + $this->syncData(true); + $this->dispatch('error', 'Log drain is not enabled on this server.'); + + return; + } + } + if ($this->application->isForceHttpsEnabled() !== $this->isForceHttpsEnabled || + $this->application->isGzipEnabled() !== $this->isGzipEnabled || + $this->application->isStripprefixEnabled() !== $this->isStripprefixEnabled + ) { + $this->dispatch('resetDefaultLabels', false); + } - return; + if ($this->application->settings->is_raw_compose_deployment_enabled) { + $this->application->oldRawParser(); + } else { + $this->application->parse(); } + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + $this->dispatch('configurationChanged'); + } catch (\Throwable $e) { + return handleError($e, $this); } - if ($this->application->settings->is_force_https_enabled !== $this->is_force_https_enabled) { - $this->application->settings->is_force_https_enabled = $this->is_force_https_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_gzip_enabled !== $this->is_gzip_enabled) { - $this->application->settings->is_gzip_enabled = $this->is_gzip_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_stripprefix_enabled !== $this->is_stripprefix_enabled) { - $this->application->settings->is_stripprefix_enabled = $this->is_stripprefix_enabled; - $this->dispatch('resetDefaultLabels', false); - } - if ($this->application->settings->is_raw_compose_deployment_enabled) { - $this->application->oldRawParser(); - } else { - $this->application->parse(); - } - $this->application->settings->save(); - $this->dispatch('success', 'Settings saved.'); - $this->dispatch('configurationChanged'); } public function submit() { - if ($this->application->settings->gpu_count && $this->application->settings->gpu_device_ids) { - $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); - $this->application->settings->gpu_count = null; - $this->application->settings->gpu_device_ids = null; - $this->application->settings->save(); + try { + if ($this->gpuCount && $this->gpuDeviceIds) { + $this->dispatch('error', 'You cannot set both GPU count and GPU device IDs.'); + $this->gpuCount = null; + $this->gpuDeviceIds = null; + $this->syncData(true); - return; + return; + } + $this->syncData(true); + $this->dispatch('success', 'Settings saved.'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->application->settings->save(); - $this->dispatch('success', 'Settings saved.'); } public function saveCustomName() { - if (str($this->application->settings->custom_internal_name)->isNotEmpty()) { - $this->application->settings->custom_internal_name = str($this->application->settings->custom_internal_name)->slug()->value(); + if (str($this->customInternalName)->isNotEmpty()) { + $this->customInternalName = str($this->customInternalName)->slug()->value(); } else { - $this->application->settings->custom_internal_name = null; + $this->customInternalName = null; } - if (is_null($this->application->settings->custom_internal_name)) { - $this->application->settings->save(); + if (is_null($this->customInternalName)) { + $this->syncData(true); $this->dispatch('success', 'Custom name saved.'); return; } - $customInternalName = $this->application->settings->custom_internal_name; + $customInternalName = $this->customInternalName; $server = $this->application->destination->server; $allApplications = $server->applications(); $foundSameInternalName = $allApplications->filter(function ($application) { - return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->application->settings->custom_internal_name; + return $application->id !== $this->application->id && $application->settings->custom_internal_name === $this->customInternalName; }); if ($foundSameInternalName->isNotEmpty()) { $this->dispatch('error', 'This custom container name is already in use by another application on this server.'); - $this->application->settings->custom_internal_name = $customInternalName; - $this->application->settings->refresh(); + $this->customInternalName = $customInternalName; + $this->syncData(true); return; } - $this->application->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Custom name saved.'); } diff --git a/app/Livewire/Project/Application/Deployment/Show.php b/app/Livewire/Project/Application/Deployment/Show.php index 3de895f8c5..04170fa280 100644 --- a/app/Livewire/Project/Application/Deployment/Show.php +++ b/app/Livewire/Project/Application/Deployment/Show.php @@ -64,7 +64,7 @@ public function polling() { $this->dispatch('deploymentFinished'); $this->application_deployment_queue->refresh(); - if (data_get($this->application_deployment_queue, 'status') == 'finished' || data_get($this->application_deployment_queue, 'status') == 'failed') { + if (data_get($this->application_deployment_queue, 'status') === 'finished' || data_get($this->application_deployment_queue, 'status') === 'failed') { $this->isKeepAliveOn = false; } } diff --git a/app/Livewire/Project/Application/DeploymentNavbar.php b/app/Livewire/Project/Application/DeploymentNavbar.php index 5fccce792a..6a6fa24823 100644 --- a/app/Livewire/Project/Application/DeploymentNavbar.php +++ b/app/Livewire/Project/Application/DeploymentNavbar.php @@ -46,8 +46,6 @@ public function force_start() try { force_start_deployment($this->application_deployment_queue); } catch (\Throwable $e) { - ray($e); - return handleError($e, $this); } } @@ -81,8 +79,6 @@ public function cancel() } instant_remote_process([$kill_command], $server); } catch (\Throwable $e) { - ray($e); - return handleError($e, $this); } finally { $this->application_deployment_queue->update([ diff --git a/app/Livewire/Project/Application/General.php b/app/Livewire/Project/Application/General.php index 2e327d80f2..f1575a01f0 100644 --- a/app/Livewire/Project/Application/General.php +++ b/app/Livewire/Project/Application/General.php @@ -241,7 +241,6 @@ public function updatedApplicationBaseDirectory() } } - public function updatedApplicationBuildPack() { if ($this->application->build_pack !== 'nixpacks') { @@ -275,10 +274,10 @@ public function getWildcardDomain() } } - public function resetDefaultLabels() + public function resetDefaultLabels($manualReset = false) { try { - if ($this->application->settings->is_container_label_readonly_enabled) { + if ($this->application->settings->is_container_label_readonly_enabled && ! $manualReset) { return; } $this->customLabels = str(implode('|coolify|', generateLabelsApplication($this->application)))->replace('|coolify|', "\n"); @@ -314,7 +313,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).'); @@ -335,9 +334,15 @@ public function submit($showToaster = true) $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); }); + $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } $this->resetDefaultLabels(); if ($this->application->isDirty('redirect')) { @@ -403,17 +408,19 @@ public function submit($showToaster = true) } $this->application->custom_labels = base64_encode($this->customLabels); $this->application->save(); - $showToaster && $this->dispatch('success', 'Application settings updated!'); + $showToaster && ! $warning && $this->dispatch('success', 'Application settings updated!'); } catch (\Throwable $e) { $originalFqdn = $this->application->getOriginal('fqdn'); if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; } + return handleError($e, $this); } finally { $this->dispatch('configurationChanged'); } } + public function downloadConfig() { $config = GenerateConfig::run($this->application, true); @@ -423,7 +430,7 @@ public function downloadConfig() echo $config; }, $fileName, [ 'Content-Type' => 'application/json', - 'Content-Disposition' => 'attachment; filename=' . $fileName, + 'Content-Disposition' => 'attachment; filename='.$fileName, ]); } } diff --git a/app/Livewire/Project/Application/Preview/Form.php b/app/Livewire/Project/Application/Preview/Form.php index 9a0b9b8518..c7b2e8184d 100644 --- a/app/Livewire/Project/Application/Preview/Form.php +++ b/app/Livewire/Project/Application/Preview/Form.php @@ -10,49 +10,53 @@ class Form extends Component { public Application $application; - public string $preview_url_template; + #[Validate('required')] + public string $previewUrlTemplate; - protected $rules = [ - 'application.preview_url_template' => 'required', - ]; - - protected $validationAttributes = [ - 'application.preview_url_template' => 'preview url template', - ]; - - public function resetToDefault() + public function mount() { - $this->application->preview_url_template = '{{pr_id}}.{{domain}}'; - $this->preview_url_template = $this->application->preview_url_template; - $this->application->save(); - $this->generate_real_url(); + try { + $this->previewUrlTemplate = $this->application->preview_url_template; + $this->generateRealUrl(); + } catch (\Throwable $e) { + return handleError($e, $this); + } } - public function generate_real_url() + public function submit() { - if (data_get($this->application, 'fqdn')) { - 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.'); - } + try { + $this->resetErrorBag(); + $this->validate(); + $this->application->preview_url_template = str_replace(' ', '', $this->previewUrlTemplate); + $this->application->save(); + $this->dispatch('success', 'Preview url template updated.'); + $this->generateRealUrl(); + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function mount() + public function resetToDefault() { - $this->generate_real_url(); + try { + $this->application->preview_url_template = '{{pr_id}}.{{domain}}'; + $this->previewUrlTemplate = $this->application->preview_url_template; + $this->application->save(); + $this->generateRealUrl(); + $this->dispatch('success', 'Preview url template updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } - public function submit() + public function generateRealUrl() { - $this->validate(); - $this->application->preview_url_template = str_replace(' ', '', $this->application->preview_url_template); - $this->application->save(); - $this->dispatch('success', 'Preview url template updated.'); - $this->generate_real_url(); + if (data_get($this->application, 'fqdn')) { + $firstFqdn = str($this->application->fqdn)->before(','); + $url = Url::fromString($firstFqdn); + $host = $url->getHost(); + $this->previewUrlTemplate = str($this->application->preview_url_template)->replace('{{domain}}', $host); + } } } diff --git a/app/Livewire/Project/Application/Previews.php b/app/Livewire/Project/Application/Previews.php index b1ba035dc4..d42bf03d78 100644 --- a/app/Livewire/Project/Application/Previews.php +++ b/app/Livewire/Project/Application/Previews.php @@ -5,6 +5,7 @@ use App\Actions\Docker\GetContainersStatus; use App\Models\Application; use App\Models\ApplicationPreview; +use Carbon\Carbon; use Illuminate\Process\InvokedProcess; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Process; @@ -239,7 +240,7 @@ private function stopContainers(array $containers, $server, int $timeout) $processes[$containerName] = $this->stopContainer($containerName, $timeout); } - $startTime = time(); + $startTime = Carbon::now()->getTimestamp(); while (count($processes) > 0) { $finishedProcesses = array_filter($processes, function ($process) { return ! $process->running(); @@ -249,7 +250,7 @@ private function stopContainers(array $containers, $server, int $timeout) $this->removeContainer($containerName, $server); } - if (time() - $startTime >= $timeout) { + if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { $this->forceStopRemainingContainers(array_keys($processes), $server); break; } diff --git a/app/Livewire/Project/Application/Source.php b/app/Livewire/Project/Application/Source.php index 426626e558..ade297d501 100644 --- a/app/Livewire/Project/Application/Source.php +++ b/app/Livewire/Project/Application/Source.php @@ -4,55 +4,92 @@ use App\Models\Application; use App\Models\PrivateKey; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Source extends Component { - public $applicationId; - public Application $application; - public $private_keys; + #[Locked] + public $privateKeys; + + #[Validate(['nullable', 'string'])] + public ?string $privateKeyName = null; + + #[Validate(['nullable', 'integer'])] + public ?int $privateKeyId = null; - protected $rules = [ - 'application.git_repository' => 'required', - 'application.git_branch' => 'required', - 'application.git_commit_sha' => 'nullable', - ]; + #[Validate(['required', 'string'])] + public string $gitRepository; - protected $validationAttributes = [ - 'application.git_repository' => 'repository', - 'application.git_branch' => 'branch', - 'application.git_commit_sha' => 'commit sha', - ]; + #[Validate(['required', 'string'])] + public string $gitBranch; + + #[Validate(['nullable', 'string'])] + public ?string $gitCommitSha = null; public function mount() { - $this->get_private_keys(); + try { + $this->syncData(); + $this->getPrivateKeys(); + } catch (\Throwable $e) { + handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->application->update([ + 'git_repository' => $this->gitRepository, + 'git_branch' => $this->gitBranch, + 'git_commit_sha' => $this->gitCommitSha, + 'private_key_id' => $this->privateKeyId, + ]); + } else { + $this->gitRepository = $this->application->git_repository; + $this->gitBranch = $this->application->git_branch; + $this->gitCommitSha = $this->application->git_commit_sha; + $this->privateKeyId = $this->application->private_key_id; + $this->privateKeyName = data_get($this->application, 'private_key.name'); + } } - private function get_private_keys() + private function getPrivateKeys() { - $this->private_keys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { - return $key->id == $this->application->private_key_id; + $this->privateKeys = PrivateKey::whereTeamId(currentTeam()->id)->get()->reject(function ($key) { + return $key->id == $this->privateKeyId; }); } - public function setPrivateKey(int $private_key_id) + public function setPrivateKey(int $privateKeyId) { - $this->application->private_key_id = $private_key_id; - $this->application->save(); - $this->application->refresh(); - $this->get_private_keys(); + try { + $this->privateKeyId = $privateKeyId; + $this->syncData(true); + $this->getPrivateKeys(); + $this->application->refresh(); + $this->privateKeyName = $this->application->private_key->name; + $this->dispatch('success', 'Private key updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); - if (! $this->application->git_commit_sha) { - $this->application->git_commit_sha = 'HEAD'; + try { + if (str($this->gitCommitSha)->isEmpty()) { + $this->gitCommitSha = 'HEAD'; + } + $this->syncData(true); + $this->dispatch('success', 'Application source updated!'); + } catch (\Throwable $e) { + return handleError($e, $this); } - $this->application->save(); - $this->dispatch('success', 'Application source updated!'); } } diff --git a/app/Livewire/Project/Application/Swarm.php b/app/Livewire/Project/Application/Swarm.php index 0151b52226..197dc41ed3 100644 --- a/app/Livewire/Project/Application/Swarm.php +++ b/app/Livewire/Project/Application/Swarm.php @@ -3,32 +3,55 @@ namespace App\Livewire\Project\Application; use App\Models\Application; +use Livewire\Attributes\Validate; use Livewire\Component; class Swarm extends Component { public Application $application; - public string $swarm_placement_constraints = ''; + #[Validate('required')] + public int $swarmReplicas; - protected $rules = [ - 'application.swarm_replicas' => 'required', - 'application.swarm_placement_constraints' => 'nullable', - 'application.settings.is_swarm_only_worker_nodes' => 'required', - ]; + #[Validate(['nullable'])] + public ?string $swarmPlacementConstraints = null; + + #[Validate('required')] + public bool $isSwarmOnlyWorkerNodes; public function mount() { - if ($this->application->swarm_placement_constraints) { - $this->swarm_placement_constraints = base64_decode($this->application->swarm_placement_constraints); + try { + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function instantSave() + public function syncData(bool $toModel = false) { - try { + if ($toModel) { $this->validate(); + $this->application->swarm_replicas = $this->swarmReplicas; + $this->application->swarm_placement_constraints = $this->swarmPlacementConstraints ? base64_encode($this->swarmPlacementConstraints) : null; + $this->application->settings->is_swarm_only_worker_nodes = $this->isSwarmOnlyWorkerNodes; + $this->application->save(); $this->application->settings->save(); + } else { + $this->swarmReplicas = $this->application->swarm_replicas; + if ($this->application->swarm_placement_constraints) { + $this->swarmPlacementConstraints = base64_decode($this->application->swarm_placement_constraints); + } else { + $this->swarmPlacementConstraints = null; + } + $this->isSwarmOnlyWorkerNodes = $this->application->settings->is_swarm_only_worker_nodes; + } + } + + public function instantSave() + { + try { + $this->syncData(true); $this->dispatch('success', 'Swarm settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); @@ -38,14 +61,7 @@ public function instantSave() public function submit() { try { - $this->validate(); - if ($this->swarm_placement_constraints) { - $this->application->swarm_placement_constraints = base64_encode($this->swarm_placement_constraints); - } else { - $this->application->swarm_placement_constraints = null; - } - $this->application->save(); - + $this->syncData(true); $this->dispatch('success', 'Swarm settings updated.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/Backup/Index.php b/app/Livewire/Project/Database/Backup/Index.php index d9a4b623d0..9ff2f48d5f 100644 --- a/app/Livewire/Project/Database/Backup/Index.php +++ b/app/Livewire/Project/Database/Backup/Index.php @@ -24,10 +24,10 @@ public function mount() } // No backups if ( - $database->getMorphClass() === 'App\Models\StandaloneRedis' || - $database->getMorphClass() === 'App\Models\StandaloneKeydb' || - $database->getMorphClass() === 'App\Models\StandaloneDragonfly' || - $database->getMorphClass() === 'App\Models\StandaloneClickhouse' + $database->getMorphClass() === \App\Models\StandaloneRedis::class || + $database->getMorphClass() === \App\Models\StandaloneKeydb::class || + $database->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $database->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { return redirect()->route('project.database.configuration', [ 'project_uuid' => $project->uuid, diff --git a/app/Livewire/Project/Database/BackupEdit.php b/app/Livewire/Project/Database/BackupEdit.php index 7e2e4a12b5..bcf2f959ea 100644 --- a/app/Livewire/Project/Database/BackupEdit.php +++ b/app/Livewire/Project/Database/BackupEdit.php @@ -2,66 +2,100 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; +use Exception; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; use Spatie\Url\Url; class BackupEdit extends Component { - public ?ScheduledDatabaseBackup $backup; + public ScheduledDatabaseBackup $backup; + #[Locked] public $s3s; + #[Locked] + public $parameters; + + #[Validate(['required', 'boolean'])] public bool $delete_associated_backups_locally = false; + #[Validate(['required', 'boolean'])] public bool $delete_associated_backups_s3 = false; + #[Validate(['required', 'boolean'])] public bool $delete_associated_backups_sftp = false; + #[Validate(['nullable', 'string'])] public ?string $status = null; - public array $parameters; - - protected $rules = [ - 'backup.enabled' => 'required|boolean', - 'backup.frequency' => 'required|string', - 'backup.number_of_backups_locally' => 'required|integer|min:1', - 'backup.save_s3' => 'required|boolean', - 'backup.s3_storage_id' => 'nullable|integer', - 'backup.databases_to_backup' => 'nullable', - 'backup.dump_all' => 'required|boolean', - ]; - - protected $validationAttributes = [ - 'backup.enabled' => 'Enabled', - 'backup.frequency' => 'Frequency', - 'backup.number_of_backups_locally' => 'Number of Backups Locally', - '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 = [ - 'backup.s3_storage_id' => 'Select a S3 Storage', - ]; + #[Validate(['required', 'boolean'])] + public bool $backupEnabled = false; + + #[Validate(['required', 'string'])] + public string $frequency = ''; + + #[Validate(['required', 'integer', 'min:1'])] + public int $numberOfBackupsLocally = 1; + + #[Validate(['required', 'boolean'])] + public bool $saveS3 = false; + + #[Validate(['required', 'integer'])] + public int $s3StorageId = 1; + + #[Validate(['nullable', 'string'])] + public ?string $databasesToBackup = null; + + #[Validate(['required', 'boolean'])] + public bool $dumpAll = false; public function mount() { - $this->parameters = get_route_parameters(); - if (is_null(data_get($this->backup, 's3_storage_id'))) { - data_set($this->backup, 's3_storage_id', 'default'); + try { + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (Exception $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->customValidate(); + $this->backup->enabled = $this->backupEnabled; + $this->backup->frequency = $this->frequency; + $this->backup->number_of_backups_locally = $this->numberOfBackupsLocally; + $this->backup->save_s3 = $this->saveS3; + $this->backup->s3_storage_id = $this->s3StorageId; + $this->backup->databases_to_backup = $this->databasesToBackup; + $this->backup->dump_all = $this->dumpAll; + $this->backup->save(); + } else { + $this->backupEnabled = $this->backup->enabled; + $this->frequency = $this->backup->frequency; + $this->numberOfBackupsLocally = $this->backup->number_of_backups_locally; + $this->saveS3 = $this->backup->save_s3; + $this->s3StorageId = $this->backup->s3_storage_id; + $this->databasesToBackup = $this->backup->databases_to_backup; + $this->dumpAll = $this->backup->dump_all; } } public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { @@ -74,7 +108,7 @@ public function delete($password) $this->backup->delete(); - if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $previousUrl = url()->previous(); $url = Url::fromString($previousUrl); $url = $url->withoutQueryParameter('selectedBackupId'); @@ -93,16 +127,14 @@ public function delete($password) public function instantSave() { try { - $this->custom_validate(); - $this->backup->save(); - $this->backup->refresh(); + $this->syncData(true); $this->dispatch('success', 'Backup updated successfully.'); } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } } - private function custom_validate() + private function customValidate() { if (! is_numeric($this->backup->s3_storage_id)) { $this->backup->s3_storage_id = null; @@ -117,25 +149,20 @@ private function custom_validate() public function submit() { try { - $this->custom_validate(); - if ($this->backup->databases_to_backup == '' || $this->backup->databases_to_backup === null) { - $this->backup->databases_to_backup = null; - } - $this->backup->save(); - $this->backup->refresh(); - $this->dispatch('success', 'Backup updated successfully'); + $this->syncData(true); + $this->dispatch('success', 'Backup updated successfully.'); } catch (\Throwable $e) { $this->dispatch('error', $e->getMessage()); } } - public function deleteAssociatedBackupsLocally() + private function deleteAssociatedBackupsLocally() { $executions = $this->backup->executions; $backupFolder = null; foreach ($executions as $execution) { - if ($this->backup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->backup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $server = $this->backup->database->service->destination->server; } else { $server = $this->backup->database->destination->server; @@ -149,17 +176,17 @@ public function deleteAssociatedBackupsLocally() $execution->delete(); } - if ($backupFolder) { + if (str($backupFolder)->isNotEmpty()) { $this->deleteEmptyBackupFolder($backupFolder, $server); } } - public function deleteAssociatedBackupsS3() + private function deleteAssociatedBackupsS3() { //Add function to delete backups from S3 } - public function deleteAssociatedBackupsSftp() + private function deleteAssociatedBackupsSftp() { //Add function to delete backups from SFTP } diff --git a/app/Livewire/Project/Database/BackupExecutions.php b/app/Livewire/Project/Database/BackupExecutions.php index c8c33a0223..f91b8bfaf3 100644 --- a/app/Livewire/Project/Database/BackupExecutions.php +++ b/app/Livewire/Project/Database/BackupExecutions.php @@ -2,10 +2,10 @@ namespace App\Livewire\Project\Database; +use App\Models\InstanceSettings; use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; -use Livewire\Attributes\On; use Livewire\Component; class BackupExecutions extends Component @@ -28,7 +28,6 @@ public function getListeners() return [ "echo-private:team.{$userId},BackupCreated" => 'refreshBackupExecutions', - 'deleteBackup', ]; } @@ -41,13 +40,14 @@ public function cleanupFailed() } } - #[On('deleteBackup')] public function deleteBackup($executionId, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $execution = $this->backup->executions()->where('id', $executionId)->first(); @@ -57,7 +57,7 @@ public function deleteBackup($executionId, $password) return; } - if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->service->destination->server); } else { delete_backup_locally($execution->filename, $execution->scheduledDatabaseBackup->database->destination->server); @@ -119,9 +119,8 @@ public function getServerTimezone() if (! $server) { return 'UTC'; } - $serverTimezone = $server->settings->server_timezone; - return $serverTimezone; + return $server->settings->server_timezone; } public function formatDateInServerTimezone($date) @@ -130,7 +129,7 @@ public function formatDateInServerTimezone($date) $dateObj = new \DateTime($date); try { $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); - } catch (\Exception $e) { + } catch (\Exception) { $dateObj->setTimezone(new \DateTimeZone('UTC')); } diff --git a/app/Livewire/Project/Database/Clickhouse/General.php b/app/Livewire/Project/Database/Clickhouse/General.php index 7a6446815b..2d39c5151f 100644 --- a/app/Livewire/Project/Database/Clickhouse/General.php +++ b/app/Livewire/Project/Database/Clickhouse/General.php @@ -7,6 +7,8 @@ use App\Models\Server; use App\Models\StandaloneClickhouse; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component @@ -15,54 +17,106 @@ class General extends Component public StandaloneClickhouse $database; - public ?string $db_url = null; - - public ?string $db_url_public = null; - - protected $listeners = ['refresh']; - - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.clickhouse_admin_user' => 'required', - 'database.clickhouse_admin_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - ]; - - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.clickhouse_admin_user' => 'Postgres User', - 'database.clickhouse_admin_password' => 'Postgres Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - ]; + #[Validate(['required', 'string'])] + public string $name; + + #[Validate(['nullable', 'string'])] + public ?string $description = null; + + #[Validate(['required', 'string'])] + public string $clickhouseAdminUser; + + #[Validate(['required', 'string'])] + public string $clickhouseAdminPassword; + + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->clickhouse_admin_user = $this->clickhouseAdminUser; + $this->database->clickhouse_admin_password = $this->clickhouseAdminPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->clickhouseAdminUser = $this->database->clickhouse_admin_user; + $this->clickhouseAdminPassword = $this->database->clickhouse_admin_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { @@ -73,16 +127,16 @@ public function instantSaveAdvanced() public function instantSave() { try { - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -92,28 +146,28 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; + $this->syncData(true); return handleError($e, $this); } } - public function refresh(): void + public function databaseProxyStopped() { - $this->database->refresh(); + $this->syncData(); } public function submit() { try { - if (str($this->database->public_port)->isEmpty()) { - $this->database->public_port = null; + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; } - $this->validate(); - $this->database->save(); + $this->syncData(true); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/Database/CreateScheduledBackup.php b/app/Livewire/Project/Database/CreateScheduledBackup.php index 5ed74a6c32..0903efdfdf 100644 --- a/app/Livewire/Project/Database/CreateScheduledBackup.php +++ b/app/Livewire/Project/Database/CreateScheduledBackup.php @@ -4,44 +4,45 @@ use App\Models\ScheduledDatabaseBackup; use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class CreateScheduledBackup extends Component { - public $database; - + #[Validate(['required', 'string'])] public $frequency; - public bool $enabled = true; + #[Validate(['required', 'boolean'])] + public bool $saveToS3 = false; - public bool $save_s3 = false; - - public $s3_storage_id; + #[Locked] + public $database; - public Collection $s3s; + public bool $enabled = true; - protected $rules = [ - 'frequency' => 'required|string', - 'save_s3' => 'required|boolean', - ]; + #[Validate(['required', 'integer'])] + public int $s3StorageId; - protected $validationAttributes = [ - 'frequency' => 'Backup Frequency', - 'save_s3' => 'Save to S3', - ]; + public Collection $definedS3s; public function mount() { - $this->s3s = currentTeam()->s3s; - if ($this->s3s->count() > 0) { - $this->s3_storage_id = $this->s3s->first()->id; + try { + $this->definedS3s = currentTeam()->s3s; + if ($this->definedS3s->count() > 0) { + $this->s3StorageId = $this->definedS3s->first()->id; + } + } catch (\Throwable $e) { + return handleError($e, $this); } } - public function submit(): void + public function submit() { try { $this->validate(); + $isValid = validate_cron_expression($this->frequency); if (! $isValid) { $this->dispatch('error', 'Invalid Cron / Human expression.'); @@ -51,8 +52,8 @@ public function submit(): void $payload = [ 'enabled' => true, 'frequency' => $this->frequency, - 'save_s3' => $this->save_s3, - 's3_storage_id' => $this->s3_storage_id, + 'save_s3' => $this->saveToS3, + 's3_storage_id' => $this->s3StorageId, 'database_id' => $this->database->id, 'database_type' => $this->database->getMorphClass(), 'team_id' => currentTeam()->id, @@ -66,16 +67,16 @@ public function submit(): void } $databaseBackup = ScheduledDatabaseBackup::create($payload); - if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $this->dispatch('refreshScheduledBackups', $databaseBackup->id); } else { $this->dispatch('refreshScheduledBackups'); } } catch (\Throwable $e) { - handleError($e, $this); + return handleError($e, $this); } finally { $this->frequency = ''; - $this->save_s3 = true; + $this->saveToS3 = true; } } } diff --git a/app/Livewire/Project/Database/Dragonfly/General.php b/app/Livewire/Project/Database/Dragonfly/General.php index 394ba6c9a3..ea6cd46b04 100644 --- a/app/Livewire/Project/Database/Dragonfly/General.php +++ b/app/Livewire/Project/Database/Dragonfly/General.php @@ -7,97 +7,131 @@ use App\Models\Server; use App\Models\StandaloneDragonfly; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; - public Server $server; public StandaloneDragonfly $database; - public ?string $db_url = null; - - public ?string $db_url_public = null; - - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.dragonfly_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - ]; - - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.dragonfly_password' => 'Redis Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - ]; + #[Validate(['required', 'string'])] + public string $name; + + #[Validate(['nullable', 'string'])] + public ?string $description = null; + + #[Validate(['required', 'string'])] + public string $dragonflyPassword; + + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() + { + $teamId = Auth::user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; + } public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + try { + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->dragonfly_password = $this->dragonflyPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; + $this->database->save(); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->dragonflyPassword = $this->database->dragonfly_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } } public function instantSaveAdvanced() { try { if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; + $this->isLogDrainEnabled = false; $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); return; } - $this->database->save(); - $this->dispatch('success', 'Database updated.'); - $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); - } catch (Exception $e) { - return handleError($e, $this); - } - } + $this->syncData(true); - public function submit() - { - try { - $this->validate(); - $this->database->save(); $this->dispatch('success', 'Database updated.'); + $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { return handleError($e, $this); - } finally { - if (is_null($this->database->config_hash)) { - $this->database->isConfigurationChanged(true); - } else { - $this->dispatch('configurationChanged'); - } } } public function instantSave() { try { - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -107,22 +141,37 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; + $this->syncData(true); return handleError($e, $this); } } - public function refresh(): void + public function databaseProxyStopped() { - $this->database->refresh(); + $this->syncData(); } - public function render() + public function submit() { - return view('livewire.project.database.dragonfly.general'); + try { + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; + } + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); + } catch (Exception $e) { + return handleError($e, $this); + } finally { + if (is_null($this->database->config_hash)) { + $this->database->isConfigurationChanged(true); + } else { + $this->dispatch('configurationChanged'); + } + } } } diff --git a/app/Livewire/Project/Database/Heading.php b/app/Livewire/Project/Database/Heading.php index 49884ff9af..fc0febd024 100644 --- a/app/Livewire/Project/Database/Heading.php +++ b/app/Livewire/Project/Database/Heading.php @@ -6,6 +6,7 @@ use App\Actions\Database\StartDatabase; use App\Actions\Database\StopDatabase; use App\Actions\Docker\GetContainersStatus; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Heading extends Component @@ -18,7 +19,7 @@ class Heading extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},DatabaseStatusChanged" => 'activityFinished', diff --git a/app/Livewire/Project/Database/Import.php b/app/Livewire/Project/Database/Import.php index dfaa4461b9..062f454b14 100644 --- a/app/Livewire/Project/Database/Import.php +++ b/app/Livewire/Project/Database/Import.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Database; use App\Models\Server; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Storage; use Livewire\Component; @@ -46,7 +47,7 @@ class Import extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},DatabaseStatusChanged" => '$refresh', @@ -77,10 +78,10 @@ public function getContainers() } if ( - $this->resource->getMorphClass() == 'App\Models\StandaloneRedis' || - $this->resource->getMorphClass() == 'App\Models\StandaloneKeydb' || - $this->resource->getMorphClass() == 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() == 'App\Models\StandaloneClickhouse' + $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { $this->unsupported = true; } @@ -88,8 +89,7 @@ public function getContainers() public function runImport() { - - if ($this->filename == '') { + if ($this->filename === '') { $this->dispatch('error', 'Please select a file to import.'); return; @@ -108,19 +108,19 @@ public function runImport() $this->importCommands[] = "docker cp {$tmpPath} {$this->container}:{$tmpPath}"; switch ($this->resource->getMorphClass()) { - case 'App\Models\StandaloneMariadb': + case \App\Models\StandaloneMariadb::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mariadbRestoreCommand} < {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandaloneMysql': + case \App\Models\StandaloneMysql::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mysqlRestoreCommand} < {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandalonePostgresql': + case \App\Models\StandalonePostgresql::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->postgresqlRestoreCommand} {$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; - case 'App\Models\StandaloneMongodb': + case \App\Models\StandaloneMongodb::class: $this->importCommands[] = "docker exec {$this->container} sh -c '{$this->mongodbRestoreCommand}{$tmpPath}'"; $this->importCommands[] = "rm {$tmpPath}"; break; diff --git a/app/Livewire/Project/Database/InitScript.php b/app/Livewire/Project/Database/InitScript.php index 3367629814..e3baa1c8ee 100644 --- a/app/Livewire/Project/Database/InitScript.php +++ b/app/Livewire/Project/Database/InitScript.php @@ -3,39 +3,39 @@ namespace App\Livewire\Project\Database; use Exception; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class InitScript extends Component { + #[Locked] public array $script; + #[Locked] public int $index; - public ?string $filename; + #[Validate(['nullable', 'string'])] + public ?string $filename = null; - public ?string $content; - - protected $rules = [ - 'filename' => 'required|string', - 'content' => 'required|string', - ]; - - protected $validationAttributes = [ - 'filename' => 'Filename', - 'content' => 'Content', - ]; + #[Validate(['nullable', 'string'])] + public ?string $content = null; public function mount() { - $this->index = data_get($this->script, 'index'); - $this->filename = data_get($this->script, 'filename'); - $this->content = data_get($this->script, 'content'); + try { + $this->index = data_get($this->script, 'index'); + $this->filename = data_get($this->script, 'filename'); + $this->content = data_get($this->script, 'content'); + } catch (Exception $e) { + return handleError($e, $this); + } } public function submit() { - $this->validate(); try { + $this->validate(); $this->script['index'] = $this->index; $this->script['content'] = $this->content; $this->script['filename'] = $this->filename; @@ -47,6 +47,10 @@ public function submit() public function delete() { - $this->dispatch('delete_init_script', $this->script); + try { + $this->dispatch('delete_init_script', $this->script); + } catch (Exception $e) { + return handleError($e, $this); + } } } diff --git a/app/Livewire/Project/Database/Keydb/General.php b/app/Livewire/Project/Database/Keydb/General.php index f976e1edde..e768495eb8 100644 --- a/app/Livewire/Project/Database/Keydb/General.php +++ b/app/Livewire/Project/Database/Keydb/General.php @@ -7,103 +7,136 @@ use App\Models\Server; use App\Models\StandaloneKeydb; use Exception; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Validate; use Livewire\Component; class General extends Component { - protected $listeners = ['refresh']; - public Server $server; public StandaloneKeydb $database; - public ?string $db_url = null; - - public ?string $db_url_public = null; - - protected $rules = [ - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.keydb_conf' => 'nullable', - 'database.keydb_password' => 'required', - 'database.image' => 'required', - 'database.ports_mappings' => 'nullable', - 'database.is_public' => 'nullable|boolean', - 'database.public_port' => 'nullable|integer', - 'database.is_log_drain_enabled' => 'nullable|boolean', - 'database.custom_docker_run_options' => 'nullable', - ]; - - protected $validationAttributes = [ - 'database.name' => 'Name', - 'database.description' => 'Description', - 'database.keydb_conf' => 'Redis Configuration', - 'database.keydb_password' => 'Redis Password', - 'database.image' => 'Image', - 'database.ports_mappings' => 'Port Mapping', - 'database.is_public' => 'Is Public', - 'database.public_port' => 'Public Port', - 'database.custom_docker_run_options' => 'Custom Docker Run Options', - ]; + #[Validate(['required', 'string'])] + public string $name; - public function mount() + #[Validate(['nullable', 'string'])] + public ?string $description = null; + + #[Validate(['nullable', 'string'])] + public ?string $keydbConf = null; + + #[Validate(['required', 'string'])] + public string $keydbPassword; + + #[Validate(['required', 'string'])] + public string $image; + + #[Validate(['nullable', 'string'])] + public ?string $portsMappings = null; + + #[Validate(['nullable', 'boolean'])] + public ?bool $isPublic = null; + + #[Validate(['nullable', 'integer'])] + public ?int $publicPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $customDockerRunOptions = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrl = null; + + #[Validate(['nullable', 'string'])] + public ?string $dbUrlPublic = null; + + #[Validate(['nullable', 'boolean'])] + public bool $isLogDrainEnabled = false; + + public function getListeners() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; - $this->server = data_get($this->database, 'destination.server'); + $teamId = Auth::user()->currentTeam()->id; + return [ + "echo-private:team.{$teamId},DatabaseProxyStopped" => 'databaseProxyStopped', + ]; } - public function instantSaveAdvanced() + public function mount() { try { - if (! $this->server->isLogDrainEnabled()) { - $this->database->is_log_drain_enabled = false; - $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + $this->syncData(); + $this->server = data_get($this->database, 'destination.server'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } - return; - } + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->database->name = $this->name; + $this->database->description = $this->description; + $this->database->keydb_conf = $this->keydbConf; + $this->database->keydb_password = $this->keydbPassword; + $this->database->image = $this->image; + $this->database->ports_mappings = $this->portsMappings; + $this->database->is_public = $this->isPublic; + $this->database->public_port = $this->publicPort; + $this->database->custom_docker_run_options = $this->customDockerRunOptions; + $this->database->is_log_drain_enabled = $this->isLogDrainEnabled; $this->database->save(); - $this->dispatch('success', 'Database updated.'); - $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); - } catch (Exception $e) { - return handleError($e, $this); + + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; + } else { + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->keydbConf = $this->database->keydb_conf; + $this->keydbPassword = $this->database->keydb_password; + $this->image = $this->database->image; + $this->portsMappings = $this->database->ports_mappings; + $this->isPublic = $this->database->is_public; + $this->publicPort = $this->database->public_port; + $this->customDockerRunOptions = $this->database->custom_docker_run_options; + $this->isLogDrainEnabled = $this->database->is_log_drain_enabled; + $this->dbUrl = $this->database->internal_db_url; + $this->dbUrlPublic = $this->database->external_db_url; } } - public function submit() + public function instantSaveAdvanced() { try { - $this->validate(); - if ($this->database->keydb_conf === '') { - $this->database->keydb_conf = null; + if (! $this->server->isLogDrainEnabled()) { + $this->isLogDrainEnabled = false; + $this->dispatch('error', 'Log drain is not enabled on the server. Please enable it first.'); + + return; } - $this->database->save(); + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); + $this->dispatch('success', 'You need to restart the service for the changes to take effect.'); } catch (Exception $e) { return handleError($e, $this); - } finally { - if (is_null($this->database->config_hash)) { - $this->database->isConfigurationChanged(true); - } else { - $this->dispatch('configurationChanged'); - } } } public function instantSave() { try { - if ($this->database->is_public && ! $this->database->public_port) { + if ($this->isPublic && ! $this->publicPort) { $this->dispatch('error', 'Public port is required.'); - $this->database->is_public = false; + $this->isPublic = false; return; } - if ($this->database->is_public) { + if ($this->isPublic) { if (! str($this->database->status)->startsWith('running')) { $this->dispatch('error', 'Database must be started to be publicly accessible.'); - $this->database->is_public = false; + $this->isPublic = false; return; } @@ -113,22 +146,37 @@ public function instantSave() StopDatabaseProxy::run($this->database); $this->dispatch('success', 'Database is no longer publicly accessible.'); } - $this->db_url_public = $this->database->external_db_url; - $this->database->save(); + $this->dbUrlPublic = $this->database->external_db_url; + $this->syncData(true); } catch (\Throwable $e) { - $this->database->is_public = ! $this->database->is_public; + $this->isPublic = ! $this->isPublic; + $this->syncData(true); return handleError($e, $this); } } - public function refresh(): void + public function databaseProxyStopped() { - $this->database->refresh(); + $this->syncData(); } - public function render() + public function submit() { - return view('livewire.project.database.keydb.general'); + try { + if (str($this->publicPort)->isEmpty()) { + $this->publicPort = null; + } + $this->syncData(true); + $this->dispatch('success', 'Database updated.'); + } catch (Exception $e) { + return handleError($e, $this); + } finally { + if (is_null($this->database->config_hash)) { + $this->database->isConfigurationChanged(true); + } else { + $this->dispatch('configurationChanged'); + } + } } } diff --git a/app/Livewire/Project/Database/Mariadb/General.php b/app/Livewire/Project/Database/Mariadb/General.php index 12d4882f3c..c9d473223f 100644 --- a/app/Livewire/Project/Database/Mariadb/General.php +++ b/app/Livewire/Project/Database/Mariadb/General.php @@ -57,7 +57,6 @@ public function mount() $this->db_url = $this->database->internal_db_url; $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - } public function instantSaveAdvanced() diff --git a/app/Livewire/Project/Database/Mongodb/General.php b/app/Livewire/Project/Database/Mongodb/General.php index ac40e7dfa0..e19895dae6 100644 --- a/app/Livewire/Project/Database/Mongodb/General.php +++ b/app/Livewire/Project/Database/Mongodb/General.php @@ -55,7 +55,6 @@ public function mount() $this->db_url = $this->database->internal_db_url; $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - } public function instantSaveAdvanced() diff --git a/app/Livewire/Project/Database/Redis/General.php b/app/Livewire/Project/Database/Redis/General.php index 72fd95de88..25a96b2926 100644 --- a/app/Livewire/Project/Database/Redis/General.php +++ b/app/Livewire/Project/Database/Redis/General.php @@ -11,12 +11,21 @@ class General extends Component { - protected $listeners = ['refresh']; + protected $listeners = [ + 'envsUpdated' => 'refresh', + 'refresh', + ]; public Server $server; public StandaloneRedis $database; + public string $redis_username; + + public string $redis_password; + + public string $redis_version; + public ?string $db_url = null; public ?string $db_url_public = null; @@ -25,33 +34,33 @@ class General extends Component 'database.name' => 'required', 'database.description' => 'nullable', 'database.redis_conf' => 'nullable', - 'database.redis_password' => 'required', 'database.image' => 'required', 'database.ports_mappings' => 'nullable', 'database.is_public' => 'nullable|boolean', 'database.public_port' => 'nullable|integer', 'database.is_log_drain_enabled' => 'nullable|boolean', 'database.custom_docker_run_options' => 'nullable', + 'redis_username' => 'required', + 'redis_password' => 'required', ]; protected $validationAttributes = [ 'database.name' => 'Name', 'database.description' => 'Description', 'database.redis_conf' => 'Redis Configuration', - 'database.redis_password' => 'Redis Password', 'database.image' => 'Image', 'database.ports_mappings' => 'Port Mapping', 'database.is_public' => 'Is Public', 'database.public_port' => 'Public Port', 'database.custom_docker_run_options' => 'Custom Docker Options', + 'redis_username' => 'Redis Username', + 'redis_password' => 'Redis Password', ]; public function mount() { - $this->db_url = $this->database->internal_db_url; - $this->db_url_public = $this->database->external_db_url; $this->server = data_get($this->database, 'destination.server'); - + $this->refreshView(); } public function instantSaveAdvanced() @@ -75,13 +84,24 @@ public function submit() { try { $this->validate(); - if ($this->database->redis_conf === '') { - $this->database->redis_conf = null; + + if (version_compare($this->redis_version, '6.0', '>=')) { + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_USERNAME'], + ['value' => $this->redis_username, 'standalone_redis_id' => $this->database->id] + ); } + $this->database->runtime_environment_variables()->updateOrCreate( + ['key' => 'REDIS_PASSWORD'], + ['value' => $this->redis_password, 'standalone_redis_id' => $this->database->id] + ); + $this->database->save(); $this->dispatch('success', 'Database updated.'); } catch (Exception $e) { return handleError($e, $this); + } finally { + $this->dispatch('refreshEnvs'); } } @@ -119,10 +139,25 @@ public function instantSave() public function refresh(): void { $this->database->refresh(); + $this->refreshView(); + } + + private function refreshView() + { + $this->db_url = $this->database->internal_db_url; + $this->db_url_public = $this->database->external_db_url; + $this->redis_version = $this->database->getRedisVersion(); + $this->redis_username = $this->database->redis_username; + $this->redis_password = $this->database->redis_password; } public function render() { return view('livewire.project.database.redis.general'); } + + public function isSharedVariable($name) + { + return $this->database->runtime_environment_variables()->where('key', $name)->where('is_shared', true)->exists(); + } } diff --git a/app/Livewire/Project/Database/ScheduledBackups.php b/app/Livewire/Project/Database/ScheduledBackups.php index 8021e25d32..412240bd4e 100644 --- a/app/Livewire/Project/Database/ScheduledBackups.php +++ b/app/Livewire/Project/Database/ScheduledBackups.php @@ -29,7 +29,7 @@ public function mount(): void $this->setSelectedBackup($this->selectedBackupId, true); } $this->parameters = get_route_parameters(); - if ($this->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($this->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $this->type = 'service-database'; } else { $this->type = 'database'; diff --git a/app/Livewire/Project/DeleteEnvironment.php b/app/Livewire/Project/DeleteEnvironment.php index e01741770a..1ee5de2691 100644 --- a/app/Livewire/Project/DeleteEnvironment.php +++ b/app/Livewire/Project/DeleteEnvironment.php @@ -7,18 +7,22 @@ class DeleteEnvironment extends Component { - public array $parameters; - public int $environment_id; public bool $disabled = false; public string $environmentName = ''; + public array $parameters; + public function mount() { - $this->parameters = get_route_parameters(); - $this->environmentName = Environment::findOrFail($this->environment_id)->name; + try { + $this->environmentName = Environment::findOrFail($this->environment_id)->name; + $this->parameters = get_route_parameters(); + } catch (\Exception $e) { + return handleError($e, $this); + } } public function delete() @@ -30,9 +34,9 @@ public function delete() if ($environment->isEmpty()) { $environment->delete(); - return redirect()->route('project.show', ['project_uuid' => $this->parameters['project_uuid']]); + return redirect()->route('project.show', parameters: ['project_uuid' => $this->parameters['project_uuid']]); } - return $this->dispatch('error', 'Environment has defined resources, please delete them first.'); + return $this->dispatch('error', "Environment {$environment->name} has defined resources, please delete them first."); } } diff --git a/app/Livewire/Project/DeleteProject.php b/app/Livewire/Project/DeleteProject.php index 360fad10a8..f320a19b0f 100644 --- a/app/Livewire/Project/DeleteProject.php +++ b/app/Livewire/Project/DeleteProject.php @@ -27,11 +27,12 @@ public function delete() 'project_id' => 'required|int', ]); $project = Project::findOrFail($this->project_id); - if ($project->applications->count() > 0) { - return $this->dispatch('error', 'Project has resources defined, please delete them first.'); + if ($project->isEmpty()) { + $project->delete(); + + return redirect()->route('project.index'); } - $project->delete(); - return redirect()->route('project.index'); + return $this->dispatch('error', "Project {$project->name} has resources defined, please delete them first."); } } diff --git a/app/Livewire/Project/Edit.php b/app/Livewire/Project/Edit.php index bebec4752f..463febb10d 100644 --- a/app/Livewire/Project/Edit.php +++ b/app/Livewire/Project/Edit.php @@ -3,34 +3,47 @@ namespace App\Livewire\Project; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class Edit extends Component { public Project $project; - protected $rules = [ - 'project.name' => 'required|min:3|max:255', - 'project.description' => 'nullable|string|max:255', - ]; + #[Validate(['required', 'string', 'min:3', 'max:255'])] + public string $name; - public function mount() + #[Validate(['nullable', 'string', 'max:255'])] + public ?string $description = null; + + public function mount(string $project_uuid) + { + try { + $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) { - $projectUuid = request()->route('project_uuid'); - $teamId = currentTeam()->id; - $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (! $project) { - return redirect()->route('dashboard'); + if ($toModel) { + $this->validate(); + $this->project->update([ + 'name' => $this->name, + 'description' => $this->description, + ]); + } else { + $this->name = $this->project->name; + $this->description = $this->project->description; } - $this->project = $project; } public function submit() { try { - $this->validate(); - $this->project->save(); - $this->dispatch('saved'); + $this->syncData(true); $this->dispatch('success', 'Project updated.'); } catch (\Throwable $e) { return handleError($e, $this); diff --git a/app/Livewire/Project/EnvironmentEdit.php b/app/Livewire/Project/EnvironmentEdit.php index 16fc7bc36b..f48220b3dd 100644 --- a/app/Livewire/Project/EnvironmentEdit.php +++ b/app/Livewire/Project/EnvironmentEdit.php @@ -4,6 +4,8 @@ use App\Models\Application; use App\Models\Project; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class EnvironmentEdit extends Component @@ -12,29 +14,45 @@ class EnvironmentEdit extends Component public Application $application; + #[Locked] public $environment; - public array $parameters; + #[Validate(['required', 'string', 'min:3', 'max:255'])] + public string $name; - protected $rules = [ - 'environment.name' => 'required|min:3|max:255', - 'environment.description' => 'nullable|min:3|max:255', - ]; + #[Validate(['nullable', 'string', 'max:255'])] + public ?string $description = null; - public function mount() + public function mount(string $project_uuid, string $environment_name) { - $this->parameters = get_route_parameters(); - $this->project = Project::ownedByCurrentTeam()->where('uuid', request()->route('project_uuid'))->first(); - $this->environment = $this->project->environments()->where('name', request()->route('environment_name'))->first(); + try { + $this->project = Project::ownedByCurrentTeam()->where('uuid', $project_uuid)->firstOrFail(); + $this->environment = $this->project->environments()->where('name', $environment_name)->firstOrFail(); + $this->syncData(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->environment->update([ + 'name' => $this->name, + 'description' => $this->description, + ]); + } else { + $this->name = $this->environment->name; + $this->description = $this->environment->description; + } } public function submit() { - $this->validate(); try { - $this->environment->save(); - - return redirect()->route('project.environment.edit', ['project_uuid' => $this->project->uuid, 'environment_name' => $this->environment->name]); + $this->syncData(true); + $this->redirectRoute('project.environment.edit', ['environment_name' => $this->environment->name, 'project_uuid' => $this->project->uuid]); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Index.php b/app/Livewire/Project/Index.php index 0e4f15a5c5..f8eb838be6 100644 --- a/app/Livewire/Project/Index.php +++ b/app/Livewire/Project/Index.php @@ -18,7 +18,11 @@ class Index extends Component public function mount() { $this->private_keys = PrivateKey::ownedByCurrentTeam()->get(); - $this->projects = Project::ownedByCurrentTeam()->get(); + $this->projects = Project::ownedByCurrentTeam()->get()->map(function ($project) { + $project->settingsRoute = route('project.edit', ['project_uuid' => $project->uuid]); + + return $project; + }); $this->servers = Server::ownedByCurrentTeam()->count(); } diff --git a/app/Livewire/Project/New/DockerImage.php b/app/Livewire/Project/New/DockerImage.php index d3f5b5261e..417fb2ea02 100644 --- a/app/Livewire/Project/New/DockerImage.php +++ b/app/Livewire/Project/New/DockerImage.php @@ -46,7 +46,6 @@ public function submit() $project = Project::where('uuid', $this->parameters['project_uuid'])->first(); $environment = $project->load(['environments'])->environments->where('name', $this->parameters['environment_name'])->first(); - ray($image, $tag); $application = Application::create([ 'name' => 'docker-image-'.new Cuid2, 'repository_project_id' => 0, diff --git a/app/Livewire/Project/New/GithubPrivateRepository.php b/app/Livewire/Project/New/GithubPrivateRepository.php index fbeb5601ff..2f4f5a25c5 100644 --- a/app/Livewire/Project/New/GithubPrivateRepository.php +++ b/app/Livewire/Project/New/GithubPrivateRepository.php @@ -153,7 +153,6 @@ public function loadBranches() protected function loadBranchByPage() { - ray('Loading page '.$this->page); $response = Http::withToken($this->token)->get("{$this->github_app->api_url}/repos/{$this->selected_repository_owner}/{$this->selected_repository_repo}/branches?per_page=100&page={$this->page}"); $json = $response->json(); if ($response->status() !== 200) { diff --git a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php index 0edafd040e..b46c4a7943 100644 --- a/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php +++ b/app/Livewire/Project/New/GithubPrivateRepositoryDeployKey.php @@ -198,7 +198,7 @@ private function get_git_source() $this->git_host = $this->repository_url_parsed->getHost(); $this->git_repository = $this->repository_url_parsed->getSegment(1).'/'.$this->repository_url_parsed->getSegment(2); - if ($this->git_host == 'github.com') { + if ($this->git_host === 'github.com') { $this->git_source = GithubApp::where('name', 'Public GitHub')->first(); return; diff --git a/app/Livewire/Project/New/PublicGitRepository.php b/app/Livewire/Project/New/PublicGitRepository.php index 971d4700ba..bd35dccef6 100644 --- a/app/Livewire/Project/New/PublicGitRepository.php +++ b/app/Livewire/Project/New/PublicGitRepository.php @@ -99,7 +99,6 @@ public function updatedBaseDirectory() $this->base_directory = '/'.$this->base_directory; } } - } public function updatedDockerComposeLocation() @@ -174,7 +173,7 @@ public function loadBranch() return; } - if (! $this->branchFound && $this->git_branch == 'main') { + if (! $this->branchFound && $this->git_branch === 'main') { try { $this->git_branch = 'master'; $this->getBranch(); @@ -197,7 +196,7 @@ private function getGitSource() } else { $this->git_branch = 'main'; } - if ($this->git_host == 'github.com') { + if ($this->git_host === 'github.com') { $this->git_source = GithubApp::where('name', 'Public GitHub')->first(); return; @@ -213,7 +212,7 @@ private function getBranch() return; } - if ($this->git_source->getMorphClass() === 'App\Models\GithubApp') { + if ($this->git_source->getMorphClass() === \App\Models\GithubApp::class) { ['rate_limit_remaining' => $this->rate_limit_remaining, 'rate_limit_reset' => $this->rate_limit_reset] = githubApi(source: $this->git_source, endpoint: "/repos/{$this->git_repository}/branches/{$this->git_branch}"); $this->rate_limit_reset = Carbon::parse((int) $this->rate_limit_reset)->format('Y-M-d H:i:s'); $this->branchFound = true; @@ -317,6 +316,7 @@ public function submit() // $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 7f82475972..2dc9abbf1c 100644 --- a/app/Livewire/Project/New/Select.php +++ b/app/Livewire/Project/New/Select.php @@ -158,7 +158,7 @@ public function loadServices() [ '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.', + 'description' => 'MariaDB is a community-developed, commercially supported fork of the MySQL relational database management system, intended to remain free and open-source.', 'logo' => '', ], [ @@ -326,7 +326,7 @@ public function whatToDoNext() public function loadServers() { - $this->servers = Server::isUsable()->get(); + $this->servers = Server::isUsable()->get()->sortBy('name'); $this->allServers = $this->servers; } } diff --git a/app/Livewire/Project/Resource/Create.php b/app/Livewire/Project/Resource/Create.php index 5c6a37d6df..9266a57fc8 100644 --- a/app/Livewire/Project/Resource/Create.php +++ b/app/Livewire/Project/Resource/Create.php @@ -100,7 +100,6 @@ public function mount() 'is_preview' => false, ]); } - }); } $service->parse(isNew: true); diff --git a/app/Livewire/Project/Resource/Index.php b/app/Livewire/Project/Resource/Index.php index 71ce2c3567..2834968875 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/Configuration.php b/app/Livewire/Project/Service/Configuration.php index a2e48fee7d..319ead3619 100644 --- a/app/Livewire/Project/Service/Configuration.php +++ b/app/Livewire/Project/Service/Configuration.php @@ -4,6 +4,7 @@ use App\Actions\Docker\GetContainersStatus; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; class Configuration extends Component @@ -20,7 +21,7 @@ class Configuration extends Component public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'check_status', diff --git a/app/Livewire/Project/Service/Database.php b/app/Livewire/Project/Service/Database.php index 9804fb5ba8..9f02db05c8 100644 --- a/app/Livewire/Project/Service/Database.php +++ b/app/Livewire/Project/Service/Database.php @@ -95,8 +95,7 @@ public function submit() $this->database->save(); updateCompose($this->database); $this->dispatch('success', 'Database saved.'); - } catch (\Throwable $e) { - ray($e); + } catch (\Throwable) { } finally { $this->dispatch('generateDockerCompose'); } diff --git a/app/Livewire/Project/Service/EditDomain.php b/app/Livewire/Project/Service/EditDomain.php index 4138f720e0..e89aeda85f 100644 --- a/app/Livewire/Project/Service/EditDomain.php +++ b/app/Livewire/Project/Service/EditDomain.php @@ -21,6 +21,7 @@ public function mount() { $this->application = ServiceApplication::find($this->applicationId); } + public function submit() { try { @@ -28,9 +29,14 @@ public function submit() $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -38,7 +44,7 @@ public function submit() if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } $this->application->service->parse(); $this->dispatch('refresh'); @@ -48,6 +54,7 @@ public function submit() if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; } + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Service/FileStorage.php b/app/Livewire/Project/Service/FileStorage.php index 2150191124..4d070bc0cc 100644 --- a/app/Livewire/Project/Service/FileStorage.php +++ b/app/Livewire/Project/Service/FileStorage.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Service; use App\Models\Application; +use App\Models\InstanceSettings; use App\Models\LocalFileVolume; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -87,10 +88,12 @@ public function convertToFile() public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { diff --git a/app/Livewire/Project/Service/Index.php b/app/Livewire/Project/Service/Index.php index 0a7b6ec901..ba4ebe2fca 100644 --- a/app/Livewire/Project/Service/Index.php +++ b/app/Livewire/Project/Service/Index.php @@ -48,7 +48,6 @@ public function mount() } catch (\Throwable $e) { return handleError($e, $this); } - } public function generateDockerCompose() diff --git a/app/Livewire/Project/Service/Navbar.php b/app/Livewire/Project/Service/Navbar.php index 70b3b5db67..ee43dc911f 100644 --- a/app/Livewire/Project/Service/Navbar.php +++ b/app/Livewire/Project/Service/Navbar.php @@ -7,6 +7,7 @@ use App\Actions\Shared\PullImage; use App\Events\ServiceStatusChanged; use App\Models\Service; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Spatie\Activitylog\Models\Activity; @@ -27,7 +28,6 @@ class Navbar extends Component public function mount() { if (str($this->service->status())->contains('running') && is_null($this->service->config_hash)) { - ray('isConfigurationChanged init'); $this->service->isConfigurationChanged(true); $this->dispatch('configurationChanged'); } @@ -35,10 +35,11 @@ public function mount() public function getListeners() { - $userId = auth()->user()->id; + $userId = Auth::id(); return [ "echo-private:user.{$userId},ServiceStatusChanged" => 'serviceStarted', + 'envsUpdated' => '$refresh', ]; } @@ -76,7 +77,7 @@ public function checkDeployments() } else { $this->isDeploymentProgress = false; } - } catch (\Throwable $e) { + } catch (\Throwable) { $this->isDeploymentProgress = false; } } diff --git a/app/Livewire/Project/Service/ServiceApplicationView.php b/app/Livewire/Project/Service/ServiceApplicationView.php index ba37313fd5..8324ee645c 100644 --- a/app/Livewire/Project/Service/ServiceApplicationView.php +++ b/app/Livewire/Project/Service/ServiceApplicationView.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Service; +use App\Models\InstanceSettings; use App\Models\ServiceApplication; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -30,11 +31,6 @@ class ServiceApplicationView extends Component 'application.is_stripprefix_enabled' => 'nullable|boolean', ]; - public function updatedApplicationFqdn() - { - - } - public function instantSave() { $this->submit(); @@ -54,10 +50,12 @@ public function instantSaveAdvanced() public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { @@ -82,10 +80,14 @@ public function submit() $this->application->fqdn = str($this->application->fqdn)->replaceStart(',', '')->trim(); $this->application->fqdn = str($this->application->fqdn)->trim()->explode(',')->map(function ($domain) { Url::fromString($domain, ['http', 'https']); + return str($domain)->trim()->lower(); }); $this->application->fqdn = $this->application->fqdn->unique()->implode(','); - + $warning = sslipDomainWarning($this->application->fqdn); + if ($warning) { + $this->dispatch('warning', __('warning.sslipdomain')); + } check_domain_usage(resource: $this->application); $this->validate(); $this->application->save(); @@ -93,7 +95,7 @@ public function submit() if (str($this->application->fqdn)->contains(',')) { $this->dispatch('warning', 'Some services do not support multiple domains, which can lead to problems and is NOT RECOMMENDED.

Only use multiple domains if you know what you are doing.'); } else { - $this->dispatch('success', 'Service saved.'); + ! $warning && $this->dispatch('success', 'Service saved.'); } $this->dispatch('generateDockerCompose'); } catch (\Throwable $e) { @@ -101,6 +103,7 @@ public function submit() if ($originalFqdn !== $this->application->fqdn) { $this->application->fqdn = $originalFqdn; } + return handleError($e, $this); } } diff --git a/app/Livewire/Project/Shared/Danger.php b/app/Livewire/Project/Shared/Danger.php index c052608995..a0b4ac2c45 100644 --- a/app/Livewire/Project/Shared/Danger.php +++ b/app/Livewire/Project/Shared/Danger.php @@ -3,6 +3,7 @@ namespace App\Livewire\Project\Shared; use App\Jobs\DeleteResourceJob; +use App\Models\InstanceSettings; use App\Models\Service; use App\Models\ServiceApplication; use App\Models\ServiceDatabase; @@ -61,37 +62,26 @@ public function mount() return; } - switch ($this->resource->type()) { - case 'application': - $this->resourceName = $this->resource->name ?? 'Application'; - break; - case 'standalone-postgresql': - case 'standalone-redis': - case 'standalone-mongodb': - case 'standalone-mysql': - case 'standalone-mariadb': - case 'standalone-keydb': - case 'standalone-dragonfly': - case 'standalone-clickhouse': - $this->resourceName = $this->resource->name ?? 'Database'; - break; - case 'service': - $this->resourceName = $this->resource->name ?? 'Service'; - break; - case 'service-application': - $this->resourceName = $this->resource->name ?? 'Service Application'; - break; - case 'service-database': - $this->resourceName = $this->resource->name ?? 'Service Database'; - break; - default: - $this->resourceName = 'Unknown Resource'; - } + $this->resourceName = match ($this->resource->type()) { + 'application' => $this->resource->name ?? 'Application', + 'standalone-postgresql', + 'standalone-redis', + 'standalone-mongodb', + 'standalone-mysql', + 'standalone-mariadb', + 'standalone-keydb', + 'standalone-dragonfly', + 'standalone-clickhouse' => $this->resource->name ?? 'Database', + 'service' => $this->resource->name ?? 'Service', + 'service-application' => $this->resource->name ?? 'Service Application', + 'service-database' => $this->resource->name ?? 'Service Database', + default => 'Unknown Resource', + }; } public function delete($password) { - if (isProduction()) { + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { if (! Hash::check($password, Auth::user()->password)) { $this->addError('password', 'The provided password is incorrect.'); diff --git a/app/Livewire/Project/Shared/Destination.php b/app/Livewire/Project/Shared/Destination.php index 7fb5c45db7..c305e817cf 100644 --- a/app/Livewire/Project/Shared/Destination.php +++ b/app/Livewire/Project/Shared/Destination.php @@ -5,7 +5,7 @@ use App\Actions\Application\StopApplicationOneServer; use App\Actions\Docker\GetContainersStatus; use App\Events\ApplicationStatusChanged; -use App\Jobs\ContainerStatusJob; +use App\Models\InstanceSettings; use App\Models\Server; use App\Models\StandaloneDocker; use Illuminate\Support\Facades\Auth; @@ -119,10 +119,12 @@ public function addServer(int $network_id, int $server_id) public function removeServer(int $network_id, int $server_id, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } if ($this->resource->destination->server->id == $server_id && $this->resource->destination->id == $network_id) { diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 5a711259b2..787d33a690 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -35,7 +35,7 @@ class All extends Component public function mount() { $this->resourceClass = get_class($this->resource); - $resourceWithPreviews = ['App\Models\Application']; + $resourceWithPreviews = [\App\Models\Application::class]; $simpleDockerfile = ! is_null(data_get($this->resource, 'dockerfile')); if (str($this->resourceClass)->contains($resourceWithPreviews) && ! $simpleDockerfile) { $this->showPreview = true; diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php index 463ceecad1..e71cd9f42a 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/Show.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/Show.php @@ -37,6 +37,7 @@ class Show extends Component 'env.is_literal' => 'required|boolean', 'env.is_shown_once' => 'required|boolean', 'env.real_value' => 'nullable', + 'env.is_required' => 'required|boolean', ]; protected $validationAttributes = [ @@ -46,6 +47,7 @@ class Show extends Component 'env.is_multiline' => 'Multiline', 'env.is_literal' => 'Literal', 'env.is_shown_once' => 'Shown Once', + 'env.is_required' => 'Required', ]; public function refresh() @@ -56,7 +58,7 @@ public function refresh() public function mount() { - if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { + if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { $this->isSharedVariable = true; } $this->modalId = new Cuid2; @@ -78,7 +80,7 @@ public function checkEnvs() public function serialize() { data_forget($this->env, 'real_value'); - if ($this->env->getMorphClass() === 'App\Models\SharedEnvironmentVariable') { + if ($this->env->getMorphClass() === \App\Models\SharedEnvironmentVariable::class) { data_forget($this->env, 'is_build_time'); } } @@ -109,15 +111,21 @@ 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->isSharedVariable && $this->env->is_required && str($this->env->real_value)->isEmpty()) { + $oldValue = $this->env->getOriginal('value'); + $this->env->value = $oldValue; + $this->dispatch('error', 'Required environment variable cannot be empty.'); + + return; + } + $this->serialize(); + + if ($this->isSharedVariable) { + unset($this->env->is_required); + } + $this->env->save(); $this->dispatch('success', 'Environment variable updated.'); $this->dispatch('envsUpdated'); diff --git a/app/Livewire/Project/Shared/ExecuteContainerCommand.php b/app/Livewire/Project/Shared/ExecuteContainerCommand.php index 90419caed1..621ab1bacb 100644 --- a/app/Livewire/Project/Shared/ExecuteContainerCommand.php +++ b/app/Livewire/Project/Shared/ExecuteContainerCommand.php @@ -52,6 +52,7 @@ public function mount() $this->servers = $this->servers->push($server); } } + $this->loadContainers(); } elseif (data_get($this->parameters, 'database_uuid')) { $this->type = 'database'; $resource = getResourceByUuid($this->parameters['database_uuid'], data_get(auth()->user()->currentTeam(), 'id')); @@ -62,12 +63,18 @@ public function mount() if ($this->resource->destination->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->destination->server); } + $this->loadContainers(); } elseif (data_get($this->parameters, 'service_uuid')) { $this->type = 'service'; $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); if ($this->resource->server->isFunctional()) { $this->servers = $this->servers->push($this->resource->server); } + $this->loadContainers(); + } elseif (data_get($this->parameters, 'server_uuid')) { + $this->type = 'server'; + $this->resource = Server::where('uuid', $this->parameters['server_uuid'])->firstOrFail(); + $this->server = $this->resource; } } @@ -125,11 +132,31 @@ public function loadContainers() } }); } - } if ($this->containers->count() > 0) { $this->container = $this->containers->first(); } + if ($this->containers->count() === 1) { + $this->selected_container = data_get($this->containers->first(), 'container.Names'); + } + } + + #[On('connectToServer')] + public function connectToServer() + { + try { + if ($this->server->isForceDisabled()) { + throw new \RuntimeException('Server is disabled.'); + } + $this->dispatch( + 'send-terminal-command', + false, + data_get($this->server, 'name'), + data_get($this->server, 'uuid') + ); + } catch (\Throwable $e) { + return handleError($e, $this); + } } #[On('connectToContainer')] @@ -156,7 +183,6 @@ public function connectToContainer() data_get($container, 'container.Names'), data_get($container, 'server.uuid') ); - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 0e140b8c1a..43fd97c341 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -39,12 +39,12 @@ class GetLogs extends Component public ?bool $showTimeStamps = true; - public int $numberOfLines = 100; + public ?int $numberOfLines = 100; public function mount() { if (! is_null($this->resource)) { - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $this->showTimeStamps = $this->resource->settings->is_include_timestamps; } else { if ($this->servicesubtype) { @@ -53,7 +53,7 @@ public function mount() $this->showTimeStamps = $this->resource->is_include_timestamps; } } - if ($this->resource?->getMorphClass() === 'App\Models\Application') { + if ($this->resource?->getMorphClass() === \App\Models\Application::class) { if (str($this->container)->contains('-pr-')) { $this->pull_request = 'Pull Request: '.str($this->container)->afterLast('-pr-')->beforeLast('_')->value(); } @@ -69,11 +69,11 @@ public function doSomethingWithThisChunkOfOutput($output) public function instantSave() { if (! is_null($this->resource)) { - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $this->resource->settings->is_include_timestamps = $this->showTimeStamps; $this->resource->settings->save(); } - if ($this->resource->getMorphClass() === 'App\Models\Service') { + if ($this->resource->getMorphClass() === \App\Models\Service::class) { $serviceName = str($this->container)->beforeLast('-')->value(); $subType = $this->resource->applications()->where('name', $serviceName)->first(); if ($subType) { @@ -95,10 +95,10 @@ public function getLogs($refresh = false) if (! $this->server->isFunctional()) { return; } - if (! $refresh && ($this->resource?->getMorphClass() === 'App\Models\Service' || str($this->container)->contains('-pr-'))) { + if (! $refresh && ($this->resource?->getMorphClass() === \App\Models\Service::class || str($this->container)->contains('-pr-'))) { return; } - if ($this->numberOfLines <= 0) { + if ($this->numberOfLines <= 0 || is_null($this->numberOfLines)) { $this->numberOfLines = 1000; } if ($this->container) { diff --git a/app/Livewire/Project/Shared/Logs.php b/app/Livewire/Project/Shared/Logs.php index 5af0a6a503..12022b1eeb 100644 --- a/app/Livewire/Project/Shared/Logs.php +++ b/app/Livewire/Project/Shared/Logs.php @@ -109,10 +109,7 @@ public function mount() $this->containers = $this->containers->filter(function ($container) { return str_contains($container, $this->query['pull_request_id']); }); - ray($this->containers); - } - } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Metrics.php b/app/Livewire/Project/Shared/Metrics.php index d9d7dd3ef8..fdc35fc0fb 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/ResourceOperations.php b/app/Livewire/Project/Shared/ResourceOperations.php index ec09eb80fa..e67df6aa90 100644 --- a/app/Livewire/Project/Shared/ResourceOperations.php +++ b/app/Livewire/Project/Shared/ResourceOperations.php @@ -41,7 +41,7 @@ public function cloneTo($destination_id) } $uuid = (string) new Cuid2; $server = $new_destination->server; - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $new_resource = $this->resource->replicate()->fill([ 'uuid' => $uuid, 'name' => $this->resource->name.'-clone-'.$uuid, @@ -78,14 +78,14 @@ public function cloneTo($destination_id) return redirect()->to($route); } elseif ( - $this->resource->getMorphClass() === 'App\Models\StandalonePostgresql' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMongodb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMysql' || - $this->resource->getMorphClass() === 'App\Models\StandaloneMariadb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneRedis' || - $this->resource->getMorphClass() === 'App\Models\StandaloneKeydb' || - $this->resource->getMorphClass() === 'App\Models\StandaloneDragonfly' || - $this->resource->getMorphClass() === 'App\Models\StandaloneClickhouse' + $this->resource->getMorphClass() === \App\Models\StandalonePostgresql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMongodb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMysql::class || + $this->resource->getMorphClass() === \App\Models\StandaloneMariadb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneRedis::class || + $this->resource->getMorphClass() === \App\Models\StandaloneKeydb::class || + $this->resource->getMorphClass() === \App\Models\StandaloneDragonfly::class || + $this->resource->getMorphClass() === \App\Models\StandaloneClickhouse::class ) { $uuid = (string) new Cuid2; $new_resource = $this->resource->replicate()->fill([ @@ -147,7 +147,6 @@ public function cloneTo($destination_id) return redirect()->to($route); } - } public function moveTo($environment_id) diff --git a/app/Livewire/Project/Shared/ScheduledTask/Add.php b/app/Livewire/Project/Shared/ScheduledTask/Add.php index f36b7b1418..adfd59217e 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Add.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Add.php @@ -55,8 +55,8 @@ public function submit() return; } - if (empty($this->container) || $this->container == 'null') { - if ($this->type == 'service') { + if (empty($this->container) || $this->container === 'null') { + if ($this->type === 'service') { $this->container = $this->subServiceName; } } diff --git a/app/Livewire/Project/Shared/ScheduledTask/All.php b/app/Livewire/Project/Shared/ScheduledTask/All.php index b383e294ad..6ab8426f39 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/All.php +++ b/app/Livewire/Project/Shared/ScheduledTask/All.php @@ -21,10 +21,10 @@ class All extends Component public function mount() { $this->parameters = get_route_parameters(); - if ($this->resource->type() == 'service') { + if ($this->resource->type() === 'service') { $this->containerNames = $this->resource->applications()->pluck('name'); $this->containerNames = $this->containerNames->merge($this->resource->databases()->pluck('name')); - } elseif ($this->resource->type() == 'application') { + } elseif ($this->resource->type() === 'application') { if ($this->resource->build_pack === 'dockercompose') { $parsed = $this->resource->parse(); $containers = collect(data_get($parsed, 'services'))->keys(); diff --git a/app/Livewire/Project/Shared/ScheduledTask/Executions.php b/app/Livewire/Project/Shared/ScheduledTask/Executions.php index 017cc9fd7e..0710e37ffc 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Executions.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Executions.php @@ -2,70 +2,77 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Models\ScheduledTask; +use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; +use Livewire\Attributes\Locked; use Livewire\Component; class Executions extends Component { - public $executions = []; + public ScheduledTask $task; - public $selectedKey; + #[Locked] + public int $taskId; - public $task; + #[Locked] + public Collection $executions; + + #[Locked] + public ?int $selectedKey = null; + + #[Locked] + public ?string $serverTimezone = null; public function getListeners() { + $teamId = Auth::user()->currentTeam()->id; + return [ - 'selectTask', + "echo-private:team.{$teamId},ScheduledTaskDone" => 'refreshExecutions', ]; } - public function selectTask($key): void - { - if ($key == $this->selectedKey) { - $this->selectedKey = null; - - return; - } - $this->selectedKey = $key; - } - - public function server() + public function mount($taskId) { - if (! $this->task) { - return null; - } - - if ($this->task->application) { - if ($this->task->application->destination && $this->task->application->destination->server) { - return $this->task->application->destination->server; + try { + $this->taskId = $taskId; + $this->task = ScheduledTask::findOrFail($taskId); + $this->executions = $this->task->executions()->take(20)->get(); + $this->serverTimezone = data_get($this->task, 'application.destination.server.settings.server_timezone'); + if (! $this->serverTimezone) { + $this->serverTimezone = data_get($this->task, 'service.destination.server.settings.server_timezone'); } - } elseif ($this->task->service) { - if ($this->task->service->destination && $this->task->service->destination->server) { - return $this->task->service->destination->server; + if (! $this->serverTimezone) { + $this->serverTimezone = 'UTC'; } + } catch (\Exception $e) { + return handleError($e); } + } - return null; + public function refreshExecutions(): void + { + $this->executions = $this->task->executions()->take(20)->get(); } - public function getServerTimezone() + public function selectTask($key): void { - $server = $this->server(); - if (! $server) { - return 'UTC'; - } - $serverTimezone = $server->settings->server_timezone; + if ($key == $this->selectedKey) { + $this->selectedKey = null; - return $serverTimezone; + return; + } + $this->selectedKey = $key; } public function formatDateInServerTimezone($date) { - $serverTimezone = $this->getServerTimezone(); + $serverTimezone = $this->serverTimezone; $dateObj = new \DateTime($date); try { $dateObj->setTimezone(new \DateTimeZone($serverTimezone)); - } catch (\Exception $e) { + } catch (\Exception) { $dateObj->setTimezone(new \DateTimeZone('UTC')); } diff --git a/app/Livewire/Project/Shared/ScheduledTask/Show.php b/app/Livewire/Project/Shared/ScheduledTask/Show.php index 37f50dd32b..0900a1d700 100644 --- a/app/Livewire/Project/Shared/ScheduledTask/Show.php +++ b/app/Livewire/Project/Shared/ScheduledTask/Show.php @@ -2,74 +2,124 @@ namespace App\Livewire\Project\Shared\ScheduledTask; +use App\Jobs\ScheduledTaskJob; use App\Models\Application; -use App\Models\ScheduledTask as ModelsScheduledTask; +use App\Models\ScheduledTask; use App\Models\Service; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; -use Visus\Cuid2\Cuid2; class Show extends Component { - public $parameters; - public Application|Service $resource; - public ModelsScheduledTask $task; + public ScheduledTask $task; - public ?string $modalId = null; + #[Locked] + public array $parameters; + #[Locked] public string $type; - public string $scheduledTaskName; + #[Validate(['boolean'])] + public bool $isEnabled = false; + + #[Validate(['string', 'required'])] + public string $name; + + #[Validate(['string', 'required'])] + public string $command; + + #[Validate(['string', 'required'])] + public string $frequency; + + #[Validate(['string', 'nullable'])] + public ?string $container = null; - protected $rules = [ - 'task.enabled' => 'required|boolean', - 'task.name' => 'required|string', - 'task.command' => 'required|string', - 'task.frequency' => 'required|string', - 'task.container' => 'nullable|string', - ]; + #[Locked] + public ?string $application_uuid; - protected $validationAttributes = [ - 'name' => 'name', - 'command' => 'command', - 'frequency' => 'frequency', - 'container' => 'container', - ]; + #[Locked] + public ?string $service_uuid; - public function mount() + #[Locked] + public string $task_uuid; + + public function mount(string $task_uuid, string $project_uuid, string $environment_name, ?string $application_uuid = null, ?string $service_uuid = null) { - $this->parameters = get_route_parameters(); - - if (data_get($this->parameters, 'application_uuid')) { - $this->type = 'application'; - $this->resource = Application::where('uuid', $this->parameters['application_uuid'])->firstOrFail(); - } elseif (data_get($this->parameters, 'service_uuid')) { - $this->type = 'service'; - $this->resource = Service::where('uuid', $this->parameters['service_uuid'])->firstOrFail(); + try { + $this->task_uuid = $task_uuid; + if ($application_uuid) { + $this->type = 'application'; + $this->application_uuid = $application_uuid; + $this->resource = Application::ownedByCurrentTeam()->where('uuid', $application_uuid)->firstOrFail(); + } elseif ($service_uuid) { + $this->type = 'service'; + $this->service_uuid = $service_uuid; + $this->resource = Service::ownedByCurrentTeam()->where('uuid', $service_uuid)->firstOrFail(); + } + $this->parameters = [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'application_uuid' => $application_uuid, + 'service_uuid' => $service_uuid, + ]; + + $this->task = $this->resource->scheduled_tasks()->where('uuid', $task_uuid)->firstOrFail(); + $this->syncData(); + } catch (\Exception $e) { + return handleError($e); } + } - $this->modalId = new Cuid2; - $this->task = ModelsScheduledTask::where('uuid', request()->route('task_uuid'))->first(); - $this->scheduledTaskName = $this->task->name; + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->task->enabled = $this->isEnabled; + $this->task->name = str($this->name)->trim()->value(); + $this->task->command = str($this->command)->trim()->value(); + $this->task->frequency = str($this->frequency)->trim()->value(); + $this->task->container = str($this->container)->trim()->value(); + $this->task->save(); + } else { + $this->isEnabled = $this->task->enabled; + $this->name = $this->task->name; + $this->command = $this->task->command; + $this->frequency = $this->task->frequency; + $this->container = $this->task->container; + } } public function instantSave() { - $this->validateOnly('task.enabled'); - $this->task->save(['enabled' => $this->task->enabled]); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + $this->refreshTasks(); + } catch (\Exception $e) { + return handleError($e); + } } public function submit() { - $this->validate(); - $this->task->name = str($this->task->name)->trim()->value(); - $this->task->container = str($this->task->container)->trim()->value(); - $this->task->save(); - $this->dispatch('success', 'Scheduled task updated.'); - $this->dispatch('refreshTasks'); + try { + $this->syncData(true); + $this->dispatch('success', 'Scheduled task updated.'); + } catch (\Exception $e) { + return handleError($e); + } + } + + public function refreshTasks() + { + try { + $this->task->refresh(); + } catch (\Exception $e) { + return handleError($e); + } } public function delete() @@ -77,13 +127,23 @@ public function delete() try { $this->task->delete(); - if ($this->type == 'application') { - return redirect()->route('project.application.configuration', $this->parameters, $this->scheduledTaskName); + if ($this->type === 'application') { + return redirect()->route('project.application.configuration', $this->parameters, $this->task->name); } else { - return redirect()->route('project.service.configuration', $this->parameters, $this->scheduledTaskName); + return redirect()->route('project.service.configuration', $this->parameters, $this->task->name); } } catch (\Exception $e) { return handleError($e); } } + + public function executeNow() + { + try { + ScheduledTaskJob::dispatch($this->task); + $this->dispatch('success', 'Scheduled task executed.'); + } catch (\Exception $e) { + return handleError($e); + } + } } diff --git a/app/Livewire/Project/Shared/Storages/Add.php b/app/Livewire/Project/Shared/Storages/Add.php index 27e0c6e447..6e250bd908 100644 --- a/app/Livewire/Project/Shared/Storages/Add.php +++ b/app/Livewire/Project/Shared/Storages/Add.php @@ -83,7 +83,7 @@ public function submitFileStorage() ]); $this->file_storage_path = trim($this->file_storage_path); $this->file_storage_path = str($this->file_storage_path)->start('/')->value(); - if ($this->resource->getMorphClass() === 'App\Models\Application') { + if ($this->resource->getMorphClass() === \App\Models\Application::class) { $fs_path = application_configuration_dir().'/'.$this->resource->uuid.$this->file_storage_path; } LocalFileVolume::create( @@ -100,7 +100,6 @@ public function submitFileStorage() } catch (\Throwable $e) { return handleError($e, $this); } - } public function submitFileStorageDirectory() @@ -127,7 +126,6 @@ public function submitFileStorageDirectory() } catch (\Throwable $e) { return handleError($e, $this); } - } public function submitPersistentVolume() @@ -144,7 +142,6 @@ public function submitPersistentVolume() 'mount_path' => $this->mount_path, 'host_path' => $this->host_path, ]); - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Project/Shared/Storages/Show.php b/app/Livewire/Project/Shared/Storages/Show.php index e4b5c9b893..54b1be3af5 100644 --- a/app/Livewire/Project/Shared/Storages/Show.php +++ b/app/Livewire/Project/Shared/Storages/Show.php @@ -2,6 +2,7 @@ namespace App\Livewire\Project\Shared\Storages; +use App\Models\InstanceSettings; use App\Models\LocalPersistentVolume; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -40,10 +41,12 @@ public function submit() public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } $this->storage->delete(); diff --git a/app/Livewire/Project/Shared/Terminal.php b/app/Livewire/Project/Shared/Terminal.php index 916db650f6..5af8f057e9 100644 --- a/app/Livewire/Project/Shared/Terminal.php +++ b/app/Livewire/Project/Shared/Terminal.php @@ -26,7 +26,6 @@ public function closeTerminal() #[On('send-terminal-command')] public function sendTerminalCommand($isContainer, $identifier, $serverUuid) { - $server = Server::ownedByCurrentTeam()->whereUuid($serverUuid)->firstOrFail(); if ($isContainer) { diff --git a/app/Livewire/Project/Shared/UploadConfig.php b/app/Livewire/Project/Shared/UploadConfig.php index dea8426518..1b10f588b8 100644 --- a/app/Livewire/Project/Shared/UploadConfig.php +++ b/app/Livewire/Project/Shared/UploadConfig.php @@ -8,8 +8,11 @@ class UploadConfig extends Component { public $config; + public $applicationId; - public function mount() { + + public function mount() + { if (isDev()) { $this->config = '{ "build_pack": "nixpacks", @@ -22,6 +25,7 @@ public function mount() { }'; } } + public function uploadConfig() { try { @@ -30,10 +34,11 @@ public function uploadConfig() $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/Project/Show.php b/app/Livewire/Project/Show.php index 1082f078c9..2335519c74 100644 --- a/app/Livewire/Project/Show.php +++ b/app/Livewire/Project/Show.php @@ -2,27 +2,46 @@ namespace App\Livewire\Project; +use App\Models\Environment; use App\Models\Project; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { public Project $project; - public $environments; + #[Validate(['required', 'string', 'min:3'])] + public string $name; - public function mount() - { - $projectUuid = request()->route('project_uuid'); - $teamId = currentTeam()->id; + #[Validate(['nullable', 'string'])] + public ?string $description = null; - $project = Project::where('team_id', $teamId)->where('uuid', $projectUuid)->first(); - if (! $project) { - return redirect()->route('dashboard'); + public function mount(string $project_uuid) + { + try { + $this->project = Project::where('team_id', currentTeam()->id)->where('uuid', $project_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); } + } - $this->environments = $project->environments->sortBy('created_at'); - $this->project = $project; + public function submit() + { + try { + $this->validate(); + $environment = Environment::create([ + 'name' => $this->name, + 'project_id' => $this->project->id, + ]); + + return redirect()->route('project.resource.index', [ + 'project_uuid' => $this->project->uuid, + 'environment_name' => $environment->name, + ]); + } catch (\Throwable $e) { + handleError($e, $this); + } } public function render() diff --git a/app/Livewire/Security/PrivateKey/Show.php b/app/Livewire/Security/PrivateKey/Show.php index 249c84f147..b9195b5430 100644 --- a/app/Livewire/Security/PrivateKey/Show.php +++ b/app/Livewire/Security/PrivateKey/Show.php @@ -28,7 +28,7 @@ public function mount() { try { $this->private_key = PrivateKey::ownedByCurrentTeam(['name', 'description', 'private_key', 'is_git_related'])->whereUuid(request()->private_key_uuid)->firstOrFail(); - } catch (\Throwable $e) { + } catch (\Throwable) { abort(404); } } diff --git a/app/Livewire/Server/Advanced.php b/app/Livewire/Server/Advanced.php new file mode 100644 index 0000000000..0852abebfc --- /dev/null +++ b/app/Livewire/Server/Advanced.php @@ -0,0 +1,115 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->parameters = get_route_parameters(); + $this->syncData(); + } catch (\Throwable) { + return redirect()->route('server.show'); + } + } + + public function syncData(bool $toModel = false) + { + if ($toModel) { + $this->validate(); + $this->server->settings->concurrent_builds = $this->concurrentBuilds; + $this->server->settings->dynamic_timeout = $this->dynamicTimeout; + $this->server->settings->force_docker_cleanup = $this->forceDockerCleanup; + $this->server->settings->docker_cleanup_frequency = $this->dockerCleanupFrequency; + $this->server->settings->docker_cleanup_threshold = $this->dockerCleanupThreshold; + $this->server->settings->server_disk_usage_notification_threshold = $this->serverDiskUsageNotificationThreshold; + $this->server->settings->delete_unused_volumes = $this->deleteUnusedVolumes; + $this->server->settings->delete_unused_networks = $this->deleteUnusedNetworks; + $this->server->settings->save(); + } else { + $this->concurrentBuilds = $this->server->settings->concurrent_builds; + $this->dynamicTimeout = $this->server->settings->dynamic_timeout; + $this->forceDockerCleanup = $this->server->settings->force_docker_cleanup; + $this->dockerCleanupFrequency = $this->server->settings->docker_cleanup_frequency; + $this->dockerCleanupThreshold = $this->server->settings->docker_cleanup_threshold; + $this->serverDiskUsageNotificationThreshold = $this->server->settings->server_disk_usage_notification_threshold; + $this->deleteUnusedVolumes = $this->server->settings->delete_unused_volumes; + $this->deleteUnusedNetworks = $this->server->settings->delete_unused_networks; + } + } + + public function instantSave() + { + try { + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + // $this->dispatch('refreshServerShow'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCleanup() + { + try { + DockerCleanupJob::dispatch($this->server, true); + $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function submit() + { + try { + if (! validate_cron_expression($this->dockerCleanupFrequency)) { + $this->dockerCleanupFrequency = $this->server->settings->getOriginal('docker_cleanup_frequency'); + throw new \Exception('Invalid Cron / Human expression for Docker Cleanup Frequency.'); + } + $this->syncData(true); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function render() + { + return view('livewire.server.advanced'); + } +} diff --git a/app/Livewire/Server/Charts.php b/app/Livewire/Server/Charts.php index 0921c7fa48..d0db87f577 100644 --- a/app/Livewire/Server/Charts.php +++ b/app/Livewire/Server/Charts.php @@ -19,6 +19,15 @@ class Charts extends Component public bool $poll = true; + public function mount(string $server_uuid) + { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function pollData() { if ($this->poll || $this->interval <= 10) { @@ -34,19 +43,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]]; - }); $this->dispatch("refreshChartData-{$this->chartId}-cpu", [ 'seriesData' => $cpuMetrics, ]); $this->dispatch("refreshChartData-{$this->chartId}-memory", [ 'seriesData' => $memoryMetrics, ]); - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/CloudflareTunnels.php b/app/Livewire/Server/CloudflareTunnels.php new file mode 100644 index 0000000000..3111964732 --- /dev/null +++ b/app/Livewire/Server/CloudflareTunnels.php @@ -0,0 +1,51 @@ +server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->isCloudflareTunnelsEnabled = $this->server->settings->is_cloudflare_tunnel; + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function instantSave() + { + try { + $this->validate(); + $this->server->settings->is_cloudflare_tunnel = $this->isCloudflareTunnelsEnabled; + $this->server->settings->save(); + $this->dispatch('success', 'Server updated.'); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function manualCloudflareConfig() + { + $this->isCloudflareTunnelsEnabled = true; + $this->server->settings->is_cloudflare_tunnel = true; + $this->server->settings->save(); + $this->server->refresh(); + $this->dispatch('success', 'Cloudflare Tunnels enabled.'); + } + + public function render() + { + return view('livewire.server.cloudflare-tunnels'); + } +} diff --git a/app/Livewire/Server/Delete.php b/app/Livewire/Server/Delete.php index ed2345b2a5..b9e3944b54 100644 --- a/app/Livewire/Server/Delete.php +++ b/app/Livewire/Server/Delete.php @@ -2,6 +2,9 @@ namespace App\Livewire\Server; +use App\Actions\Server\DeleteServer; +use App\Models\InstanceSettings; +use App\Models\Server; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Hash; @@ -11,14 +14,25 @@ class Delete extends Component { use AuthorizesRequests; - public $server; + public Server $server; + + public function mount(string $server_uuid) + { + try { + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } public function delete($password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } try { $this->authorize('delete', $this->server); @@ -28,6 +42,7 @@ public function delete($password) return; } $this->server->delete(); + DeleteServer::dispatch($this->server); return redirect()->route('server.index'); } catch (\Throwable $e) { diff --git a/app/Livewire/Server/Destination/Show.php b/app/Livewire/Server/Destination/Show.php deleted file mode 100644 index 986e16cbfd..0000000000 --- a/app/Livewire/Server/Destination/Show.php +++ /dev/null @@ -1,31 +0,0 @@ -parameters = get_route_parameters(); - try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function render() - { - return view('livewire.server.destination.show'); - } -} diff --git a/app/Livewire/Server/Destinations.php b/app/Livewire/Server/Destinations.php new file mode 100644 index 0000000000..dbab6e03f0 --- /dev/null +++ b/app/Livewire/Server/Destinations.php @@ -0,0 +1,90 @@ +networks = collect(); + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + private function createNetworkAndAttachToProxy() + { + $connectProxyToDockerNetworks = connectProxyToNetworks($this->server); + instant_remote_process($connectProxyToDockerNetworks, $this->server, false); + } + + public function add($name) + { + if ($this->server->isSwarm()) { + $found = $this->server->swarmDockers()->where('network', $name)->first(); + if ($found) { + $this->dispatch('error', 'Network already added to this server.'); + + return; + } else { + SwarmDocker::create([ + 'name' => $this->server->name.'-'.$name, + 'network' => $this->name, + 'server_id' => $this->server->id, + ]); + } + } else { + $found = $this->server->standaloneDockers()->where('network', $name)->first(); + if ($found) { + $this->dispatch('error', 'Network already added to this server.'); + + return; + } else { + StandaloneDocker::create([ + 'name' => $this->server->name.'-'.$name, + 'network' => $name, + 'server_id' => $this->server->id, + ]); + } + $this->createNetworkAndAttachToProxy(); + } + } + + public function scan() + { + if ($this->server->isSwarm()) { + $alreadyAddedNetworks = $this->server->swarmDockers; + } else { + $alreadyAddedNetworks = $this->server->standaloneDockers; + } + $networks = instant_remote_process(['docker network ls --format "{{json .}}"'], $this->server, false); + $this->networks = format_docker_command_output_to_json($networks)->filter(function ($network) { + return $network['Name'] !== 'bridge' && $network['Name'] !== 'host' && $network['Name'] !== 'none'; + })->filter(function ($network) use ($alreadyAddedNetworks) { + return ! $alreadyAddedNetworks->contains('network', $network['Name']); + }); + if ($this->networks->count() === 0) { + $this->dispatch('success', 'No new destinations found on this server.'); + + return; + } + $this->dispatch('success', 'Scan done.'); + } + + public function render() + { + return view('livewire.server.destinations'); + } +} diff --git a/app/Livewire/Server/Form.php b/app/Livewire/Server/Form.php deleted file mode 100644 index c4f25c79d5..0000000000 --- a/app/Livewire/Server/Form.php +++ /dev/null @@ -1,283 +0,0 @@ -user()->currentTeam()->id; - - return [ - "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'cloudflareTunnelConfigured', - 'refreshServerShow' => 'serverInstalled', - 'revalidate' => '$refresh', - ]; - } - - protected $rules = [ - 'server.name' => 'required', - 'server.description' => 'nullable', - 'server.ip' => 'required', - 'server.user' => 'required', - 'server.port' => 'required', - 'server.settings.is_cloudflare_tunnel' => 'required|boolean', - 'server.settings.is_reachable' => 'required', - 'server.settings.is_swarm_manager' => 'required|boolean', - 'server.settings.is_swarm_worker' => 'required|boolean', - 'server.settings.is_build_server' => 'required|boolean', - 'server.settings.concurrent_builds' => 'required|integer|min:1', - 'server.settings.dynamic_timeout' => 'required|integer|min:1', - 'server.settings.is_metrics_enabled' => 'required|boolean', - 'server.settings.metrics_token' => 'required', - 'server.settings.metrics_refresh_rate_seconds' => 'required|integer|min:1', - 'server.settings.metrics_history_days' => 'required|integer|min:1', - 'wildcard_domain' => 'nullable|url', - 'server.settings.is_server_api_enabled' => 'required|boolean', - 'server.settings.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 = [ - 'server.name' => 'Name', - 'server.description' => 'Description', - 'server.ip' => 'IP address/Domain', - 'server.user' => 'User', - 'server.port' => 'Port', - 'server.settings.is_cloudflare_tunnel' => 'Cloudflare Tunnel', - 'server.settings.is_reachable' => 'Is reachable', - 'server.settings.is_swarm_manager' => 'Swarm Manager', - 'server.settings.is_swarm_worker' => 'Swarm Worker', - 'server.settings.is_build_server' => 'Build Server', - 'server.settings.concurrent_builds' => 'Concurrent Builds', - 'server.settings.dynamic_timeout' => 'Dynamic Timeout', - 'server.settings.is_metrics_enabled' => 'Metrics', - 'server.settings.metrics_token' => 'Metrics Token', - 'server.settings.metrics_refresh_rate_seconds' => 'Metrics Interval', - 'server.settings.metrics_history_days' => 'Metrics History', - 'server.settings.is_server_api_enabled' => 'Server API', - 'server.settings.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) - { - $this->server = $server; - $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); - $this->wildcard_domain = $this->server->settings->wildcard_domain; - $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; - $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency; - $this->server->settings->delete_unused_volumes = $server->settings->delete_unused_volumes; - $this->server->settings->delete_unused_networks = $server->settings->delete_unused_networks; - } - - public function updated($field) - { - if ($field === 'server.settings.docker_cleanup_frequency') { - $frequency = $this->server->settings->docker_cleanup_frequency; - if (empty($frequency) || ! validate_cron_expression($frequency)) { - $this->dispatch('error', 'Invalid Cron / Human expression for Docker Cleanup Frequency. Resetting to default 10 minutes.'); - $this->server->settings->docker_cleanup_frequency = '*/10 * * * *'; - } - } - } - - public function cloudflareTunnelConfigured() - { - $this->serverInstalled(); - $this->dispatch('success', 'Cloudflare Tunnels configured successfully.'); - } - - public function serverInstalled() - { - $this->server->refresh(); - $this->server->settings->refresh(); - } - - public function updatedServerSettingsIsBuildServer() - { - $this->dispatch('refreshServerShow'); - $this->dispatch('serverRefresh'); - $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 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); - } - $this->server->settings->save(); - // $this->checkPortForServerApi(); - - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function restartSentinel() - { - try { - $version = get_latest_sentinel_version(); - StartSentinel::run($this->server, $version, true); - $this->dispatch('success', 'Sentinel restarted.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function revalidate() - { - $this->revalidate = true; - } - - public function checkLocalhostConnection() - { - $this->submit(); - ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); - if ($uptime) { - $this->dispatch('success', 'Server is reachable.'); - $this->server->settings->is_reachable = true; - $this->server->settings->is_usable = true; - $this->server->settings->save(); - $this->dispatch('proxyStatusUpdated'); - } else { - $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

Error: '.$error); - - return; - } - } - - public function validateServer($install = true) - { - $this->server->update([ - 'validation_logs' => null, - ]); - $this->dispatch('init', $install); - } - - public function submit() - { - try { - if (isCloud() && ! isDev()) { - $this->validate(); - $this->validate([ - 'server.ip' => 'required', - ]); - } else { - $this->validate(); - } - $uniqueIPs = Server::all()->reject(function (Server $server) { - return $server->id === $this->server->id; - })->pluck('ip')->toArray(); - if (in_array($this->server->ip, $uniqueIPs)) { - $this->dispatch('error', 'IP address is already in use by another team.'); - - return; - } - refresh_server_connection($this->server->privateKey); - $this->server->settings->wildcard_domain = $this->wildcard_domain; - if ($this->server->settings->force_docker_cleanup) { - $this->server->settings->docker_cleanup_frequency = $this->server->settings->docker_cleanup_frequency; - } else { - $this->server->settings->docker_cleanup_threshold = $this->server->settings->docker_cleanup_threshold; - } - $currentTimezone = $this->server->settings->getOriginal('server_timezone'); - $newTimezone = $this->server->settings->server_timezone; - if ($currentTimezone !== $newTimezone || $currentTimezone === '') { - $this->server->settings->server_timezone = $newTimezone; - $this->server->settings->save(); - } - $this->server->settings->save(); - $this->server->save(); - - $this->dispatch('success', 'Server updated.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function updatedServerSettingsServerTimezone($value) - { - $this->server->settings->server_timezone = $value; - $this->server->settings->save(); - $this->dispatch('success', 'Server timezone updated.'); - } - - public function manualCleanup() - { - try { - DockerCleanupJob::dispatch($this->server, true); - $this->dispatch('success', 'Manual cleanup job started. Depending on the amount of data, this might take a while.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function manualCloudflareConfig() - { - $this->server->settings->is_cloudflare_tunnel = true; - $this->server->settings->save(); - $this->server->refresh(); - $this->dispatch('success', 'Cloudflare Tunnels enabled.'); - } -} diff --git a/app/Livewire/Server/LogDrains.php b/app/Livewire/Server/LogDrains.php index 6e09eecdd5..6599149c4e 100644 --- a/app/Livewire/Server/LogDrains.php +++ b/app/Livewire/Server/LogDrains.php @@ -2,84 +2,132 @@ namespace App\Livewire\Server; -use App\Actions\Server\InstallLogDrain; +use App\Actions\Server\StartLogDrain; use App\Actions\Server\StopLogDrain; use App\Models\Server; +use Livewire\Attributes\Validate; use Livewire\Component; class LogDrains extends Component { public Server $server; - public $parameters = []; - - protected $rules = [ - 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', - 'server.settings.logdrain_newrelic_license_key' => 'required|string', - 'server.settings.logdrain_newrelic_base_uri' => 'required|string', - 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', - 'server.settings.logdrain_highlight_project_id' => 'required|string', - 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', - 'server.settings.logdrain_axiom_dataset_name' => 'required|string', - 'server.settings.logdrain_axiom_api_key' => 'required|string', - 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', - 'server.settings.logdrain_custom_config' => 'required|string', - 'server.settings.logdrain_custom_config_parser' => 'nullable', - ]; - - protected $validationAttributes = [ - 'server.settings.is_logdrain_newrelic_enabled' => 'New Relic log drain', - 'server.settings.logdrain_newrelic_license_key' => 'New Relic license key', - 'server.settings.logdrain_newrelic_base_uri' => 'New Relic base URI', - 'server.settings.is_logdrain_highlight_enabled' => 'Highlight log drain', - 'server.settings.logdrain_highlight_project_id' => 'Highlight project ID', - 'server.settings.is_logdrain_axiom_enabled' => 'Axiom log drain', - 'server.settings.logdrain_axiom_dataset_name' => 'Axiom dataset name', - 'server.settings.logdrain_axiom_api_key' => 'Axiom API key', - 'server.settings.is_logdrain_custom_enabled' => 'Custom log drain', - 'server.settings.logdrain_custom_config' => 'Custom log drain configuration', - 'server.settings.logdrain_custom_config_parser' => 'Custom log drain configuration parser', - ]; - - public function mount() + #[Validate(['boolean'])] + public bool $isLogDrainNewRelicEnabled = false; + + #[Validate(['boolean'])] + public bool $isLogDrainCustomEnabled = false; + + #[Validate(['boolean'])] + public bool $isLogDrainAxiomEnabled = false; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainNewRelicLicenseKey = null; + + #[Validate(['url', 'nullable'])] + public ?string $logDrainNewRelicBaseUri = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainAxiomDatasetName = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainAxiomApiKey = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainCustomConfig = null; + + #[Validate(['string', 'nullable'])] + public ?string $logDrainCustomConfigParser = null; + + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($server)) { - return redirect()->route('server.index'); - } - $this->server = $server; + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function configureLogDrain() + public function syncData(bool $toModel = false) { - try { - InstallLogDrain::run($this->server); - if (! $this->server->isLogDrainEnabled()) { - $this->dispatch('serverRefresh'); - $this->dispatch('success', 'Log drain service stopped.'); + if ($toModel) { + $this->customValidation(); + $this->server->settings->is_logdrain_newrelic_enabled = $this->isLogDrainNewRelicEnabled; + $this->server->settings->is_logdrain_axiom_enabled = $this->isLogDrainAxiomEnabled; + $this->server->settings->is_logdrain_custom_enabled = $this->isLogDrainCustomEnabled; + + $this->server->settings->logdrain_newrelic_license_key = $this->logDrainNewRelicLicenseKey; + $this->server->settings->logdrain_newrelic_base_uri = $this->logDrainNewRelicBaseUri; + $this->server->settings->logdrain_axiom_dataset_name = $this->logDrainAxiomDatasetName; + $this->server->settings->logdrain_axiom_api_key = $this->logDrainAxiomApiKey; + $this->server->settings->logdrain_custom_config = $this->logDrainCustomConfig; + $this->server->settings->logdrain_custom_config_parser = $this->logDrainCustomConfigParser; + + $this->server->settings->save(); + } else { + $this->isLogDrainNewRelicEnabled = $this->server->settings->is_logdrain_newrelic_enabled; + $this->isLogDrainAxiomEnabled = $this->server->settings->is_logdrain_axiom_enabled; + $this->isLogDrainCustomEnabled = $this->server->settings->is_logdrain_custom_enabled; + + $this->logDrainNewRelicLicenseKey = $this->server->settings->logdrain_newrelic_license_key; + $this->logDrainNewRelicBaseUri = $this->server->settings->logdrain_newrelic_base_uri; + $this->logDrainAxiomDatasetName = $this->server->settings->logdrain_axiom_dataset_name; + $this->logDrainAxiomApiKey = $this->server->settings->logdrain_axiom_api_key; + $this->logDrainCustomConfig = $this->server->settings->logdrain_custom_config; + $this->logDrainCustomConfigParser = $this->server->settings->logdrain_custom_config_parser; + } + } + + public function customValidation() + { + if ($this->isLogDrainNewRelicEnabled) { + try { + $this->validate([ + 'logDrainNewRelicLicenseKey' => ['required'], + 'logDrainNewRelicBaseUri' => ['required', 'url'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainNewRelicEnabled = false; - return; + throw $e; + } + } elseif ($this->isLogDrainAxiomEnabled) { + try { + $this->validate([ + 'logDrainAxiomDatasetName' => ['required'], + 'logDrainAxiomApiKey' => ['required'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainAxiomEnabled = false; + + throw $e; + } + } elseif ($this->isLogDrainCustomEnabled) { + try { + $this->validate([ + 'logDrainCustomConfig' => ['required'], + 'logDrainCustomConfigParser' => ['string', 'nullable'], + ]); + } catch (\Throwable $e) { + $this->isLogDrainCustomEnabled = false; + + throw $e; } - $this->dispatch('serverRefresh'); - $this->dispatch('success', 'Log drain service started.'); - } catch (\Throwable $e) { - return handleError($e, $this); } } - public function instantSave(string $type) + public function instantSave() { try { - $ok = $this->submit($type); - if (! $ok) { - return; + $this->syncData(true); + if ($this->server->isLogDrainEnabled()) { + StartLogDrain::run($this->server); + $this->dispatch('success', 'Log drain service started.'); + } else { + StopLogDrain::run($this->server); + $this->dispatch('success', 'Log drain service stopped.'); } - $this->configureLogDrain(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -88,79 +136,10 @@ public function instantSave(string $type) public function submit(string $type) { try { - $this->resetErrorBag(); - if ($type === 'newrelic') { - $this->validate([ - 'server.settings.is_logdrain_newrelic_enabled' => 'required|boolean', - 'server.settings.logdrain_newrelic_license_key' => 'required|string', - 'server.settings.logdrain_newrelic_base_uri' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'highlight') { - $this->validate([ - 'server.settings.is_logdrain_highlight_enabled' => 'required|boolean', - 'server.settings.logdrain_highlight_project_id' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'axiom') { - $this->validate([ - 'server.settings.is_logdrain_axiom_enabled' => 'required|boolean', - 'server.settings.logdrain_axiom_dataset_name' => 'required|string', - 'server.settings.logdrain_axiom_api_key' => 'required|string', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_custom_enabled' => false, - ]); - } elseif ($type === 'custom') { - $this->validate([ - 'server.settings.is_logdrain_custom_enabled' => 'required|boolean', - 'server.settings.logdrain_custom_config' => 'required|string', - 'server.settings.logdrain_custom_config_parser' => 'nullable', - ]); - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - 'is_logdrain_highlight_enabled' => false, - 'is_logdrain_axiom_enabled' => false, - ]); - } - if (! $this->server->isLogDrainEnabled()) { - StopLogDrain::dispatch($this->server); - } - $this->server->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Settings saved.'); - - return true; } catch (\Throwable $e) { - if ($type === 'newrelic') { - $this->server->settings->update([ - 'is_logdrain_newrelic_enabled' => false, - ]); - } elseif ($type === 'highlight') { - $this->server->settings->update([ - 'is_logdrain_highlight_enabled' => false, - ]); - } elseif ($type === 'axiom') { - $this->server->settings->update([ - 'is_logdrain_axiom_enabled' => false, - ]); - } elseif ($type === 'custom') { - $this->server->settings->update([ - 'is_logdrain_custom_enabled' => false, - ]); - } - handleError($e, $this); - - return false; + return handleError($e, $this); } } diff --git a/app/Livewire/Server/PrivateKey/Show.php b/app/Livewire/Server/PrivateKey/Show.php index 0ad820428e..64aa1884ba 100644 --- a/app/Livewire/Server/PrivateKey/Show.php +++ b/app/Livewire/Server/PrivateKey/Show.php @@ -8,26 +8,63 @@ class Show extends Component { - public ?Server $server = null; + public Server $server; public $privateKeys = []; public $parameters = []; - public function mount() + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); $this->privateKeys = PrivateKey::ownedByCurrentTeam()->get()->where('is_git_related', false); } catch (\Throwable $e) { return handleError($e, $this); } } + public function setPrivateKey($privateKeyId) + { + $ownedPrivateKey = PrivateKey::ownedByCurrentTeam()->find($privateKeyId); + if (is_null($ownedPrivateKey)) { + $this->dispatch('error', 'You are not allowed to use this private key.'); + + return; + } + + $originalPrivateKeyId = $this->server->getOriginal('private_key_id'); + try { + $this->server->update(['private_key_id' => $privateKeyId]); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(justCheckingNewKey: true); + if ($uptime) { + $this->dispatch('success', 'Private key updated successfully.'); + } else { + throw new \Exception($error); + } + } catch (\Exception $e) { + $this->server->update(['private_key_id' => $originalPrivateKeyId]); + $this->server->validateConnection(); + $this->dispatch('error', $e->getMessage()); + } + } + + public function checkConnection() + { + try { + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + } else { + $this->dispatch('error', 'Server is not reachable.

Check this documentation for further help.

Error: '.$error); + + return; + } + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function render() { return view('livewire.server.private-key.show'); diff --git a/app/Livewire/Server/Proxy.php b/app/Livewire/Server/Proxy.php index 55d0c4966f..94ea3509a2 100644 --- a/app/Livewire/Server/Proxy.php +++ b/app/Livewire/Server/Proxy.php @@ -99,7 +99,6 @@ public function loadProxyConfiguration() } else { $this->dispatch('traefikDashboardAvailable', false); } - } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Proxy/Deploy.php b/app/Livewire/Server/Proxy/Deploy.php index eaa3126635..8fcff85d6a 100644 --- a/app/Livewire/Server/Proxy/Deploy.php +++ b/app/Livewire/Server/Proxy/Deploy.php @@ -6,6 +6,7 @@ use App\Actions\Proxy\StartProxy; use App\Events\ProxyStatusChanged; use App\Models\Server; +use Carbon\Carbon; use Illuminate\Process\InvokedProcess; use Illuminate\Support\Facades\Process; use Livewire\Component; @@ -102,9 +103,9 @@ public function stop(bool $forceStop = true) $process = $this->stopContainer($containerName, $timeout); - $startTime = time(); + $startTime = Carbon::now()->getTimestamp(); while ($process->running()) { - if (time() - $startTime >= $timeout) { + if (Carbon::now()->getTimestamp() - $startTime >= $timeout) { $this->forceStopContainer($containerName); break; } diff --git a/app/Livewire/Server/Proxy/Modal.php b/app/Livewire/Server/Proxy/Modal.php deleted file mode 100644 index 5679944d04..0000000000 --- a/app/Livewire/Server/Proxy/Modal.php +++ /dev/null @@ -1,16 +0,0 @@ -dispatch('proxyStatusUpdated'); - } -} diff --git a/app/Livewire/Server/Proxy/Show.php b/app/Livewire/Server/Proxy/Show.php index d70e44e559..5ecb56a691 100644 --- a/app/Livewire/Server/Proxy/Show.php +++ b/app/Livewire/Server/Proxy/Show.php @@ -22,10 +22,7 @@ public function mount() { $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->firstOrFail(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Resources.php b/app/Livewire/Server/Resources.php index 800344ac3e..f549b43cbd 100644 --- a/app/Livewire/Server/Resources.php +++ b/app/Livewire/Server/Resources.php @@ -15,7 +15,9 @@ class Resources extends Component public $parameters = []; - public Collection $unmanagedContainers; + public Collection $containers; + + public $activeTab = 'managed'; public function getListeners() { @@ -50,14 +52,29 @@ public function stopUnmanaged($id) public function refreshStatus() { $this->server->refresh(); - $this->loadUnmanagedContainers(); + if ($this->activeTab === 'managed') { + $this->loadManagedContainers(); + } else { + $this->loadUnmanagedContainers(); + } $this->dispatch('success', 'Resource statuses refreshed.'); } + public function loadManagedContainers() + { + try { + $this->activeTab = 'managed'; + $this->containers = $this->server->refresh()->definedResources(); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + public function loadUnmanagedContainers() { + $this->activeTab = 'unmanaged'; try { - $this->unmanagedContainers = $this->server->loadUnmanagedContainers(); + $this->containers = $this->server->loadUnmanagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } @@ -65,13 +82,14 @@ public function loadUnmanagedContainers() public function mount() { - $this->unmanagedContainers = collect(); + $this->containers = collect(); $this->parameters = get_route_parameters(); try { $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); if (is_null($this->server)) { return redirect()->route('server.index'); } + $this->loadManagedContainers(); } catch (\Throwable $e) { return handleError($e, $this); } diff --git a/app/Livewire/Server/Show.php b/app/Livewire/Server/Show.php index a5e94a19a1..524e219083 100644 --- a/app/Livewire/Server/Show.php +++ b/app/Livewire/Server/Show.php @@ -2,42 +2,243 @@ namespace App\Livewire\Server; +use App\Actions\Server\StartSentinel; +use App\Actions\Server\StopSentinel; use App\Models\Server; -use Illuminate\Foundation\Auth\Access\AuthorizesRequests; +use Livewire\Attributes\Validate; use Livewire\Component; class Show extends Component { - use AuthorizesRequests; + public Server $server; - public ?Server $server = null; + #[Validate(['required'])] + public string $name; - public $parameters = []; + #[Validate(['nullable'])] + public ?string $description; - protected $listeners = ['refreshServerShow']; + #[Validate(['required'])] + public string $ip; - public function mount() + #[Validate(['required'])] + public string $user; + + #[Validate(['required'])] + public string $port; + + #[Validate(['nullable'])] + public ?string $validationLogs = null; + + #[Validate(['nullable', 'url'])] + public ?string $wildcardDomain; + + #[Validate(['required'])] + public bool $isReachable; + + #[Validate(['required'])] + public bool $isUsable; + + #[Validate(['required'])] + public bool $isSwarmManager; + + #[Validate(['required'])] + public bool $isSwarmWorker; + + #[Validate(['required'])] + public bool $isBuildServer; + + #[Validate(['required'])] + public bool $isMetricsEnabled; + + #[Validate(['required'])] + public string $sentinelToken; + + #[Validate(['nullable'])] + public ?string $sentinelUpdatedAt; + + #[Validate(['required', 'integer', 'min:1'])] + public int $sentinelMetricsRefreshRateSeconds; + + #[Validate(['required', 'integer', 'min:1'])] + public int $sentinelMetricsHistoryDays; + + #[Validate(['required', 'integer', 'min:10'])] + public int $sentinelPushIntervalSeconds; + + #[Validate(['nullable', 'url'])] + public ?string $sentinelCustomUrl; + + #[Validate(['required'])] + public bool $isSentinelEnabled; + + #[Validate(['required'])] + public bool $isSentinelDebugEnabled; + + #[Validate(['required'])] + public string $serverTimezone; + + public array $timezones; + + public function getListeners() + { + $teamId = auth()->user()->currentTeam()->id; + + return [ + "echo-private:team.{$teamId},CloudflareTunnelConfigured" => 'refresh', + 'refreshServerShow' => 'refresh', + ]; + } + + public function mount(string $server_uuid) { - $this->parameters = get_route_parameters(); try { - $this->server = Server::ownedByCurrentTeam()->whereUuid(request()->server_uuid)->first(); - if (is_null($this->server)) { - return redirect()->route('server.index'); - } + $this->server = Server::ownedByCurrentTeam()->whereUuid($server_uuid)->firstOrFail(); + $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); + $this->syncData(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function refreshServerShow() + public function syncData(bool $toModel = false) { - $this->server->refresh(); + if ($toModel) { + $this->validate(); + $this->server->name = $this->name; + $this->server->description = $this->description; + $this->server->ip = $this->ip; + $this->server->user = $this->user; + $this->server->port = $this->port; + $this->server->validation_logs = $this->validationLogs; + $this->server->save(); + + $this->server->settings->is_swarm_manager = $this->isSwarmManager; + $this->server->settings->is_swarm_worker = $this->isSwarmWorker; + $this->server->settings->is_build_server = $this->isBuildServer; + $this->server->settings->is_metrics_enabled = $this->isMetricsEnabled; + $this->server->settings->sentinel_token = $this->sentinelToken; + $this->server->settings->sentinel_metrics_refresh_rate_seconds = $this->sentinelMetricsRefreshRateSeconds; + $this->server->settings->sentinel_metrics_history_days = $this->sentinelMetricsHistoryDays; + $this->server->settings->sentinel_push_interval_seconds = $this->sentinelPushIntervalSeconds; + $this->server->settings->sentinel_custom_url = $this->sentinelCustomUrl; + $this->server->settings->is_sentinel_enabled = $this->isSentinelEnabled; + $this->server->settings->is_sentinel_debug_enabled = $this->isSentinelDebugEnabled; + $this->server->settings->server_timezone = $this->serverTimezone; + $this->server->settings->save(); + } else { + $this->name = $this->server->name; + $this->description = $this->server->description; + $this->ip = $this->server->ip; + $this->user = $this->server->user; + $this->port = $this->server->port; + $this->wildcardDomain = $this->server->settings->wildcard_domain; + $this->isReachable = $this->server->settings->is_reachable; + $this->isUsable = $this->server->settings->is_usable; + $this->isSwarmManager = $this->server->settings->is_swarm_manager; + $this->isSwarmWorker = $this->server->settings->is_swarm_worker; + $this->isBuildServer = $this->server->settings->is_build_server; + $this->isMetricsEnabled = $this->server->settings->is_metrics_enabled; + $this->sentinelToken = $this->server->settings->sentinel_token; + $this->sentinelMetricsRefreshRateSeconds = $this->server->settings->sentinel_metrics_refresh_rate_seconds; + $this->sentinelMetricsHistoryDays = $this->server->settings->sentinel_metrics_history_days; + $this->sentinelPushIntervalSeconds = $this->server->settings->sentinel_push_interval_seconds; + $this->sentinelCustomUrl = $this->server->settings->sentinel_custom_url; + $this->isSentinelEnabled = $this->server->settings->is_sentinel_enabled; + $this->isSentinelDebugEnabled = $this->server->settings->is_sentinel_debug_enabled; + $this->sentinelUpdatedAt = $this->server->settings->updated_at; + $this->serverTimezone = $this->server->settings->server_timezone; + } + } + + public function refresh() + { + $this->syncData(); $this->dispatch('$refresh'); } + public function validateServer($install = true) + { + try { + $this->validationLogs = $this->server->validation_logs = null; + $this->server->save(); + $this->dispatch('init', $install); + } catch (\Throwable $e) { + return handleError($e, $this); + } + } + + public function checkLocalhostConnection() + { + $this->syncData(true); + ['uptime' => $uptime, 'error' => $error] = $this->server->validateConnection(); + if ($uptime) { + $this->dispatch('success', 'Server is reachable.'); + $this->server->settings->is_reachable = $this->isReachable = true; + $this->server->settings->is_usable = $this->isUsable = true; + $this->server->settings->save(); + $this->dispatch('proxyStatusUpdated'); + } else { + $this->dispatch('error', 'Server is not reachable.', 'Please validate your configuration and connection.

Check this documentation for further help.

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

Check this documentation for further help.

Error: '.$error); - - return; - } - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function mount() - { - $this->parameters = get_route_parameters(); - } -} diff --git a/app/Livewire/Settings/Index.php b/app/Livewire/Settings/Index.php index 754f0929bd..2991b8ae84 100644 --- a/app/Livewire/Settings/Index.php +++ b/app/Livewire/Settings/Index.php @@ -5,62 +5,95 @@ use App\Jobs\CheckForUpdatesJob; use App\Models\InstanceSettings; use App\Models\Server; +use Illuminate\Support\Facades\Auth; +use Illuminate\Support\Facades\Hash; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class Index extends Component { public InstanceSettings $settings; - public bool $do_not_track; + protected Server $server; + + #[Locked] + public $timezones; + #[Validate('boolean')] public bool $is_auto_update_enabled; - public bool $is_registration_enabled; + #[Validate('nullable|string|max:255')] + public ?string $fqdn = null; - public bool $is_dns_validation_enabled; + #[Validate('nullable|string|max:255')] + public ?string $resale_license = null; - public bool $is_api_enabled; + #[Validate('required|integer|min:1025|max:65535')] + public int $public_port_min; + + #[Validate('required|integer|min:1025|max:65535')] + public int $public_port_max; + + #[Validate('nullable|string')] + public ?string $custom_dns_servers = null; + + #[Validate('nullable|string|max:255')] + public ?string $instance_name = null; + + #[Validate('nullable|string')] + public ?string $allowed_ips = null; + #[Validate('nullable|string')] + public ?string $public_ipv4 = null; + + #[Validate('nullable|string')] + public ?string $public_ipv6 = null; + + #[Validate('string')] public string $auto_update_frequency; + #[Validate('string')] public string $update_check_frequency; - protected string $dynamic_config_path = '/data/coolify/proxy/dynamic'; + #[Validate('required|string|timezone')] + public string $instance_timezone; - protected Server $server; + #[Validate('boolean')] + public bool $do_not_track; + + #[Validate('boolean')] + public bool $is_registration_enabled; - protected $rules = [ - 'settings.fqdn' => 'nullable', - 'settings.resale_license' => 'nullable', - 'settings.public_port_min' => 'required', - 'settings.public_port_max' => 'required', - 'settings.custom_dns_servers' => 'nullable', - 'settings.instance_name' => 'nullable', - 'settings.allowed_ips' => 'nullable', - 'settings.is_auto_update_enabled' => 'boolean', - 'auto_update_frequency' => 'string', - 'update_check_frequency' => 'string', - 'settings.instance_timezone' => 'required|string|timezone', - ]; - - protected $validationAttributes = [ - 'settings.fqdn' => 'FQDN', - 'settings.resale_license' => 'Resale License', - 'settings.public_port_min' => 'Public port min', - 'settings.public_port_max' => 'Public port max', - 'settings.custom_dns_servers' => 'Custom DNS servers', - 'settings.allowed_ips' => 'Allowed IPs', - 'settings.is_auto_update_enabled' => 'Auto Update Enabled', - 'auto_update_frequency' => 'Auto Update Frequency', - 'update_check_frequency' => 'Update Check Frequency', - ]; + #[Validate('boolean')] + public bool $is_dns_validation_enabled; - public $timezones; + #[Validate('boolean')] + public bool $is_api_enabled; + + #[Validate('boolean')] + public bool $disable_two_step_confirmation; + + public function render() + { + return view('livewire.settings.index'); + } public function mount() { - if (isInstanceAdmin()) { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } else { $this->settings = instanceSettings(); + $this->fqdn = $this->settings->fqdn; + $this->resale_license = $this->settings->resale_license; + $this->public_port_min = $this->settings->public_port_min; + $this->public_port_max = $this->settings->public_port_max; + $this->custom_dns_servers = $this->settings->custom_dns_servers; + $this->instance_name = $this->settings->instance_name; + $this->allowed_ips = $this->settings->allowed_ips; + $this->public_ipv4 = $this->settings->public_ipv4; + $this->public_ipv6 = $this->settings->public_ipv6; $this->do_not_track = $this->settings->do_not_track; $this->is_auto_update_enabled = $this->settings->is_auto_update_enabled; $this->is_registration_enabled = $this->settings->is_registration_enabled; @@ -69,13 +102,22 @@ public function mount() $this->auto_update_frequency = $this->settings->auto_update_frequency; $this->update_check_frequency = $this->settings->update_check_frequency; $this->timezones = collect(timezone_identifiers_list())->sort()->values()->toArray(); - } else { - return redirect()->route('dashboard'); + $this->instance_timezone = $this->settings->instance_timezone; + $this->disable_two_step_confirmation = $this->settings->disable_two_step_confirmation; } } - public function instantSave() + public function instantSave($isSave = true) { + $this->settings->fqdn = $this->fqdn; + $this->settings->resale_license = $this->resale_license; + $this->settings->public_port_min = $this->public_port_min; + $this->settings->public_port_max = $this->public_port_max; + $this->settings->custom_dns_servers = $this->custom_dns_servers; + $this->settings->instance_name = $this->instance_name; + $this->settings->allowed_ips = $this->allowed_ips; + $this->settings->public_ipv4 = $this->public_ipv4; + $this->settings->public_ipv6 = $this->public_ipv6; $this->settings->do_not_track = $this->do_not_track; $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; $this->settings->is_registration_enabled = $this->is_registration_enabled; @@ -83,8 +125,12 @@ public function instantSave() $this->settings->is_api_enabled = $this->is_api_enabled; $this->settings->auto_update_frequency = $this->auto_update_frequency; $this->settings->update_check_frequency = $this->update_check_frequency; - $this->settings->save(); - $this->dispatch('success', 'Settings updated!'); + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation; + $this->settings->instance_timezone = $this->instance_timezone; + if ($isSave) { + $this->settings->save(); + $this->dispatch('success', 'Settings updated!'); + } } public function submit() @@ -141,13 +187,8 @@ public function submit() $this->settings->allowed_ips = $this->settings->allowed_ips->unique(); $this->settings->allowed_ips = $this->settings->allowed_ips->implode(','); - $this->settings->do_not_track = $this->do_not_track; - $this->settings->is_auto_update_enabled = $this->is_auto_update_enabled; - $this->settings->is_registration_enabled = $this->is_registration_enabled; - $this->settings->is_dns_validation_enabled = $this->is_dns_validation_enabled; - $this->settings->is_api_enabled = $this->is_api_enabled; - $this->settings->auto_update_frequency = $this->auto_update_frequency; - $this->settings->update_check_frequency = $this->update_check_frequency; + $this->instantSave(isSave: false); + $this->settings->save(); $this->server->setupDynamicProxyConfiguration(); if (! $error_show) { @@ -170,15 +211,16 @@ public function checkManually() } } - public function updatedSettingsInstanceTimezone($value) + public function toggleTwoStepConfirmation($password) { - $this->settings->instance_timezone = $value; - $this->settings->save(); - $this->dispatch('success', 'Instance timezone updated.'); - } + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - public function render() - { - return view('livewire.settings.index'); + return; + } + + $this->settings->disable_two_step_confirmation = $this->disable_two_step_confirmation = true; + $this->settings->save(); + $this->dispatch('success', 'Two step confirmation has been disabled.'); } } diff --git a/app/Livewire/Settings/License.php b/app/Livewire/Settings/License.php index ca0c9c1ae3..79f8269f3f 100644 --- a/app/Livewire/Settings/License.php +++ b/app/Livewire/Settings/License.php @@ -28,6 +28,9 @@ public function mount() if (! isCloud()) { abort(404); } + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->instance_id = config('app.id'); $this->settings = instanceSettings(); } @@ -47,7 +50,6 @@ public function submit() $this->dispatch('reloadWindow'); } catch (\Throwable $e) { session()->flash('error', 'Something went wrong. Please contact support.
Error: '.$e->getMessage()); - ray($e->getMessage()); return redirect()->route('settings.license'); } diff --git a/app/Livewire/SettingsBackup.php b/app/Livewire/SettingsBackup.php index 9240aa96d2..6dc5d6ab39 100644 --- a/app/Livewire/SettingsBackup.php +++ b/app/Livewire/SettingsBackup.php @@ -2,50 +2,59 @@ namespace App\Livewire; -use App\Jobs\DatabaseBackupJob; use App\Models\InstanceSettings; use App\Models\S3Storage; use App\Models\ScheduledDatabaseBackup; use App\Models\Server; use App\Models\StandalonePostgresql; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Validate; use Livewire\Component; class SettingsBackup extends Component { public InstanceSettings $settings; - public $s3s; - public ?StandalonePostgresql $database = null; public ScheduledDatabaseBackup|null|array $backup = []; + #[Locked] + public $s3s; + + #[Locked] public $executions = []; - protected $rules = [ - 'database.uuid' => 'required', - 'database.name' => 'required', - 'database.description' => 'nullable', - 'database.postgres_user' => 'required', - 'database.postgres_password' => 'required', + #[Validate(['required'])] + public string $uuid; - ]; + #[Validate(['required'])] + public string $name; - protected $validationAttributes = [ - 'database.uuid' => 'uuid', - 'database.name' => 'name', - 'database.description' => 'description', - 'database.postgres_user' => 'postgres user', - 'database.postgres_password' => 'postgres password', - ]; + #[Validate(['nullable'])] + public ?string $description = null; + + #[Validate(['required'])] + public string $postgres_user; + + #[Validate(['required'])] + public string $postgres_password; public function mount() { - if (isInstanceAdmin()) { + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } else { $settings = instanceSettings(); $this->database = StandalonePostgresql::whereName('coolify-db')->first(); $s3s = S3Storage::whereTeamId(0)->get() ?? []; if ($this->database) { + $this->uuid = $this->database->uuid; + $this->name = $this->database->name; + $this->description = $this->database->description; + $this->postgres_user = $this->database->postgres_user; + $this->postgres_password = $this->database->postgres_password; + if ($this->database->status !== 'running') { $this->database->status = 'running'; $this->database->save(); @@ -55,13 +64,10 @@ public function mount() } $this->settings = $settings; $this->s3s = $s3s; - - } else { - return redirect()->route('dashboard'); } } - public function add_coolify_database() + public function addCoolifyDatabase() { try { $server = Server::findOrFail(0); @@ -78,7 +84,7 @@ public function add_coolify_database() 'postgres_password' => $postgres_password, 'postgres_db' => $postgres_db, 'status' => 'running', - 'destination_type' => 'App\Models\StandaloneDocker', + 'destination_type' => \App\Models\StandaloneDocker::class, 'destination_id' => 0, ]); $this->backup = ScheduledDatabaseBackup::create([ @@ -87,7 +93,7 @@ public function add_coolify_database() 'save_s3' => false, 'frequency' => '0 0 * * *', 'database_id' => $this->database->id, - 'database_type' => 'App\Models\StandalonePostgresql', + 'database_type' => \App\Models\StandalonePostgresql::class, 'team_id' => currentTeam()->id, ]); $this->database->refresh(); @@ -98,16 +104,14 @@ public function add_coolify_database() } } - public function backup_now() - { - dispatch(new DatabaseBackupJob( - backup: $this->backup - )); - $this->dispatch('success', 'Backup queued. It will be available in a few minutes.'); - } - public function submit() { + $this->database->update([ + 'name' => $this->name, + 'description' => $this->description, + 'postgres_user' => $this->postgres_user, + 'postgres_password' => $this->postgres_password, + ]); $this->dispatch('success', 'Backup updated.'); } } diff --git a/app/Livewire/SettingsEmail.php b/app/Livewire/SettingsEmail.php index 4515df9a7d..0ab5754f20 100644 --- a/app/Livewire/SettingsEmail.php +++ b/app/Livewire/SettingsEmail.php @@ -3,131 +3,113 @@ namespace App\Livewire; use App\Models\InstanceSettings; -use App\Notifications\TransactionalEmails\Test; +use Livewire\Attributes\Validate; use Livewire\Component; class SettingsEmail extends Component { public InstanceSettings $settings; - public string $emails; - - protected $rules = [ - 'settings.smtp_enabled' => 'nullable|boolean', - 'settings.smtp_host' => 'required', - 'settings.smtp_port' => 'required|numeric', - 'settings.smtp_encryption' => 'nullable', - 'settings.smtp_username' => 'nullable', - 'settings.smtp_password' => 'nullable', - 'settings.smtp_timeout' => 'nullable', - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.resend_enabled' => 'nullable|boolean', - 'settings.resend_api_key' => 'nullable', - - ]; - - protected $validationAttributes = [ - 'settings.smtp_from_address' => 'From Address', - 'settings.smtp_from_name' => 'From Name', - 'settings.smtp_recipients' => 'Recipients', - 'settings.smtp_host' => 'Host', - 'settings.smtp_port' => 'Port', - 'settings.smtp_encryption' => 'Encryption', - 'settings.smtp_username' => 'Username', - 'settings.smtp_password' => 'Password', - 'settings.smtp_timeout' => 'Timeout', - 'settings.resend_api_key' => 'Resend API Key', - ]; + #[Validate(['boolean'])] + public bool $smtpEnabled = false; + + #[Validate(['nullable', 'string'])] + public ?string $smtpHost = null; + + #[Validate(['nullable', 'numeric', 'min:1', 'max:65535'])] + public ?int $smtpPort = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpEncryption = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpUsername = null; + + #[Validate(['nullable'])] + public ?string $smtpPassword = null; + + #[Validate(['nullable', 'numeric'])] + public ?int $smtpTimeout = null; + + #[Validate(['nullable', 'email'])] + public ?string $smtpFromAddress = null; + + #[Validate(['nullable', 'string'])] + public ?string $smtpFromName = null; + + #[Validate(['boolean'])] + public bool $resendEnabled = false; + + #[Validate(['nullable', 'string'])] + public ?string $resendApiKey = null; public function mount() { - if (isInstanceAdmin()) { - $this->settings = instanceSettings(); - $this->emails = auth()->user()->email; - } else { + if (isInstanceAdmin() === false) { return redirect()->route('dashboard'); } - + $this->settings = instanceSettings(); + $this->syncData(); } - public function submitFromFields() + public function syncData(bool $toModel = false) { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - ]); + if ($toModel) { + $this->validate(); + $this->settings->smtp_enabled = $this->smtpEnabled; + $this->settings->smtp_host = $this->smtpHost; + $this->settings->smtp_port = $this->smtpPort; + $this->settings->smtp_encryption = $this->smtpEncryption; + $this->settings->smtp_username = $this->smtpUsername; + $this->settings->smtp_password = $this->smtpPassword; + $this->settings->smtp_timeout = $this->smtpTimeout; + + $this->settings->resend_enabled = $this->resendEnabled; + $this->settings->resend_api_key = $this->resendApiKey; $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); + } else { + $this->smtpEnabled = $this->settings->smtp_enabled; + $this->smtpHost = $this->settings->smtp_host; + $this->smtpPort = $this->settings->smtp_port; + $this->smtpEncryption = $this->settings->smtp_encryption; + $this->smtpUsername = $this->settings->smtp_username; + $this->smtpPassword = $this->settings->smtp_password; + $this->smtpTimeout = $this->settings->smtp_timeout; + $this->smtpFromAddress = $this->settings->smtp_from_address; + $this->smtpFromName = $this->settings->smtp_from_name; + + $this->resendEnabled = $this->settings->resend_enabled; + $this->resendApiKey = $this->settings->resend_api_key; } } - public function submitResend() + public function submit() { try { $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.resend_api_key' => 'required', - ]); - $this->settings->save(); + $this->syncData(true); $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - $this->settings->resend_enabled = false; - - return handleError($e, $this); - } - } - - public function instantSaveResend() - { - try { - $this->settings->smtp_enabled = false; - $this->submitResend(); } catch (\Throwable $e) { return handleError($e, $this); } } - public function instantSave() + public function instantSave(string $type) { try { - $this->settings->resend_enabled = false; - $this->submit(); + if ($type === 'SMTP') { + $this->resendEnabled = false; + } else { + $this->smtpEnabled = false; + } + $this->syncData(true); + if ($this->smtpEnabled || $this->resendEnabled) { + $this->dispatch('success', "{$type} enabled."); + } else { + $this->dispatch('success', "{$type} disabled."); + } } catch (\Throwable $e) { return handleError($e, $this); } } - - public function submit() - { - try { - $this->resetErrorBag(); - $this->validate([ - 'settings.smtp_from_address' => 'required|email', - 'settings.smtp_from_name' => 'required', - 'settings.smtp_host' => 'required', - 'settings.smtp_port' => 'required|numeric', - 'settings.smtp_encryption' => 'nullable', - 'settings.smtp_username' => 'nullable', - 'settings.smtp_password' => 'nullable', - 'settings.smtp_timeout' => 'nullable', - ]); - $this->settings->save(); - $this->dispatch('success', 'Settings saved.'); - } catch (\Throwable $e) { - return handleError($e, $this); - } - } - - public function sendTestNotification() - { - $this->settings?->notify(new Test($this->emails)); - $this->dispatch('success', 'Test email sent.'); - } } diff --git a/app/Livewire/SettingsOauth.php b/app/Livewire/SettingsOauth.php index c3884589f6..17b3b89a3b 100644 --- a/app/Livewire/SettingsOauth.php +++ b/app/Livewire/SettingsOauth.php @@ -24,6 +24,9 @@ protected function rules() public function mount() { + if (! isInstanceAdmin()) { + return redirect()->route('home'); + } $this->oauth_settings_map = OauthSetting::all()->sortBy('provider')->reduce(function ($carry, $setting) { $carry[$setting->provider] = $setting; diff --git a/app/Livewire/Source/Github/Change.php b/app/Livewire/Source/Github/Change.php index 193b650ffe..07cef54f9f 100644 --- a/app/Livewire/Source/Github/Change.php +++ b/app/Livewire/Source/Github/Change.php @@ -4,7 +4,6 @@ use App\Jobs\GithubAppPermissionJob; use App\Models\GithubApp; -use Illuminate\Support\Facades\Http; use Livewire\Component; class Change extends Component @@ -93,51 +92,53 @@ public function checkPermissions() // } public function mount() { - $github_app_uuid = request()->github_app_uuid; - $this->github_app = GithubApp::where('uuid', $github_app_uuid)->first(); - if (! $this->github_app) { - return redirect()->route('source.all'); - } - $this->applications = $this->github_app->applications; - $settings = instanceSettings(); - $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); + try { + $github_app_uuid = request()->github_app_uuid; + $this->github_app = GithubApp::ownedByCurrentTeam()->whereUuid($github_app_uuid)->firstOrFail(); - $this->name = str($this->github_app->name)->kebab(); - $this->fqdn = $settings->fqdn; + $this->applications = $this->github_app->applications; + $settings = instanceSettings(); + $this->github_app->makeVisible('client_secret')->makeVisible('webhook_secret'); - if ($settings->public_ipv4) { - $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); - } - if ($settings->public_ipv6) { - $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); - } - if ($this->github_app->installation_id && session('from')) { - $source_id = data_get(session('from'), 'source_id'); - if (! $source_id || $this->github_app->id !== $source_id) { - session()->forget('from'); + $this->name = str($this->github_app->name)->kebab(); + $this->fqdn = $settings->fqdn; + + if ($settings->public_ipv4) { + $this->ipv4 = 'http://'.$settings->public_ipv4.':'.config('app.port'); + } + if ($settings->public_ipv6) { + $this->ipv6 = 'http://'.$settings->public_ipv6.':'.config('app.port'); + } + if ($this->github_app->installation_id && session('from')) { + $source_id = data_get(session('from'), 'source_id'); + if (! $source_id || $this->github_app->id !== $source_id) { + session()->forget('from'); + } else { + $parameters = data_get(session('from'), 'parameters'); + $back = data_get(session('from'), 'back'); + $environment_name = data_get($parameters, 'environment_name'); + $project_uuid = data_get($parameters, 'project_uuid'); + $type = data_get($parameters, 'type'); + $destination = data_get($parameters, 'destination'); + session()->forget('from'); + + return redirect()->route($back, [ + 'environment_name' => $environment_name, + 'project_uuid' => $project_uuid, + 'type' => $type, + 'destination' => $destination, + ]); + } + } + $this->parameters = get_route_parameters(); + if (isCloud() && ! isDev()) { + $this->webhook_endpoint = config('app.url'); } else { - $parameters = data_get(session('from'), 'parameters'); - $back = data_get(session('from'), 'back'); - $environment_name = data_get($parameters, 'environment_name'); - $project_uuid = data_get($parameters, 'project_uuid'); - $type = data_get($parameters, 'type'); - $destination = data_get($parameters, 'destination'); - session()->forget('from'); - - return redirect()->route($back, [ - 'environment_name' => $environment_name, - 'project_uuid' => $project_uuid, - 'type' => $type, - 'destination' => $destination, - ]); + $this->webhook_endpoint = $this->ipv4; + $this->is_system_wide = $this->github_app->is_system_wide; } - } - $this->parameters = get_route_parameters(); - if (isCloud() && ! isDev()) { - $this->webhook_endpoint = config('app.url'); - } else { - $this->webhook_endpoint = $this->ipv4; - $this->is_system_wide = $this->github_app->is_system_wide; + } catch (\Throwable $e) { + return handleError($e, $this); } } diff --git a/app/Livewire/Source/Github/Create.php b/app/Livewire/Source/Github/Create.php index f85e8646e1..103c5c9fbb 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/Subscription/PricingPlans.php b/app/Livewire/Subscription/PricingPlans.php index 9bc11d862d..6b2d3fb364 100644 --- a/app/Livewire/Subscription/PricingPlans.php +++ b/app/Livewire/Subscription/PricingPlans.php @@ -2,55 +2,23 @@ namespace App\Livewire\Subscription; +use Illuminate\Support\Facades\Auth; use Livewire\Component; use Stripe\Checkout\Session; use Stripe\Stripe; class PricingPlans extends Component { - public bool $isTrial = false; - - public function mount() - { - $this->isTrial = ! data_get(currentTeam(), 'subscription.stripe_trial_already_ended'); - if (config('constants.limits.trial_period') == 0) { - $this->isTrial = false; - } - } - public function subscribeStripe($type) { - $team = currentTeam(); Stripe::setApiKey(config('subscription.stripe_api_key')); - switch ($type) { - case 'basic-monthly': - $priceId = config('subscription.stripe_price_id_basic_monthly'); - break; - case 'basic-yearly': - $priceId = config('subscription.stripe_price_id_basic_yearly'); - break; - case 'pro-monthly': - $priceId = config('subscription.stripe_price_id_pro_monthly'); - break; - case 'pro-yearly': - $priceId = config('subscription.stripe_price_id_pro_yearly'); - break; - case 'ultimate-monthly': - $priceId = config('subscription.stripe_price_id_ultimate_monthly'); - break; - case 'ultimate-yearly': - $priceId = config('subscription.stripe_price_id_ultimate_yearly'); - break; - case 'dynamic-monthly': - $priceId = config('subscription.stripe_price_id_dynamic_monthly'); - break; - case 'dynamic-yearly': - $priceId = config('subscription.stripe_price_id_dynamic_yearly'); - break; - default: - $priceId = config('subscription.stripe_price_id_basic_monthly'); - break; - } + + $priceId = match ($type) { + 'dynamic-monthly' => config('subscription.stripe_price_id_dynamic_monthly'), + 'dynamic-yearly' => config('subscription.stripe_price_id_dynamic_yearly'), + default => config('subscription.stripe_price_id_dynamic_monthly'), + }; + if (! $priceId) { $this->dispatch('error', 'Price ID not found! Please contact the administrator.'); @@ -59,10 +27,14 @@ public function subscribeStripe($type) $payload = [ 'allow_promotion_codes' => true, 'billing_address_collection' => 'required', - 'client_reference_id' => auth()->user()->id.':'.currentTeam()->id, + 'client_reference_id' => Auth::id().':'.currentTeam()->id, 'line_items' => [[ 'price' => $priceId, - 'quantity' => 1, + 'adjustable_quantity' => [ + 'enabled' => true, + 'minimum' => 2, + ], + 'quantity' => 2, ]], 'tax_id_collection' => [ 'enabled' => true, @@ -70,39 +42,18 @@ public function subscribeStripe($type) 'automatic_tax' => [ 'enabled' => true, ], - + 'subscription_data' => [ + 'metadata' => [ + 'user_id' => Auth::id(), + 'team_id' => currentTeam()->id, + ], + ], + 'payment_method_collection' => 'if_required', 'mode' => 'subscription', 'success_url' => route('dashboard', ['success' => true]), 'cancel_url' => route('subscription.index', ['cancelled' => true]), ]; - if (str($type)->contains('ultimate')) { - $payload['line_items'][0]['adjustable_quantity'] = [ - 'enabled' => true, - 'minimum' => 10, - ]; - $payload['line_items'][0]['quantity'] = 10; - } - if (str($type)->contains('dynamic')) { - $payload['line_items'][0]['adjustable_quantity'] = [ - 'enabled' => true, - 'minimum' => 2, - ]; - $payload['line_items'][0]['quantity'] = 2; - } - if (! data_get($team, 'subscription.stripe_trial_already_ended')) { - if (config('constants.limits.trial_period') > 0) { - $payload['subscription_data'] = [ - 'trial_period_days' => config('constants.limits.trial_period'), - 'trial_settings' => [ - 'end_behavior' => [ - 'missing_payment_method' => 'cancel', - ], - ], - ]; - } - $payload['payment_method_collection'] = 'if_required'; - } $customer = currentTeam()->subscription?->stripe_customer_id ?? null; if ($customer) { $payload['customer'] = $customer; @@ -110,7 +61,7 @@ public function subscribeStripe($type) 'name' => 'auto', ]; } else { - $payload['customer_email'] = auth()->user()->email; + $payload['customer_email'] = Auth::user()->email; } $session = Session::create($payload); diff --git a/app/Livewire/Tags/Deployments.php b/app/Livewire/Tags/Deployments.php index 270aa176a1..e4afa5b606 100644 --- a/app/Livewire/Tags/Deployments.php +++ b/app/Livewire/Tags/Deployments.php @@ -7,19 +7,19 @@ class Deployments extends Component { - public $deployments_per_tag_per_server = []; + public $deploymentsPerTagPerServer = []; - public $resource_ids = []; + public $resourceIds = []; public function render() { return view('livewire.tags.deployments'); } - public function get_deployments() + public function getDeployments() { try { - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $this->resourceIds)->get([ 'id', 'application_id', 'application_name', @@ -29,7 +29,7 @@ public function get_deployments() 'server_id', 'status', ])->sortBy('id')->groupBy('server_name')->toArray(); - $this->dispatch('deployments', $this->deployments_per_tag_per_server); + $this->dispatch('deployments', $this->deploymentsPerTagPerServer); } catch (\Exception $e) { return handleError($e, $this); } diff --git a/app/Livewire/Tags/Index.php b/app/Livewire/Tags/Index.php deleted file mode 100644 index a01d00a70e..0000000000 --- a/app/Livewire/Tags/Index.php +++ /dev/null @@ -1,79 +0,0 @@ - 'update_deployments']; - - public function update_deployments($deployments) - { - $this->deployments_per_tag_per_server = $deployments; - } - - public function tag_updated() - { - if ($this->tag == '') { - return; - } - $tag = $this->tags->where('name', $this->tag)->first(); - if (! $tag) { - $this->dispatch('error', "Tag ({$this->tag}) not found."); - $this->tag = ''; - - return; - } - $this->webhook = generatTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - } - - public function redeploy_all() - { - try { - $this->applications->each(function ($resource) { - $deploy = new DeployController; - $deploy->deploy_resource($resource); - }); - $this->services->each(function ($resource) { - $deploy = new DeployController; - $deploy->deploy_resource($resource); - }); - $this->dispatch('success', 'Mass deployment started.'); - } catch (\Exception $e) { - return handleError($e, $this); - } - } - - public function mount() - { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - if ($this->tag) { - $this->tag_updated(); - } - } - - public function render() - { - return view('livewire.tags.index'); - } -} diff --git a/app/Livewire/Tags/Show.php b/app/Livewire/Tags/Show.php index 668101edb6..fc5b133749 100644 --- a/app/Livewire/Tags/Show.php +++ b/app/Livewire/Tags/Show.php @@ -5,41 +5,57 @@ use App\Http\Controllers\Api\DeployController; use App\Models\ApplicationDeploymentQueue; use App\Models\Tag; +use Illuminate\Support\Collection; +use Livewire\Attributes\Locked; +use Livewire\Attributes\Title; use Livewire\Component; +#[Title('Tags | Coolify')] class Show extends Component { - public $tags; + #[Locked] + public ?string $tagName = null; - public Tag $tag; + #[Locked] + public ?Collection $tags = null; - public $applications; + #[Locked] + public ?Tag $tag = null; - public $services; + #[Locked] + public ?Collection $applications = null; - public $webhook = null; + #[Locked] + public ?Collection $services = null; - public $deployments_per_tag_per_server = []; + #[Locked] + public ?string $webhook = null; + + #[Locked] + public ?array $deploymentsPerTagPerServer = null; public function mount() { - $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); - $tag = $this->tags->where('name', request()->tag_name)->first(); - if (! $tag) { - return redirect()->route('tags.index'); + try { + $this->tags = Tag::ownedByCurrentTeam()->get()->unique('name')->sortBy('name'); + if (str($this->tagName)->isNotEmpty()) { + $tag = $this->tags->where('name', $this->tagName)->first(); + $this->webhook = generateTagDeployWebhook($tag->name); + $this->applications = $tag->applications()->get(); + $this->services = $tag->services()->get(); + $this->tag = $tag; + $this->getDeployments(); + } + } catch (\Exception $e) { + return handleError($e, $this); } - $this->webhook = generatTagDeployWebhook($tag->name); - $this->applications = $tag->applications()->get(); - $this->services = $tag->services()->get(); - $this->tag = $tag; - $this->get_deployments(); } - public function get_deployments() + public function getDeployments() { try { $resource_ids = $this->applications->pluck('id'); - $this->deployments_per_tag_per_server = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ + $this->deploymentsPerTagPerServer = ApplicationDeploymentQueue::whereIn('status', ['in_progress', 'queued'])->whereIn('application_id', $resource_ids)->get([ 'id', 'application_id', 'application_name', @@ -54,7 +70,7 @@ public function get_deployments() } } - public function redeploy_all() + public function redeployAll() { try { $message = collect([]); diff --git a/app/Livewire/Team/AdminView.php b/app/Livewire/Team/AdminView.php index 3026cb2971..cfb47d9d83 100644 --- a/app/Livewire/Team/AdminView.php +++ b/app/Livewire/Team/AdminView.php @@ -2,6 +2,7 @@ namespace App\Livewire\Team; +use App\Models\InstanceSettings; use App\Models\Team; use App\Models\User; use Illuminate\Support\Facades\Auth; @@ -58,29 +59,30 @@ private function finalizeDeletion(User $user, Team $team) foreach ($servers as $server) { $resources = $server->definedResources(); foreach ($resources as $resource) { - ray('Deleting resource: '.$resource->name); $resource->forceDelete(); } - ray('Deleting server: '.$server->name); $server->forceDelete(); } $projects = $team->projects; foreach ($projects as $project) { - ray('Deleting project: '.$project->name); $project->forceDelete(); } $team->members()->detach($user->id); - ray('Deleting team: '.$team->name); $team->delete(); } public function delete($id, $password) { - if (! Hash::check($password, Auth::user()->password)) { - $this->addError('password', 'The provided password is incorrect.'); + if (! isInstanceAdmin()) { + return redirect()->route('dashboard'); + } + if (! data_get(InstanceSettings::get(), 'disable_two_step_confirmation')) { + if (! Hash::check($password, Auth::user()->password)) { + $this->addError('password', 'The provided password is incorrect.'); - return; + return; + } } if (! auth()->user()->isInstanceAdmin()) { return $this->dispatch('error', 'You are not authorized to delete users'); @@ -88,29 +90,23 @@ public function delete($id, $password) $user = User::find($id); $teams = $user->teams; foreach ($teams as $team) { - ray($team->name); $user_alone_in_team = $team->members->count() === 1; if ($team->id === 0) { if ($user_alone_in_team) { - ray('user is alone in the root team, do nothing'); - return $this->dispatch('error', 'User is alone in the root team, cannot delete'); } } if ($user_alone_in_team) { - ray('user is alone in the team'); $this->finalizeDeletion($user, $team); continue; } - ray('user is not alone in the team'); if ($user->isOwner()) { $found_other_owner_or_admin = $team->members->filter(function ($member) { return $member->pivot->role === 'owner' || $member->pivot->role === 'admin'; })->where('id', '!=', $user->id)->first(); if ($found_other_owner_or_admin) { - ray('found other owner or admin'); $team->members()->detach($user->id); continue; @@ -119,24 +115,19 @@ public function delete($id, $password) return $member->pivot->role === 'member'; })->first(); if ($found_other_member_who_is_not_owner) { - ray('found other member who is not owner'); $found_other_member_who_is_not_owner->pivot->role = 'owner'; $found_other_member_who_is_not_owner->pivot->save(); $team->members()->detach($user->id); } else { - // This should never happen as if the user is the only member in the team, the team should be deleted already. - ray('found no other member who is not owner'); $this->finalizeDeletion($user, $team); } continue; } } else { - ray('user is not owner'); $team->members()->detach($user->id); } } - ray('Deleting user: '.$user->name); $user->delete(); $this->getUsers(); } diff --git a/app/Livewire/Team/Create.php b/app/Livewire/Team/Create.php index 992833da5e..f805d61222 100644 --- a/app/Livewire/Team/Create.php +++ b/app/Livewire/Team/Create.php @@ -3,28 +3,21 @@ namespace App\Livewire\Team; use App\Models\Team; +use Livewire\Attributes\Validate; use Livewire\Component; class Create extends Component { + #[Validate(['required', 'min:3', 'max:255'])] public string $name = ''; + #[Validate(['nullable', 'min:3', 'max:255'])] public ?string $description = null; - protected $rules = [ - 'name' => 'required|min:3|max:255', - 'description' => 'nullable|min:3|max:255', - ]; - - protected $validationAttributes = [ - 'name' => 'name', - 'description' => 'description', - ]; - public function submit() { - $this->validate(); try { + $this->validate(); $team = Team::create([ 'name' => $this->name, 'description' => $this->description, diff --git a/app/Livewire/Team/Index.php b/app/Livewire/Team/Index.php index 45600dbfe6..0972e7364f 100644 --- a/app/Livewire/Team/Index.php +++ b/app/Livewire/Team/Index.php @@ -4,6 +4,7 @@ use App\Models\Team; use App\Models\TeamInvitation; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; use Livewire\Component; @@ -55,7 +56,7 @@ public function delete() $currentTeam->delete(); $currentTeam->members->each(function ($user) use ($currentTeam) { - if ($user->id === auth()->user()->id) { + if ($user->id === Auth::id()) { return; } $user->teams()->detach($currentTeam); diff --git a/app/Livewire/Team/Invitations.php b/app/Livewire/Team/Invitations.php index 6a32a1d161..93432efc86 100644 --- a/app/Livewire/Team/Invitations.php +++ b/app/Livewire/Team/Invitations.php @@ -13,17 +13,18 @@ class Invitations extends Component public function deleteInvitation(int $invitation_id) { - $initiation_found = TeamInvitation::find($invitation_id); - if (! $initiation_found) { + try { + $initiation_found = TeamInvitation::ownedByCurrentTeam()->findOrFail($invitation_id); + $initiation_found->delete(); + $this->refreshInvitations(); + $this->dispatch('success', 'Invitation revoked.'); + } catch (\Exception) { return $this->dispatch('error', 'Invitation not found.'); } - $initiation_found->delete(); - $this->refreshInvitations(); - $this->dispatch('success', 'Invitation revoked.'); } public function refreshInvitations() { - $this->invitations = TeamInvitation::whereTeamId(currentTeam()->id)->get(); + $this->invitations = TeamInvitation::ownedByCurrentTeam()->get(); } } diff --git a/app/Livewire/Team/InviteLink.php b/app/Livewire/Team/InviteLink.php index 6c9e405fc9..25f8a1ff51 100644 --- a/app/Livewire/Team/InviteLink.php +++ b/app/Livewire/Team/InviteLink.php @@ -41,6 +41,9 @@ private function generate_invite_link(bool $sendEmail = false) { try { $this->validate(); + if (auth()->user()->role() === 'admin' && $this->role === 'owner') { + throw new \Exception('Admins cannot invite owners.'); + } $member_emails = currentTeam()->members()->get()->pluck('email'); if ($member_emails->contains($this->email)) { return handleError(livewire: $this, customErrorMessage: "$this->email is already a member of ".currentTeam()->name.'.'); diff --git a/app/Livewire/Team/Member.php b/app/Livewire/Team/Member.php index 680cb901b4..890d640a07 100644 --- a/app/Livewire/Team/Member.php +++ b/app/Livewire/Team/Member.php @@ -2,6 +2,7 @@ namespace App\Livewire\Team; +use App\Enums\Role; use App\Models\User; use Illuminate\Support\Facades\Cache; use Livewire\Component; @@ -12,29 +13,66 @@ class Member extends Component public function makeAdmin() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'admin']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::ADMIN->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeOwner() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'owner']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::OWNER) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::OWNER->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function makeReadonly() { - $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => 'member']); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->updateExistingPivot(currentTeam()->id, ['role' => Role::MEMBER->value]); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } } public function remove() { - $this->member->teams()->detach(currentTeam()); - Cache::forget("team:{$this->member->id}"); - Cache::remember('team:'.$this->member->id, 3600, function () { - return $this->member->teams()->first(); - }); - $this->dispatch('reloadWindow'); + try { + if (Role::from(auth()->user()->role())->lt(Role::ADMIN) + || Role::from($this->getMemberRole())->gt(auth()->user()->role())) { + throw new \Exception('You are not authorized to perform this action.'); + } + $this->member->teams()->detach(currentTeam()); + Cache::forget("team:{$this->member->id}"); + Cache::remember('team:'.$this->member->id, 3600, function () { + return $this->member->teams()->first(); + }); + $this->dispatch('reloadWindow'); + } catch (\Exception $e) { + $this->dispatch('error', $e->getMessage()); + } + } + + private function getMemberRole() + { + return $this->member->teams()->where('teams.id', currentTeam()->id)->first()?->pivot?->role; } } diff --git a/app/Livewire/Upgrade.php b/app/Livewire/Upgrade.php index dfbd945f59..88ed88cb72 100644 --- a/app/Livewire/Upgrade.php +++ b/app/Livewire/Upgrade.php @@ -23,11 +23,9 @@ public function checkUpdate() try { $this->latestVersion = get_latest_version_of_coolify(); $this->isUpgradeAvailable = data_get(InstanceSettings::get(), 'new_version_available', false); - } catch (\Throwable $e) { return handleError($e, $this); } - } public function upgrade() diff --git a/app/Livewire/VerifyEmail.php b/app/Livewire/VerifyEmail.php index d1f79c835b..fab3265b65 100644 --- a/app/Livewire/VerifyEmail.php +++ b/app/Livewire/VerifyEmail.php @@ -15,10 +15,7 @@ public function again() $this->rateLimit(1, 300); auth()->user()->sendVerificationEmail(); $this->dispatch('success', 'Email verification link sent!'); - } catch (\Exception $e) { - ray($e); - return handleError($e, $this); } } diff --git a/app/Models/Application.php b/app/Models/Application.php index 07aeb4c5b7..0ef787b2e1 100644 --- a/app/Models/Application.php +++ b/app/Models/Application.php @@ -114,17 +114,34 @@ class Application extends BaseModel protected static function booted() { static::saving(function ($application) { - if ($application->fqdn == '') { + if ($application->fqdn === '') { $application->fqdn = null; } - $application->forceFill([ - 'fqdn' => $application->fqdn, - 'install_command' => str($application->install_command)->trim(), - 'build_command' => str($application->build_command)->trim(), - 'start_command' => str($application->start_command)->trim(), - 'base_directory' => str($application->base_directory)->trim(), - 'publish_directory' => str($application->publish_directory)->trim(), - ]); + $payload = []; + if ($application->isDirty('fqdn')) { + $payload['fqdn'] = $application->fqdn; + } + if ($application->isDirty('install_command')) { + $payload['install_command'] = str($application->install_command)->trim(); + } + if ($application->isDirty('build_command')) { + $payload['build_command'] = str($application->build_command)->trim(); + } + if ($application->isDirty('start_command')) { + $payload['start_command'] = str($application->start_command)->trim(); + } + if ($application->isDirty('base_directory')) { + $payload['base_directory'] = str($application->base_directory)->trim(); + } + if ($application->isDirty('publish_directory')) { + $payload['publish_directory'] = str($application->publish_directory)->trim(); + } + if ($application->isDirty('status')) { + $payload['last_online_at'] = now(); + } + if (count($payload) > 0) { + $application->forceFill($payload); + } }); static::created(function ($application) { ApplicationSetting::create([ @@ -155,6 +172,11 @@ public static function ownedByCurrentTeamAPI(int $teamId) return Application::whereRelation('environment.project.team', 'id', $teamId)->orderBy('name'); } + public static function ownedByCurrentTeam() + { + return Application::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function getContainersToStop(bool $previewDeployments = false): array { $containers = $previewDeployments @@ -221,7 +243,6 @@ public function delete_volumes(?Collection $persistentStorages) { if ($this->build_pack === 'dockercompose') { $server = data_get($this, 'destination.server'); - ray('Deleting volumes'); instant_remote_process(["cd {$this->dirOnServer()} && docker compose down -v"], $server, false); } else { if ($persistentStorages->count() === 0) { @@ -937,7 +958,7 @@ public function generateGitImportCommands(string $deployment_uuid, int $pull_req $source_html_url_host = $url['host']; $source_html_url_scheme = $url['scheme']; - if ($this->source->getMorphClass() == 'App\Models\GithubApp') { + if ($this->source->getMorphClass() === \App\Models\GithubApp::class) { if ($this->source->is_public) { $fullRepoUrl = "{$this->source->html_url}/{$customRepository}"; $git_clone_command = "{$git_clone_command} {$this->source->html_url}/{$customRepository} {$baseDir}"; @@ -1246,13 +1267,11 @@ public function parseContainerLabels(?ApplicationPreview $preview = null) return; } if (base64_encode(base64_decode($customLabels, true)) !== $customLabels) { - ray('custom_labels is not base64 encoded'); $this->custom_labels = str($customLabels)->replace(',', "\n"); $this->custom_labels = base64_encode($customLabels); } $customLabels = base64_decode($this->custom_labels); if (mb_detect_encoding($customLabels, 'ASCII', true) === false) { - ray('custom_labels contains non-ascii characters'); $customLabels = str(implode('|coolify|', generateLabelsApplication($this, $preview)))->replace('|coolify|', "\n"); } $this->custom_labels = base64_encode($customLabels); @@ -1400,29 +1419,48 @@ 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); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); if (str($metrics)->contains('error')) { $error = json_decode($metrics, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { + if ($error === 'Unauthorized') { $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } throw new \Exception($error); } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); + return $parsedCollection->toArray(); + } + } + + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + if ($server->isMetricsEnabled()) { + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; }); return $parsedCollection->toArray(); @@ -1459,9 +1497,9 @@ public function generateConfig($is_json = false) return $config; } - public function setConfig($config) { - $config = $config; + public function setConfig($config) + { $validator = Validator::make(['config' => $config], [ 'config' => 'required|json', ]); diff --git a/app/Models/ApplicationPreview.php b/app/Models/ApplicationPreview.php index 04a0ab27ef..bf2bf05bf1 100644 --- a/app/Models/ApplicationPreview.php +++ b/app/Models/ApplicationPreview.php @@ -28,6 +28,11 @@ protected static function booted() }); } }); + static::saving(function ($preview) { + if ($preview->isDirty('status')) { + $preview->forceFill(['last_online_at' => now()]); + } + }); } public static function findPreviewByApplicationAndPullId(int $application_id, int $pull_request_id) diff --git a/app/Models/Environment.php b/app/Models/Environment.php index c892d7ba17..71e8bbd218 100644 --- a/app/Models/Environment.php +++ b/app/Models/Environment.php @@ -27,10 +27,8 @@ protected static function booted() static::deleting(function ($environment) { $shared_variables = $environment->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting environment shared variable: '.$shared_variable->name); $shared_variable->delete(); } - }); } diff --git a/app/Models/EnvironmentVariable.php b/app/Models/EnvironmentVariable.php index 9f8e4b342f..08f23d7ab1 100644 --- a/app/Models/EnvironmentVariable.php +++ b/app/Models/EnvironmentVariable.php @@ -44,7 +44,7 @@ class EnvironmentVariable extends Model 'version' => 'string', ]; - protected $appends = ['real_value', 'is_shared']; + protected $appends = ['real_value', 'is_shared', 'is_really_required']; protected static function booted() { @@ -74,6 +74,9 @@ protected static function booted() 'version' => config('version'), ]); }); + static::saving(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->updateIsShared(); + }); } public function service() @@ -130,6 +133,13 @@ public function realValue(): Attribute ); } + protected function isReallyRequired(): Attribute + { + return Attribute::make( + get: fn () => $this->is_required && str($this->real_value)->isEmpty(), + ); + } + protected function isShared(): Attribute { return Attribute::make( @@ -146,13 +156,12 @@ protected function isShared(): Attribute private function get_real_environment_variables(?string $environment_variable = null, $resource = null) { - if ((is_null($environment_variable) && $environment_variable == '') || is_null($resource)) { + if ((is_null($environment_variable) && $environment_variable === '') || is_null($resource)) { return null; } $environment_variable = trim($environment_variable); $sharedEnvsFound = str($environment_variable)->matchAll('/{{(.*?)}}/'); if ($sharedEnvsFound->isEmpty()) { - return $environment_variable; } @@ -192,7 +201,7 @@ private function get_environment_variables(?string $environment_variable = null) private function set_environment_variables(?string $environment_variable = null): ?string { - if (is_null($environment_variable) && $environment_variable == '') { + if (is_null($environment_variable) && $environment_variable === '') { return null; } $environment_variable = trim($environment_variable); @@ -210,4 +219,11 @@ protected function key(): Attribute set: fn (string $value) => str($value)->trim()->replace(' ', '_')->value, ); } + + protected function updateIsShared(): void + { + $type = str($this->value)->after('{{')->before('.')->value; + $isShared = str($this->value)->startsWith('{{'.$type) && str($this->value)->endsWith('}}'); + $this->is_shared = $isShared; + } } diff --git a/app/Models/GithubApp.php b/app/Models/GithubApp.php index 66ecdd9670..0b0e93b128 100644 --- a/app/Models/GithubApp.php +++ b/app/Models/GithubApp.php @@ -31,6 +31,11 @@ protected static function booted(): void }); } + public static function ownedByCurrentTeam() + { + return GithubApp::whereTeamId(currentTeam()->id); + } + public static function public() { return GithubApp::whereTeamId(currentTeam()->id)->whereisPublic(true)->whereNotNull('app_id')->get(); @@ -60,7 +65,7 @@ public function type(): Attribute { return Attribute::make( get: function () { - if ($this->getMorphClass() === 'App\Models\GithubApp') { + if ($this->getMorphClass() === \App\Models\GithubApp::class) { return 'github'; } }, diff --git a/app/Models/GitlabApp.php b/app/Models/GitlabApp.php index a789a7e658..2112a4a664 100644 --- a/app/Models/GitlabApp.php +++ b/app/Models/GitlabApp.php @@ -9,6 +9,11 @@ class GitlabApp extends BaseModel 'app_secret', ]; + public static function ownedByCurrentTeam() + { + return GitlabApp::whereTeamId(currentTeam()->id); + } + public function applications() { return $this->morphMany(Application::class, 'source'); diff --git a/app/Models/InstanceSettings.php b/app/Models/InstanceSettings.php index bb3d1478b6..eeb8039252 100644 --- a/app/Models/InstanceSettings.php +++ b/app/Models/InstanceSettings.php @@ -2,6 +2,7 @@ namespace App\Models; +use App\Jobs\PullHelperImageJob; use App\Notifications\Channels\SendsEmail; use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; @@ -21,8 +22,22 @@ class InstanceSettings extends Model implements SendsEmail 'is_auto_update_enabled' => 'boolean', 'auto_update_frequency' => 'string', 'update_check_frequency' => 'string', + 'sentinel_token' => 'encrypted', ]; + protected static function booted(): void + { + static::updated(function ($settings) { + if ($settings->isDirty('helper_version')) { + Server::chunkById(100, function ($servers) { + foreach ($servers as $server) { + PullHelperImageJob::dispatch($server); + } + }); + } + }); + } + public function fqdn(): Attribute { return Attribute::make( @@ -86,16 +101,16 @@ public function getTitleDisplayName(): string return "[{$instanceName}]"; } - public function helperVersion(): Attribute - { - return Attribute::make( - get: function ($value) { - if (isDev()) { - return 'latest'; - } - - return $value; - } - ); - } + // public function helperVersion(): Attribute + // { + // return Attribute::make( + // get: function ($value) { + // if (isDev()) { + // return 'latest'; + // } + + // return $value; + // } + // ); + // } } diff --git a/app/Models/LocalFileVolume.php b/app/Models/LocalFileVolume.php index d528099ff8..2c223be779 100644 --- a/app/Models/LocalFileVolume.php +++ b/app/Models/LocalFileVolume.php @@ -72,7 +72,6 @@ public function deleteStorageOnServer() if ($path && $path != '/' && $path != '.' && $path != '..') { if ($isFile === 'OK') { $commands->push("rm -rf $path > /dev/null 2>&1 || true"); - } elseif ($isDir === 'OK') { $commands->push("rm -rf $path > /dev/null 2>&1 || true"); $commands->push("rmdir $path > /dev/null 2>&1 || true"); @@ -113,15 +112,15 @@ public function saveStorageOnServer() } $isFile = instant_remote_process(["test -f $path && echo OK || echo NOK"], $server); $isDir = instant_remote_process(["test -d $path && echo OK || echo NOK"], $server); - if ($isFile == 'OK' && $this->is_directory) { + if ($isFile === 'OK' && $this->is_directory) { $content = instant_remote_process(["cat $path"], $server, false); $this->is_directory = false; $this->content = $content; $this->save(); FileStorageChanged::dispatch(data_get($server, 'team_id')); throw new \Exception('The following file is a file on the server, but you are trying to mark it as a directory. Please delete the file on the server or mark it as directory.'); - } elseif ($isDir == 'OK' && ! $this->is_directory) { - if ($path == '/' || $path == '.' || $path == '..' || $path == '' || str($path)->isEmpty() || is_null($path)) { + } elseif ($isDir === 'OK' && ! $this->is_directory) { + if ($path === '/' || $path === '.' || $path === '..' || $path === '' || str($path)->isEmpty() || is_null($path)) { $this->is_directory = true; $this->save(); throw new \Exception('The following file is a directory on the server, but you are trying to mark it as a file.

Please delete the directory on the server or mark it as directory.'); @@ -132,7 +131,7 @@ public function saveStorageOnServer() ], $server, false); FileStorageChanged::dispatch(data_get($server, 'team_id')); } - if ($isDir == 'NOK' && ! $this->is_directory) { + if ($isDir === 'NOK' && ! $this->is_directory) { $chmod = data_get($this, 'chmod'); $chown = data_get($this, 'chown'); if ($content) { @@ -148,7 +147,7 @@ public function saveStorageOnServer() if ($chmod) { $commands->push("chmod $chmod $path"); } - } elseif ($isDir == 'NOK' && $this->is_directory) { + } elseif ($isDir === 'NOK' && $this->is_directory) { $commands->push("mkdir -p $path > /dev/null 2>&1 || true"); } diff --git a/app/Models/Project.php b/app/Models/Project.php index 5a9dd964a5..f27e6c2083 100644 --- a/app/Models/Project.php +++ b/app/Models/Project.php @@ -47,7 +47,6 @@ protected static function booted() $project->settings()->delete(); $shared_variables = $project->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting project shared variable: '.$shared_variable->name); $shared_variable->delete(); } }); @@ -123,9 +122,18 @@ public function mariadbs() return $this->hasManyThrough(StandaloneMariadb::class, Environment::class); } - public function resource_count() + public function isEmpty() { - return $this->applications()->count() + $this->postgresqls()->count() + $this->redis()->count() + $this->mongodbs()->count() + $this->mysqls()->count() + $this->mariadbs()->count() + $this->keydbs()->count() + $this->dragonflies()->count() + $this->clickhouses()->count() + $this->services()->count(); + return $this->applications()->count() == 0 && + $this->redis()->count() == 0 && + $this->postgresqls()->count() == 0 && + $this->mysqls()->count() == 0 && + $this->keydbs()->count() == 0 && + $this->dragonflies()->count() == 0 && + $this->clickhouses()->count() == 0 && + $this->mariadbs()->count() == 0 && + $this->mongodbs()->count() == 0 && + $this->services()->count() == 0; } public function databases() diff --git a/app/Models/ScheduledDatabaseBackup.php b/app/Models/ScheduledDatabaseBackup.php index 3921e32e4f..473fc7b4bb 100644 --- a/app/Models/ScheduledDatabaseBackup.php +++ b/app/Models/ScheduledDatabaseBackup.php @@ -51,7 +51,6 @@ public function server() } } - return null; } } diff --git a/app/Models/ScheduledTask.php b/app/Models/ScheduledTask.php index 3cee5a875a..264a04d1f7 100644 --- a/app/Models/ScheduledTask.php +++ b/app/Models/ScheduledTask.php @@ -34,21 +34,15 @@ public function server() { if ($this->application) { if ($this->application->destination && $this->application->destination->server) { - $server = $this->application->destination->server; - - return $server; + return $this->application->destination->server; } } elseif ($this->service) { if ($this->service->destination && $this->service->destination->server) { - $server = $this->service->destination->server; - - return $server; + return $this->service->destination->server; } } elseif ($this->database) { if ($this->database->destination && $this->database->destination->server) { - $server = $this->database->destination->server; - - return $server; + return $this->database->destination->server; } } diff --git a/app/Models/Server.php b/app/Models/Server.php index 0eca3c1684..3076308ade 100644 --- a/app/Models/Server.php +++ b/app/Models/Server.php @@ -3,13 +3,17 @@ namespace App\Models; use App\Actions\Server\InstallDocker; +use App\Actions\Server\StartSentinel; use App\Enums\ProxyTypes; -use App\Jobs\PullSentinelImageJob; +use App\Jobs\CheckAndStartSentinelJob; +use App\Notifications\Server\Reachable; +use App\Notifications\Server\Unreachable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Casts\Attribute; +use Illuminate\Database\Eloquent\SoftDeletes; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Facades\DB; -use Illuminate\Support\Facades\Process; use Illuminate\Support\Facades\Storage; use Illuminate\Support\Stringable; use OpenApi\Attributes as OA; @@ -43,7 +47,7 @@ class Server extends BaseModel { - use SchemalessAttributesTrait; + use SchemalessAttributesTrait, SoftDeletes; public static $batch_counter = 0; @@ -59,6 +63,11 @@ protected static function booted() } $server->forceFill($payload); }); + static::saved(function ($server) { + if ($server->privateKey->isDirty()) { + refresh_server_connection($server->privateKey); + } + }); static::created(function ($server) { ServerSetting::create([ 'server_id' => $server->id, @@ -95,7 +104,8 @@ protected static function booted() } } }); - static::deleting(function ($server) { + + static::forceDeleting(function ($server) { $server->destinations()->each(function ($destination) { $destination->delete(); }); @@ -103,12 +113,15 @@ protected static function booted() }); } - public $casts = [ + protected $casts = [ 'proxy' => SchemalessAttributes::class, 'logdrain_axiom_api_key' => 'encrypted', 'logdrain_newrelic_license_key' => 'encrypted', 'delete_unused_volumes' => 'boolean', 'delete_unused_networks' => 'boolean', + 'unreachable_notification_sent' => 'boolean', + 'is_build_server' => 'boolean', + 'force_disabled' => 'boolean', ]; protected $schemalessAttributes = [ @@ -127,6 +140,11 @@ protected static function booted() protected $guarded = []; + public function type() + { + return 'server'; + } + public static function isReachable() { return Server::ownedByCurrentTeam()->whereRelation('settings', 'is_reachable', true); @@ -401,7 +419,7 @@ public function setupDynamicProxyConfiguration() "echo '$base64' | base64 -d | tee $file > /dev/null", ], $this); - if (config('app.env') == 'local') { + if (config('app.env') === 'local') { // ray($yaml); } } @@ -489,20 +507,6 @@ public static function buildServers($teamId) return Server::whereTeamId($teamId)->whereRelation('settings', 'is_reachable', true)->whereRelation('settings', 'is_build_server', true); } - public function skipServer() - { - if ($this->ip === '1.2.3.4') { - // ray('skipping 1.2.3.4'); - return true; - } - if ($this->settings->force_disabled === true) { - // ray('force_disabled'); - return true; - } - - return false; - } - public function isForceDisabled() { return $this->settings->force_disabled; @@ -510,95 +514,83 @@ public function isForceDisabled() public function forceEnableServer() { - $this->settings->update([ - 'force_disabled' => false, - ]); + $this->settings->force_disabled = false; + $this->settings->save(); } public function forceDisableServer() { - $this->settings->update([ - 'force_disabled' => true, - ]); + $this->settings->force_disabled = true; + $this->settings->save(); $sshKeyFileLocation = "id.root@{$this->uuid}"; Storage::disk('ssh-keys')->delete($sshKeyFileLocation); Storage::disk('ssh-mux')->delete($this->muxFilename()); } - public function isSentinelEnabled() + public function sentinelHeartbeat(bool $isReset = false) { - return $this->isMetricsEnabled() || $this->isServerApiEnabled(); + $this->sentinel_updated_at = $isReset ? now()->subMinutes(6000) : now(); + $this->save(); } - public function isMetricsEnabled() + /** + * Get the wait time for Sentinel to push before performing an SSH check. + * + * @return int The wait time in seconds. + */ + public function waitBeforeDoingSshCheck(): int { - return $this->settings->is_metrics_enabled; + $wait = $this->settings->sentinel_push_interval_seconds * 3; + if ($wait < 120) { + $wait = 120; + } + + return $wait; } - public function isServerApiEnabled() + public function isSentinelLive() { - return $this->settings->is_server_api_enabled; + return Carbon::parse($this->sentinel_updated_at)->isAfter(now()->subSeconds($this->waitBeforeDoingSshCheck())); } - public function checkServerApi() + public function isSentinelEnabled() { - if ($this->isServerApiEnabled()) { - $server_ip = $this->ip; - if (isDev()) { - if ($this->id === 0) { - $server_ip = 'localhost'; - } - } - $command = "curl -s http://{$server_ip}:12172/api/health"; - $process = Process::timeout(5)->run($command); - if ($process->failed()) { - ray($process->exitCode(), $process->output(), $process->errorOutput()); - throw new \Exception("Server API is not reachable on http://{$server_ip}:12172"); - } + return ($this->isMetricsEnabled() || $this->isServerApiEnabled()) && ! $this->isBuildServer(); + } - } + public function isMetricsEnabled() + { + return $this->settings->is_metrics_enabled; + } + + public function isServerApiEnabled() + { + return $this->settings->is_sentinel_enabled; } public function checkSentinel() { - // ray("Checking sentinel on server: {$this->name}"); - if ($this->isSentinelEnabled()) { - $sentinel_found = instant_remote_process(['docker inspect coolify-sentinel'], $this, false); - $sentinel_found = json_decode($sentinel_found, true); - $status = data_get($sentinel_found, '0.State.Status', 'exited'); - if ($status !== 'running') { - // ray('Sentinel is not running, starting it...'); - PullSentinelImageJob::dispatch($this); - } else { - // ray('Sentinel is running'); - } - } + CheckAndStartSentinelJob::dispatch($this); } public function getCpuMetrics(int $mins = 5) { if ($this->isMetricsEnabled()) { $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->metrics_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); + $cpu = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$this->settings->sentinel_token}\" http://localhost:8888/api/cpu/history?from=$from'"], $this, false); if (str($cpu)->contains('error')) { $error = json_decode($cpu, true); $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { + if ($error === 'Unauthorized') { $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } 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); - return $parsedCollection->toArray(); + return collect($cpu)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); } } @@ -606,98 +598,28 @@ 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); + $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?'); - if ($error == 'Unauthorized') { + if ($error === 'Unauthorized') { $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } throw new \Exception($error); } - $memory = str($memory)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($memory)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $used, $free, $usedPercent] = explode(',', trim($line)); - $usedPercent = number_format($usedPercent, 0); - - return [(int) $time, (float) $usedPercent]; - }); + $memory = json_decode($memory, true); + $parsedCollection = collect($memory)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['usedPercent']]; }); return $parsedCollection->toArray(); } } - public function isServerReady(int $tries = 3) - { - if ($this->skipServer()) { - return false; - } - $serverUptimeCheckNumber = $this->unreachable_count; - if ($this->unreachable_count < $tries) { - $serverUptimeCheckNumber = $this->unreachable_count + 1; - } - if ($this->unreachable_count > $tries) { - $serverUptimeCheckNumber = $tries; - } - - $serverUptimeCheckNumberMax = $tries; - - // ray('server: ' . $this->name); - // ray('serverUptimeCheckNumber: ' . $serverUptimeCheckNumber); - // ray('serverUptimeCheckNumberMax: ' . $serverUptimeCheckNumberMax); - - ['uptime' => $uptime] = $this->validateConnection(); - if ($uptime) { - if ($this->unreachable_notification_sent === true) { - $this->update(['unreachable_notification_sent' => false]); - } - - return true; - } else { - if ($serverUptimeCheckNumber >= $serverUptimeCheckNumberMax) { - // Reached max number of retries - if ($this->unreachable_notification_sent === false) { - ray('Server unreachable, sending notification...'); - // $this->team?->notify(new Unreachable($this)); - $this->update(['unreachable_notification_sent' => true]); - } - if ($this->settings->is_reachable === true) { - $this->settings()->update([ - 'is_reachable' => false, - ]); - } - - foreach ($this->applications() as $application) { - $application->update(['status' => 'exited']); - } - foreach ($this->databases() as $database) { - $database->update(['status' => 'exited']); - } - foreach ($this->services()->get() as $service) { - $apps = $service->applications()->get(); - $dbs = $service->databases()->get(); - foreach ($apps as $app) { - $app->update(['status' => 'exited']); - } - foreach ($dbs as $db) { - $db->update(['status' => 'exited']); - } - } - } else { - $this->update([ - 'unreachable_count' => $this->unreachable_count + 1, - ]); - } - - return false; - } - } - public function getDiskUsage(): ?string { - return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); + return instant_remote_process(['df / --output=pcent | tr -cd 0-9'], $this, false); + // return instant_remote_process(["df /| tail -1 | awk '{ print $5}' | sed 's/%//g'"], $this, false); } public function definedResources() @@ -755,7 +677,7 @@ public function getContainers() } } } else { - $containers = instant_remote_process(["docker container inspect $(docker container ls -q) --format '{{json .}}'"], $this, false); + $containers = instant_remote_process(["docker container inspect $(docker container ls -aq) --format '{{json .}}'"], $this, false); $containers = format_docker_command_output_to_json($containers); $containerReplicates = collect([]); } @@ -899,9 +821,7 @@ public function user(): Attribute { return Attribute::make( get: function ($value) { - $sanitizedValue = preg_replace('/[^A-Za-z0-9\-_]/', '', $value); - - return $sanitizedValue; + return preg_replace('/[^A-Za-z0-9\-_]/', '', $value); } ); } @@ -945,8 +865,6 @@ public function destinations() $standalone_docker = $this->hasMany(StandaloneDocker::class)->get(); $swarm_docker = $this->hasMany(SwarmDocker::class)->get(); - // $additional_dockers = $this->belongsToMany(StandaloneDocker::class, 'additional_destinations')->withPivot('server_id')->get(); - // return $standalone_docker->concat($swarm_docker)->concat($additional_dockers); return $standalone_docker->concat($swarm_docker); } @@ -977,18 +895,31 @@ 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; } return true; } + public function skipServer() + { + if ($this->ip === '1.2.3.4') { + return true; + } + if ($this->settings->force_disabled === true) { + return true; + } + + return false; + } + public function isFunctional() { - $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && ! $this->settings->force_disabled; + $isFunctional = $this->settings->is_reachable && $this->settings->is_usable && $this->settings->force_disabled === false && $this->ip !== '1.2.3.4'; - if (! $isFunctional) { + if ($isFunctional === false) { Storage::disk('ssh-mux')->delete($this->muxFilename()); } @@ -1041,39 +972,110 @@ public function isSwarmWorker() return data_get($this, 'settings.is_swarm_worker'); } - public function validateConnection($isManualCheck = true) + public function serverStatus(): bool { - config()->set('constants.ssh.mux_enabled', ! $isManualCheck); - // ray('Manual Check: ' . ($isManualCheck ? 'true' : 'false')); + if ($this->status() === false) { + return false; + } + if ($this->isFunctional() === false) { + return false; + } + + return true; + } + + public function status(): bool + { + ['uptime' => $uptime] = $this->validateConnection(false); + if ($uptime === false) { + foreach ($this->applications() as $application) { + $application->status = 'exited'; + $application->save(); + } + foreach ($this->databases() as $database) { + $database->status = 'exited'; + $database->save(); + } + foreach ($this->services() as $service) { + $apps = $service->applications()->get(); + $dbs = $service->databases()->get(); + foreach ($apps as $app) { + $app->status = 'exited'; + $app->save(); + } + foreach ($dbs as $db) { + $db->status = 'exited'; + $db->save(); + } + } - $server = Server::find($this->id); - if (! $server) { - return ['uptime' => false, 'error' => 'Server not found.']; + return false; } - if ($server->skipServer()) { + + return true; + } + + public function isReachableChanged() + { + $this->refresh(); + $unreachableNotificationSent = (bool) $this->unreachable_notification_sent; + $isReachable = (bool) $this->settings->is_reachable; + // If the server is reachable, send the reachable notification if it was sent before + if ($isReachable === true) { + if ($unreachableNotificationSent === true) { + $this->sendReachableNotification(); + } + } else { + // If the server is unreachable, send the unreachable notification if it was not sent before + if ($unreachableNotificationSent === false) { + $this->sendUnreachableNotification(); + } + } + } + + public function sendReachableNotification() + { + $this->unreachable_notification_sent = false; + $this->save(); + $this->refresh(); + $this->team->notify(new Reachable($this)); + } + + public function sendUnreachableNotification() + { + $this->unreachable_notification_sent = true; + $this->save(); + $this->refresh(); + $this->team->notify(new Unreachable($this)); + } + + public function validateConnection(bool $isManualCheck = true, bool $justCheckingNewKey = false) + { + config()->set('constants.ssh.mux_enabled', ! $isManualCheck); + + if ($this->skipServer()) { return ['uptime' => false, 'error' => 'Server skipped.']; } try { // Make sure the private key is stored - if ($server->privateKey) { - $server->privateKey->storeInFileSystem(); + if ($this->privateKey) { + $this->privateKey->storeInFileSystem(); } - instant_remote_process(['ls /'], $server); - $server->settings()->update([ - 'is_reachable' => true, - ]); - $server->update([ - 'unreachable_count' => 0, - ]); - if (data_get($server, 'unreachable_notification_sent') === true) { - $server->update(['unreachable_notification_sent' => false]); + instant_remote_process(['ls /'], $this); + if ($this->settings->is_reachable === false) { + $this->settings->is_reachable = true; + $this->settings->save(); } return ['uptime' => true, 'error' => null]; } catch (\Throwable $e) { - $server->settings()->update([ - 'is_reachable' => false, - ]); + if ($justCheckingNewKey) { + return ['uptime' => false, 'error' => 'This key is not valid for this server.']; + } + if ($this->settings->is_reachable === true) { + $this->settings->is_reachable = false; + $this->settings->save(); + } return ['uptime' => false, 'error' => $e->getMessage()]; } @@ -1081,9 +1083,7 @@ public function validateConnection($isManualCheck = true) public function installDocker() { - $activity = InstallDocker::run($this); - - return $activity; + return InstallDocker::run($this); } public function validateDockerEngine($throwError = false) @@ -1228,4 +1228,27 @@ public function isIpv6(): bool { return str($this->ip)->contains(':'); } + + public function restartSentinel(bool $async = true) + { + try { + if ($async) { + StartSentinel::dispatch($this, true); + } else { + StartSentinel::run($this, true); + } + } catch (\Throwable $e) { + return handleError($e); + } + } + + public function url() + { + return base_url().'/server/'.$this->uuid; + } + + public function restartContainer(string $containerName) + { + return instant_remote_process(['docker restart '.$containerName], $this, false); + } } diff --git a/app/Models/ServerSetting.php b/app/Models/ServerSetting.php index c44a393b4f..fc2c5a0f4b 100644 --- a/app/Models/ServerSetting.php +++ b/app/Models/ServerSetting.php @@ -4,6 +4,7 @@ use Illuminate\Database\Eloquent\Casts\Attribute; use Illuminate\Database\Eloquent\Model; +use Illuminate\Support\Facades\Log; use OpenApi\Attributes as OA; #[OA\Schema( @@ -24,7 +25,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 +36,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 +54,85 @@ class ServerSetting extends Model protected $casts = [ 'force_docker_cleanup' => 'boolean', 'docker_cleanup_threshold' => 'integer', + 'sentinel_token' => 'encrypted', + 'is_reachable' => 'boolean', + 'is_usable' => 'boolean', ]; + protected static function booted() + { + static::creating(function ($setting) { + try { + if (str($setting->sentinel_token)->isEmpty()) { + $setting->generateSentinelToken(save: false, ignoreEvent: true); + } + if (str($setting->sentinel_custom_url)->isEmpty()) { + $setting->generateSentinelUrl(save: false, ignoreEvent: true); + } + } catch (\Throwable $e) { + Log::error('Error creating server setting: '.$e->getMessage()); + } + }); + static::updated(function ($settings) { + if ( + $settings->isDirty('sentinel_token') || + $settings->isDirty('sentinel_custom_url') || + $settings->isDirty('sentinel_metrics_refresh_rate_seconds') || + $settings->isDirty('sentinel_metrics_history_days') || + $settings->isDirty('sentinel_push_interval_seconds') + ) { + $settings->server->restartSentinel(); + } + if ($settings->isDirty('is_reachable')) { + $settings->server->isReachableChanged(); + } + }); + } + + public function generateSentinelToken(bool $save = true, bool $ignoreEvent = false) + { + $data = [ + 'server_uuid' => $this->server->uuid, + ]; + $token = json_encode($data); + $encrypted = encrypt($token); + $this->sentinel_token = $encrypted; + if ($save) { + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } + } + + return $token; + } + + public function generateSentinelUrl(bool $save = true, bool $ignoreEvent = false) + { + $domain = null; + $settings = InstanceSettings::get(); + if ($this->server->isLocalhost()) { + $domain = 'http://host.docker.internal:8000'; + } elseif ($settings->fqdn) { + $domain = $settings->fqdn; + } elseif ($settings->public_ipv4) { + $domain = 'http://'.$settings->public_ipv4.':8000'; + } elseif ($settings->public_ipv6) { + $domain = 'http://'.$settings->public_ipv6.':8000'; + } + $this->sentinel_custom_url = $domain; + if ($save) { + if ($ignoreEvent) { + $this->saveQuietly(); + } else { + $this->save(); + } + } + + return $domain; + } + public function server() { return $this->belongsTo(Server::class); diff --git a/app/Models/Service.php b/app/Models/Service.php index 0036a9fda4..0c9e081a1e 100644 --- a/app/Models/Service.php +++ b/app/Models/Service.php @@ -133,6 +133,11 @@ public function tags() return $this->morphToMany(Tag::class, 'taggable'); } + public static function ownedByCurrentTeam() + { + return Service::whereRelation('environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function getContainersToStop(): array { $containersToStop = []; @@ -297,7 +302,7 @@ public function extraFields() 'key' => 'CP_DISABLE_HTTPS', 'value' => data_get($disable_https, 'value'), 'rules' => 'required', - 'customHelper' => "If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS", + 'customHelper' => 'If you want to use https, set this to 0. Variable name: CP_DISABLE_HTTPS', ], ]); } @@ -366,7 +371,6 @@ public function extraFields() ]); } $password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_LANGFUSE')->first(); - ray('password', $password); if ($password) { $data = $data->merge([ 'Admin Password' => [ @@ -997,8 +1001,8 @@ public function extraFields() break; case $image->contains('mysql'): $userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS', 'MYSQL_USER']; - $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD','SERVICE_PASSWORD_64_MYSQL']; - $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT','SERVICE_PASSWORD_64_MYSQLROOT']; + $passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS', 'MYSQL_PASSWORD', 'SERVICE_PASSWORD_64_MYSQL']; + $rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT', 'SERVICE_PASSWORD_64_MYSQLROOT']; $dbNameVariables = ['MYSQL_DATABASE']; $mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first(); $mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first(); @@ -1096,7 +1100,6 @@ public function extraFields() } $fields->put('MariaDB', $data->toArray()); break; - } } @@ -1108,7 +1111,6 @@ 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; @@ -1232,7 +1234,6 @@ public function scheduled_tasks(): HasMany public function environment_variables(): HasMany { - return $this->hasMany(EnvironmentVariable::class)->orderByRaw("LOWER(key) LIKE LOWER('SERVICE%') DESC, LOWER(key) ASC"); } @@ -1307,13 +1308,26 @@ public function parse(bool $isNew = false): Collection } else { return collect([]); } - } public function networks() { - $networks = getTopLevelNetworks($this); + return getTopLevelNetworks($this); + } - 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 0e79e1e2e8..5cafc90422 100644 --- a/app/Models/ServiceApplication.php +++ b/app/Models/ServiceApplication.php @@ -19,6 +19,11 @@ protected static function booted() $service->persistentStorages()->delete(); $service->fileStorages()->delete(); }); + static::saving(function ($service) { + if ($service->isDirty('status')) { + $service->forceFill(['last_online_at' => now()]); + } + }); } public function restart() @@ -32,6 +37,11 @@ public static function ownedByCurrentTeamAPI(int $teamId) return ServiceApplication::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); } + public static function ownedByCurrentTeam() + { + return ServiceApplication::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); + } + public function isRunning() { return str($this->status)->contains('running'); diff --git a/app/Models/ServiceDatabase.php b/app/Models/ServiceDatabase.php index 9275271182..5fdd52637f 100644 --- a/app/Models/ServiceDatabase.php +++ b/app/Models/ServiceDatabase.php @@ -17,6 +17,21 @@ protected static function booted() $service->persistentStorages()->delete(); $service->fileStorages()->delete(); }); + static::saving(function ($service) { + if ($service->isDirty('status')) { + $service->forceFill(['last_online_at' => now()]); + } + }); + } + + public static function ownedByCurrentTeamAPI(int $teamId) + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', $teamId)->orderBy('name'); + } + + public static function ownedByCurrentTeam() + { + return ServiceDatabase::whereRelation('service.environment.project.team', 'id', currentTeam()->id)->orderBy('name'); } public function restart() diff --git a/app/Models/StandaloneClickhouse.php b/app/Models/StandaloneClickhouse.php index e4341b1b9d..6d66c68546 100644 --- a/app/Models/StandaloneClickhouse.php +++ b/app/Models/StandaloneClickhouse.php @@ -38,6 +38,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -266,33 +271,48 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneDragonfly.php b/app/Models/StandaloneDragonfly.php index 94ab2d7459..f7d83f0a31 100644 --- a/app/Models/StandaloneDragonfly.php +++ b/app/Models/StandaloneDragonfly.php @@ -38,6 +38,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -266,33 +271,48 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneKeydb.php b/app/Models/StandaloneKeydb.php index 335c8931c9..083c743d95 100644 --- a/app/Models/StandaloneKeydb.php +++ b/app/Models/StandaloneKeydb.php @@ -38,6 +38,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -266,33 +271,48 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneMariadb.php b/app/Models/StandaloneMariadb.php index c6c08dee5f..833dad6c4f 100644 --- a/app/Models/StandaloneMariadb.php +++ b/app/Models/StandaloneMariadb.php @@ -38,6 +38,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -266,33 +271,48 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneMongodb.php b/app/Models/StandaloneMongodb.php index 99893b1d13..dd8893180b 100644 --- a/app/Models/StandaloneMongodb.php +++ b/app/Models/StandaloneMongodb.php @@ -42,6 +42,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -286,33 +291,48 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandaloneMysql.php b/app/Models/StandaloneMysql.php index f2a5b5c14e..710fea1bc8 100644 --- a/app/Models/StandaloneMysql.php +++ b/app/Models/StandaloneMysql.php @@ -39,6 +39,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -267,33 +272,48 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() diff --git a/app/Models/StandalonePostgresql.php b/app/Models/StandalonePostgresql.php index 1b18a5ca73..4a457a6cf9 100644 --- a/app/Models/StandalonePostgresql.php +++ b/app/Models/StandalonePostgresql.php @@ -39,6 +39,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } public function workdir() @@ -71,7 +76,6 @@ public function delete_volumes(Collection $persistentStorages) } $server = data_get($this, 'destination.server'); foreach ($persistentStorages as $storage) { - ray('Deleting volume: '.$storage->name); instant_remote_process(["docker volume rm -f $storage->name"], $server, false); } } @@ -268,37 +272,52 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function isBackupSolutionAvailable() + { + return true; + } + + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); - - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); - - return $parsedCollection->toArray(); + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); + + return $parsedCollection->toArray(); } - public function isBackupSolutionAvailable() + public function getMemoryMetrics(int $mins = 5) { - return true; + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } } diff --git a/app/Models/StandaloneRedis.php b/app/Models/StandaloneRedis.php index a5868e2438..826bb951c5 100644 --- a/app/Models/StandaloneRedis.php +++ b/app/Models/StandaloneRedis.php @@ -34,6 +34,11 @@ protected static function booted() $database->environment_variables()->delete(); $database->tags()->detach(); }); + static::saving(function ($database) { + if ($database->isDirty('status')) { + $database->forceFill(['last_online_at' => now()]); + } + }); } protected function serverStatus(): Attribute @@ -210,7 +215,12 @@ public function databaseType(): Attribute protected function internalDbUrl(): Attribute { return new Attribute( - get: fn () => "redis://:{$this->redis_password}@{$this->uuid}:6379/0", + get: function () { + $redis_version = $this->getRedisVersion(); + $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : ''; + + return "redis://{$username_part}{$this->redis_password}@{$this->uuid}:6379/0"; + } ); } @@ -219,7 +229,10 @@ protected function externalDbUrl(): Attribute return new Attribute( get: function () { if ($this->is_public && $this->public_port) { - return "redis://:{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; + $redis_version = $this->getRedisVersion(); + $username_part = version_compare($redis_version, '6.0', '>=') ? "{$this->redis_username}:" : ''; + + return "redis://{$username_part}{$this->redis_password}@{$this->destination->server->getIp}:{$this->public_port}/0"; } return null; @@ -227,6 +240,13 @@ protected function externalDbUrl(): Attribute ); } + public function getRedisVersion() + { + $image_parts = explode(':', $this->image); + + return $image_parts[1] ?? '0.0'; + } + public function environment() { return $this->belongsTo(Environment::class); @@ -262,37 +282,81 @@ public function scheduledBackups() return $this->morphMany(ScheduledDatabaseBackup::class, 'database'); } - public function getMetrics(int $mins = 5) + public function getCpuMetrics(int $mins = 5) { $server = $this->destination->server; $container_name = $this->uuid; - if ($server->isMetricsEnabled()) { - $from = now()->subMinutes($mins)->toIso8601ZuluString(); - $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->metrics_token}\" http://localhost:8888/api/container/{$container_name}/metrics/history?from=$from'"], $server, false); - if (str($metrics)->contains('error')) { - $error = json_decode($metrics, true); - $error = data_get($error, 'error', 'Something is not okay, are you okay?'); - if ($error == 'Unauthorized') { - $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; - } - throw new \Exception($error); + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/cpu/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; } - $metrics = str($metrics)->explode("\n")->skip(1)->all(); - $parsedCollection = collect($metrics)->flatMap(function ($item) { - return collect(explode("\n", trim($item)))->map(function ($line) { - [$time, $cpu_usage_percent, $memory_usage, $memory_usage_percent] = explode(',', trim($line)); - $cpu_usage_percent = number_format($cpu_usage_percent, 2); + throw new \Exception($error); + } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['percent']]; + }); - return [(int) $time, (float) $cpu_usage_percent, (int) $memory_usage]; - }); - }); + return $parsedCollection->toArray(); + } - return $parsedCollection->toArray(); + public function getMemoryMetrics(int $mins = 5) + { + $server = $this->destination->server; + $container_name = $this->uuid; + $from = now()->subMinutes($mins)->toIso8601ZuluString(); + $metrics = instant_remote_process(["docker exec coolify-sentinel sh -c 'curl -H \"Authorization: Bearer {$server->settings->sentinel_token}\" http://localhost:8888/api/container/{$container_name}/memory/history?from=$from'"], $server, false); + if (str($metrics)->contains('error')) { + $error = json_decode($metrics, true); + $error = data_get($error, 'error', 'Something is not okay, are you okay?'); + if ($error === 'Unauthorized') { + $error = 'Unauthorized, please check your metrics token or restart Sentinel to set a new token.'; + } + throw new \Exception($error); } + $metrics = json_decode($metrics, true); + $parsedCollection = collect($metrics)->map(function ($metric) { + return [(int) $metric['time'], (float) $metric['used']]; + }); + + return $parsedCollection->toArray(); } public function isBackupSolutionAvailable() { return false; } + + public function redisPassword(): Attribute + { + return new Attribute( + get: function () { + $password = $this->runtime_environment_variables()->where('key', 'REDIS_PASSWORD')->first(); + if (! $password) { + return null; + } + + return $password->value; + }, + + ); + } + + public function redisUsername(): Attribute + { + return new Attribute( + get: function () { + $username = $this->runtime_environment_variables()->where('key', 'REDIS_USERNAME')->first(); + if (! $username) { + return null; + } + + return $username->value; + } + ); + } } diff --git a/app/Models/Team.php b/app/Models/Team.php index 3f8e97bc54..db485054b5 100644 --- a/app/Models/Team.php +++ b/app/Models/Team.php @@ -34,6 +34,7 @@ 'smtp_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via SMTP.'], 'smtp_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via SMTP.'], 'smtp_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via SMTP.'], + 'smtp_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via SMTP.'], 'discord_enabled' => ['type' => 'boolean', 'description' => 'Whether Discord is enabled or not.'], 'discord_webhook_url' => ['type' => 'string', 'description' => 'The Discord webhook URL.'], 'discord_notifications_test' => ['type' => 'boolean', 'description' => 'Whether to send test notifications via Discord.'], @@ -41,6 +42,7 @@ 'discord_notifications_status_changes' => ['type' => 'boolean', 'description' => 'Whether to send status change notifications via Discord.'], 'discord_notifications_database_backups' => ['type' => 'boolean', 'description' => 'Whether to send database backup notifications via Discord.'], 'discord_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Discord.'], + 'discord_notifications_server_disk_usage' => ['type' => 'boolean', 'description' => 'Whether to send server disk usage notifications via Discord.'], 'show_boarding' => ['type' => 'boolean', 'description' => 'Whether to show the boarding screen or not.'], 'resend_enabled' => ['type' => 'boolean', 'description' => 'Whether to enable resending or not.'], 'resend_api_key' => ['type' => 'string', 'description' => 'The resending API key.'], @@ -56,6 +58,7 @@ 'telegram_notifications_deployments_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram deployment message thread ID.'], 'telegram_notifications_status_changes_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram status change message thread ID.'], 'telegram_notifications_database_backups_message_thread_id' => ['type' => 'string', 'description' => 'The Telegram database backup message thread ID.'], + 'custom_server_limit' => ['type' => 'string', 'description' => 'The custom server limit.'], 'telegram_notifications_scheduled_tasks' => ['type' => 'boolean', 'description' => 'Whether to send scheduled task notifications via Telegram.'], 'telegram_notifications_scheduled_tasks_thread_id' => ['type' => 'string', 'description' => 'The Telegram scheduled task message thread ID.'], @@ -90,27 +93,22 @@ protected static function booted() static::deleting(function ($team) { $keys = $team->privateKeys; foreach ($keys as $key) { - ray('Deleting key: '.$key->name); $key->delete(); } $sources = $team->sources(); foreach ($sources as $source) { - ray('Deleting source: '.$source->name); $source->delete(); } $tags = Tag::whereTeamId($team->id)->get(); foreach ($tags as $tag) { - ray('Deleting tag: '.$tag->name); $tag->delete(); } $shared_variables = $team->environment_variables(); foreach ($shared_variables as $shared_variable) { - ray('Deleting team shared variable: '.$shared_variable->name); $shared_variable->delete(); } $s3s = $team->s3s; foreach ($s3s as $s3) { - ray('Deleting s3: '.$s3->name); $s3->delete(); } }); @@ -133,9 +131,7 @@ public function getRecepients($notification) { $recipients = data_get($notification, 'emails', null); if (is_null($recipients)) { - $recipients = $this->members()->pluck('email')->toArray(); - - return $recipients; + return $this->members()->pluck('email')->toArray(); } return explode(',', $recipients); @@ -164,8 +160,12 @@ public static function serverLimit() if (currentTeam()->id === 0 && isDev()) { return 9999999; } + $team = Team::find(currentTeam()->id); + if (! $team) { + return 0; + } - return Team::find(currentTeam()->id)->limits['serverLimit']; + return data_get($team, 'limits', 0); } public function limits(): Attribute @@ -187,9 +187,8 @@ public function limits(): Attribute } else { $serverLimit = config('constants.limits.server')[strtolower($subscription)]; } - $sharedEmailEnabled = config('constants.limits.email')[strtolower($subscription)]; - return ['serverLimit' => $serverLimit, 'sharedEmailEnabled' => $sharedEmailEnabled]; + return $serverLimit ?? 2; } ); @@ -249,9 +248,8 @@ public function sources() $sources = collect([]); $github_apps = $this->hasMany(GithubApp::class)->whereisPublic(false)->get(); $gitlab_apps = $this->hasMany(GitlabApp::class)->whereisPublic(false)->get(); - $sources = $sources->merge($github_apps)->merge($gitlab_apps); - return $sources; + return $sources->merge($github_apps)->merge($gitlab_apps); } public function s3s() diff --git a/app/Models/TeamInvitation.php b/app/Models/TeamInvitation.php index c202710e25..bc1a90d586 100644 --- a/app/Models/TeamInvitation.php +++ b/app/Models/TeamInvitation.php @@ -20,11 +20,16 @@ public function team() return $this->belongsTo(Team::class); } + public static function ownedByCurrentTeam() + { + return TeamInvitation::whereTeamId(currentTeam()->id); + } + public function isValid() { $createdAt = $this->created_at; - $diff = $createdAt->diffInMinutes(now()); - if ($diff <= config('constants.invitation.link.expiration')) { + $diff = $createdAt->diffInDays(now()); + if ($diff <= config('constants.invitation.link.expiration_days')) { return true; } else { $this->delete(); diff --git a/app/Models/User.php b/app/Models/User.php index ecc4ef6b66..25fb33d668 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -10,6 +10,7 @@ use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notifiable; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\URL; @@ -158,7 +159,7 @@ public function isMember() public function isAdminFromSession() { - if (auth()->user()->id === 0) { + if (Auth::id() === 0) { return true; } $teams = $this->teams()->get(); @@ -178,9 +179,9 @@ public function isAdminFromSession() public function isInstanceAdmin() { - $found_root_team = auth()->user()->teams->filter(function ($team) { + $found_root_team = Auth::user()->teams->filter(function ($team) { if ($team->id == 0) { - if (! auth()->user()->isAdmin()) { + if (! Auth::user()->isAdmin()) { return false; } @@ -195,9 +196,9 @@ public function isInstanceAdmin() public function currentTeam() { - return Cache::remember('team:'.auth()->user()->id, 3600, function () { - if (is_null(data_get(session('currentTeam'), 'id')) && auth()->user()->teams->count() > 0) { - return auth()->user()->teams[0]; + return Cache::remember('team:'.Auth::id(), 3600, function () { + if (is_null(data_get(session('currentTeam'), 'id')) && Auth::user()->teams->count() > 0) { + return Auth::user()->teams[0]; } return Team::find(session('currentTeam')->id); @@ -206,7 +207,7 @@ public function currentTeam() public function otherTeams() { - return auth()->user()->teams->filter(function ($team) { + return Auth::user()->teams->filter(function ($team) { return $team->id != currentTeam()->id; }); } @@ -216,7 +217,7 @@ public function role() if (data_get($this, 'pivot')) { return $this->pivot->role; } - $user = auth()->user()->teams->where('id', currentTeam()->id)->first(); + $user = Auth::user()->teams->where('id', currentTeam()->id)->first(); return data_get($user, 'pivot.role'); } diff --git a/app/Notifications/Application/DeploymentFailed.php b/app/Notifications/Application/DeploymentFailed.php index 1809da3683..242980e007 100644 --- a/app/Notifications/Application/DeploymentFailed.php +++ b/app/Notifications/Application/DeploymentFailed.php @@ -4,6 +4,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -72,14 +73,42 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { if ($this->preview) { - $message = 'Coolify: Pull request #'.$this->preview->pull_request_id.' of '.$this->application_name.' ('.$this->preview->fqdn.') deployment failed: '; - $message .= '[View Deployment Logs]('.$this->deployment_url.')'; + $message = new DiscordMessage( + title: ':cross_mark: Deployment failed', + description: 'Pull request: '.$this->preview->pull_request_id, + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); + if ($this->fqdn) { + $message->addField('Domain', $this->fqdn, true); + } } else { - $message = 'Coolify: Deployment failed of '.$this->application_name.' ('.$this->fqdn.'): '; - $message .= '[View Deployment Logs]('.$this->deployment_url.')'; + if ($this->fqdn) { + $description = '[Open application]('.$this->fqdn.')'; + } else { + $description = ''; + } + $message = new DiscordMessage( + title: ':cross_mark: Deployment failed', + description: $description, + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment Logs', '[Link]('.$this->deployment_url.')'); } return $message; diff --git a/app/Notifications/Application/DeploymentSuccess.php b/app/Notifications/Application/DeploymentSuccess.php index 5085065c20..946a622ca5 100644 --- a/app/Notifications/Application/DeploymentSuccess.php +++ b/app/Notifications/Application/DeploymentSuccess.php @@ -4,6 +4,7 @@ use App\Models\Application; use App\Models\ApplicationPreview; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -51,7 +52,7 @@ public function via(object $notifiable): array $channels = setNotificationChannels($notifiable, 'deployments'); if (isCloud()) { // TODO: Make batch notifications work with email - $channels = array_diff($channels, ['App\Notifications\Channels\EmailChannel']); + $channels = array_diff($channels, [\App\Notifications\Channels\EmailChannel::class]); } return $channels; @@ -78,24 +79,39 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { if ($this->preview) { - $message = 'Coolify: New PR'.$this->preview->pull_request_id.' version successfully deployed of '.$this->application_name.' + $message = new DiscordMessage( + title: ':white_check_mark: Preview deployment successful', + description: 'Pull request: '.$this->preview->pull_request_id, + color: DiscordMessage::successColor(), + ); -'; if ($this->preview->fqdn) { - $message .= '[Open Application]('.$this->preview->fqdn.') | '; + $message->addField('Application', '[Link]('.$this->preview->fqdn.')'); } - $message .= '[Deployment logs]('.$this->deployment_url.')'; - } else { - $message = 'Coolify: New version successfully deployed of '.$this->application_name.' -'; + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); + } else { if ($this->fqdn) { - $message .= '[Open Application]('.$this->fqdn.') | '; + $description = '[Open application]('.$this->fqdn.')'; + } else { + $description = ''; } - $message .= '[Deployment logs]('.$this->deployment_url.')'; + $message = new DiscordMessage( + title: ':white_check_mark: New version successfully deployed', + description: $description, + color: DiscordMessage::successColor(), + ); + $message->addField('Project', data_get($this->application, 'environment.project.name'), true); + $message->addField('Environment', $this->environment_name, true); + $message->addField('Name', $this->application_name, true); + + $message->addField('Deployment logs', '[Link]('.$this->deployment_url.')'); } return $message; diff --git a/app/Notifications/Application/StatusChanged.php b/app/Notifications/Application/StatusChanged.php index 53ed8a5896..852c6b5260 100644 --- a/app/Notifications/Application/StatusChanged.php +++ b/app/Notifications/Application/StatusChanged.php @@ -3,6 +3,7 @@ namespace App\Notifications\Application; use App\Models\Application; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -55,14 +56,14 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = 'Coolify: '.$this->resource_name.' has been stopped. - -'; - $message .= '[Open Application in Coolify]('.$this->resource_url.')'; - - return $message; + return new DiscordMessage( + title: ':cross_mark: Application stopped', + description: '[Open Application in Coolify]('.$this->resource_url.')', + color: DiscordMessage::errorColor(), + isCritical: true, + ); } public function toTelegram(): array diff --git a/app/Notifications/Channels/DiscordChannel.php b/app/Notifications/Channels/DiscordChannel.php index f1706f1380..86276fec96 100644 --- a/app/Notifications/Channels/DiscordChannel.php +++ b/app/Notifications/Channels/DiscordChannel.php @@ -12,11 +12,11 @@ class DiscordChannel */ public function send(SendsDiscord $notifiable, Notification $notification): void { - $message = $notification->toDiscord($notifiable); + $message = $notification->toDiscord(); $webhookUrl = $notifiable->routeNotificationForDiscord(); if (! $webhookUrl) { return; } - dispatch(new SendMessageToDiscordJob($message, $webhookUrl)); + dispatch(new SendMessageToDiscordJob($message, $webhookUrl))->onQueue('high'); } } diff --git a/app/Notifications/Channels/EmailChannel.php b/app/Notifications/Channels/EmailChannel.php index 413d3de536..af9af978d0 100644 --- a/app/Notifications/Channels/EmailChannel.php +++ b/app/Notifications/Channels/EmailChannel.php @@ -32,7 +32,6 @@ public function send(SendsEmail $notifiable, Notification $notification): void if ($error === 'No email settings found.') { throw $e; } - ray($e->getMessage()); $message = "EmailChannel error: {$e->getMessage()}. Failed to send email to:"; if (isset($recipients)) { $message .= implode(', ', $recipients); diff --git a/app/Notifications/Channels/TelegramChannel.php b/app/Notifications/Channels/TelegramChannel.php index b1a6076513..b3d4e384bb 100644 --- a/app/Notifications/Channels/TelegramChannel.php +++ b/app/Notifications/Channels/TelegramChannel.php @@ -18,29 +18,29 @@ public function send($notifiable, $notification): void $topicsInstance = get_class($notification); switch ($topicsInstance) { - case 'App\Notifications\Test': + case \App\Notifications\Test::class: $topicId = data_get($notifiable, 'telegram_notifications_test_message_thread_id'); break; - case 'App\Notifications\Application\StatusChanged': - case 'App\Notifications\Container\ContainerRestarted': - case 'App\Notifications\Container\ContainerStopped': + case \App\Notifications\Application\StatusChanged::class: + case \App\Notifications\Container\ContainerRestarted::class: + case \App\Notifications\Container\ContainerStopped::class: $topicId = data_get($notifiable, 'telegram_notifications_status_changes_message_thread_id'); break; - case 'App\Notifications\Application\DeploymentSuccess': - case 'App\Notifications\Application\DeploymentFailed': + case \App\Notifications\Application\DeploymentSuccess::class: + case \App\Notifications\Application\DeploymentFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_deployments_message_thread_id'); break; - case 'App\Notifications\Database\BackupSuccess': - case 'App\Notifications\Database\BackupFailed': + case \App\Notifications\Database\BackupSuccess::class: + case \App\Notifications\Database\BackupFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_database_backups_message_thread_id'); break; - case 'App\Notifications\ScheduledTask\TaskFailed': + case \App\Notifications\ScheduledTask\TaskFailed::class: $topicId = data_get($notifiable, 'telegram_notifications_scheduled_tasks_thread_id'); break; } if (! $telegramToken || ! $chatId || ! $message) { return; } - dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId)); + dispatch(new SendMessageToTelegramJob($message, $buttons, $telegramToken, $chatId, $topicId))->onQueue('high'); } } diff --git a/app/Notifications/Container/ContainerRestarted.php b/app/Notifications/Container/ContainerRestarted.php index 23f6de2642..182a1f5fc1 100644 --- a/app/Notifications/Container/ContainerRestarted.php +++ b/app/Notifications/Container/ContainerRestarted.php @@ -3,6 +3,7 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -34,9 +35,17 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: A resource ({$this->name}) has been restarted automatically on {$this->server->name}"; + $message = new DiscordMessage( + title: ':warning: Resource restarted', + description: "{$this->name} has been restarted automatically on {$this->server->name}.", + color: DiscordMessage::infoColor(), + ); + + if ($this->url) { + $message->addField('Resource', '[Link]('.$this->url.')'); + } return $message; } diff --git a/app/Notifications/Container/ContainerStopped.php b/app/Notifications/Container/ContainerStopped.php index bcf5e67a5c..33a55c65a2 100644 --- a/app/Notifications/Container/ContainerStopped.php +++ b/app/Notifications/Container/ContainerStopped.php @@ -3,6 +3,7 @@ namespace App\Notifications\Container; use App\Models\Server; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -34,9 +35,17 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: A resource ($this->name) has been stopped unexpectedly on {$this->server->name}"; + $message = new DiscordMessage( + title: ':cross_mark: Resource stopped', + description: "{$this->name} has been stopped unexpectedly on {$this->server->name}.", + color: DiscordMessage::errorColor(), + ); + + if ($this->url) { + $message->addField('Resource', '[Link]('.$this->url.')'); + } return $message; } diff --git a/app/Notifications/Database/BackupFailed.php b/app/Notifications/Database/BackupFailed.php index 77024c05bf..8e2733339d 100644 --- a/app/Notifications/Database/BackupFailed.php +++ b/app/Notifications/Database/BackupFailed.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -45,9 +46,19 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was FAILED.\n\nReason:\n{$this->output}"; + $message = new DiscordMessage( + title: ':cross_mark: Database backup failed', + description: "Database backup for {$this->name} (db:{$this->database_name}) has FAILED.", + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Frequency', $this->frequency, true); + $message->addField('Output', $this->output); + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Database/BackupSuccess.php b/app/Notifications/Database/BackupSuccess.php index f8dc6eb565..5128c8ed66 100644 --- a/app/Notifications/Database/BackupSuccess.php +++ b/app/Notifications/Database/BackupSuccess.php @@ -3,6 +3,7 @@ namespace App\Notifications\Database; use App\Models\ScheduledDatabaseBackup; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -44,15 +45,22 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; + $message = new DiscordMessage( + title: ':white_check_mark: Database backup successful', + description: "Database backup for {$this->name} (db:{$this->database_name}) was successful.", + color: DiscordMessage::successColor(), + ); + + $message->addField('Frequency', $this->frequency, true); + + return $message; } public function toTelegram(): array { $message = "Coolify: Database backup for {$this->name} (db:{$this->database_name}) with frequency of {$this->frequency} was successful."; - ray($message); return [ 'message' => $message, diff --git a/app/Notifications/Database/DailyBackup.php b/app/Notifications/Database/DailyBackup.php deleted file mode 100644 index a51ac62839..0000000000 --- a/app/Notifications/Database/DailyBackup.php +++ /dev/null @@ -1,50 +0,0 @@ -subject('Coolify: Daily backup statuses'); - $mail->view('emails.daily-backup', [ - 'databases' => $this->databases, - ]); - - return $mail; - } - - public function toDiscord(): string - { - return 'Coolify: Daily backup statuses'; - } - - public function toTelegram(): array - { - $message = 'Coolify: Daily backup statuses'; - - return [ - 'message' => $message, - ]; - } -} diff --git a/app/Notifications/Dto/DiscordMessage.php b/app/Notifications/Dto/DiscordMessage.php new file mode 100644 index 0000000000..856753dcae --- /dev/null +++ b/app/Notifications/Dto/DiscordMessage.php @@ -0,0 +1,83 @@ +fields[] = [ + 'name' => $name, + 'value' => $value, + 'inline' => $inline, + ]; + + return $this; + } + + public function toPayload(): array + { + $footerText = 'Coolify v'.config('version'); + if (isCloud()) { + $footerText = 'Coolify Cloud'; + } + $payload = [ + 'embeds' => [ + [ + 'title' => $this->title, + 'description' => $this->description, + 'color' => $this->color, + 'fields' => $this->addTimestampToFields($this->fields), + 'footer' => [ + 'text' => $footerText, + ], + ], + ], + ]; + if ($this->isCritical) { + $payload['content'] = '@here'; + } + + return $payload; + } + + private function addTimestampToFields(array $fields): array + { + $fields[] = [ + 'name' => 'Time', + 'value' => 'timestamp.':R>', + 'inline' => true, + ]; + + return $fields; + } +} diff --git a/app/Notifications/Internal/GeneralNotification.php b/app/Notifications/Internal/GeneralNotification.php index 1d4d648c86..48e7d8340c 100644 --- a/app/Notifications/Internal/GeneralNotification.php +++ b/app/Notifications/Internal/GeneralNotification.php @@ -4,6 +4,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -32,9 +33,13 @@ public function via(object $notifiable): array return $channels; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return $this->message; + return new DiscordMessage( + title: 'Coolify: General Notification', + description: $this->message, + color: DiscordMessage::infoColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/ScheduledTask/TaskFailed.php b/app/Notifications/ScheduledTask/TaskFailed.php index 479cc1aa14..c3501a8eb4 100644 --- a/app/Notifications/ScheduledTask/TaskFailed.php +++ b/app/Notifications/ScheduledTask/TaskFailed.php @@ -3,6 +3,7 @@ namespace App\Notifications\ScheduledTask; use App\Models\ScheduledTask; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -29,7 +30,6 @@ public function __construct(public ScheduledTask $task, public string $output) public function via(object $notifiable): array { - return setNotificationChannels($notifiable, 'scheduled_tasks'); } @@ -46,9 +46,19 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - return "Coolify: Scheduled task ({$this->task->name}, [link]({$this->url})) failed with output: {$this->output}"; + $message = new DiscordMessage( + title: ':cross_mark: Scheduled task failed', + description: "Scheduled task ({$this->task->name}) failed.", + color: DiscordMessage::errorColor(), + ); + + if ($this->url) { + $message->addField('Scheduled task', '[Link]('.$this->url.')'); + } + + return $message; } public function toTelegram(): array diff --git a/app/Notifications/Server/DockerCleanup.php b/app/Notifications/Server/DockerCleanup.php index 682ed7a1a7..7ea1b84c2f 100644 --- a/app/Notifications/Server/DockerCleanup.php +++ b/app/Notifications/Server/DockerCleanup.php @@ -5,6 +5,7 @@ use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Notification; @@ -49,11 +50,13 @@ public function via(object $notifiable): array // return $mail; // } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' cleanup job done!\n\n{$this->message}"; - - return $message; + return new DiscordMessage( + title: ':white_check_mark: Server cleanup job done', + description: $this->message, + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/ForceDisabled.php b/app/Notifications/Server/ForceDisabled.php index 6377f2f150..a26c803ee6 100644 --- a/app/Notifications/Server/ForceDisabled.php +++ b/app/Notifications/Server/ForceDisabled.php @@ -6,6 +6,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -50,9 +51,15 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server ({$this->server->name}) disabled because it is not paid!\n All automations and integrations are stopped.\nPlease update your subscription to enable the server again [here](https://app.coolify.io/subscriptions)."; + $message = new DiscordMessage( + title: ':cross_mark: Server disabled', + description: "Server ({$this->server->name}) disabled because it is not paid!", + color: DiscordMessage::errorColor(), + ); + + $message->addField('Please update your subscription to enable the server again!', '[Link](https://app.coolify.io/subscriptions)'); return $message; } diff --git a/app/Notifications/Server/ForceEnabled.php b/app/Notifications/Server/ForceEnabled.php index 83594d6434..65b65a10c7 100644 --- a/app/Notifications/Server/ForceEnabled.php +++ b/app/Notifications/Server/ForceEnabled.php @@ -6,6 +6,7 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -50,11 +51,13 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server ({$this->server->name}) enabled again!"; - - return $message; + return new DiscordMessage( + title: ':white_check_mark: Server enabled', + description: "Server '{$this->server->name}' enabled again!", + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/HighDiskUsage.php b/app/Notifications/Server/HighDiskUsage.php index 34cb220910..e373abc031 100644 --- a/app/Notifications/Server/HighDiskUsage.php +++ b/app/Notifications/Server/HighDiskUsage.php @@ -3,9 +3,7 @@ namespace App\Notifications\Server; use App\Models\Server; -use App\Notifications\Channels\DiscordChannel; -use App\Notifications\Channels\EmailChannel; -use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; @@ -17,26 +15,11 @@ class HighDiskUsage extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server, public int $disk_usage, public int $docker_cleanup_threshold) {} + public function __construct(public Server $server, public int $disk_usage, public int $server_disk_usage_notification_threshold) {} public function via(object $notifiable): array { - $channels = []; - $isEmailEnabled = isEmailEnabled($notifiable); - $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); - $isTelegramEnabled = data_get($notifiable, 'telegram_enabled'); - - if ($isDiscordEnabled) { - $channels[] = DiscordChannel::class; - } - if ($isEmailEnabled) { - $channels[] = EmailChannel::class; - } - if ($isTelegramEnabled) { - $channels[] = TelegramChannel::class; - } - - return $channels; + return setNotificationChannels($notifiable, 'server_disk_usage'); } public function toMail(): MailMessage @@ -46,15 +29,25 @@ public function toMail(): MailMessage $mail->view('emails.high-disk-usage', [ 'name' => $this->server->name, 'disk_usage' => $this->disk_usage, - 'threshold' => $this->docker_cleanup_threshold, + 'threshold' => $this->server_disk_usage_notification_threshold, ]); return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup."; + $message = new DiscordMessage( + title: ':cross_mark: High disk usage detected', + description: "Server '{$this->server->name}' high disk usage detected!", + color: DiscordMessage::errorColor(), + isCritical: true, + ); + + $message->addField('Disk usage', "{$this->disk_usage}%", true); + $message->addField('Threshold', "{$this->server_disk_usage_notification_threshold}%", true); + $message->addField('What to do?', '[Link](https://coolify.io/docs/knowledge-base/server/automated-cleanup)', true); + $message->addField('Change Settings', '[Threshold]('.base_url().'/server/'.$this->server->uuid.'#advanced) | [Notification]('.base_url().'/notifications/discord)'); return $message; } @@ -62,7 +55,7 @@ public function toDiscord(): string public function toTelegram(): array { return [ - 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->docker_cleanup_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", + 'message' => "Coolify: Server '{$this->server->name}' high disk usage detected!\nDisk usage: {$this->disk_usage}%. Threshold: {$this->server_disk_usage_notification_threshold}%.\nPlease cleanup your disk to prevent data-loss.\nHere are some tips: https://coolify.io/docs/knowledge-base/server/automated-cleanup.", ]; } } diff --git a/app/Notifications/Server/Revived.php b/app/Notifications/Server/Reachable.php similarity index 64% rename from app/Notifications/Server/Revived.php rename to app/Notifications/Server/Reachable.php index 3f2b3b6962..9b54501d94 100644 --- a/app/Notifications/Server/Revived.php +++ b/app/Notifications/Server/Reachable.php @@ -2,35 +2,37 @@ namespace App\Notifications\Server; -use App\Actions\Docker\GetContainersStatus; -use App\Jobs\ContainerStatusJob; use App\Models\Server; use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\RateLimiter; -class Revived extends Notification implements ShouldQueue +class Reachable extends Notification implements ShouldQueue { use Queueable; public $tries = 1; + protected bool $isRateLimited = false; + public function __construct(public Server $server) { - if ($this->server->unreachable_notification_sent === false) { - return; - } - GetContainersStatus::dispatch($server)->onQueue('high'); - // dispatch(new ContainerStatusJob($server)); + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-reachable:'.$this->server->id, + ); } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -45,20 +47,8 @@ public function via(object $notifiable): array if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } - $executed = RateLimiter::attempt( - 'notification-server-revived-'.$this->server->uuid, - 1, - function () use ($channels) { - return $channels; - }, - 7200, - ); - - if (! $executed) { - return []; - } - return $executed; + return $channels; } public function toMail(): MailMessage @@ -72,11 +62,13 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = "Coolify: Server '{$this->server->name}' revived. All automations & integrations are turned on again!"; - - return $message; + return new DiscordMessage( + title: ":white_check_mark: Server '{$this->server->name}' revived", + description: 'All automations & integrations are turned on again!', + color: DiscordMessage::successColor(), + ); } public function toTelegram(): array diff --git a/app/Notifications/Server/Unreachable.php b/app/Notifications/Server/Unreachable.php index 2fb83559aa..5bc568e829 100644 --- a/app/Notifications/Server/Unreachable.php +++ b/app/Notifications/Server/Unreachable.php @@ -6,11 +6,11 @@ use App\Notifications\Channels\DiscordChannel; use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; -use Illuminate\Support\Facades\RateLimiter; class Unreachable extends Notification implements ShouldQueue { @@ -18,10 +18,21 @@ class Unreachable extends Notification implements ShouldQueue public $tries = 1; - public function __construct(public Server $server) {} + protected bool $isRateLimited = false; + + public function __construct(public Server $server) + { + $this->isRateLimited = isEmailRateLimited( + limiterKey: 'server-unreachable:'.$this->server->id, + ); + } public function via(object $notifiable): array { + if ($this->isRateLimited) { + return []; + } + $channels = []; $isEmailEnabled = isEmailEnabled($notifiable); $isDiscordEnabled = data_get($notifiable, 'discord_enabled'); @@ -36,23 +47,11 @@ public function via(object $notifiable): array if ($isTelegramEnabled) { $channels[] = TelegramChannel::class; } - $executed = RateLimiter::attempt( - 'notification-server-unreachable-'.$this->server->uuid, - 1, - function () use ($channels) { - return $channels; - }, - 7200, - ); - - if (! $executed) { - return []; - } - return $executed; + return $channels; } - public function toMail(): MailMessage + public function toMail(): ?MailMessage { $mail = new MailMessage; $mail->subject("Coolify: Your server ({$this->server->name}) is unreachable."); @@ -63,14 +62,20 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): ?DiscordMessage { - $message = "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations."; + $message = new DiscordMessage( + title: ':cross_mark: Server unreachable', + description: "Your server '{$this->server->name}' is unreachable.", + color: DiscordMessage::errorColor(), + ); + + $message->addField('IMPORTANT', 'We automatically try to revive your server and turn on all automations & integrations.'); return $message; } - public function toTelegram(): array + public function toTelegram(): ?array { return [ 'message' => "Coolify: Your server '{$this->server->name}' is unreachable. All automations & integrations are turned off! Please check your server! IMPORTANT: We automatically try to revive your server and turn on all automations & integrations.", diff --git a/app/Notifications/Test.php b/app/Notifications/Test.php index 3b46a9a249..a43b1e1533 100644 --- a/app/Notifications/Test.php +++ b/app/Notifications/Test.php @@ -2,10 +2,12 @@ namespace App\Notifications; +use App\Notifications\Dto\DiscordMessage; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Notifications\Notification; +use Illuminate\Queue\Middleware\RateLimited; class Test extends Notification implements ShouldQueue { @@ -20,6 +22,14 @@ public function via(object $notifiable): array return setNotificationChannels($notifiable, 'test'); } + public function middleware(object $notifiable, string $channel) + { + return match ($channel) { + \App\Notifications\Channels\EmailChannel::class => [new RateLimited('email')], + default => [], + }; + } + public function toMail(): MailMessage { $mail = new MailMessage; @@ -29,11 +39,15 @@ public function toMail(): MailMessage return $mail; } - public function toDiscord(): string + public function toDiscord(): DiscordMessage { - $message = 'Coolify: This is a test Discord notification from Coolify.'; - $message .= "\n\n"; - $message .= '[Go to your dashboard]('.base_url().')'; + $message = new DiscordMessage( + title: ':white_check_mark: Test Success', + description: 'This is a test Discord notification from Coolify. :cross_mark: :warning: :information_source:', + color: DiscordMessage::successColor(), + ); + + $message->addField(name: 'Dashboard', value: '[Link]('.base_url().')', inline: true); return $message; } diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 8b4c2eef2d..015434bd22 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -5,16 +5,30 @@ use App\Models\PersonalAccessToken; use Illuminate\Support\Facades\Http; use Illuminate\Support\ServiceProvider; +use Illuminate\Validation\Rules\Password; use Laravel\Sanctum\Sanctum; class AppServiceProvider extends ServiceProvider { - public function register(): void {} + public function register(): void + { + if ($this->app->environment('local')) { + $this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class); + } + } public function boot(): void { Sanctum::usePersonalAccessTokenModel(PersonalAccessToken::class); + Password::defaults(function () { + $rule = Password::min(8); + + return $this->app->isProduction() + ? $rule->mixedCase()->letters()->numbers()->symbols() + : $rule; + }); + Http::macro('github', function (string $api_url, ?string $github_access_token = null) { if ($github_access_token) { return Http::withHeaders([ diff --git a/app/Providers/DuskServiceProvider.php b/app/Providers/DuskServiceProvider.php new file mode 100644 index 0000000000..07e0e8709f --- /dev/null +++ b/app/Providers/DuskServiceProvider.php @@ -0,0 +1,21 @@ +visit('/login') + ->type('email', 'test@example.com') + ->type('password', 'password') + ->press('Login'); + }); + } +} diff --git a/app/Providers/FortifyServiceProvider.php b/app/Providers/FortifyServiceProvider.php index b916b62340..e8784bab3c 100644 --- a/app/Providers/FortifyServiceProvider.php +++ b/app/Providers/FortifyServiceProvider.php @@ -75,7 +75,8 @@ public function boot(): void }); Fortify::authenticateUsing(function (Request $request) { - $user = User::where('email', $request->email)->with('teams')->first(); + $email = strtolower($request->email); + $user = User::where('email', $email)->with('teams')->first(); if ( $user && Hash::check($request->password, $user->password) diff --git a/app/View/Components/Forms/Input.php b/app/View/Components/Forms/Input.php index fbd7b0b158..7283ef20f8 100644 --- a/app/View/Components/Forms/Input.php +++ b/app/View/Components/Forms/Input.php @@ -23,6 +23,8 @@ public function __construct( public bool $isMultiline = false, public string $defaultClass = 'input', public string $autocomplete = 'off', + public ?int $minlength = null, + public ?int $maxlength = null, ) {} public function render(): View|Closure|string diff --git a/app/View/Components/Forms/Textarea.php b/app/View/Components/Forms/Textarea.php index 3f887877c2..6081c2a8ae 100644 --- a/app/View/Components/Forms/Textarea.php +++ b/app/View/Components/Forms/Textarea.php @@ -30,7 +30,9 @@ public function __construct( public bool $realtimeValidation = false, public bool $allowToPeak = true, public string $defaultClass = 'input scrollbar font-mono', - public string $defaultClassInput = 'input' + public string $defaultClassInput = 'input', + public ?int $minlength = null, + public ?int $maxlength = null, ) { // } diff --git a/app/View/Components/Server/Sidebar.php b/app/View/Components/Server/Sidebar.php deleted file mode 100644 index f968b6d0cf..0000000000 --- a/app/View/Components/Server/Sidebar.php +++ /dev/null @@ -1,27 +0,0 @@ -map(function ($d) { + return $data->map(function ($d) { $d = collect($d)->sortKeys(); $created_at = data_get($d, 'created_at'); $updated_at = data_get($d, 'updated_at'); if ($created_at) { unset($d['created_at']); $d['created_at'] = $created_at; - } if ($updated_at) { unset($d['updated_at']); @@ -50,8 +49,6 @@ function serializeApiResponse($data) return $d; }); - - return $data; } else { $d = collect($data)->sortKeys(); $created_at = data_get($d, 'created_at'); @@ -59,7 +56,6 @@ function serializeApiResponse($data) if ($created_at) { unset($d['created_at']); $d['created_at'] = $created_at; - } if ($updated_at) { unset($d['updated_at']); diff --git a/bootstrap/helpers/applications.php b/bootstrap/helpers/applications.php index b3e8011b93..eb331f8c23 100644 --- a/bootstrap/helpers/applications.php +++ b/bootstrap/helpers/applications.php @@ -91,7 +91,7 @@ function next_queuable(string $server_id, string $application_id): bool $server = Server::find($server_id); $concurrent_builds = $server->settings->concurrent_builds; - ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); + // ray("serverId:{$server->id}", "concurrentBuilds:{$concurrent_builds}", "deployments:{$deployments->count()}", "sameApplicationDeployments:{$same_application_deployments->count()}")->green(); if ($deployments->count() > $concurrent_builds) { return false; diff --git a/bootstrap/helpers/databases.php b/bootstrap/helpers/databases.php index 950eb67b6e..e12910f82b 100644 --- a/bootstrap/helpers/databases.php +++ b/bootstrap/helpers/databases.php @@ -1,5 +1,6 @@ name = generate_database_name('redis'); - $database->redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); + $redis_password = \Illuminate\Support\Str::password(length: 64, symbols: false); $database->environment_id = $environment_id; $database->destination_id = $destination->id; $database->destination_type = $destination->getMorphClass(); @@ -57,6 +58,20 @@ function create_standalone_redis($environment_id, $destination_uuid, ?array $oth } $database->save(); + EnvironmentVariable::create([ + 'key' => 'REDIS_PASSWORD', + 'value' => $redis_password, + 'standalone_redis_id' => $database->id, + 'is_shared' => false, + ]); + + EnvironmentVariable::create([ + 'key' => 'REDIS_USERNAME', + 'value' => 'default', + 'standalone_redis_id' => $database->id, + 'is_shared' => false, + ]); + return $database; } diff --git a/bootstrap/helpers/docker.php b/bootstrap/helpers/docker.php index 397bce029d..2e583b94d9 100644 --- a/bootstrap/helpers/docker.php +++ b/bootstrap/helpers/docker.php @@ -32,9 +32,8 @@ function getCurrentApplicationContainerStatus(Server $server, int $id, ?int $pul return null; }); - $containers = $containers->filter(); - return $containers; + return $containers->filter(); } return $containers; @@ -46,9 +45,8 @@ function getCurrentServiceContainerStatus(Server $server, int $id): Collection if (! $server->isSwarm()) { $containers = instant_remote_process(["docker ps -a --filter='label=coolify.serviceId={$id}' --format '{{json .}}' "], $server); $containers = format_docker_command_output_to_json($containers); - $containers = $containers->filter(); - return $containers; + return $containers->filter(); } return $containers; @@ -67,7 +65,7 @@ function format_docker_command_output_to_json($rawOutput): Collection return $outputLines ->reject(fn ($line) => empty($line)) ->map(fn ($outputLine) => json_decode($outputLine, true, flags: JSON_THROW_ON_ERROR)); - } catch (\Throwable $e) { + } catch (\Throwable) { return collect([]); } } @@ -104,7 +102,7 @@ function format_docker_envs_to_json($rawOutput) return [$env[0] => $env[1]]; }); - } catch (\Throwable $e) { + } catch (\Throwable) { return collect([]); } } @@ -207,12 +205,12 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica } function generateServiceSpecificFqdns(ServiceApplication|Application $resource) { - if ($resource->getMorphClass() === 'App\Models\ServiceApplication') { + if ($resource->getMorphClass() === \App\Models\ServiceApplication::class) { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'service.server'); $environment_variables = data_get($resource, 'service.environment_variables'); $type = $resource->serviceType(); - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { $uuid = data_get($resource, 'uuid'); $server = data_get($resource, 'destination.server'); $environment_variables = data_get($resource, 'environment_variables'); @@ -279,7 +277,6 @@ function fqdnLabelsForCaddy(string $network, string $uuid, Collection $domains, $labels->push("caddy_ingress_network={$network}"); } foreach ($domains as $loop => $domain) { - $loop = $loop; $url = Url::fromString($domain); $host = $url->getHost(); $path = $url->getPath(); @@ -335,10 +332,11 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if (preg_match('/coolify\.traefik\.middlewares=(.*)/', $item, $matches)) { return explode(',', $matches[1]); } + return null; })->flatten() - ->filter() - ->unique(); + ->filter() + ->unique(); } foreach ($domains as $loop => $domain) { try { @@ -388,7 +386,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ if ($path !== '/') { // Middleware handling $middlewares = collect([]); - if ($is_stripprefix_enabled && !str($image)->contains('ghost')) { + if ($is_stripprefix_enabled && ! str($image)->contains('ghost')) { $labels->push("traefik.http.middlewares.{$https_label}-stripprefix.stripprefix.prefixes={$path}"); $middlewares->push("{$https_label}-stripprefix"); } @@ -402,7 +400,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $labels = $labels->merge($redirect_to_non_www); $middlewares->push($to_non_www_name); } - if ($redirect_direction === 'www' && !str($host)->startsWith('www.')) { + if ($redirect_direction === 'www' && ! str($host)->startsWith('www.')) { $labels = $labels->merge($redirect_to_www); $middlewares->push($to_www_name); } @@ -417,7 +415,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ $middlewares = collect([]); if ($is_gzip_enabled) { $middlewares->push('gzip'); - } + } if (str($image)->contains('ghost')) { $middlewares->push('redir-ghost'); } @@ -510,7 +508,7 @@ function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_ } } } - } catch (\Throwable $e) { + } catch (\Throwable) { continue; } } @@ -581,7 +579,6 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview redirect_direction: $application->redirect )); } - } } else { if (data_get($preview, 'fqdn')) { @@ -633,7 +630,6 @@ function generateLabelsApplication(Application $application, ?ApplicationPreview is_stripprefix_enabled: $application->isStripprefixEnabled() )); } - } return $labels->all(); @@ -658,7 +654,7 @@ function isDatabaseImage(?string $image = null) return false; } -function convert_docker_run_to_compose(?string $custom_docker_run_options = null) +function convertDockerRunToCompose(?string $custom_docker_run_options = null) { $options = []; $compose_options = collect([]); @@ -683,9 +679,17 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null '--privileged' => 'privileged', '--ip' => 'ip', '--shm-size' => 'shm_size', + '--gpus' => 'gpus', ]); foreach ($matches as $match) { $option = $match[1]; + if ($option === '--gpus') { + $regexForParsingDeviceIds = '/device=([0-9A-Za-z-,]+)/'; + preg_match($regexForParsingDeviceIds, $custom_docker_run_options, $device_matches); + $value = $device_matches[1] ?? 'all'; + $options[$option][] = $value; + $options[$option] = array_unique($options[$option]); + } if (isset($match[2]) && $match[2] !== '') { $value = $match[2]; $options[$option][] = $value; @@ -698,7 +702,6 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null $options = collect($options); // Easily get mappings from https://github.com/composerize/composerize/blob/master/packages/composerize/src/mappings.js foreach ($options as $option => $value) { - // ray($option,$value); if (! data_get($mapping, $option)) { continue; } @@ -727,6 +730,28 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null if (! is_null($value) && is_array($value) && count($value) > 0) { $compose_options->put($mapping[$option], $value[0]); } + } elseif ($option === '--gpus') { + $payload = [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + ]; + if (! is_null($value) && is_array($value) && count($value) > 0) { + if (str($value[0]) != 'all') { + if (str($value[0])->contains(',')) { + $payload['device_ids'] = str($value[0])->explode(',')->toArray(); + } else { + $payload['device_ids'] = [$value[0]]; + } + } + } + ray($payload); + $compose_options->put('deploy', [ + 'resources' => [ + 'reservations' => [ + 'devices' => [$payload], + ], + ], + ]); } else { if ($list_options->contains($option)) { if ($compose_options->has($mapping[$option])) { @@ -748,7 +773,7 @@ function convert_docker_run_to_compose(?string $custom_docker_run_options = null return $compose_options->toArray(); } -function generate_custom_docker_run_options_for_databases($docker_run_options, $docker_compose, $container_name, $network) +function generateCustomDockerRunOptionsForDatabases($docker_run_options, $docker_compose, $container_name, $network) { $ipv4 = data_get($docker_run_options, 'ip.0'); $ipv6 = data_get($docker_run_options, 'ip6.0'); diff --git a/bootstrap/helpers/github.php b/bootstrap/helpers/github.php index 97deb0b1c8..529ac82b10 100644 --- a/bootstrap/helpers/github.php +++ b/bootstrap/helpers/github.php @@ -3,6 +3,7 @@ use App\Models\GithubApp; use App\Models\GitlabApp; use Carbon\Carbon; +use Carbon\CarbonImmutable; use Illuminate\Support\Facades\Http; use Illuminate\Support\Str; use Lcobucci\JWT\Encoding\ChainedFormatter; @@ -16,7 +17,7 @@ function generate_github_installation_token(GithubApp $source) $signingKey = InMemory::plainText($source->privateKey->private_key); $algorithm = new Sha256; $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); - $now = new DateTimeImmutable; + $now = CarbonImmutable::now(); $now = $now->setTime($now->format('H'), $now->format('i')); $issuedToken = $tokenBuilder ->issuedBy($source->app_id) @@ -40,16 +41,15 @@ function generate_github_jwt_token(GithubApp $source) $signingKey = InMemory::plainText($source->privateKey->private_key); $algorithm = new Sha256; $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); - $now = new DateTimeImmutable; + $now = CarbonImmutable::now(); $now = $now->setTime($now->format('H'), $now->format('i')); - $issuedToken = $tokenBuilder + + return $tokenBuilder ->issuedBy($source->app_id) ->issuedAt($now->modify('-1 minute')) ->expiresAt($now->modify('+10 minutes')) ->getToken($algorithm, $signingKey) ->toString(); - - return $issuedToken; } function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $method = 'get', ?array $data = null, bool $throwError = true) @@ -57,7 +57,7 @@ function githubApi(GithubApp|GitlabApp|null $source, string $endpoint, string $m if (is_null($source)) { throw new \Exception('Not implemented yet.'); } - if ($source->getMorphClass() == 'App\Models\GithubApp') { + if ($source->getMorphClass() === \App\Models\GithubApp::class) { if ($source->is_public) { $response = Http::github($source->api_url)->$method($endpoint); } else { diff --git a/bootstrap/helpers/proxy.php b/bootstrap/helpers/proxy.php index 309ccee4a9..a8ef0fe5a7 100644 --- a/bootstrap/helpers/proxy.php +++ b/bootstrap/helpers/proxy.php @@ -16,12 +16,10 @@ function collectProxyDockerNetworksByServer(Server $server) return collect(); } $networks = instant_remote_process(['docker inspect --format="{{json .NetworkSettings.Networks }}" coolify-proxy'], $server, false); - $networks = collect($networks)->map(function ($network) { + + return collect($networks)->map(function ($network) { return collect(json_decode($network))->keys(); })->flatten()->unique(); - - return $networks; - } function collectDockerNetworksByServer(Server $server) { @@ -241,9 +239,11 @@ function generate_default_proxy_configuration(Server $server) 'ports' => [ '80:80', '443:443', + '443:443/udp', ], 'labels' => [ 'coolify.managed=true', + 'coolify.proxy=true', ], 'volumes' => [ '/var/run/docker.sock:/var/run/docker.sock:ro', diff --git a/bootstrap/helpers/remoteProcess.php b/bootstrap/helpers/remoteProcess.php index 67b60d6b75..c7dd2cb83f 100644 --- a/bootstrap/helpers/remoteProcess.php +++ b/bootstrap/helpers/remoteProcess.php @@ -124,7 +124,7 @@ function decode_remote_command_output(?ApplicationDeploymentQueue $application_d associative: true, flags: JSON_THROW_ON_ERROR ); - } catch (\JsonException $exception) { + } catch (\JsonException) { return collect([]); } $seenCommands = collect(); @@ -204,7 +204,7 @@ function checkRequiredCommands(Server $server) } try { instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'apt update && apt install -y {$command}'"], $server); - } catch (\Throwable $e) { + } catch (\Throwable) { break; } $commandFound = instant_remote_process(["docker run --rm --privileged --net=host --pid=host --ipc=host --volume /:/host busybox chroot /host bash -c 'command -v {$command}'"], $server, false); diff --git a/bootstrap/helpers/services.php b/bootstrap/helpers/services.php index eba88d0002..fd2e1231fb 100644 --- a/bootstrap/helpers/services.php +++ b/bootstrap/helpers/services.php @@ -24,7 +24,7 @@ function replaceVariables(string $variable): Stringable function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Application $oneService, bool $isInit = false) { try { - if ($oneService->getMorphClass() === 'App\Models\Application') { + if ($oneService->getMorphClass() === \App\Models\Application::class) { $workdir = $oneService->workdir(); $server = $oneService->destination->server; } else { @@ -51,7 +51,7 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli // Exists and is a directory $isDir = instant_remote_process(["test -d $fileLocation && echo OK || echo NOK"], $server); - if ($isFile == 'OK') { + if ($isFile === 'OK') { // If its a file & exists $filesystemContent = instant_remote_process(["cat $fileLocation"], $server); if ($fileVolume->is_based_on_git) { @@ -59,12 +59,12 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli } $fileVolume->is_directory = false; $fileVolume->save(); - } elseif ($isDir == 'OK') { + } elseif ($isDir === 'OK') { // If its a directory & exists $fileVolume->content = null; $fileVolume->is_directory = true; $fileVolume->save(); - } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && $content) { + } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && $content) { // Does not exists (no dir or file), not flagged as directory, is init, has content $fileVolume->content = $content; $fileVolume->is_directory = false; @@ -75,13 +75,13 @@ function getFilesystemVolumesFromServer(ServiceApplication|ServiceDatabase|Appli "mkdir -p $dir", "echo '$content' | base64 -d | tee $fileLocation", ], $server); - } elseif ($isFile == 'NOK' && $isDir == 'NOK' && $fileVolume->is_directory && $isInit) { + } elseif ($isFile === 'NOK' && $isDir === 'NOK' && $fileVolume->is_directory && $isInit) { // Does not exists (no dir or file), flagged as directory, is init $fileVolume->content = null; $fileVolume->is_directory = true; $fileVolume->save(); instant_remote_process(["mkdir -p $fileLocation"], $server); - } elseif ($isFile == 'NOK' && $isDir == 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) { + } elseif ($isFile === 'NOK' && $isDir === 'NOK' && ! $fileVolume->is_directory && $isInit && is_null($content)) { // Does not exists (no dir or file), not flagged as directory, is init, has no content => create directory $fileVolume->content = null; $fileVolume->is_directory = true; @@ -245,8 +245,5 @@ function updateCompose(ServiceApplication|ServiceDatabase $resource) } function serviceKeys() { - $services = get_service_templates(); - $serviceKeys = $services->keys(); - - return $serviceKeys; + return get_service_templates()->keys(); } diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index cfdea81fbd..f6875cc814 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -28,17 +28,20 @@ use App\Notifications\Channels\EmailChannel; use App\Notifications\Channels\TelegramChannel; use App\Notifications\Internal\GeneralNotification; +use Carbon\CarbonImmutable; use DanHarrin\LivewireRateLimiting\Exceptions\TooManyRequestsException; use Illuminate\Database\UniqueConstraintViolationException; use Illuminate\Mail\Message; use Illuminate\Notifications\Messages\MailMessage; use Illuminate\Process\Pool; use Illuminate\Support\Collection; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Cache; use Illuminate\Support\Facades\File; use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Process; +use Illuminate\Support\Facades\RateLimiter; use Illuminate\Support\Facades\Request; use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Validator; @@ -98,12 +101,12 @@ function isInstanceAdmin() function currentTeam() { - return auth()?->user()?->currentTeam() ?? null; + return Auth::user()?->currentTeam() ?? null; } function showBoarding(): bool { - if (auth()->user()?->isMember()) { + if (Auth::user()?->isMember()) { return false; } @@ -112,21 +115,20 @@ function showBoarding(): bool function refreshSession(?Team $team = null): void { if (! $team) { - if (auth()->user()?->currentTeam()) { - $team = Team::find(auth()->user()->currentTeam()->id); + if (Auth::user()->currentTeam()) { + $team = Team::find(Auth::user()->currentTeam()->id); } else { - $team = User::find(auth()->user()->id)->teams->first(); + $team = User::find(Auth::id())->teams->first(); } } - Cache::forget('team:'.auth()->user()->id); - Cache::remember('team:'.auth()->user()->id, 3600, function () use ($team) { + Cache::forget('team:'.Auth::id()); + Cache::remember('team:'.Auth::id(), 3600, function () use ($team) { return $team; }); session(['currentTeam' => $team]); } function handleError(?Throwable $error = null, ?Livewire\Component $livewire = null, ?string $customErrorMessage = null) { - ray($error); if ($error instanceof TooManyRequestsException) { if (isset($livewire)) { return $livewire->dispatch('error', "Too many requests. Please try again in {$error->secondsUntilAvailable} seconds."); @@ -142,6 +144,10 @@ function handleError(?Throwable $error = null, ?Livewire\Component $livewire = n return 'Duplicate entry found. Please use a different name.'; } + if ($error instanceof \Illuminate\Database\Eloquent\ModelNotFoundException) { + abort(404); + } + if ($error instanceof Throwable) { $message = $error->getMessage(); } else { @@ -164,14 +170,11 @@ function get_route_parameters(): array function get_latest_sentinel_version(): string { try { - $response = Http::get('https://cdn.coollabs.io/sentinel/versions.json'); + $response = Http::get('https://cdn.coollabs.io/coolify/versions.json'); $versions = $response->json(); - return data_get($versions, 'sentinel.version'); - } catch (\Throwable $e) { - //throw $e; - ray($e->getMessage()); - + return data_get($versions, 'coolify.sentinel.version'); + } catch (\Throwable) { return '0.0.0'; } } @@ -300,7 +303,7 @@ function getFqdnWithoutPort(string $fqdn) $path = $url->getPath(); return "$scheme://$host$path"; - } catch (\Throwable $e) { + } catch (\Throwable) { return $fqdn; } } @@ -368,6 +371,9 @@ function translate_cron_expression($expression_to_validate): string } function validate_cron_expression($expression_to_validate): bool { + if (empty($expression_to_validate)) { + return false; + } $isValid = false; $expression = new CronExpression($expression_to_validate); $isValid = $expression->isValid(); @@ -496,9 +502,8 @@ function generateFqdn(Server $server, string $random, bool $forceHttps = false): if ($forceHttps) { $scheme = 'https'; } - $finalFqdn = "$scheme://{$random}.$host$path"; - return $finalFqdn; + return "$scheme://{$random}.$host$path"; } function sslip(Server $server) { @@ -536,7 +541,7 @@ function get_service_templates(bool $force = false): Collection $services = $response->json(); return collect($services); - } catch (\Throwable $e) { + } catch (\Throwable) { $services = File::get(base_path('templates/service-templates.json')); return collect(json_decode($services))->sortKeys(); @@ -643,14 +648,13 @@ function queryResourcesByUuid(string $uuid) return $resource; } -function generatTagDeployWebhook($tag_name) +function generateTagDeployWebhook($tag_name) { $baseUrl = base_url(); $api = Url::fromString($baseUrl).'/api/v1'; $endpoint = "/deploy?tag=$tag_name"; - $url = $api.$endpoint; - return $url; + return $api.$endpoint; } function generateDeployWebhook($resource) { @@ -658,20 +662,18 @@ function generateDeployWebhook($resource) $api = Url::fromString($baseUrl).'/api/v1'; $endpoint = '/deploy'; $uuid = data_get($resource, 'uuid'); - $url = $api.$endpoint."?uuid=$uuid&force=false"; - return $url; + return $api.$endpoint."?uuid=$uuid&force=false"; } function generateGitManualWebhook($resource, $type) { if ($resource->source_id !== 0 && ! is_null($resource->source_id)) { return null; } - if ($resource->getMorphClass() === 'App\Models\Application') { + if ($resource->getMorphClass() === \App\Models\Application::class) { $baseUrl = base_url(); - $api = Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; - return $api; + return Url::fromString($baseUrl)."/webhooks/source/$type/events/manual"; } return null; @@ -683,7 +685,7 @@ function removeAnsiColors($text) function getTopLevelNetworks(Service|Application $resource) { - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); @@ -738,7 +740,7 @@ function getTopLevelNetworks(Service|Application $resource) return $topLevelNetworks->keys(); } - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); } catch (\Exception $e) { @@ -945,7 +947,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n $key = InMemory::plainText($signingKey); $algorithm = new Sha256; $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); - $now = new DateTimeImmutable; + $now = CarbonImmutable::now(); $now = $now->setTime($now->format('H'), $now->format('i')); $token = $tokenBuilder ->issuedBy('supabase') @@ -965,7 +967,7 @@ function generateEnvValue(string $command, Service|Application|null $service = n $key = InMemory::plainText($signingKey); $algorithm = new Sha256; $tokenBuilder = (new Builder(new JoseEncoder, ChainedFormatter::default())); - $now = new DateTimeImmutable; + $now = CarbonImmutable::now(); $now = $now->setTime($now->format('H'), $now->format('i')); $token = $tokenBuilder ->issuedBy('supabase') @@ -1048,7 +1050,7 @@ function validate_dns_entry(string $fqdn, Server $server) } } } - } catch (\Exception $e) { + } catch (\Exception) { } } ray("Found match: $found_matching_ip"); @@ -1145,7 +1147,7 @@ function checkIfDomainIsAlreadyUsed(Collection|array $domains, ?string $teamId = function check_domain_usage(ServiceApplication|Application|null $resource = null, ?string $domain = null) { if ($resource) { - if ($resource->getMorphClass() === 'App\Models\Application' && $resource->build_pack === 'dockercompose') { + if ($resource->getMorphClass() === \App\Models\Application::class && $resource->build_pack === 'dockercompose') { $domains = data_get(json_decode($resource->docker_compose_domains, true), '*.domain'); $domains = collect($domains); } else { @@ -1338,13 +1340,6 @@ function isAnyDeploymentInprogress() exit(0); } -function generateSentinelToken() -{ - $token = Str::random(64); - - return $token; -} - function isBase64Encoded($strValue) { return base64_encode(base64_decode($strValue, true)) === $strValue; @@ -1416,7 +1411,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull if ($source->value() === '/tmp' || $source->value() === '/tmp/') { return $volume; } - if (get_class($resource) === "App\Models\Application") { + if (get_class($resource) === \App\Models\Application::class) { $dir = base_configuration_dir().'/applications/'.$resource->uuid; } else { $dir = base_configuration_dir().'/services/'.$resource->service->uuid; @@ -1456,7 +1451,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull } } $slugWithoutUuid = Str::slug($source, '-'); - if (get_class($resource) === "App\Models\Application") { + if (get_class($resource) === \App\Models\Application::class) { $name = "{$resource->uuid}_{$slugWithoutUuid}"; } else { $name = "{$resource->service->uuid}_{$slugWithoutUuid}"; @@ -1499,7 +1494,7 @@ function parseServiceVolumes($serviceVolumes, $resource, $topLevelVolumes, $pull function parseDockerComposeFile(Service|Application $resource, bool $isNew = false, int $pull_request_id = 0, ?int $preview_id = null) { - if ($resource->getMorphClass() === 'App\Models\Service') { + if ($resource->getMorphClass() === \App\Models\Service::class) { if ($resource->docker_compose_raw) { try { $yaml = Yaml::parse($resource->docker_compose_raw); @@ -2213,10 +2208,10 @@ function parseDockerComposeFile(Service|Application $resource, bool $isNew = fal } else { return collect([]); } - } elseif ($resource->getMorphClass() === 'App\Models\Application') { + } elseif ($resource->getMorphClass() === \App\Models\Application::class) { try { $yaml = Yaml::parse($resource->docker_compose_raw); - } catch (\Exception $e) { + } catch (\Exception) { return; } $server = $resource->destination->server; @@ -2962,7 +2957,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int try { $yaml = Yaml::parse($compose); - } catch (\Exception $e) { + } catch (\Exception) { return collect([]); } @@ -3099,7 +3094,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int } } - if ($value && get_class($value) === 'Illuminate\Support\Stringable' && $value->startsWith('/')) { + if ($value && get_class($value) === \Illuminate\Support\Stringable::class && $value->startsWith('/')) { $path = $value->value(); if ($path !== '/') { $fqdn = "$fqdn$path"; @@ -3190,7 +3185,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'is_build_time' => false, 'is_preview' => false, ]); - } else { $value = generateEnvValue($command, $resource); $resource->environment_variables()->where('key', $key->value())->where($nameOfId, $resource->id)->firstOrCreate([ @@ -3569,6 +3563,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int ]); } else { if ($value->startsWith('$')) { + $isRequired = false; if ($value->contains(':-')) { $value = replaceVariables($value); $key = $value->before(':'); @@ -3583,11 +3578,13 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int $key = $value->before(':'); $value = $value->after(':?'); + $isRequired = true; } elseif ($value->contains('?')) { $value = replaceVariables($value); $key = $value->before('?'); $value = $value->after('?'); + $isRequired = true; } if ($originalValue->value() === $value->value()) { // This means the variable does not have a default value, so it needs to be created in Coolify @@ -3598,6 +3595,7 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int ], [ 'is_build_time' => false, 'is_preview' => false, + 'is_required' => $isRequired, ]); // Add the variable to the environment so it will be shown in the deployable compose file $environment[$parsedKeyValue->value()] = $resource->environment_variables()->where('key', $parsedKeyValue)->where($nameOfId, $resource->id)->first()->value; @@ -3611,9 +3609,9 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int 'value' => $value, 'is_build_time' => false, 'is_preview' => false, + 'is_required' => $isRequired, ]); } - } } if ($isApplication) { @@ -3787,7 +3785,6 @@ function newParser(Application|Service $resource, int $pull_request_id = 0, ?int service_name: $serviceName, image: $image, predefinedPort: $predefinedPort - )); } } @@ -3978,20 +3975,19 @@ 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) { - +function loadConfigFromGit(string $repository, string $branch, string $base_directory, int $server_id, int $team_id) +{ $server = Server::find($server_id)->where('team_id', $team_id)->first(); - if (!$server) { + if (! $server) { return; } - $uuid = new Cuid2(); + $uuid = new Cuid2; $cloneCommand = "git clone --no-checkout -b $branch $repository ."; $workdir = rtrim($base_directory, '/'); $fileList = collect([".$workdir/coolify.json"]); @@ -4008,7 +4004,61 @@ function loadConfigFromGit(string $repository, string $branch, string $base_dire ]); try { return instant_remote_process($commands, $server); - } catch (\Exception $e) { - // continue + } catch (\Exception) { + // continue } } + +function loggy($message = null, array $context = []) +{ + if (! isDev()) { + return; + } + if (function_exists('ray') && config('app.debug')) { + ray($message, $context); + } + if (is_null($message)) { + return app('log'); + } + + return app('log')->debug($message, $context); +} +function sslipDomainWarning(string $domains) +{ + $domains = str($domains)->trim()->explode(','); + $showSslipHttpsWarning = false; + $domains->each(function ($domain) use (&$showSslipHttpsWarning) { + if (str($domain)->contains('https') && str($domain)->contains('sslip')) { + $showSslipHttpsWarning = true; + } + }); + + return $showSslipHttpsWarning; +} + +function isEmailRateLimited(string $limiterKey, int $decaySeconds = 3600, ?callable $callbackOnSuccess = null): bool +{ + if (isDev()) { + $decaySeconds = 120; + } + $rateLimited = false; + $executed = RateLimiter::attempt( + $limiterKey, + $maxAttempts = 0, + function () use (&$rateLimited, &$limiterKey, $callbackOnSuccess) { + isDev() && loggy('Rate limit not reached for '.$limiterKey); + $rateLimited = false; + + if ($callbackOnSuccess) { + $callbackOnSuccess(); + } + }, + $decaySeconds, + ); + if (! $executed) { + isDev() && loggy('Rate limit reached for '.$limiterKey.'. Rate limiter will be disabled for '.$decaySeconds.' seconds.'); + $rateLimited = true; + } + + return $rateLimited; +} diff --git a/bootstrap/helpers/socialite.php b/bootstrap/helpers/socialite.php index a23dc24d3e..cad9de7fa8 100644 --- a/bootstrap/helpers/socialite.php +++ b/bootstrap/helpers/socialite.php @@ -7,7 +7,7 @@ function get_socialite_provider(string $provider) { $oauth_setting = OauthSetting::firstWhere('provider', $provider); - if ($provider == 'azure') { + if ($provider === 'azure') { $azure_config = new \SocialiteProviders\Manager\Config( $oauth_setting->client_id, $oauth_setting->client_secret, diff --git a/bootstrap/helpers/subscriptions.php b/bootstrap/helpers/subscriptions.php index aadd2dd344..8ddb1331c6 100644 --- a/bootstrap/helpers/subscriptions.php +++ b/bootstrap/helpers/subscriptions.php @@ -55,12 +55,11 @@ function getStripeCustomerPortalSession(Team $team) if (! $stripe_customer_id) { return null; } - $session = \Stripe\BillingPortal\Session::create([ + + return \Stripe\BillingPortal\Session::create([ 'customer' => $stripe_customer_id, 'return_url' => $return_url, ]); - - return $session; } function allowedPathsForUnsubscribedAccounts() { diff --git a/composer.json b/composer.json index fbd77d0cf9..2bae1149c4 100644 --- a/composer.json +++ b/composer.json @@ -1,25 +1,28 @@ { - "name": "laravel/laravel", + "name": "coollabsio/coolify", + "description": "The Coolify project.", + "license": "Apache-2.0", "type": "project", - "description": "The Laravel Framework.", "keywords": [ - "framework", - "laravel" + "coolify", + "deployment", + "docker", + "self-hosted", + "server" ], - "license": "MIT", "require": { "php": "^8.2", "danharrin/livewire-rate-limiting": "^1.1", "doctrine/dbal": "^3.6", "guzzlehttp/guzzle": "^7.5.0", - "laravel/fortify": "^v1.16.0", - "laravel/framework": "^v11", + "laravel/fortify": "^1.16.0", + "laravel/framework": "^11", "laravel/horizon": "^5.29.1", + "laravel/pail": "^1.1", "laravel/prompts": "^0.1.6", - "laravel/sanctum": "^v4.0", - "laravel/socialite": "^v5.14.0", - "laravel/telescope": "^5.2", - "laravel/tinker": "^v2.8.1", + "laravel/sanctum": "^4.0", + "laravel/socialite": "^5.14.0", + "laravel/tinker": "^2.8.1", "laravel/ui": "^4.2", "lcobucci/jwt": "^5.0.0", "league/flysystem-aws-s3-v3": "^3.0", @@ -28,7 +31,7 @@ "log1x/laravel-webfonts": "^1.0", "lorisleiva/laravel-actions": "^2.7", "nubs/random-name-generator": "^2.2", - "phpseclib/phpseclib": "~3.0", + "phpseclib/phpseclib": "^3.0", "pion/laravel-chunk-upload": "^1.5", "poliander/cron": "^3.0", "purplepixie/phpdns": "^2.1", @@ -49,46 +52,65 @@ }, "require-dev": { "barryvdh/laravel-debugbar": "^3.13", - "fakerphp/faker": "^v1.21.0", - "laravel/dusk": "^v8.0", + "fakerphp/faker": "^1.21.0", + "laravel/dusk": "^8.0", "laravel/pint": "^1.16", + "laravel/telescope": "^5.2", "mockery/mockery": "^1.5.1", - "nunomaduro/collision": "^v8.1", + "nunomaduro/collision": "^8.1", "pestphp/pest": "^2.16", "phpstan/phpstan": "^1.10", "phpunit/phpunit": "^10.0.19", - "serversideup/spin": "^v1.1.0", + "serversideup/spin": "^1.1.0", "spatie/laravel-ignition": "^2.1.0", "symfony/http-client": "^6.2" }, + "minimum-stability": "stable", + "prefer-stable": true, "autoload": { - "files": [ - "bootstrap/includeHelpers.php" - ], "psr-4": { "App\\": "app/", "Database\\Factories\\": "database/factories/", "Database\\Seeders\\": "database/seeders/" - } + }, + "files": [ + "bootstrap/includeHelpers.php" + ] }, "autoload-dev": { "psr-4": { "Tests\\": "tests/" } }, + "config": { + "allow-plugins": { + "pestphp/pest-plugin": true, + "php-http/discovery": true + }, + "optimize-autoloader": true, + "preferred-install": "dist", + "sort-packages": true + }, + "extra": { + "laravel": { + "dont-discover": [ + "laravel/telescope" + ] + } + }, "scripts": { - "post-autoload-dump": [ - "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", - "@php artisan package:discover --ansi" + "post-install-cmd": [ + "cp -r 'hooks/' '.git/hooks/'", + "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"", + "php -r \"chmod('.git/hooks/pre-commit', 0777);\"" ], "post-update-cmd": [ "@php artisan vendor:publish --tag=laravel-assets --ansi --force", "Illuminate\\Foundation\\ComposerScripts::postUpdate" ], - "post-install-cmd": [ - "cp -r 'hooks/' '.git/hooks/'", - "php -r \"copy('hooks/pre-commit', '.git/hooks/pre-commit');\"", - "php -r \"chmod('.git/hooks/pre-commit', 0777);\"" + "post-autoload-dump": [ + "Illuminate\\Foundation\\ComposerScripts::postAutoloadDump", + "@php artisan package:discover --ansi" ], "post-root-package-install": [ "@php -r \"file_exists('.env') || copy('.env.example', '.env');\"" @@ -96,23 +118,5 @@ "post-create-project-cmd": [ "@php artisan key:generate --ansi" ] - }, - "extra": { - "laravel": { - "dont-discover": [ - "laravel/telescope" - ] - } - }, - "config": { - "optimize-autoloader": true, - "preferred-install": "dist", - "sort-packages": true, - "allow-plugins": { - "pestphp/pest-plugin": true, - "php-http/discovery": true - } - }, - "minimum-stability": "stable", - "prefer-stable": true -} + } +} \ No newline at end of file diff --git a/composer.lock b/composer.lock index 0b8da82d0a..5eb03b5fc3 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": "c47adf3684eb727e22503937435c0914", + "content-hash": "3f2342fe6b1ba920c8875f8a8fe41962", "packages": [ { "name": "amphp/amp", @@ -3144,6 +3144,83 @@ }, "time": "2024-10-08T18:23:02+00:00" }, + { + "name": "laravel/pail", + "version": "v1.1.5", + "source": { + "type": "git", + "url": "https://github.com/laravel/pail.git", + "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pail/zipball/b33ad8321416fe86efed7bf398f3306c47b4871b", + "reference": "b33ad8321416fe86efed7bf398f3306c47b4871b", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "illuminate/console": "^10.24|^11.0", + "illuminate/contracts": "^10.24|^11.0", + "illuminate/log": "^10.24|^11.0", + "illuminate/process": "^10.24|^11.0", + "illuminate/support": "^10.24|^11.0", + "nunomaduro/termwind": "^1.15|^2.0", + "php": "^8.2", + "symfony/console": "^6.0|^7.0" + }, + "require-dev": { + "laravel/pint": "^1.13", + "orchestra/testbench": "^8.12|^9.0", + "pestphp/pest": "^2.20", + "pestphp/pest-plugin-type-coverage": "^2.3", + "phpstan/phpstan": "^1.10", + "symfony/var-dumper": "^6.3|^7.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "1.x-dev" + }, + "laravel": { + "providers": [ + "Laravel\\Pail\\PailServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Pail\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Easily delve into your Laravel application's log files directly from the command line.", + "homepage": "https://github.com/laravel/pail", + "keywords": [ + "laravel", + "logs", + "php", + "tail" + ], + "support": { + "issues": "https://github.com/laravel/pail/issues", + "source": "https://github.com/laravel/pail" + }, + "time": "2024-10-15T20:06:24+00:00" + }, { "name": "laravel/prompts", "version": "v0.1.25", @@ -3399,75 +3476,6 @@ }, "time": "2024-09-03T09:46:57+00:00" }, - { - "name": "laravel/telescope", - "version": "v5.2.2", - "source": { - "type": "git", - "url": "https://github.com/laravel/telescope.git", - "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/laravel/telescope/zipball/daaf95dee9fab2dd80f59b5f6611c6c0eff44878", - "reference": "daaf95dee9fab2dd80f59b5f6611c6c0eff44878", - "shasum": "" - }, - "require": { - "ext-json": "*", - "laravel/framework": "^8.37|^9.0|^10.0|^11.0", - "php": "^8.0", - "symfony/console": "^5.3|^6.0|^7.0", - "symfony/var-dumper": "^5.0|^6.0|^7.0" - }, - "require-dev": { - "ext-gd": "*", - "guzzlehttp/guzzle": "^6.0|^7.0", - "laravel/octane": "^1.4|^2.0|dev-develop", - "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0", - "phpstan/phpstan": "^1.10", - "phpunit/phpunit": "^9.0|^10.5" - }, - "type": "library", - "extra": { - "laravel": { - "providers": [ - "Laravel\\Telescope\\TelescopeServiceProvider" - ] - } - }, - "autoload": { - "psr-4": { - "Laravel\\Telescope\\": "src/", - "Laravel\\Telescope\\Database\\Factories\\": "database/factories/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - }, - { - "name": "Mohamed Said", - "email": "mohamed@laravel.com" - } - ], - "description": "An elegant debug assistant for the Laravel framework.", - "keywords": [ - "debugging", - "laravel", - "monitoring" - ], - "support": { - "issues": "https://github.com/laravel/telescope/issues", - "source": "https://github.com/laravel/telescope/tree/v5.2.2" - }, - "time": "2024-08-26T12:40:52+00:00" - }, { "name": "laravel/tinker", "version": "v2.10.0", @@ -9105,16 +9113,16 @@ }, { "name": "symfony/http-foundation", - "version": "v7.1.5", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/http-foundation.git", - "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b" + "reference": "5183b61657807099d98f3367bcccb850238b17a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-foundation/zipball/e30ef73b1e44eea7eb37ba69600a354e553f694b", - "reference": "e30ef73b1e44eea7eb37ba69600a354e553f694b", + "url": "https://api.github.com/repos/symfony/http-foundation/zipball/5183b61657807099d98f3367bcccb850238b17a9", + "reference": "5183b61657807099d98f3367bcccb850238b17a9", "shasum": "" }, "require": { @@ -9162,7 +9170,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.5" + "source": "https://github.com/symfony/http-foundation/tree/v7.1.7" }, "funding": [ { @@ -9178,7 +9186,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-11-06T09:02:46+00:00" }, { "name": "symfony/http-kernel", @@ -9376,16 +9384,16 @@ }, { "name": "symfony/mime", - "version": "v7.1.5", + "version": "v7.1.6", "source": { "type": "git", "url": "https://github.com/symfony/mime.git", - "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff" + "reference": "caa1e521edb2650b8470918dfe51708c237f0598" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/mime/zipball/711d2e167e8ce65b05aea6b258c449671cdd38ff", - "reference": "711d2e167e8ce65b05aea6b258c449671cdd38ff", + "url": "https://api.github.com/repos/symfony/mime/zipball/caa1e521edb2650b8470918dfe51708c237f0598", + "reference": "caa1e521edb2650b8470918dfe51708c237f0598", "shasum": "" }, "require": { @@ -9440,7 +9448,7 @@ "mime-type" ], "support": { - "source": "https://github.com/symfony/mime/tree/v7.1.5" + "source": "https://github.com/symfony/mime/tree/v7.1.6" }, "funding": [ { @@ -9456,7 +9464,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:28:38+00:00" + "time": "2024-10-25T15:11:02+00:00" }, { "name": "symfony/options-resolver", @@ -10243,16 +10251,16 @@ }, { "name": "symfony/process", - "version": "v7.1.5", + "version": "v7.1.7", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "5c03ee6369281177f07f7c68252a280beccba847" + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/5c03ee6369281177f07f7c68252a280beccba847", - "reference": "5c03ee6369281177f07f7c68252a280beccba847", + "url": "https://api.github.com/repos/symfony/process/zipball/9b8a40b7289767aa7117e957573c2a535efe6585", + "reference": "9b8a40b7289767aa7117e957573c2a535efe6585", "shasum": "" }, "require": { @@ -10284,7 +10292,7 @@ "description": "Executes commands in sub-processes", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/process/tree/v7.1.5" + "source": "https://github.com/symfony/process/tree/v7.1.7" }, "funding": [ { @@ -10300,7 +10308,7 @@ "type": "tidelift" } ], - "time": "2024-09-19T21:48:23+00:00" + "time": "2024-11-06T09:25:12+00:00" }, { "name": "symfony/psr-http-message-bridge", @@ -12390,6 +12398,75 @@ }, "time": "2024-09-24T17:22:50+00:00" }, + { + "name": "laravel/telescope", + "version": "v5.2.4", + "source": { + "type": "git", + "url": "https://github.com/laravel/telescope.git", + "reference": "749369e996611d803e7c1b57929b482dd676008d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/telescope/zipball/749369e996611d803e7c1b57929b482dd676008d", + "reference": "749369e996611d803e7c1b57929b482dd676008d", + "shasum": "" + }, + "require": { + "ext-json": "*", + "laravel/framework": "^8.37|^9.0|^10.0|^11.0", + "php": "^8.0", + "symfony/console": "^5.3|^6.0|^7.0", + "symfony/var-dumper": "^5.0|^6.0|^7.0" + }, + "require-dev": { + "ext-gd": "*", + "guzzlehttp/guzzle": "^6.0|^7.0", + "laravel/octane": "^1.4|^2.0|dev-develop", + "orchestra/testbench": "^6.40|^7.37|^8.17|^9.0", + "phpstan/phpstan": "^1.10", + "phpunit/phpunit": "^9.0|^10.5" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Laravel\\Telescope\\TelescopeServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Laravel\\Telescope\\": "src/", + "Laravel\\Telescope\\Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Taylor Otwell", + "email": "taylor@laravel.com" + }, + { + "name": "Mohamed Said", + "email": "mohamed@laravel.com" + } + ], + "description": "An elegant debug assistant for the Laravel framework.", + "keywords": [ + "debugging", + "laravel", + "monitoring" + ], + "support": { + "issues": "https://github.com/laravel/telescope/issues", + "source": "https://github.com/laravel/telescope/tree/v5.2.4" + }, + "time": "2024-10-29T15:35:13+00:00" + }, { "name": "maximebf/debugbar", "version": "v1.23.2", @@ -14897,16 +14974,16 @@ }, { "name": "symfony/http-client", - "version": "v6.4.12", + "version": "v6.4.14", "source": { "type": "git", "url": "https://github.com/symfony/http-client.git", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56" + "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/http-client/zipball/fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", - "reference": "fbebfcce21084d3e91ea987ae5bdd8c71ff0fd56", + "url": "https://api.github.com/repos/symfony/http-client/zipball/05d88cbd816ad6e0202edd9a9963cb9d615b8826", + "reference": "05d88cbd816ad6e0202edd9a9963cb9d615b8826", "shasum": "" }, "require": { @@ -14970,7 +15047,7 @@ "http" ], "support": { - "source": "https://github.com/symfony/http-client/tree/v6.4.12" + "source": "https://github.com/symfony/http-client/tree/v6.4.14" }, "funding": [ { @@ -14986,7 +15063,7 @@ "type": "tidelift" } ], - "time": "2024-09-20T08:21:33+00:00" + "time": "2024-11-05T16:39:55+00:00" }, { "name": "symfony/http-client-contracts", diff --git a/config/app.php b/config/app.php index 34484fe410..371ac44ecc 100644 --- a/config/app.php +++ b/config/app.php @@ -199,8 +199,6 @@ App\Providers\EventServiceProvider::class, App\Providers\HorizonServiceProvider::class, App\Providers\RouteServiceProvider::class, - App\Providers\TelescopeServiceProvider::class, - ], /* diff --git a/config/constants.php b/config/constants.php index 5792b358c4..1bec2e3bfd 100644 --- a/config/constants.php +++ b/config/constants.php @@ -1,6 +1,7 @@ '26.0', 'docs' => [ 'base_url' => 'https://coolify.io/docs', 'contact' => 'https://coolify.io/docs/contact', @@ -18,7 +19,7 @@ 'invitation' => [ 'link' => [ 'base_url' => '/invitations/', - 'expiration' => 10, + 'expiration_days' => 3, ], ], 'services' => [ diff --git a/config/coolify.php b/config/coolify.php index f9878fff7b..225dfe6fa5 100644 --- a/config/coolify.php +++ b/config/coolify.php @@ -1,6 +1,7 @@ env('SENTRY_DSN'), 'docs' => 'https://coolify.io/docs/', 'contact' => 'https://coolify.io/docs/contact', 'feedback_discord_webhook' => env('FEEDBACK_DISCORD_WEBHOOK'), diff --git a/config/debugbar.php b/config/debugbar.php index eae406ba77..daeea96b6e 100644 --- a/config/debugbar.php +++ b/config/debugbar.php @@ -18,6 +18,7 @@ 'except' => [ 'telescope*', 'horizon*', + 'api*', ], /* diff --git a/config/horizon.php b/config/horizon.php index 939d74883d..6086b30dad 100644 --- a/config/horizon.php +++ b/config/horizon.php @@ -197,6 +197,7 @@ 'production' => [ 's6' => [ 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), @@ -206,6 +207,7 @@ 'local' => [ 's6' => [ 'autoScalingStrategy' => 'size', + 'minProcesses' => env('HORIZON_MIN_PROCESSES', 1), 'maxProcesses' => env('HORIZON_MAX_PROCESSES', 6), 'balanceMaxShift' => env('HORIZON_BALANCE_MAX_SHIFT', 1), 'balanceCooldown' => env('HORIZON_BALANCE_COOLDOWN', 1), diff --git a/config/sentry.php b/config/sentry.php index ade6923ace..33ae36c731 100644 --- a/config/sentry.php +++ b/config/sentry.php @@ -3,11 +3,11 @@ return [ // @see https://docs.sentry.io/product/sentry-basics/dsn-explainer/ - 'dsn' => 'https://89552af6db48f4ca6a871ec0fc42964d@o1082494.ingest.us.sentry.io/4505347448045568', + 'dsn' => config('coolify.sentry_dsn'), // 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.360', + '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/telescope.php b/config/telescope.php index 24077c24d8..c940bec8a8 100644 --- a/config/telescope.php +++ b/config/telescope.php @@ -76,8 +76,8 @@ */ 'queue' => [ - 'connection' => env('TELESCOPE_QUEUE_CONNECTION', null), - 'queue' => env('TELESCOPE_QUEUE', null), + 'connection' => env('TELESCOPE_QUEUE_CONNECTION', 'redis'), + 'queue' => env('TELESCOPE_QUEUE', 'default'), ], /* @@ -115,7 +115,6 @@ 'livewire*', 'nova-api*', 'pulse*', - 'broadcasting/auth', ], 'ignore_commands' => [ @@ -161,20 +160,20 @@ Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true), Watchers\GateWatcher::class => [ - 'enabled' => env('TELESCOPE_GATE_WATCHER', false), + 'enabled' => env('TELESCOPE_GATE_WATCHER', true), 'ignore_abilities' => [], 'ignore_packages' => true, 'ignore_paths' => [], ], - Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', false), + Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true), Watchers\LogWatcher::class => [ 'enabled' => env('TELESCOPE_LOG_WATCHER', true), - 'level' => 'debug', + 'level' => 'error', ], - Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', false), + Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true), Watchers\ModelWatcher::class => [ 'enabled' => env('TELESCOPE_MODEL_WATCHER', true), @@ -182,7 +181,7 @@ 'hydrations' => true, ], - Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', false), + Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true), Watchers\QueryWatcher::class => [ 'enabled' => env('TELESCOPE_QUERY_WATCHER', true), @@ -200,7 +199,7 @@ 'ignore_status_codes' => [], ], - Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', false), + Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true), Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true), ], ]; diff --git a/config/testing.php b/config/testing.php new file mode 100644 index 0000000000..41b8eadf01 --- /dev/null +++ b/config/testing.php @@ -0,0 +1,6 @@ + env('DUSK_TEST_EMAIL', 'test@example.com'), + 'dusk_test_password' => env('DUSK_TEST_PASSWORD', 'password'), +]; diff --git a/config/version.php b/config/version.php index 5639fc8a8d..0e83ff40e7 100644 --- a/config/version.php +++ b/config/version.php @@ -1,3 +1,3 @@ string('stripe_plan_id')->nullable()->after('stripe_cancel_at_period_end'); - }); } diff --git a/database/migrations/2023_08_22_071054_add_stripe_reasons.php b/database/migrations/2023_08_22_071054_add_stripe_reasons.php index efd611aac9..6ffe37e982 100644 --- a/database/migrations/2023_08_22_071054_add_stripe_reasons.php +++ b/database/migrations/2023_08_22_071054_add_stripe_reasons.php @@ -14,7 +14,6 @@ public function up(): void Schema::table('subscriptions', function (Blueprint $table) { $table->string('stripe_feedback')->nullable()->after('stripe_cancel_at_period_end'); $table->string('stripe_comment')->nullable()->after('stripe_feedback'); - }); } diff --git a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php index c22317e6be..61fcbda6b3 100644 --- a/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php +++ b/database/migrations/2023_08_22_071059_add_stripe_trial_ended.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('subscriptions', function (Blueprint $table) { $table->boolean('stripe_trial_already_ended')->default(false)->after('stripe_cancel_at_period_end'); - }); } diff --git a/database/migrations/2023_08_22_071060_change_invitation_link_length.php b/database/migrations/2023_08_22_071060_change_invitation_link_length.php index 4efb033518..9d14c3f265 100644 --- a/database/migrations/2023_08_22_071060_change_invitation_link_length.php +++ b/database/migrations/2023_08_22_071060_change_invitation_link_length.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('team_invitations', function (Blueprint $table) { $table->text('link')->change(); - }); } diff --git a/database/migrations/2023_09_20_082541_update_services_table.php b/database/migrations/2023_09_20_082541_update_services_table.php index 8c6b350f7f..c70cd28f7c 100644 --- a/database/migrations/2023_09_20_082541_update_services_table.php +++ b/database/migrations/2023_09_20_082541_update_services_table.php @@ -16,7 +16,6 @@ public function up(): void $table->longText('description')->nullable(); $table->longText('docker_compose_raw'); $table->longText('docker_compose')->nullable(); - }); } diff --git a/database/migrations/2023_09_20_083549_update_environment_variables_table.php b/database/migrations/2023_09_20_083549_update_environment_variables_table.php index 40eb6aa445..a96d096bbf 100644 --- a/database/migrations/2023_09_20_083549_update_environment_variables_table.php +++ b/database/migrations/2023_09_20_083549_update_environment_variables_table.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('environment_variables', function (Blueprint $table) { $table->foreignId('service_id')->nullable(); - }); } diff --git a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php index 920f44a720..729146a4ad 100644 --- a/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php +++ b/database/migrations/2023_09_23_111809_remove_destination_from_services_table.php @@ -14,7 +14,6 @@ public function up(): void Schema::table('services', function (Blueprint $table) { $table->dropColumn('destination_type'); $table->dropColumn('destination_id'); - }); } diff --git a/database/migrations/2023_09_23_111819_add_server_emails.php b/database/migrations/2023_09_23_111819_add_server_emails.php index 03c1e6bd22..775e820108 100644 --- a/database/migrations/2023_09_23_111819_add_server_emails.php +++ b/database/migrations/2023_09_23_111819_add_server_emails.php @@ -26,6 +26,5 @@ public function down(): void $table->dropColumn('unreachable_email_sent'); $table->integer('unreachable_count')->default(0); }); - } }; diff --git a/database/migrations/2023_11_16_220647_add_log_drains.php b/database/migrations/2023_11_16_220647_add_log_drains.php index 05b1ed0541..f5161b3d7f 100644 --- a/database/migrations/2023_11_16_220647_add_log_drains.php +++ b/database/migrations/2023_11_16_220647_add_log_drains.php @@ -22,7 +22,6 @@ public function up(): void $table->boolean('is_logdrain_axiom_enabled')->default(false); $table->string('logdrain_axiom_dataset_name')->nullable(); $table->string('logdrain_axiom_api_key')->nullable(); - }); } diff --git a/database/migrations/2023_12_13_110214_add_soft_deletes.php b/database/migrations/2023_12_13_110214_add_soft_deletes.php index ab7b562b41..72350b77fb 100644 --- a/database/migrations/2023_12_13_110214_add_soft_deletes.php +++ b/database/migrations/2023_12_13_110214_add_soft_deletes.php @@ -66,6 +66,5 @@ public function down(): void Schema::table('service_databases', function (Blueprint $table) { $table->dropSoftDeletes(); }); - } }; diff --git a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php index eeb2769fe6..f28b2670e3 100644 --- a/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php +++ b/database/migrations/2023_12_17_155616_add_custom_docker_compose_start_command.php @@ -14,7 +14,6 @@ public function up(): void Schema::table('applications', function (Blueprint $table) { $table->string('docker_compose_custom_start_command')->nullable(); $table->string('docker_compose_custom_build_command')->nullable(); - }); } diff --git a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php index 7df53ec06e..b3f2c1920c 100644 --- a/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php +++ b/database/migrations/2024_04_09_095517_make_custom_docker_commands_longer.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('applications', function (Blueprint $table) { $table->text('custom_docker_run_options')->nullable()->change(); - }); } diff --git a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php index aeae6f77de..bea410eab1 100644 --- a/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php +++ b/database/migrations/2024_04_25_073615_add_docker_network_to_application_settings.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('application_settings', function (Blueprint $table) { $table->boolean('connect_to_docker_network')->default(false); - }); } diff --git a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php index 716f1f44c2..b46a072033 100644 --- a/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php +++ b/database/migrations/2024_05_23_091713_add_gitea_webhook_to_applications.php @@ -13,7 +13,6 @@ public function up(): void { Schema::table('applications', function (Blueprint $table) { $table->string('manual_webhook_secret_gitea')->nullable(); - }); } 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 26a1d1684e..a6bccd16a1 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_06_25_184323_update_db.php b/database/migrations/2024_06_25_184323_update_db.php index f1b175a9c6..d9cddb15f9 100644 --- a/database/migrations/2024_06_25_184323_update_db.php +++ b/database/migrations/2024_06_25_184323_update_db.php @@ -4,6 +4,8 @@ use App\Models\Server; use Illuminate\Database\Migrations\Migration; use Illuminate\Database\Schema\Blueprint; +use Illuminate\Support\Facades\DB; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Schema; use Visus\Cuid2\Cuid2; @@ -14,44 +16,45 @@ */ public function up(): void { - Schema::table('applications', function (Blueprint $table) { - $table->dropColumn('docker_compose_pr_location'); - $table->dropColumn('docker_compose_pr'); - $table->dropColumn('docker_compose_pr_raw'); - }); - Schema::table('subscriptions', function (Blueprint $table) { - $table->dropColumn('lemon_subscription_id'); - $table->dropColumn('lemon_order_id'); - $table->dropColumn('lemon_product_id'); - $table->dropColumn('lemon_variant_id'); - $table->dropColumn('lemon_variant_name'); - $table->dropColumn('lemon_customer_id'); - $table->dropColumn('lemon_status'); - $table->dropColumn('lemon_renews_at'); - $table->dropColumn('lemon_update_payment_menthod_url'); - $table->dropColumn('lemon_trial_ends_at'); - $table->dropColumn('lemon_ends_at'); - }); - Schema::table('environment_variables', function (Blueprint $table) { - $table->string('uuid')->nullable()->after('id'); - }); + try { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('docker_compose_pr_location'); + $table->dropColumn('docker_compose_pr'); + $table->dropColumn('docker_compose_pr_raw'); + }); + Schema::table('subscriptions', function (Blueprint $table) { + $table->dropColumn('lemon_subscription_id'); + $table->dropColumn('lemon_order_id'); + $table->dropColumn('lemon_product_id'); + $table->dropColumn('lemon_variant_id'); + $table->dropColumn('lemon_variant_name'); + $table->dropColumn('lemon_customer_id'); + $table->dropColumn('lemon_status'); + $table->dropColumn('lemon_renews_at'); + $table->dropColumn('lemon_update_payment_menthod_url'); + $table->dropColumn('lemon_trial_ends_at'); + $table->dropColumn('lemon_ends_at'); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable()->after('id'); + }); - EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) { - $environmentVariable->update([ - 'uuid' => (string) new Cuid2, - ]); - }); - Schema::table('environment_variables', function (Blueprint $table) { - $table->string('uuid')->nullable(false)->change(); - }); - Schema::table('server_settings', function (Blueprint $table) { - $table->integer('metrics_history_days')->default(7)->change(); - }); - Server::all()->each(function (Server $server) { - $server->settings->update([ - 'metrics_history_days' => 7, - ]); - }); + EnvironmentVariable::all()->each(function (EnvironmentVariable $environmentVariable) { + $environmentVariable->update([ + 'uuid' => (string) new Cuid2, + ]); + }); + Schema::table('environment_variables', function (Blueprint $table) { + $table->string('uuid')->nullable(false)->change(); + }); + Schema::table('server_settings', function (Blueprint $table) { + $table->integer('metrics_history_days')->default(7)->change(); + }); + + DB::table('server_settings')->update(['metrics_history_days' => 7]); + } catch (\Exception $e) { + Log::error('Error updating db: '.$e->getMessage()); + } } /** 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 a33665bd0e..ea3695b3f5 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_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php index b5300c9057..e3bdc68c60 100644 --- a/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php +++ b/database/migrations/2024_08_09_215659_add_server_cleanup_fields_to_server_settings_table.php @@ -30,7 +30,6 @@ public function up() $serverSetting->docker_cleanup_threshold = $serverSetting->cleanup_after_percentage; $serverSetting->save(); } - } /** diff --git a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php index 19274ad9bb..e16181ac7e 100644 --- a/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php +++ b/database/migrations/2024_09_16_111428_encrypt_existing_private_keys.php @@ -20,6 +20,5 @@ public function up() echo 'Encrypting private keys failed.'; echo $e->getMessage(); } - } } 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 0000000000..4fde0c2bbf --- /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 0000000000..d5c38501f1 --- /dev/null +++ b/database/migrations/2024_10_14_090416_update_metrics_token_in_server_settings.php @@ -0,0 +1,54 @@ +dropColumn('metrics_token'); + $table->dropColumn('metrics_refresh_rate_seconds'); + $table->dropColumn('metrics_history_days'); + $table->dropColumn('is_server_api_enabled'); + + $table->boolean('is_sentinel_enabled')->default(false); + $table->text('sentinel_token')->nullable(); + $table->integer('sentinel_metrics_refresh_rate_seconds')->default(10); + $table->integer('sentinel_metrics_history_days')->default(7); + $table->integer('sentinel_push_interval_seconds')->default(60); + $table->string('sentinel_custom_url')->nullable(); + }); + Schema::table('servers', function (Blueprint $table) { + $table->dateTime('sentinel_updated_at')->default(now()); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->string('metrics_token')->nullable(); + $table->integer('metrics_refresh_rate_seconds')->default(5); + $table->integer('metrics_history_days')->default(30); + $table->boolean('is_server_api_enabled')->default(false); + + $table->dropColumn('is_sentinel_enabled'); + $table->dropColumn('sentinel_token'); + $table->dropColumn('sentinel_metrics_refresh_rate_seconds'); + $table->dropColumn('sentinel_metrics_history_days'); + $table->dropColumn('sentinel_push_interval_seconds'); + $table->dropColumn('sentinel_custom_url'); + }); + Schema::table('servers', function (Blueprint $table) { + $table->dropColumn('sentinel_updated_at'); + }); + } +}; diff --git a/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php new file mode 100644 index 0000000000..eb878e2f6b --- /dev/null +++ b/database/migrations/2024_10_15_172139_add_is_shared_to_environment_variables.php @@ -0,0 +1,22 @@ +boolean('is_shared')->default(false); + }); + } + + public function down() + { + Schema::table('environment_variables', function (Blueprint $table) { + $table->dropColumn('is_shared'); + }); + } +} diff --git a/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php new file mode 100644 index 0000000000..fa01e8e85b --- /dev/null +++ b/database/migrations/2024_10_16_120026_move_redis_password_to_envs.php @@ -0,0 +1,41 @@ +where('id', $redis->id)->value('redis_password'); + EnvironmentVariable::create([ + 'standalone_redis_id' => $redis->id, + 'key' => 'REDIS_PASSWORD', + 'value' => $redis_password, + ]); + EnvironmentVariable::create([ + 'standalone_redis_id' => $redis->id, + 'key' => 'REDIS_USERNAME', + 'value' => 'default', + ]); + } + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->dropColumn('redis_password'); + }); + } catch (\Exception $e) { + echo 'Moving Redis passwords to envs failed.'; + echo $e->getMessage(); + } + } +} diff --git a/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php new file mode 100644 index 0000000000..7040daf44a --- /dev/null +++ b/database/migrations/2024_10_16_192133_add_confirmation_settings_to_instance_settings_table.php @@ -0,0 +1,22 @@ +boolean('disable_two_step_confirmation')->default(false); + }); + } + + public function down() + { + Schema::table('instance_settings', function (Blueprint $table) { + $table->dropColumn('disable_two_step_confirmation'); + }); + } +}; diff --git a/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php new file mode 100644 index 0000000000..7a7f28e249 --- /dev/null +++ b/database/migrations/2024_10_17_093722_add_soft_delete_to_servers.php @@ -0,0 +1,28 @@ +softDeletes(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('servers', function (Blueprint $table) { + $table->dropSoftDeletes(); + }); + } +}; diff --git a/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php new file mode 100644 index 0000000000..76ccd13529 --- /dev/null +++ b/database/migrations/2024_10_22_105745_add_server_disk_usage_threshold.php @@ -0,0 +1,28 @@ +integer('server_disk_usage_notification_threshold')->default(80)->after('docker_cleanup_threshold'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('server_disk_usage_notification_threshold'); + }); + } +}; diff --git a/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php new file mode 100644 index 0000000000..a2aa381b7e --- /dev/null +++ b/database/migrations/2024_10_22_121223_add_server_disk_usage_notification.php @@ -0,0 +1,32 @@ +boolean('discord_notifications_server_disk_usage')->default(true)->after('discord_enabled'); + $table->boolean('smtp_notifications_server_disk_usage')->default(true)->after('smtp_enabled'); + $table->boolean('telegram_notifications_server_disk_usage')->default(true)->after('telegram_enabled'); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('teams', function (Blueprint $table) { + $table->dropColumn('discord_notifications_server_disk_usage'); + $table->dropColumn('smtp_notifications_server_disk_usage'); + $table->dropColumn('telegram_notifications_server_disk_usage'); + }); + } +}; diff --git a/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php new file mode 100644 index 0000000000..d8ab1313bf --- /dev/null +++ b/database/migrations/2024_10_29_093927_add_is_sentinel_debug_enabled_to_server_settings.php @@ -0,0 +1,28 @@ +boolean('is_sentinel_debug_enabled')->default(false); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('server_settings', function (Blueprint $table) { + $table->dropColumn('is_sentinel_debug_enabled'); + }); + } +}; diff --git a/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php new file mode 100644 index 0000000000..51b8fb3ba8 --- /dev/null +++ b/database/migrations/2024_11_02_213214_add_last_online_at_to_resources.php @@ -0,0 +1,96 @@ +timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('application_previews', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->timestamp('last_online_at')->default(now())->after('updated_at'); + }); + + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('applications', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('application_previews', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('service_applications', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('service_databases', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_postgresqls', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_redis', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mongodbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mysqls', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_mariadbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_keydbs', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_dragonflies', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + Schema::table('standalone_clickhouses', function (Blueprint $table) { + $table->dropColumn('last_online_at'); + }); + + } +}; diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php index be50831087..6e66c64f4a 100644 --- a/database/seeders/DatabaseSeeder.php +++ b/database/seeders/DatabaseSeeder.php @@ -26,6 +26,8 @@ public function run(): void S3StorageSeeder::class, StandalonePostgresqlSeeder::class, OauthSettingSeeder::class, + DisableTwoStepConfirmationSeeder::class, + SentinelSeeder::class, ]); } } diff --git a/database/seeders/DisableTwoStepConfirmationSeeder.php b/database/seeders/DisableTwoStepConfirmationSeeder.php new file mode 100644 index 0000000000..c43bf1b015 --- /dev/null +++ b/database/seeders/DisableTwoStepConfirmationSeeder.php @@ -0,0 +1,20 @@ +updateOrInsert( + [], + ['disable_two_step_confirmation' => true] + ); + } +} diff --git a/database/seeders/PopulateSshKeysDirectorySeeder.php b/database/seeders/PopulateSshKeysDirectorySeeder.php index e2543ee02e..d528179c0e 100644 --- a/database/seeders/PopulateSshKeysDirectorySeeder.php +++ b/database/seeders/PopulateSshKeysDirectorySeeder.php @@ -33,7 +33,6 @@ public function run() } } catch (\Throwable $e) { echo "Error: {$e->getMessage()}\n"; - ray($e->getMessage()); } } } diff --git a/database/seeders/ProductionSeeder.php b/database/seeders/ProductionSeeder.php index 206f04d6b6..3e820a1629 100644 --- a/database/seeders/ProductionSeeder.php +++ b/database/seeders/ProductionSeeder.php @@ -126,7 +126,6 @@ public function run(): void echo "Your localhost connection won't work until then."; } } - } if (config('coolify.is_windows_docker_desktop')) { PrivateKey::updateOrCreate( @@ -186,6 +185,6 @@ public function run(): void $this->call(OauthSettingSeeder::class); $this->call(PopulateSshKeysDirectorySeeder::class); - + $this->call(SentinelSeeder::class); } } diff --git a/database/seeders/SentinelSeeder.php b/database/seeders/SentinelSeeder.php new file mode 100644 index 0000000000..3cf9139333 --- /dev/null +++ b/database/seeders/SentinelSeeder.php @@ -0,0 +1,32 @@ +settings->sentinel_token)->isEmpty()) { + $server->settings->generateSentinelToken(ignoreEvent: true); + } + if (str($server->settings->sentinel_custom_url)->isEmpty()) { + $url = $server->settings->generateSentinelUrl(ignoreEvent: true); + if (str($url)->isEmpty()) { + $server->settings->is_sentinel_enabled = false; + $server->settings->save(); + } + } + } catch (\Throwable $e) { + Log::error('Error seeding sentinel: '.$e->getMessage()); + } + } + }); + } +} diff --git a/docker/coolify-helper/Dockerfile b/docker/coolify-helper/Dockerfile index 7aa9d8722c..48f401da4a 100644 --- a/docker/coolify-helper/Dockerfile +++ b/docker/coolify-helper/Dockerfile @@ -10,7 +10,7 @@ ARG DOCKER_BUILDX_VERSION=0.14.1 # https://github.com/buildpacks/pack/releases ARG PACK_VERSION=0.35.1 # https://github.com/railwayapp/nixpacks/releases -ARG NIXPACKS_VERSION=1.28.0 +ARG NIXPACKS_VERSION=1.29.0 USER root WORKDIR /artifacts diff --git a/docker/coolify-realtime/package-lock.json b/docker/coolify-realtime/package-lock.json new file mode 100644 index 0000000000..f5fd1ba18d --- /dev/null +++ b/docker/coolify-realtime/package-lock.json @@ -0,0 +1,190 @@ +{ + "name": "coolify-realtime", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "axios": "1.7.5", + "cookie": "1.0.1", + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" + } + }, + "node_modules/@xterm/addon-fit": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", + "integrity": "sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==", + "license": "MIT", + "peerDependencies": { + "@xterm/xterm": "^5.0.0" + } + }, + "node_modules/@xterm/xterm": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/@xterm/xterm/-/xterm-5.5.0.tgz", + "integrity": "sha512-hqJHYaQb5OptNunnyAnkHyM8aCjZ1MEIDTQu1iIbbTD/xops91NB5yq1ZK/dC2JDbVWtF23zUtl9JE2NqwT87A==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.5.tgz", + "integrity": "sha512-fZu86yCo+svH3uqJ/yTdQ0QHpQu5oL+/QE+QPSv6BZSkDAoky9vytxp7u5qk83OJFS3kEBcesWni9WTZAv3tSw==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cookie": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.1.tgz", + "integrity": "sha512-Xd8lFX4LM9QEEwxQpF9J9NTUh8pmdJO0cyRJhFiDoLTk2eH8FXlRv2IFGYVadZpqI3j8fhNrSdKCeYPxiAhLXw==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz", + "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nan": { + "version": "2.22.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", + "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", + "license": "MIT" + }, + "node_modules/node-pty": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-pty/-/node-pty-1.0.0.tgz", + "integrity": "sha512-wtBMWWS7dFZm/VgqElrTvtfMq4GzJ6+edFI0Y0zyzygUSZMgZdraDUMUhCIvkjhJjme15qWmbyJbtAx4ot4uZA==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "nan": "^2.17.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/docker/coolify-realtime/package.json b/docker/coolify-realtime/package.json index 90d4f77db8..faeb80f54b 100644 --- a/docker/coolify-realtime/package.json +++ b/docker/coolify-realtime/package.json @@ -2,12 +2,12 @@ "private": true, "type": "module", "dependencies": { - "@xterm/addon-fit": "^0.10.0", - "@xterm/xterm": "^5.5.0", - "cookie": "^0.6.0", + "@xterm/addon-fit": "0.10.0", + "@xterm/xterm": "5.5.0", + "cookie": "1.0.1", "axios": "1.7.5", - "dotenv": "^16.4.5", - "node-pty": "^1.0.0", - "ws": "^8.17.0" + "dotenv": "16.4.5", + "node-pty": "1.0.0", + "ws": "8.18.0" } } diff --git a/docker/dev/Dockerfile b/docker/dev/Dockerfile index 63832dc36d..d2381f7643 100644 --- a/docker/dev/Dockerfile +++ b/docker/dev/Dockerfile @@ -5,34 +5,38 @@ ARG TARGETPLATFORM ARG CLOUDFLARED_VERSION=2024.4.1 ARG POSTGRES_VERSION=15 -RUN apt-get update -# Postgres version requirements -RUN apt install dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl -y -RUN curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null -RUN echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list +# Use build arguments for caching +ARG BUILDTIME_DEPS="dirmngr ca-certificates software-properties-common gnupg gnupg2 apt-transport-https curl" +ARG RUNTIME_DEPS="postgresql-client-$POSTGRES_VERSION php8.2-pgsql openssh-client git git-lfs jq lsof" -RUN apt-get update -RUN apt-get install postgresql-client-$POSTGRES_VERSION -y +# Install dependencies +RUN --mount=type=cache,target=/var/cache/apt \ + apt-get update && \ + apt-get install -y $BUILDTIME_DEPS && \ + curl -fSsL https://www.postgresql.org/media/keys/ACCC4CF8.asc | gpg --dearmor | tee /usr/share/keyrings/postgresql.gpg > /dev/null && \ + echo deb [arch=amd64,arm64,ppc64el signed-by=/usr/share/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt/ jammy-pgdg main | tee -a /etc/apt/sources.list.d/postgresql.list && \ + apt-get update && \ + apt-get install -y $RUNTIME_DEPS && \ + apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* -# Coolify requirements -RUN apt-get install -y php8.2-pgsql openssh-client git git-lfs jq lsof -RUN apt-get -y autoremove && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* /usr/share/doc/* COPY --chmod=755 docker/dev/etc/s6-overlay/ /etc/s6-overlay/ COPY docker/dev/nginx.conf /etc/nginx/conf.d/custom.conf -RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc -RUN echo "alias a='php artisan'" >>/etc/bash.bashrc +RUN echo "alias ll='ls -al'" >>/etc/bash.bashrc && \ + echo "alias a='php artisan'" >>/etc/bash.bashrc RUN mkdir -p /usr/local/bin -RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ +RUN --mount=type=cache,target=/root/.cache \ + /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/amd64' ]]; then \ echo 'amd64' && \ curl -sSL https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" -RUN /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ +RUN --mount=type=cache,target=/root/.cache \ + /bin/bash -c "if [[ ${TARGETPLATFORM} == 'linux/arm64' ]]; then \ echo 'arm64' && \ curl -L https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-arm64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared \ ;fi" diff --git a/docker/prod/Dockerfile b/docker/prod/Dockerfile index d0cebcbca6..37e0481bb9 100644 --- a/docker/prod/Dockerfile +++ b/docker/prod/Dockerfile @@ -1,10 +1,10 @@ -FROM serversideup/php:8.2-fpm-nginx-v2.2.1 as base +FROM serversideup/php:8.2-fpm-nginx-v2.2.1 AS base WORKDIR /var/www/html COPY composer.json composer.lock ./ RUN composer install --no-dev --no-interaction --no-plugins --no-scripts --prefer-dist -FROM node:20 as static-assets +FROM node:20 AS static-assets WORKDIR /app COPY . . COPY --from=base --chown=9999:9999 /var/www/html . @@ -45,6 +45,8 @@ RUN composer dump-autoload COPY --from=static-assets --chown=9999:9999 /app/public/build ./public/build COPY --chmod=755 docker/prod/etc/s6-overlay/ /etc/s6-overlay/ +RUN php artisan route:clear +RUN php artisan view:clear RUN php artisan route:cache RUN php artisan view:cache diff --git a/lang/ar.json b/lang/ar.json index c5ec96c8d7..4b9afbe996 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 fa69c7035a..5ea474b028 100644 --- a/lang/en.json +++ b/lang/en.json @@ -33,5 +33,6 @@ "resource.delete_volumes": "Permanently delete all volumes associated with this resource.", "resource.delete_connected_networks": "Permanently delete all non-predefined networks associated with this resource.", "resource.delete_configurations": "Permanently delete all configuration files from the server.", - "database.delete_backups_locally": "All backups will be permanently deleted from local storage." + "database.delete_backups_locally": "All backups will be permanently deleted from local storage.", + "warning.sslipdomain": "Your configuration is saved, but sslip domain with https is NOT recommended, because Let's Encrypt servers with this public domain are rate limited (SSL certificate validation will fail).

Use your own domain instead." } diff --git a/lang/ro.json b/lang/ro.json new file mode 100644 index 0000000000..db1aa85db5 --- /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 91d5c14439..d2616e9c6a 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -98,6 +98,10 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' install_command: type: string description: 'The install command.' @@ -323,6 +327,10 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' install_command: type: string description: 'The install command.' @@ -548,6 +556,10 @@ paths: is_static: type: boolean description: 'The flag to indicate if the application is static.' + static_image: + type: string + enum: ['nginx:alpine'] + description: 'The static image.' install_command: type: string description: 'The install command.' @@ -3093,7 +3105,7 @@ paths: security: - bearerAuth: [] - /healthcheck: + /health: get: summary: Healthcheck description: 'Healthcheck endpoint.' @@ -4959,7 +4971,7 @@ components: type: boolean is_reachable: type: boolean - is_server_api_enabled: + is_sentinel_enabled: type: boolean is_swarm_manager: type: boolean @@ -4981,11 +4993,11 @@ components: type: string logdrain_newrelic_license_key: type: string - metrics_history_days: + sentinel_metrics_history_days: type: integer - metrics_refresh_rate_seconds: + sentinel_metrics_refresh_rate_seconds: type: integer - metrics_token: + sentinel_token: type: string docker_cleanup_frequency: type: string diff --git a/other/nightly/install.sh b/other/nightly/install.sh index 04faf50eab..29ce52b77c 100755 --- a/other/nightly/install.sh +++ b/other/nightly/install.sh @@ -5,7 +5,7 @@ set -e # Exit immediately if a command exits with a non-zero status ## $1 could be empty, so we need to disable this check #set -u # Treat unset variables as an error and exit set -o pipefail # Cause a pipeline to return the status of the last command that exited with a non-zero status -CDN="https://cdn.coollabs.io/coolify-nightly" +CDN="https://cdn.coollabs.io/coolify" DATE=$(date +"%Y%m%d-%H%M%S") VERSION="1.6" @@ -13,7 +13,7 @@ DOCKER_VERSION="26.0" # TODO: Ask for a user CURRENT_USER=$USER -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic @@ -164,7 +164,6 @@ sles | opensuse-leap | opensuse-tumbleweed) esac - echo -e "2. Check OpenSSH server configuration. " # Detect OpenSSH server @@ -262,9 +261,14 @@ if ! [ -x "$(command -v docker)" ]; then fi ;; *) - curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 + if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then + echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)." + echo "Please install Docker manually." + exit 1 + fi + curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1 if ! [ -x "$(command -v docker)" ]; then - curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 + curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1 if ! [ -x "$(command -v docker)" ]; then echo " - Docker installation failed." echo " Maybe your OS is not supported?" @@ -287,7 +291,10 @@ test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon "log-opts": { "max-size": "10m", "max-file": "3" - } + }, + "default-address-pools": [ + {"base":"10.0.0.0/8","size":24} + ] } EOL cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json.coolify </dev/null 2>&1 +bash /data/coolify/source/upgrade.sh "${LATEST_VERSION:-latest}" "${LATEST_HELPER_VERSION:-latest}" echo " - Coolify installed successfully." rm -f $ENV_FILE-$DATE diff --git a/other/nightly/versions.json b/other/nightly/versions.json index c04a3dee6c..dbb2955909 100644 --- a/other/nightly/versions.json +++ b/other/nightly/versions.json @@ -1,16 +1,19 @@ { "coolify": { "v4": { - "version": "4.0.0-beta.354" + "version": "4.0.0-beta.360" }, "nightly": { - "version": "4.0.0-beta.355" + "version": "4.0.0-beta.362" }, "helper": { - "version": "1.0.2" + "version": "1.0.3" }, "realtime": { - "version": "1.0.3" + "version": "1.0.4" + }, + "sentinel": { + "version": "0.0.15" } } } diff --git a/public/svgs/affine.svg b/public/svgs/affine.svg new file mode 100644 index 0000000000..d8063e9206 --- /dev/null +++ b/public/svgs/affine.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/calcom.svg b/public/svgs/calcom.svg new file mode 100644 index 0000000000..446b16655d --- /dev/null +++ b/public/svgs/calcom.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/cloudbeaver.svg b/public/svgs/cloudbeaver.svg new file mode 100644 index 0000000000..4a76347669 --- /dev/null +++ b/public/svgs/cloudbeaver.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/public/svgs/coder.svg b/public/svgs/coder.svg new file mode 100644 index 0000000000..45b7f795c6 --- /dev/null +++ b/public/svgs/coder.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/public/svgs/cryptgeon.png b/public/svgs/cryptgeon.png new file mode 100644 index 0000000000..be121cfd08 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 0000000000..326acf789c Binary files /dev/null and b/public/svgs/dify.png differ diff --git a/public/svgs/edgedb.svg b/public/svgs/edgedb.svg new file mode 100644 index 0000000000..a906f7f7e1 --- /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 0000000000..6b0be0d2a7 Binary files /dev/null and b/public/svgs/flowise.png differ diff --git a/public/svgs/foundryvtt.png b/public/svgs/foundryvtt.png new file mode 100644 index 0000000000..c6a04508f3 Binary files /dev/null and b/public/svgs/foundryvtt.png differ diff --git a/public/svgs/freshrss.png b/public/svgs/freshrss.png new file mode 100644 index 0000000000..d1a75118f9 Binary files /dev/null and b/public/svgs/freshrss.png differ diff --git a/public/svgs/heyform.svg b/public/svgs/heyform.svg new file mode 100644 index 0000000000..ff29ca6548 --- /dev/null +++ b/public/svgs/heyform.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/public/svgs/immich.svg b/public/svgs/immich.svg new file mode 100644 index 0000000000..9d844a772b --- /dev/null +++ b/public/svgs/immich.svg @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/jenkins.svg b/public/svgs/jenkins.svg new file mode 100644 index 0000000000..0529fff1e3 --- /dev/null +++ b/public/svgs/jenkins.svg @@ -0,0 +1,283 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/public/svgs/jitsi.svg b/public/svgs/jitsi.svg new file mode 100644 index 0000000000..6257659ee5 --- /dev/null +++ b/public/svgs/jitsi.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/svgs/kimai.svg b/public/svgs/kimai.svg new file mode 100644 index 0000000000..35b1469726 --- /dev/null +++ b/public/svgs/kimai.svg @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/libretranslate.svg b/public/svgs/libretranslate.svg new file mode 100644 index 0000000000..103d47d609 --- /dev/null +++ b/public/svgs/libretranslate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/litequeen.svg b/public/svgs/litequeen.svg new file mode 100644 index 0000000000..aa0b8e0387 --- /dev/null +++ b/public/svgs/litequeen.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/martin.png b/public/svgs/martin.png new file mode 100644 index 0000000000..d1a99e1480 Binary files /dev/null and b/public/svgs/martin.png differ diff --git a/public/svgs/mindsdb.svg b/public/svgs/mindsdb.svg new file mode 100644 index 0000000000..53799dd1c4 --- /dev/null +++ b/public/svgs/mindsdb.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/public/svgs/mosquitto.png b/public/svgs/mosquitto.png new file mode 100644 index 0000000000..eb287a7cdd Binary files /dev/null and b/public/svgs/mosquitto.png differ diff --git a/public/svgs/ntfy.svg b/public/svgs/ntfy.svg new file mode 100644 index 0000000000..9e5b5136fd --- /dev/null +++ b/public/svgs/ntfy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/osticket.png b/public/svgs/osticket.png new file mode 100644 index 0000000000..65885b71b8 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 0000000000..83631e3f5d --- /dev/null +++ b/public/svgs/owncloud.svg @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/svgs/peppermint.png b/public/svgs/peppermint.png new file mode 100644 index 0000000000..38db83de0c Binary files /dev/null and b/public/svgs/peppermint.png differ diff --git a/public/svgs/qbittorrent.svg b/public/svgs/qbittorrent.svg new file mode 100644 index 0000000000..69d8cf62ae --- /dev/null +++ b/public/svgs/qbittorrent.svg @@ -0,0 +1,16 @@ + + + qbittorrent-new-light + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/svgs/traccar.png b/public/svgs/traccar.png new file mode 100644 index 0000000000..c747aea054 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 0000000000..9a11f77f45 --- /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 0000000000..f5ff6fabc6 --- /dev/null +++ b/public/svgs/unsend.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/svgs/vvveb.svg b/public/svgs/vvveb.svg new file mode 100644 index 0000000000..2b66b3087b --- /dev/null +++ b/public/svgs/vvveb.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/svgs/wireguard.svg b/public/svgs/wireguard.svg new file mode 100644 index 0000000000..81823b3eb7 --- /dev/null +++ b/public/svgs/wireguard.svg @@ -0,0 +1,7 @@ + + \ No newline at end of file diff --git a/public/svgs/zep.png b/public/svgs/zep.png new file mode 100644 index 0000000000..7d51b32dc0 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 0000000000..2b8f6972d5 Binary files /dev/null and b/public/svgs/zipline.png differ diff --git a/public/vendor/telescope/app.js b/public/vendor/telescope/app.js index d1c0e7f28d..378d6cf438 100644 --- a/public/vendor/telescope/app.js +++ b/public/vendor/telescope/app.js @@ -1,2 +1,2 @@ /*! For license information please see app.js.LICENSE.txt */ -(()=>{var t,e={2110:(t,e,n)=>{"use strict";var o=Object.freeze({}),p=Array.isArray;function M(t){return null==t}function b(t){return null!=t}function c(t){return!0===t}function r(t){return"string"==typeof t||"number"==typeof t||"symbol"==typeof t||"boolean"==typeof t}function z(t){return"function"==typeof t}function a(t){return null!==t&&"object"==typeof t}var i=Object.prototype.toString;function O(t){return"[object Object]"===i.call(t)}function s(t){return"[object RegExp]"===i.call(t)}function A(t){var e=parseFloat(String(t));return e>=0&&Math.floor(e)===e&&isFinite(t)}function u(t){return b(t)&&"function"==typeof t.then&&"function"==typeof t.catch}function l(t){return null==t?"":Array.isArray(t)||O(t)&&t.toString===i?JSON.stringify(t,null,2):String(t)}function d(t){var e=parseFloat(t);return isNaN(e)?t:e}function f(t,e){for(var n=Object.create(null),o=t.split(","),p=0;p-1)return t.splice(o,1)}}var v=Object.prototype.hasOwnProperty;function R(t,e){return v.call(t,e)}function m(t){var e=Object.create(null);return function(n){return e[n]||(e[n]=t(n))}}var g=/-(\w)/g,L=m((function(t){return t.replace(g,(function(t,e){return e?e.toUpperCase():""}))})),y=m((function(t){return t.charAt(0).toUpperCase()+t.slice(1)})),_=/\B([A-Z])/g,N=m((function(t){return t.replace(_,"-$1").toLowerCase()}));var E=Function.prototype.bind?function(t,e){return t.bind(e)}:function(t,e){function n(n){var o=arguments.length;return o?o>1?t.apply(e,arguments):t.call(e,n):t.call(e)}return n._length=t.length,n};function T(t,e){e=e||0;for(var n=t.length-e,o=new Array(n);n--;)o[n]=t[n+e];return o}function B(t,e){for(var n in e)t[n]=e[n];return t}function C(t){for(var e={},n=0;n0,tt=Z&&Z.indexOf("edge/")>0;Z&&Z.indexOf("android");var et=Z&&/iphone|ipad|ipod|ios/.test(Z);Z&&/chrome\/\d+/.test(Z),Z&&/phantomjs/.test(Z);var nt,ot=Z&&Z.match(/firefox\/(\d+)/),pt={}.watch,Mt=!1;if(K)try{var bt={};Object.defineProperty(bt,"passive",{get:function(){Mt=!0}}),window.addEventListener("test-passive",null,bt)}catch(t){}var ct=function(){return void 0===nt&&(nt=!K&&void 0!==n.g&&(n.g.process&&"server"===n.g.process.env.VUE_ENV)),nt},rt=K&&window.__VUE_DEVTOOLS_GLOBAL_HOOK__;function zt(t){return"function"==typeof t&&/native code/.test(t.toString())}var at,it="undefined"!=typeof Symbol&&zt(Symbol)&&"undefined"!=typeof Reflect&&zt(Reflect.ownKeys);at="undefined"!=typeof Set&&zt(Set)?Set:function(){function t(){this.set=Object.create(null)}return t.prototype.has=function(t){return!0===this.set[t]},t.prototype.add=function(t){this.set[t]=!0},t.prototype.clear=function(){this.set=Object.create(null)},t}();var Ot=null;function st(t){void 0===t&&(t=null),t||Ot&&Ot._scope.off(),Ot=t,t&&t._scope.on()}var At=function(){function t(t,e,n,o,p,M,b,c){this.tag=t,this.data=e,this.children=n,this.text=o,this.elm=p,this.ns=void 0,this.context=M,this.fnContext=void 0,this.fnOptions=void 0,this.fnScopeId=void 0,this.key=e&&e.key,this.componentOptions=b,this.componentInstance=void 0,this.parent=void 0,this.raw=!1,this.isStatic=!1,this.isRootInsert=!0,this.isComment=!1,this.isCloned=!1,this.isOnce=!1,this.asyncFactory=c,this.asyncMeta=void 0,this.isAsyncPlaceholder=!1}return Object.defineProperty(t.prototype,"child",{get:function(){return this.componentInstance},enumerable:!1,configurable:!0}),t}(),ut=function(t){void 0===t&&(t="");var e=new At;return e.text=t,e.isComment=!0,e};function lt(t){return new At(void 0,void 0,void 0,String(t))}function dt(t){var e=new At(t.tag,t.data,t.children&&t.children.slice(),t.text,t.elm,t.context,t.componentOptions,t.asyncFactory);return e.ns=t.ns,e.isStatic=t.isStatic,e.key=t.key,e.isComment=t.isComment,e.fnContext=t.fnContext,e.fnOptions=t.fnOptions,e.fnScopeId=t.fnScopeId,e.asyncMeta=t.asyncMeta,e.isCloned=!0,e}var ft=0,qt=[],ht=function(){for(var t=0;t0&&(Vt((o=Kt(o,"".concat(e||"","_").concat(n)))[0])&&Vt(a)&&(i[z]=lt(a.text+o[0].text),o.shift()),i.push.apply(i,o)):r(o)?Vt(a)?i[z]=lt(a.text+o):""!==o&&i.push(lt(o)):Vt(o)&&Vt(a)?i[z]=lt(a.text+o.text):(c(t._isVList)&&b(o.tag)&&M(o.key)&&b(e)&&(o.key="__vlist".concat(e,"_").concat(n,"__")),i.push(o)));return i}var Zt=1,Qt=2;function Jt(t,e,n,o,M,i){return(p(n)||r(n))&&(M=o,o=n,n=void 0),c(i)&&(M=Qt),function(t,e,n,o,M){if(b(n)&&b(n.__ob__))return ut();b(n)&&b(n.is)&&(e=n.is);if(!e)return ut();0;p(o)&&z(o[0])&&((n=n||{}).scopedSlots={default:o[0]},o.length=0);M===Qt?o=$t(o):M===Zt&&(o=function(t){for(var e=0;e0,c=e?!!e.$stable:!b,r=e&&e.$key;if(e){if(e._normalized)return e._normalized;if(c&&p&&p!==o&&r===p.$key&&!b&&!p.$hasNormal)return p;for(var z in M={},e)e[z]&&"$"!==z[0]&&(M[z]=he(t,n,z,e[z]))}else M={};for(var a in n)a in M||(M[a]=We(n,a));return e&&Object.isExtensible(e)&&(e._normalized=M),Y(M,"$stable",c),Y(M,"$key",r),Y(M,"$hasNormal",b),M}function he(t,e,n,o){var M=function(){var e=Ot;st(t);var n=arguments.length?o.apply(null,arguments):o({}),M=(n=n&&"object"==typeof n&&!p(n)?[n]:$t(n))&&n[0];return st(e),n&&(!M||1===n.length&&M.isComment&&!fe(M))?void 0:n};return o.proxy&&Object.defineProperty(e,n,{get:M,enumerable:!0,configurable:!0}),M}function We(t,e){return function(){return t[e]}}function ve(t){return{get attrs(){if(!t._attrsProxy){var e=t._attrsProxy={};Y(e,"_v_attr_proxy",!0),Re(e,t.$attrs,o,t,"$attrs")}return t._attrsProxy},get listeners(){t._listenersProxy||Re(t._listenersProxy={},t.$listeners,o,t,"$listeners");return t._listenersProxy},get slots(){return function(t){t._slotsProxy||ge(t._slotsProxy={},t.$scopedSlots);return t._slotsProxy}(t)},emit:E(t.$emit,t),expose:function(e){e&&Object.keys(e).forEach((function(n){return Ut(t,e,n)}))}}}function Re(t,e,n,o,p){var M=!1;for(var b in e)b in t?e[b]!==n[b]&&(M=!0):(M=!0,me(t,b,o,p));for(var b in t)b in e||(M=!0,delete t[b]);return M}function me(t,e,n,o){Object.defineProperty(t,e,{enumerable:!0,configurable:!0,get:function(){return n[o][e]}})}function ge(t,e){for(var n in e)t[n]=e[n];for(var n in t)n in e||delete t[n]}var Le,ye=null;function _e(t,e){return(t.__esModule||it&&"Module"===t[Symbol.toStringTag])&&(t=t.default),a(t)?e.extend(t):t}function Ne(t){if(p(t))for(var e=0;edocument.createEvent("Event").timeStamp&&(Ye=function(){return $e.now()})}var Ve=function(t,e){if(t.post){if(!e.post)return 1}else if(e.post)return-1;return t.id-e.id};function Ke(){var t,e;for(Ge=Ye(),Fe=!0,De.sort(Ve),He=0;HeHe&&De[n].id>t.id;)n--;De.splice(n+1,0,t)}else De.push(t);je||(je=!0,ln(Ke))}}var Qe="watcher";"".concat(Qe," callback"),"".concat(Qe," getter"),"".concat(Qe," cleanup");var Je;var tn=function(){function t(t){void 0===t&&(t=!1),this.detached=t,this.active=!0,this.effects=[],this.cleanups=[],this.parent=Je,!t&&Je&&(this.index=(Je.scopes||(Je.scopes=[])).push(this)-1)}return t.prototype.run=function(t){if(this.active){var e=Je;try{return Je=this,t()}finally{Je=e}}else 0},t.prototype.on=function(){Je=this},t.prototype.off=function(){Je=this.parent},t.prototype.stop=function(t){if(this.active){var e=void 0,n=void 0;for(e=0,n=this.effects.length;e-1)if(M&&!R(p,"default"))b=!1;else if(""===b||b===N(t)){var r=eo(String,p.type);(r<0||c-1:"string"==typeof t?t.split(",").indexOf(e)>-1:!!s(t)&&t.test(e)}function bo(t,e){var n=t.cache,o=t.keys,p=t._vnode;for(var M in n){var b=n[M];if(b){var c=b.name;c&&!e(c)&&co(n,M,o,p)}}}function co(t,e,n,o){var p=t[e];!p||o&&p.tag===o.tag||p.componentInstance.$destroy(),t[e]=null,W(n,e)}!function(t){t.prototype._init=function(t){var e=this;e._uid=Bn++,e._isVue=!0,e.__v_skip=!0,e._scope=new tn(!0),e._scope._vm=!0,t&&t._isComponent?function(t,e){var n=t.$options=Object.create(t.constructor.options),o=e._parentVnode;n.parent=e.parent,n._parentVnode=o;var p=o.componentOptions;n.propsData=p.propsData,n._parentListeners=p.listeners,n._renderChildren=p.children,n._componentTag=p.tag,e.render&&(n.render=e.render,n.staticRenderFns=e.staticRenderFns)}(e,t):e.$options=Vn(Cn(e.constructor),t||{},e),e._renderProxy=e,e._self=e,function(t){var e=t.$options,n=e.parent;if(n&&!e.abstract){for(;n.$options.abstract&&n.$parent;)n=n.$parent;n.$children.push(t)}t.$parent=n,t.$root=n?n.$root:t,t.$children=[],t.$refs={},t._provided=n?n._provided:Object.create(null),t._watcher=null,t._inactive=null,t._directInactive=!1,t._isMounted=!1,t._isDestroyed=!1,t._isBeingDestroyed=!1}(e),function(t){t._events=Object.create(null),t._hasHookEvent=!1;var e=t.$options._parentListeners;e&&Ce(t,e)}(e),function(t){t._vnode=null,t._staticTrees=null;var e=t.$options,n=t.$vnode=e._parentVnode,p=n&&n.context;t.$slots=le(e._renderChildren,p),t.$scopedSlots=n?qe(t.$parent,n.data.scopedSlots,t.$slots):o,t._c=function(e,n,o,p){return Jt(t,e,n,o,p,!1)},t.$createElement=function(e,n,o,p){return Jt(t,e,n,o,p,!0)};var M=n&&n.data;wt(t,"$attrs",M&&M.attrs||o,null,!0),wt(t,"$listeners",e._parentListeners||o,null,!0)}(e),Ie(e,"beforeCreate",void 0,!1),function(t){var e=Tn(t.$options.inject,t);e&&(Et(!1),Object.keys(e).forEach((function(n){wt(t,n,e[n])})),Et(!0))}(e),gn(e),function(t){var e=t.$options.provide;if(e){var n=z(e)?e.call(t):e;if(!a(n))return;for(var o=en(t),p=it?Reflect.ownKeys(n):Object.keys(n),M=0;M1?T(n):n;for(var o=T(arguments,1),p='event handler for "'.concat(t,'"'),M=0,b=n.length;MparseInt(this.max)&&co(e,n[0],n,this._vnode),this.vnodeToCache=null}}},created:function(){this.cache=Object.create(null),this.keys=[]},destroyed:function(){for(var t in this.cache)co(this.cache,t,this.keys)},mounted:function(){var t=this;this.cacheVNode(),this.$watch("include",(function(e){bo(t,(function(t){return Mo(e,t)}))})),this.$watch("exclude",(function(e){bo(t,(function(t){return!Mo(e,t)}))}))},updated:function(){this.cacheVNode()},render:function(){var t=this.$slots.default,e=Ne(t),n=e&&e.componentOptions;if(n){var o=po(n),p=this.include,M=this.exclude;if(p&&(!o||!Mo(p,o))||M&&o&&Mo(M,o))return e;var b=this.cache,c=this.keys,r=null==e.key?n.Ctor.cid+(n.tag?"::".concat(n.tag):""):e.key;b[r]?(e.componentInstance=b[r].componentInstance,W(c,r),c.push(r)):(this.vnodeToCache=e,this.keyToCache=r),e.data.keepAlive=!0}return e||t&&t[0]}},ao={KeepAlive:zo};!function(t){var e={get:function(){return F}};Object.defineProperty(t,"config",e),t.util={warn:Un,extend:B,mergeOptions:Vn,defineReactive:wt},t.set=St,t.delete=Xt,t.nextTick=ln,t.observable=function(t){return Ct(t),t},t.options=Object.create(null),U.forEach((function(e){t.options[e+"s"]=Object.create(null)})),t.options._base=t,B(t.options.components,ao),function(t){t.use=function(t){var e=this._installedPlugins||(this._installedPlugins=[]);if(e.indexOf(t)>-1)return this;var n=T(arguments,1);return n.unshift(this),z(t.install)?t.install.apply(t,n):z(t)&&t.apply(null,n),e.push(t),this}}(t),function(t){t.mixin=function(t){return this.options=Vn(this.options,t),this}}(t),oo(t),function(t){U.forEach((function(e){t[e]=function(t,n){return n?("component"===e&&O(n)&&(n.name=n.name||t,n=this.options._base.extend(n)),"directive"===e&&z(n)&&(n={bind:n,update:n}),this.options[e+"s"][t]=n,n):this.options[e+"s"][t]}}))}(t)}(no),Object.defineProperty(no.prototype,"$isServer",{get:ct}),Object.defineProperty(no.prototype,"$ssrContext",{get:function(){return this.$vnode&&this.$vnode.ssrContext}}),Object.defineProperty(no,"FunctionalRenderContext",{value:wn}),no.version="2.7.14";var io=f("style,class"),Oo=f("input,textarea,option,select,progress"),so=function(t,e,n){return"value"===n&&Oo(t)&&"button"!==e||"selected"===n&&"option"===t||"checked"===n&&"input"===t||"muted"===n&&"video"===t},Ao=f("contenteditable,draggable,spellcheck"),uo=f("events,caret,typing,plaintext-only"),lo=function(t,e){return vo(e)||"false"===e?"false":"contenteditable"===t&&uo(e)?e:"true"},fo=f("allowfullscreen,async,autofocus,autoplay,checked,compact,controls,declare,default,defaultchecked,defaultmuted,defaultselected,defer,disabled,enabled,formnovalidate,hidden,indeterminate,inert,ismap,itemscope,loop,multiple,muted,nohref,noresize,noshade,novalidate,nowrap,open,pauseonexit,readonly,required,reversed,scoped,seamless,selected,sortable,truespeed,typemustmatch,visible"),qo="http://www.w3.org/1999/xlink",ho=function(t){return":"===t.charAt(5)&&"xlink"===t.slice(0,5)},Wo=function(t){return ho(t)?t.slice(6,t.length):""},vo=function(t){return null==t||!1===t};function Ro(t){for(var e=t.data,n=t,o=t;b(o.componentInstance);)(o=o.componentInstance._vnode)&&o.data&&(e=mo(o.data,e));for(;b(n=n.parent);)n&&n.data&&(e=mo(e,n.data));return function(t,e){if(b(t)||b(e))return go(t,Lo(e));return""}(e.staticClass,e.class)}function mo(t,e){return{staticClass:go(t.staticClass,e.staticClass),class:b(t.class)?[t.class,e.class]:e.class}}function go(t,e){return t?e?t+" "+e:t:e||""}function Lo(t){return Array.isArray(t)?function(t){for(var e,n="",o=0,p=t.length;o-1?Qo(t,e,n):fo(e)?vo(n)?t.removeAttribute(e):(n="allowfullscreen"===e&&"EMBED"===t.tagName?"true":e,t.setAttribute(e,n)):Ao(e)?t.setAttribute(e,lo(e,n)):ho(e)?vo(n)?t.removeAttributeNS(qo,Wo(e)):t.setAttributeNS(qo,e,n):Qo(t,e,n)}function Qo(t,e,n){if(vo(n))t.removeAttribute(e);else{if(Q&&!J&&"TEXTAREA"===t.tagName&&"placeholder"===e&&""!==n&&!t.__ieph){var o=function(e){e.stopImmediatePropagation(),t.removeEventListener("input",o)};t.addEventListener("input",o),t.__ieph=!0}t.setAttribute(e,n)}}var Jo={create:Ko,update:Ko};function tp(t,e){var n=e.elm,o=e.data,p=t.data;if(!(M(o.staticClass)&&M(o.class)&&(M(p)||M(p.staticClass)&&M(p.class)))){var c=Ro(e),r=n._transitionClasses;b(r)&&(c=go(c,Lo(r))),c!==n._prevClass&&(n.setAttribute("class",c),n._prevClass=c)}}var ep,np,op,pp,Mp,bp,cp={create:tp,update:tp},rp=/[\w).+\-_$\]]/;function zp(t){var e,n,o,p,M,b=!1,c=!1,r=!1,z=!1,a=0,i=0,O=0,s=0;for(o=0;o=0&&" "===(u=t.charAt(A));A--);u&&rp.test(u)||(z=!0)}}else void 0===p?(s=o+1,p=t.slice(0,o).trim()):l();function l(){(M||(M=[])).push(t.slice(s,o).trim()),s=o+1}if(void 0===p?p=t.slice(0,o).trim():0!==s&&l(),M)for(o=0;o-1?{exp:t.slice(0,pp),key:'"'+t.slice(pp+1)+'"'}:{exp:t,key:null};np=t,pp=Mp=bp=0;for(;!Lp();)yp(op=gp())?Np(op):91===op&&_p(op);return{exp:t.slice(0,Mp),key:t.slice(Mp+1,bp)}}(t);return null===n.key?"".concat(t,"=").concat(e):"$set(".concat(n.exp,", ").concat(n.key,", ").concat(e,")")}function gp(){return np.charCodeAt(++pp)}function Lp(){return pp>=ep}function yp(t){return 34===t||39===t}function _p(t){var e=1;for(Mp=pp;!Lp();)if(yp(t=gp()))Np(t);else if(91===t&&e++,93===t&&e--,0===e){bp=pp;break}}function Np(t){for(var e=t;!Lp()&&(t=gp())!==e;);}var Ep,Tp="__r",Bp="__c";function Cp(t,e,n){var o=Ep;return function p(){null!==e.apply(null,arguments)&&Xp(t,p,n,o)}}var wp=cn&&!(ot&&Number(ot[1])<=53);function Sp(t,e,n,o){if(wp){var p=Ge,M=e;e=M._wrapper=function(t){if(t.target===t.currentTarget||t.timeStamp>=p||t.timeStamp<=0||t.target.ownerDocument!==document)return M.apply(this,arguments)}}Ep.addEventListener(t,e,Mt?{capture:n,passive:o}:n)}function Xp(t,e,n,o){(o||Ep).removeEventListener(t,e._wrapper||e,n)}function xp(t,e){if(!M(t.data.on)||!M(e.data.on)){var n=e.data.on||{},o=t.data.on||{};Ep=e.elm||t.elm,function(t){if(b(t[Tp])){var e=Q?"change":"input";t[e]=[].concat(t[Tp],t[e]||[]),delete t[Tp]}b(t[Bp])&&(t.change=[].concat(t[Bp],t.change||[]),delete t[Bp])}(n),Ht(n,o,Sp,Xp,Cp,e.context),Ep=void 0}}var kp,Ip={create:xp,update:xp,destroy:function(t){return xp(t,Io)}};function Dp(t,e){if(!M(t.data.domProps)||!M(e.data.domProps)){var n,o,p=e.elm,r=t.data.domProps||{},z=e.data.domProps||{};for(n in(b(z.__ob__)||c(z._v_attr_proxy))&&(z=e.data.domProps=B({},z)),r)n in z||(p[n]="");for(n in z){if(o=z[n],"textContent"===n||"innerHTML"===n){if(e.children&&(e.children.length=0),o===r[n])continue;1===p.childNodes.length&&p.removeChild(p.childNodes[0])}if("value"===n&&"PROGRESS"!==p.tagName){p._value=o;var a=M(o)?"":String(o);Pp(p,a)&&(p.value=a)}else if("innerHTML"===n&&No(p.tagName)&&M(p.innerHTML)){(kp=kp||document.createElement("div")).innerHTML="".concat(o,"");for(var i=kp.firstChild;p.firstChild;)p.removeChild(p.firstChild);for(;i.firstChild;)p.appendChild(i.firstChild)}else if(o!==r[n])try{p[n]=o}catch(t){}}}}function Pp(t,e){return!t.composing&&("OPTION"===t.tagName||function(t,e){var n=!0;try{n=document.activeElement!==t}catch(t){}return n&&t.value!==e}(t,e)||function(t,e){var n=t.value,o=t._vModifiers;if(b(o)){if(o.number)return d(n)!==d(e);if(o.trim)return n.trim()!==e.trim()}return n!==e}(t,e))}var Up={create:Dp,update:Dp},jp=m((function(t){var e={},n=/:(.+)/;return t.split(/;(?![^(]*\))/g).forEach((function(t){if(t){var o=t.split(n);o.length>1&&(e[o[0].trim()]=o[1].trim())}})),e}));function Fp(t){var e=Hp(t.style);return t.staticStyle?B(t.staticStyle,e):e}function Hp(t){return Array.isArray(t)?C(t):"string"==typeof t?jp(t):t}var Gp,Yp=/^--/,$p=/\s*!important$/,Vp=function(t,e,n){if(Yp.test(e))t.style.setProperty(e,n);else if($p.test(n))t.style.setProperty(N(e),n.replace($p,""),"important");else{var o=Zp(e);if(Array.isArray(n))for(var p=0,M=n.length;p-1?e.split(tM).forEach((function(e){return t.classList.add(e)})):t.classList.add(e);else{var n=" ".concat(t.getAttribute("class")||""," ");n.indexOf(" "+e+" ")<0&&t.setAttribute("class",(n+e).trim())}}function nM(t,e){if(e&&(e=e.trim()))if(t.classList)e.indexOf(" ")>-1?e.split(tM).forEach((function(e){return t.classList.remove(e)})):t.classList.remove(e),t.classList.length||t.removeAttribute("class");else{for(var n=" ".concat(t.getAttribute("class")||""," "),o=" "+e+" ";n.indexOf(o)>=0;)n=n.replace(o," ");(n=n.trim())?t.setAttribute("class",n):t.removeAttribute("class")}}function oM(t){if(t){if("object"==typeof t){var e={};return!1!==t.css&&B(e,pM(t.name||"v")),B(e,t),e}return"string"==typeof t?pM(t):void 0}}var pM=m((function(t){return{enterClass:"".concat(t,"-enter"),enterToClass:"".concat(t,"-enter-to"),enterActiveClass:"".concat(t,"-enter-active"),leaveClass:"".concat(t,"-leave"),leaveToClass:"".concat(t,"-leave-to"),leaveActiveClass:"".concat(t,"-leave-active")}})),MM=K&&!J,bM="transition",cM="animation",rM="transition",zM="transitionend",aM="animation",iM="animationend";MM&&(void 0===window.ontransitionend&&void 0!==window.onwebkittransitionend&&(rM="WebkitTransition",zM="webkitTransitionEnd"),void 0===window.onanimationend&&void 0!==window.onwebkitanimationend&&(aM="WebkitAnimation",iM="webkitAnimationEnd"));var OM=K?window.requestAnimationFrame?window.requestAnimationFrame.bind(window):setTimeout:function(t){return t()};function sM(t){OM((function(){OM(t)}))}function AM(t,e){var n=t._transitionClasses||(t._transitionClasses=[]);n.indexOf(e)<0&&(n.push(e),eM(t,e))}function uM(t,e){t._transitionClasses&&W(t._transitionClasses,e),nM(t,e)}function lM(t,e,n){var o=fM(t,e),p=o.type,M=o.timeout,b=o.propCount;if(!p)return n();var c=p===bM?zM:iM,r=0,z=function(){t.removeEventListener(c,a),n()},a=function(e){e.target===t&&++r>=b&&z()};setTimeout((function(){r0&&(n=bM,a=b,i=M.length):e===cM?z>0&&(n=cM,a=z,i=r.length):i=(n=(a=Math.max(b,z))>0?b>z?bM:cM:null)?n===bM?M.length:r.length:0,{type:n,timeout:a,propCount:i,hasTransform:n===bM&&dM.test(o[rM+"Property"])}}function qM(t,e){for(;t.length1}function gM(t,e){!0!==e.data.show&&WM(e)}var LM=function(t){var e,n,o={},z=t.modules,a=t.nodeOps;for(e=0;eA?h(t,M(n[d+1])?null:n[d+1].elm,n,s,d,o):s>d&&v(e,i,A)}(i,u,d,n,z):b(d)?(b(t.text)&&a.setTextContent(i,""),h(i,null,d,0,d.length-1,n)):b(u)?v(u,0,u.length-1):b(t.text)&&a.setTextContent(i,""):t.text!==e.text&&a.setTextContent(i,e.text),b(A)&&b(s=A.hook)&&b(s=s.postpatch)&&s(t,e)}}}function L(t,e,n){if(c(n)&&b(t.parent))t.parent.data.pendingInsert=e;else for(var o=0;o-1,b.selected!==M&&(b.selected=M);else if(x(TM(b),o))return void(t.selectedIndex!==c&&(t.selectedIndex=c));p||(t.selectedIndex=-1)}}function EM(t,e){return e.every((function(e){return!x(e,t)}))}function TM(t){return"_value"in t?t._value:t.value}function BM(t){t.target.composing=!0}function CM(t){t.target.composing&&(t.target.composing=!1,wM(t.target,"input"))}function wM(t,e){var n=document.createEvent("HTMLEvents");n.initEvent(e,!0,!0),t.dispatchEvent(n)}function SM(t){return!t.componentInstance||t.data&&t.data.transition?t:SM(t.componentInstance._vnode)}var XM={bind:function(t,e,n){var o=e.value,p=(n=SM(n)).data&&n.data.transition,M=t.__vOriginalDisplay="none"===t.style.display?"":t.style.display;o&&p?(n.data.show=!0,WM(n,(function(){t.style.display=M}))):t.style.display=o?M:"none"},update:function(t,e,n){var o=e.value;!o!=!e.oldValue&&((n=SM(n)).data&&n.data.transition?(n.data.show=!0,o?WM(n,(function(){t.style.display=t.__vOriginalDisplay})):vM(n,(function(){t.style.display="none"}))):t.style.display=o?t.__vOriginalDisplay:"none")},unbind:function(t,e,n,o,p){p||(t.style.display=t.__vOriginalDisplay)}},xM={model:yM,show:XM},kM={name:String,appear:Boolean,css:Boolean,mode:String,type:String,enterClass:String,leaveClass:String,enterToClass:String,leaveToClass:String,enterActiveClass:String,leaveActiveClass:String,appearClass:String,appearActiveClass:String,appearToClass:String,duration:[Number,String,Object]};function IM(t){var e=t&&t.componentOptions;return e&&e.Ctor.options.abstract?IM(Ne(e.children)):t}function DM(t){var e={},n=t.$options;for(var o in n.propsData)e[o]=t[o];var p=n._parentListeners;for(var o in p)e[L(o)]=p[o];return e}function PM(t,e){if(/\d-keep-alive$/.test(e.tag))return t("keep-alive",{props:e.componentOptions.propsData})}var UM=function(t){return t.tag||fe(t)},jM=function(t){return"show"===t.name},FM={name:"transition",props:kM,abstract:!0,render:function(t){var e=this,n=this.$slots.default;if(n&&(n=n.filter(UM)).length){0;var o=this.mode;0;var p=n[0];if(function(t){for(;t=t.parent;)if(t.data.transition)return!0}(this.$vnode))return p;var M=IM(p);if(!M)return p;if(this._leaving)return PM(t,p);var b="__transition-".concat(this._uid,"-");M.key=null==M.key?M.isComment?b+"comment":b+M.tag:r(M.key)?0===String(M.key).indexOf(b)?M.key:b+M.key:M.key;var c=(M.data||(M.data={})).transition=DM(this),z=this._vnode,a=IM(z);if(M.data.directives&&M.data.directives.some(jM)&&(M.data.show=!0),a&&a.data&&!function(t,e){return e.key===t.key&&e.tag===t.tag}(M,a)&&!fe(a)&&(!a.componentInstance||!a.componentInstance._vnode.isComment)){var i=a.data.transition=B({},c);if("out-in"===o)return this._leaving=!0,Gt(i,"afterLeave",(function(){e._leaving=!1,e.$forceUpdate()})),PM(t,p);if("in-out"===o){if(fe(M))return z;var O,s=function(){O()};Gt(c,"afterEnter",s),Gt(c,"enterCancelled",s),Gt(i,"delayLeave",(function(t){O=t}))}}return p}}},HM=B({tag:String,moveClass:String},kM);delete HM.mode;var GM={props:HM,beforeMount:function(){var t=this,e=this._update;this._update=function(n,o){var p=Se(t);t.__patch__(t._vnode,t.kept,!1,!0),t._vnode=t.kept,p(),e.call(t,n,o)}},render:function(t){for(var e=this.tag||this.$vnode.data.tag||"span",n=Object.create(null),o=this.prevChildren=this.children,p=this.$slots.default||[],M=this.children=[],b=DM(this),c=0;c-1?Bo[t]=e.constructor===window.HTMLUnknownElement||e.constructor===window.HTMLElement:Bo[t]=/HTMLUnknownElement/.test(e.toString())},B(no.options.directives,xM),B(no.options.components,KM),no.prototype.__patch__=K?LM:w,no.prototype.$mount=function(t,e){return function(t,e,n){var o;t.$el=e,t.$options.render||(t.$options.render=ut),Ie(t,"beforeMount"),o=function(){t._update(t._render(),n)},new vn(t,o,w,{before:function(){t._isMounted&&!t._isDestroyed&&Ie(t,"beforeUpdate")}},!0),n=!1;var p=t._preWatchers;if(p)for(var M=0;M\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,rb=/^\s*((?:v-[\w-]+:|@|:|#)\[[^=]+?\][^\s"'<>\/=]*)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/,zb="[a-zA-Z_][\\-\\.0-9_a-zA-Z".concat(H.source,"]*"),ab="((?:".concat(zb,"\\:)?").concat(zb,")"),ib=new RegExp("^<".concat(ab)),Ob=/^\s*(\/?)>/,sb=new RegExp("^<\\/".concat(ab,"[^>]*>")),Ab=/^]+>/i,ub=/^",""":'"',"&":"&"," ":"\n"," ":"\t","'":"'"},hb=/&(?:lt|gt|quot|amp|#39);/g,Wb=/&(?:lt|gt|quot|amp|#39|#10|#9);/g,vb=f("pre,textarea",!0),Rb=function(t,e){return t&&vb(t)&&"\n"===e[0]};function mb(t,e){var n=e?Wb:hb;return t.replace(n,(function(t){return qb[t]}))}function gb(t,e){for(var n,o,p=[],M=e.expectHTML,b=e.isUnaryTag||S,c=e.canBeLeftOpenTag||S,r=0,z=function(){if(n=t,o&&db(o)){var z=0,O=o.toLowerCase(),s=fb[O]||(fb[O]=new RegExp("([\\s\\S]*?)(]*>)","i"));v=t.replace(s,(function(t,n,o){return z=o.length,db(O)||"noscript"===O||(n=n.replace(//g,"$1").replace(//g,"$1")),Rb(O,n)&&(n=n.slice(1)),e.chars&&e.chars(n),""}));r+=t.length-v.length,t=v,i(O,r-z,r)}else{var A=t.indexOf("<");if(0===A){if(ub.test(t)){var u=t.indexOf("--\x3e");if(u>=0)return e.shouldKeepComment&&e.comment&&e.comment(t.substring(4,u),r,r+u+3),a(u+3),"continue"}if(lb.test(t)){var l=t.indexOf("]>");if(l>=0)return a(l+2),"continue"}var d=t.match(Ab);if(d)return a(d[0].length),"continue";var f=t.match(sb);if(f){var q=r;return a(f[0].length),i(f[1],q,r),"continue"}var h=function(){var e=t.match(ib);if(e){var n={tagName:e[1],attrs:[],start:r};a(e[0].length);for(var o=void 0,p=void 0;!(o=t.match(Ob))&&(p=t.match(rb)||t.match(cb));)p.start=r,a(p[0].length),p.end=r,n.attrs.push(p);if(o)return n.unarySlash=o[1],a(o[0].length),n.end=r,n}}();if(h)return function(t){var n=t.tagName,r=t.unarySlash;M&&("p"===o&&bb(n)&&i(o),c(n)&&o===n&&i(n));for(var z=b(n)||!!r,a=t.attrs.length,O=new Array(a),s=0;s=0){for(v=t.slice(A);!(sb.test(v)||ib.test(v)||ub.test(v)||lb.test(v)||(R=v.indexOf("<",1))<0);)A+=R,v=t.slice(A);W=t.substring(0,A)}A<0&&(W=t),W&&a(W.length),e.chars&&W&&e.chars(W,r-W.length,r)}if(t===n)return e.chars&&e.chars(t),"break"};t;){if("break"===z())break}function a(e){r+=e,t=t.substring(e)}function i(t,n,M){var b,c;if(null==n&&(n=r),null==M&&(M=r),t)for(c=t.toLowerCase(),b=p.length-1;b>=0&&p[b].lowerCasedTag!==c;b--);else b=0;if(b>=0){for(var z=p.length-1;z>=b;z--)e.end&&e.end(p[z].tag,n,M);p.length=b,o=b&&p[b-1].tag}else"br"===c?e.start&&e.start(t,[],!0,n,M):"p"===c&&(e.start&&e.start(t,[],!1,n,M),e.end&&e.end(t,n,M))}i()}var Lb,yb,_b,Nb,Eb,Tb,Bb,Cb,wb=/^@|^v-on:/,Sb=/^v-|^@|^:|^#/,Xb=/([\s\S]*?)\s+(?:in|of)\s+([\s\S]*)/,xb=/,([^,\}\]]*)(?:,([^,\}\]]*))?$/,kb=/^\(|\)$/g,Ib=/^\[.*\]$/,Db=/:(.*)$/,Pb=/^:|^\.|^v-bind:/,Ub=/\.[^.\]]+(?=[^\]]*$)/g,jb=/^v-slot(:|$)|^#/,Fb=/[\r\n]/,Hb=/[ \f\t\r\n]+/g,Gb=m(ob),Yb="_empty_";function $b(t,e,n){return{type:1,tag:t,attrsList:e,attrsMap:ec(e),rawAttrsMap:{},parent:n,children:[]}}function Vb(t,e){Lb=e.warn||ip,Tb=e.isPreTag||S,Bb=e.mustUseProp||S,Cb=e.getTagNamespace||S;var n=e.isReservedTag||S;(function(t){return!(!(t.component||t.attrsMap[":is"]||t.attrsMap["v-bind:is"])&&(t.attrsMap.is?n(t.attrsMap.is):n(t.tag)))}),_b=Op(e.modules,"transformNode"),Nb=Op(e.modules,"preTransformNode"),Eb=Op(e.modules,"postTransformNode"),yb=e.delimiters;var o,p,M=[],b=!1!==e.preserveWhitespace,c=e.whitespace,r=!1,z=!1;function a(t){if(i(t),r||t.processed||(t=Kb(t,e)),M.length||t===o||o.if&&(t.elseif||t.else)&&Qb(o,{exp:t.elseif,block:t}),p&&!t.forbidden)if(t.elseif||t.else)b=t,c=function(t){for(var e=t.length;e--;){if(1===t[e].type)return t[e];t.pop()}}(p.children),c&&c.if&&Qb(c,{exp:b.elseif,block:b});else{if(t.slotScope){var n=t.slotTarget||'"default"';(p.scopedSlots||(p.scopedSlots={}))[n]=t}p.children.push(t),t.parent=p}var b,c;t.children=t.children.filter((function(t){return!t.slotScope})),i(t),t.pre&&(r=!1),Tb(t.tag)&&(z=!1);for(var a=0;ar&&(c.push(M=t.slice(r,p)),b.push(JSON.stringify(M)));var z=zp(o[1].trim());b.push("_s(".concat(z,")")),c.push({"@binding":z}),r=p+o[0].length}return r-1")+("true"===M?":(".concat(e,")"):":_q(".concat(e,",").concat(M,")"))),fp(t,"change","var $$a=".concat(e,",")+"$$el=$event.target,"+"$$c=$$el.checked?(".concat(M,"):(").concat(b,");")+"if(Array.isArray($$a)){"+"var $$v=".concat(o?"_n("+p+")":p,",")+"$$i=_i($$a,$$v);"+"if($$el.checked){$$i<0&&(".concat(mp(e,"$$a.concat([$$v])"),")}")+"else{$$i>-1&&(".concat(mp(e,"$$a.slice(0,$$i).concat($$a.slice($$i+1))"),")}")+"}else{".concat(mp(e,"$$c"),"}"),null,!0)}(t,o,p);else if("input"===M&&"radio"===b)!function(t,e,n){var o=n&&n.number,p=qp(t,"value")||"null";p=o?"_n(".concat(p,")"):p,sp(t,"checked","_q(".concat(e,",").concat(p,")")),fp(t,"change",mp(e,p),null,!0)}(t,o,p);else if("input"===M||"textarea"===M)!function(t,e,n){var o=t.attrsMap.type;0;var p=n||{},M=p.lazy,b=p.number,c=p.trim,r=!M&&"range"!==o,z=M?"change":"range"===o?Tp:"input",a="$event.target.value";c&&(a="$event.target.value.trim()");b&&(a="_n(".concat(a,")"));var i=mp(e,a);r&&(i="if($event.target.composing)return;".concat(i));sp(t,"value","(".concat(e,")")),fp(t,z,i,null,!0),(c||b)&&fp(t,"blur","$forceUpdate()")}(t,o,p);else{if(!F.isReservedTag(M))return Rp(t,o,p),!1}return!0},text:function(t,e){e.value&&sp(t,"textContent","_s(".concat(e.value,")"),e)},html:function(t,e){e.value&&sp(t,"innerHTML","_s(".concat(e.value,")"),e)}},ac={expectHTML:!0,modules:bc,directives:zc,isPreTag:function(t){return"pre"===t},isUnaryTag:pb,mustUseProp:so,canBeLeftOpenTag:Mb,isReservedTag:Eo,getTagNamespace:To,staticKeys:function(t){return t.reduce((function(t,e){return t.concat(e.staticKeys||[])}),[]).join(",")}(bc)},ic=m((function(t){return f("type,tag,attrsList,attrsMap,plain,parent,children,attrs,start,end,rawAttrsMap"+(t?","+t:""))}));function Oc(t,e){t&&(cc=ic(e.staticKeys||""),rc=e.isReservedTag||S,sc(t),Ac(t,!1))}function sc(t){if(t.static=function(t){if(2===t.type)return!1;if(3===t.type)return!0;return!(!t.pre&&(t.hasBindings||t.if||t.for||q(t.tag)||!rc(t.tag)||function(t){for(;t.parent;){if("template"!==(t=t.parent).tag)return!1;if(t.for)return!0}return!1}(t)||!Object.keys(t).every(cc)))}(t),1===t.type){if(!rc(t.tag)&&"slot"!==t.tag&&null==t.attrsMap["inline-template"])return;for(var e=0,n=t.children.length;e|^function(?:\s+[\w$]+)?\s*\(/,lc=/\([^)]*?\);*$/,dc=/^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*|\['[^']*?']|\["[^"]*?"]|\[\d+]|\[[A-Za-z_$][\w$]*])*$/,fc={esc:27,tab:9,enter:13,space:32,up:38,left:37,right:39,down:40,delete:[8,46]},qc={esc:["Esc","Escape"],tab:"Tab",enter:"Enter",space:[" ","Spacebar"],up:["Up","ArrowUp"],left:["Left","ArrowLeft"],right:["Right","ArrowRight"],down:["Down","ArrowDown"],delete:["Backspace","Delete","Del"]},hc=function(t){return"if(".concat(t,")return null;")},Wc={stop:"$event.stopPropagation();",prevent:"$event.preventDefault();",self:hc("$event.target !== $event.currentTarget"),ctrl:hc("!$event.ctrlKey"),shift:hc("!$event.shiftKey"),alt:hc("!$event.altKey"),meta:hc("!$event.metaKey"),left:hc("'button' in $event && $event.button !== 0"),middle:hc("'button' in $event && $event.button !== 1"),right:hc("'button' in $event && $event.button !== 2")};function vc(t,e){var n=e?"nativeOn:":"on:",o="",p="";for(var M in t){var b=Rc(t[M]);t[M]&&t[M].dynamic?p+="".concat(M,",").concat(b,","):o+='"'.concat(M,'":').concat(b,",")}return o="{".concat(o.slice(0,-1),"}"),p?n+"_d(".concat(o,",[").concat(p.slice(0,-1),"])"):n+o}function Rc(t){if(!t)return"function(){}";if(Array.isArray(t))return"[".concat(t.map((function(t){return Rc(t)})).join(","),"]");var e=dc.test(t.value),n=uc.test(t.value),o=dc.test(t.value.replace(lc,""));if(t.modifiers){var p="",M="",b=[],c=function(e){if(Wc[e])M+=Wc[e],fc[e]&&b.push(e);else if("exact"===e){var n=t.modifiers;M+=hc(["ctrl","shift","alt","meta"].filter((function(t){return!n[t]})).map((function(t){return"$event.".concat(t,"Key")})).join("||"))}else b.push(e)};for(var r in t.modifiers)c(r);b.length&&(p+=function(t){return"if(!$event.type.indexOf('key')&&"+"".concat(t.map(mc).join("&&"),")return null;")}(b)),M&&(p+=M);var z=e?"return ".concat(t.value,".apply(null, arguments)"):n?"return (".concat(t.value,").apply(null, arguments)"):o?"return ".concat(t.value):t.value;return"function($event){".concat(p).concat(z,"}")}return e||n?t.value:"function($event){".concat(o?"return ".concat(t.value):t.value,"}")}function mc(t){var e=parseInt(t,10);if(e)return"$event.keyCode!==".concat(e);var n=fc[t],o=qc[t];return"_k($event.keyCode,"+"".concat(JSON.stringify(t),",")+"".concat(JSON.stringify(n),",")+"$event.key,"+"".concat(JSON.stringify(o))+")"}var gc={on:function(t,e){t.wrapListeners=function(t){return"_g(".concat(t,",").concat(e.value,")")}},bind:function(t,e){t.wrapData=function(n){return"_b(".concat(n,",'").concat(t.tag,"',").concat(e.value,",").concat(e.modifiers&&e.modifiers.prop?"true":"false").concat(e.modifiers&&e.modifiers.sync?",true":"",")")}},cloak:w},Lc=function(t){this.options=t,this.warn=t.warn||ip,this.transforms=Op(t.modules,"transformCode"),this.dataGenFns=Op(t.modules,"genData"),this.directives=B(B({},gc),t.directives);var e=t.isReservedTag||S;this.maybeComponent=function(t){return!!t.component||!e(t.tag)},this.onceId=0,this.staticRenderFns=[],this.pre=!1};function yc(t,e){var n=new Lc(e),o=t?"script"===t.tag?"null":_c(t,n):'_c("div")';return{render:"with(this){return ".concat(o,"}"),staticRenderFns:n.staticRenderFns}}function _c(t,e){if(t.parent&&(t.pre=t.pre||t.parent.pre),t.staticRoot&&!t.staticProcessed)return Nc(t,e);if(t.once&&!t.onceProcessed)return Ec(t,e);if(t.for&&!t.forProcessed)return Cc(t,e);if(t.if&&!t.ifProcessed)return Tc(t,e);if("template"!==t.tag||t.slotTarget||e.pre){if("slot"===t.tag)return function(t,e){var n=t.slotName||'"default"',o=xc(t,e),p="_t(".concat(n).concat(o?",function(){return ".concat(o,"}"):""),M=t.attrs||t.dynamicAttrs?Dc((t.attrs||[]).concat(t.dynamicAttrs||[]).map((function(t){return{name:L(t.name),value:t.value,dynamic:t.dynamic}}))):null,b=t.attrsMap["v-bind"];!M&&!b||o||(p+=",null");M&&(p+=",".concat(M));b&&(p+="".concat(M?"":",null",",").concat(b));return p+")"}(t,e);var n=void 0;if(t.component)n=function(t,e,n){var o=e.inlineTemplate?null:xc(e,n,!0);return"_c(".concat(t,",").concat(wc(e,n)).concat(o?",".concat(o):"",")")}(t.component,t,e);else{var o=void 0,p=e.maybeComponent(t);(!t.plain||t.pre&&p)&&(o=wc(t,e));var M=void 0,b=e.options.bindings;p&&b&&!1!==b.__isScriptSetup&&(M=function(t,e){var n=L(e),o=y(n),p=function(p){return t[e]===p?e:t[n]===p?n:t[o]===p?o:void 0},M=p("setup-const")||p("setup-reactive-const");if(M)return M;var b=p("setup-let")||p("setup-ref")||p("setup-maybe-ref");if(b)return b}(b,t.tag)),M||(M="'".concat(t.tag,"'"));var c=t.inlineTemplate?null:xc(t,e,!0);n="_c(".concat(M).concat(o?",".concat(o):"").concat(c?",".concat(c):"",")")}for(var r=0;r>>0}(b)):"",")")}(t,t.scopedSlots,e),",")),t.model&&(n+="model:{value:".concat(t.model.value,",callback:").concat(t.model.callback,",expression:").concat(t.model.expression,"},")),t.inlineTemplate){var M=function(t,e){var n=t.children[0];0;if(n&&1===n.type){var o=yc(n,e.options);return"inlineTemplate:{render:function(){".concat(o.render,"},staticRenderFns:[").concat(o.staticRenderFns.map((function(t){return"function(){".concat(t,"}")})).join(","),"]}")}}(t,e);M&&(n+="".concat(M,","))}return n=n.replace(/,$/,"")+"}",t.dynamicAttrs&&(n="_b(".concat(n,',"').concat(t.tag,'",').concat(Dc(t.dynamicAttrs),")")),t.wrapData&&(n=t.wrapData(n)),t.wrapListeners&&(n=t.wrapListeners(n)),n}function Sc(t){return 1===t.type&&("slot"===t.tag||t.children.some(Sc))}function Xc(t,e){var n=t.attrsMap["slot-scope"];if(t.if&&!t.ifProcessed&&!n)return Tc(t,e,Xc,"null");if(t.for&&!t.forProcessed)return Cc(t,e,Xc);var o=t.slotScope===Yb?"":String(t.slotScope),p="function(".concat(o,"){")+"return ".concat("template"===t.tag?t.if&&n?"(".concat(t.if,")?").concat(xc(t,e)||"undefined",":undefined"):xc(t,e)||"undefined":_c(t,e),"}"),M=o?"":",proxy:true";return"{key:".concat(t.slotTarget||'"default"',",fn:").concat(p).concat(M,"}")}function xc(t,e,n,o,p){var M=t.children;if(M.length){var b=M[0];if(1===M.length&&b.for&&"template"!==b.tag&&"slot"!==b.tag){var c=n?e.maybeComponent(b)?",1":",0":"";return"".concat((o||_c)(b,e)).concat(c)}var r=n?function(t,e){for(var n=0,o=0;o':'
',Hc.innerHTML.indexOf(" ")>0}var Vc=!!K&&$c(!1),Kc=!!K&&$c(!0),Zc=m((function(t){var e=wo(t);return e&&e.innerHTML})),Qc=no.prototype.$mount;no.prototype.$mount=function(t,e){if((t=t&&wo(t))===document.body||t===document.documentElement)return this;var n=this.$options;if(!n.render){var o=n.template;if(o)if("string"==typeof o)"#"===o.charAt(0)&&(o=Zc(o));else{if(!o.nodeType)return this;o=o.innerHTML}else t&&(o=function(t){if(t.outerHTML)return t.outerHTML;var e=document.createElement("div");return e.appendChild(t.cloneNode(!0)),e.innerHTML}(t));if(o){0;var p=Yc(o,{outputSourceRange:!1,shouldDecodeNewlines:Vc,shouldDecodeNewlinesForHref:Kc,delimiters:n.delimiters,comments:n.comments},this),M=p.render,b=p.staticRenderFns;n.render=M,n.staticRenderFns=b}}return Qc.call(this,t,e)},no.compile=Yc;var Jc=n(6486),tr=n.n(Jc),er=n(8),nr=n.n(er);const or={computed:{Telescope:function(t){function e(){return t.apply(this,arguments)}return e.toString=function(){return t.toString()},e}((function(){return Telescope}))},methods:{timeAgo:function(t){nr().updateLocale("en",{relativeTime:{future:"in %s",past:"%s ago",s:function(t){return t+"s ago"},ss:"%ds ago",m:"1m ago",mm:"%dm ago",h:"1h ago",hh:"%dh ago",d:"1d ago",dd:"%dd ago",M:"a month ago",MM:"%d months ago",y:"a year ago",yy:"%d years ago"}});var e=nr()().diff(t,"seconds"),n=nr()("2018-01-01").startOf("day").seconds(e);return e>300?nr()(t).fromNow(!0):e<60?n.format("s")+"s ago":n.format("m:ss")+"m ago"},localTime:function(t){return nr()(t).local().format("MMMM Do YYYY, h:mm:ss A")},truncate:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:70;return tr().truncate(t,{length:e,separator:/,? +/})},debouncer:tr().debounce((function(t){return t()}),500),alertError:function(t){this.$root.alert.type="error",this.$root.alert.autoClose=!1,this.$root.alert.message=t},alertSuccess:function(t,e){this.$root.alert.type="success",this.$root.alert.autoClose=e,this.$root.alert.message=t},alertConfirm:function(t,e,n){this.$root.alert.type="confirmation",this.$root.alert.autoClose=!1,this.$root.alert.message=t,this.$root.alert.confirmationProceed=e,this.$root.alert.confirmationCancel=n}}};var pr=n(7066);const Mr=[{path:"/",redirect:"/requests"},{path:"/mail/:id",name:"mail-preview",component:n(7776).Z},{path:"/mail",name:"mail",component:n(4456).Z},{path:"/exceptions/:id",name:"exception-preview",component:n(8882).Z},{path:"/exceptions",name:"exceptions",component:n(5323).Z},{path:"/dumps",name:"dumps",component:n(7208).Z},{path:"/logs/:id",name:"log-preview",component:n(8360).Z},{path:"/logs",name:"logs",component:n(1929).Z},{path:"/notifications/:id",name:"notification-preview",component:n(3590).Z},{path:"/notifications",name:"notifications",component:n(624).Z},{path:"/jobs/:id",name:"job-preview",component:n(4142).Z},{path:"/jobs",name:"jobs",component:n(558).Z},{path:"/batches/:id",name:"batch-preview",component:n(8159).Z},{path:"/batches",name:"batches",component:n(7374).Z},{path:"/events/:id",name:"event-preview",component:n(5701).Z},{path:"/events",name:"events",component:n(8814).Z},{path:"/cache/:id",name:"cache-preview",component:n(2246).Z},{path:"/cache",name:"cache",component:n(896).Z},{path:"/queries/:id",name:"query-preview",component:n(3992).Z},{path:"/queries",name:"queries",component:n(4652).Z},{path:"/models/:id",name:"model-preview",component:n(706).Z},{path:"/models",name:"models",component:n(1556).Z},{path:"/requests/:id",name:"request-preview",component:n(1619).Z},{path:"/requests",name:"requests",component:n(9751).Z},{path:"/commands/:id",name:"command-preview",component:n(1241).Z},{path:"/commands",name:"commands",component:n(7210).Z},{path:"/schedule/:id",name:"schedule-preview",component:n(4622).Z},{path:"/schedule",name:"schedule",component:n(8244).Z},{path:"/redis/:id",name:"redis-preview",component:n(5799).Z},{path:"/redis",name:"redis",component:n(7837).Z},{path:"/monitored-tags",name:"monitored-tags",component:n(5505).Z},{path:"/gates/:id",name:"gate-preview",component:n(6581).Z},{path:"/gates",name:"gates",component:n(4840).Z},{path:"/views/:id",name:"view-preview",component:n(6968).Z},{path:"/views",name:"views",component:n(3395).Z},{path:"/client-requests/:id",name:"client-request-preview",component:n(9101).Z},{path:"/client-requests",name:"client-requests",component:n(2935).Z}];function br(t,e){for(var n in e)t[n]=e[n];return t}var cr=/[!'()*]/g,rr=function(t){return"%"+t.charCodeAt(0).toString(16)},zr=/%2C/g,ar=function(t){return encodeURIComponent(t).replace(cr,rr).replace(zr,",")};function ir(t){try{return decodeURIComponent(t)}catch(t){0}return t}var Or=function(t){return null==t||"object"==typeof t?t:String(t)};function sr(t){var e={};return(t=t.trim().replace(/^(\?|#|&)/,""))?(t.split("&").forEach((function(t){var n=t.replace(/\+/g," ").split("="),o=ir(n.shift()),p=n.length>0?ir(n.join("=")):null;void 0===e[o]?e[o]=p:Array.isArray(e[o])?e[o].push(p):e[o]=[e[o],p]})),e):e}function Ar(t){var e=t?Object.keys(t).map((function(e){var n=t[e];if(void 0===n)return"";if(null===n)return ar(e);if(Array.isArray(n)){var o=[];return n.forEach((function(t){void 0!==t&&(null===t?o.push(ar(e)):o.push(ar(e)+"="+ar(t)))})),o.join("&")}return ar(e)+"="+ar(n)})).filter((function(t){return t.length>0})).join("&"):null;return e?"?"+e:""}var ur=/\/?$/;function lr(t,e,n,o){var p=o&&o.options.stringifyQuery,M=e.query||{};try{M=dr(M)}catch(t){}var b={name:e.name||t&&t.name,meta:t&&t.meta||{},path:e.path||"/",hash:e.hash||"",query:M,params:e.params||{},fullPath:hr(e,p),matched:t?qr(t):[]};return n&&(b.redirectedFrom=hr(n,p)),Object.freeze(b)}function dr(t){if(Array.isArray(t))return t.map(dr);if(t&&"object"==typeof t){var e={};for(var n in t)e[n]=dr(t[n]);return e}return t}var fr=lr(null,{path:"/"});function qr(t){for(var e=[];t;)e.unshift(t),t=t.parent;return e}function hr(t,e){var n=t.path,o=t.query;void 0===o&&(o={});var p=t.hash;return void 0===p&&(p=""),(n||"/")+(e||Ar)(o)+p}function Wr(t,e,n){return e===fr?t===e:!!e&&(t.path&&e.path?t.path.replace(ur,"")===e.path.replace(ur,"")&&(n||t.hash===e.hash&&vr(t.query,e.query)):!(!t.name||!e.name)&&(t.name===e.name&&(n||t.hash===e.hash&&vr(t.query,e.query)&&vr(t.params,e.params))))}function vr(t,e){if(void 0===t&&(t={}),void 0===e&&(e={}),!t||!e)return t===e;var n=Object.keys(t).sort(),o=Object.keys(e).sort();return n.length===o.length&&n.every((function(n,p){var M=t[n];if(o[p]!==n)return!1;var b=e[n];return null==M||null==b?M===b:"object"==typeof M&&"object"==typeof b?vr(M,b):String(M)===String(b)}))}function Rr(t){for(var e=0;e=0&&(e=t.slice(o),t=t.slice(0,o));var p=t.indexOf("?");return p>=0&&(n=t.slice(p+1),t=t.slice(0,p)),{path:t,query:n,hash:e}}(p.path||""),z=e&&e.path||"/",a=r.path?Lr(r.path,z,n||p.append):z,i=function(t,e,n){void 0===e&&(e={});var o,p=n||sr;try{o=p(t||"")}catch(t){o={}}for(var M in e){var b=e[M];o[M]=Array.isArray(b)?b.map(Or):Or(b)}return o}(r.query,p.query,o&&o.options.parseQuery),O=p.hash||r.hash;return O&&"#"!==O.charAt(0)&&(O="#"+O),{_normalized:!0,path:a,query:i,hash:O}}var $r,Vr=function(){},Kr={name:"RouterLink",props:{to:{type:[String,Object],required:!0},tag:{type:String,default:"a"},custom:Boolean,exact:Boolean,exactPath:Boolean,append:Boolean,replace:Boolean,activeClass:String,exactActiveClass:String,ariaCurrentValue:{type:String,default:"page"},event:{type:[String,Array],default:"click"}},render:function(t){var e=this,n=this.$router,o=this.$route,p=n.resolve(this.to,o,this.append),M=p.location,b=p.route,c=p.href,r={},z=n.options.linkActiveClass,a=n.options.linkExactActiveClass,i=null==z?"router-link-active":z,O=null==a?"router-link-exact-active":a,s=null==this.activeClass?i:this.activeClass,A=null==this.exactActiveClass?O:this.exactActiveClass,u=b.redirectedFrom?lr(null,Yr(b.redirectedFrom),null,n):b;r[A]=Wr(o,u,this.exactPath),r[s]=this.exact||this.exactPath?r[A]:function(t,e){return 0===t.path.replace(ur,"/").indexOf(e.path.replace(ur,"/"))&&(!e.hash||t.hash===e.hash)&&function(t,e){for(var n in e)if(!(n in t))return!1;return!0}(t.query,e.query)}(o,u);var l=r[A]?this.ariaCurrentValue:null,d=function(t){Zr(t)&&(e.replace?n.replace(M,Vr):n.push(M,Vr))},f={click:Zr};Array.isArray(this.event)?this.event.forEach((function(t){f[t]=d})):f[this.event]=d;var q={class:r},h=!this.$scopedSlots.$hasNormal&&this.$scopedSlots.default&&this.$scopedSlots.default({href:c,route:b,navigate:d,isActive:r[s],isExactActive:r[A]});if(h){if(1===h.length)return h[0];if(h.length>1||!h.length)return 0===h.length?t():t("span",{},h)}if("a"===this.tag)q.on=f,q.attrs={href:c,"aria-current":l};else{var W=Qr(this.$slots.default);if(W){W.isStatic=!1;var v=W.data=br({},W.data);for(var R in v.on=v.on||{},v.on){var m=v.on[R];R in f&&(v.on[R]=Array.isArray(m)?m:[m])}for(var g in f)g in v.on?v.on[g].push(f[g]):v.on[g]=d;var L=W.data.attrs=br({},W.data.attrs);L.href=c,L["aria-current"]=l}else q.on=f}return t(this.tag,q,this.$slots.default)}};function Zr(t){if(!(t.metaKey||t.altKey||t.ctrlKey||t.shiftKey||t.defaultPrevented||void 0!==t.button&&0!==t.button)){if(t.currentTarget&&t.currentTarget.getAttribute){var e=t.currentTarget.getAttribute("target");if(/\b_blank\b/i.test(e))return}return t.preventDefault&&t.preventDefault(),!0}}function Qr(t){if(t)for(var e,n=0;n-1&&(c.params[O]=n.params[O]);return c.path=Gr(a.path,c.params),r(a,c,b)}if(c.path){c.params={};for(var s=0;s-1}function Ez(t,e){return Nz(t)&&t._isRouter&&(null==e||t.type===e)}function Tz(t,e,n){var o=function(p){p>=t.length?n():t[p]?e(t[p],(function(){o(p+1)})):o(p+1)};o(0)}function Bz(t){return function(e,n,o){var p=!1,M=0,b=null;Cz(t,(function(t,e,n,c){if("function"==typeof t&&void 0===t.cid){p=!0,M++;var r,z=Xz((function(e){var p;((p=e).__esModule||Sz&&"Module"===p[Symbol.toStringTag])&&(e=e.default),t.resolved="function"==typeof e?e:$r.extend(e),n.components[c]=e,--M<=0&&o()})),a=Xz((function(t){var e="Failed to resolve async component "+c+": "+t;b||(b=Nz(t)?t:new Error(e),o(b))}));try{r=t(z,a)}catch(t){a(t)}if(r)if("function"==typeof r.then)r.then(z,a);else{var i=r.component;i&&"function"==typeof i.then&&i.then(z,a)}}})),p||o()}}function Cz(t,e){return wz(t.map((function(t){return Object.keys(t.components).map((function(n){return e(t.components[n],t.instances[n],t,n)}))})))}function wz(t){return Array.prototype.concat.apply([],t)}var Sz="function"==typeof Symbol&&"symbol"==typeof Symbol.toStringTag;function Xz(t){var e=!1;return function(){for(var n=[],o=arguments.length;o--;)n[o]=arguments[o];if(!e)return e=!0,t.apply(this,n)}}var xz=function(t,e){this.router=t,this.base=function(t){if(!t)if(Jr){var e=document.querySelector("base");t=(t=e&&e.getAttribute("href")||"/").replace(/^https?:\/\/[^\/]+/,"")}else t="/";"/"!==t.charAt(0)&&(t="/"+t);return t.replace(/\/$/,"")}(e),this.current=fr,this.pending=null,this.ready=!1,this.readyCbs=[],this.readyErrorCbs=[],this.errorCbs=[],this.listeners=[]};function kz(t,e,n,o){var p=Cz(t,(function(t,o,p,M){var b=function(t,e){"function"!=typeof t&&(t=$r.extend(t));return t.options[e]}(t,e);if(b)return Array.isArray(b)?b.map((function(t){return n(t,o,p,M)})):n(b,o,p,M)}));return wz(o?p.reverse():p)}function Iz(t,e){if(e)return function(){return t.apply(e,arguments)}}xz.prototype.listen=function(t){this.cb=t},xz.prototype.onReady=function(t,e){this.ready?t():(this.readyCbs.push(t),e&&this.readyErrorCbs.push(e))},xz.prototype.onError=function(t){this.errorCbs.push(t)},xz.prototype.transitionTo=function(t,e,n){var o,p=this;try{o=this.router.match(t,this.current)}catch(t){throw this.errorCbs.forEach((function(e){e(t)})),t}var M=this.current;this.confirmTransition(o,(function(){p.updateRoute(o),e&&e(o),p.ensureURL(),p.router.afterHooks.forEach((function(t){t&&t(o,M)})),p.ready||(p.ready=!0,p.readyCbs.forEach((function(t){t(o)})))}),(function(t){n&&n(t),t&&!p.ready&&(Ez(t,mz.redirected)&&M===fr||(p.ready=!0,p.readyErrorCbs.forEach((function(e){e(t)}))))}))},xz.prototype.confirmTransition=function(t,e,n){var o=this,p=this.current;this.pending=t;var M,b,c=function(t){!Ez(t)&&Nz(t)&&o.errorCbs.length&&o.errorCbs.forEach((function(e){e(t)})),n&&n(t)},r=t.matched.length-1,z=p.matched.length-1;if(Wr(t,p)&&r===z&&t.matched[r]===p.matched[z])return this.ensureURL(),t.hash&&Oz(this.router,p,t,!1),c(((b=yz(M=p,t,mz.duplicated,'Avoided redundant navigation to current location: "'+M.fullPath+'".')).name="NavigationDuplicated",b));var a=function(t,e){var n,o=Math.max(t.length,e.length);for(n=0;n0)){var e=this.router,n=e.options.scrollBehavior,o=Wz&&n;o&&this.listeners.push(iz());var p=function(){var n=t.current,p=Pz(t.base);t.current===fr&&p===t._startLocation||t.transitionTo(p,(function(t){o&&Oz(e,t,n,!0)}))};window.addEventListener("popstate",p),this.listeners.push((function(){window.removeEventListener("popstate",p)}))}},e.prototype.go=function(t){window.history.go(t)},e.prototype.push=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){vz(yr(o.base+t.fullPath)),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){Rz(yr(o.base+t.fullPath)),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.ensureURL=function(t){if(Pz(this.base)!==this.current.fullPath){var e=yr(this.base+this.current.fullPath);t?vz(e):Rz(e)}},e.prototype.getCurrentLocation=function(){return Pz(this.base)},e}(xz);function Pz(t){var e=window.location.pathname,n=e.toLowerCase(),o=t.toLowerCase();return!t||n!==o&&0!==n.indexOf(yr(o+"/"))||(e=e.slice(t.length)),(e||"/")+window.location.search+window.location.hash}var Uz=function(t){function e(e,n,o){t.call(this,e,n),o&&function(t){var e=Pz(t);if(!/^\/#/.test(e))return window.location.replace(yr(t+"/#"+e)),!0}(this.base)||jz()}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.setupListeners=function(){var t=this;if(!(this.listeners.length>0)){var e=this.router.options.scrollBehavior,n=Wz&&e;n&&this.listeners.push(iz());var o=function(){var e=t.current;jz()&&t.transitionTo(Fz(),(function(o){n&&Oz(t.router,o,e,!0),Wz||Yz(o.fullPath)}))},p=Wz?"popstate":"hashchange";window.addEventListener(p,o),this.listeners.push((function(){window.removeEventListener(p,o)}))}},e.prototype.push=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){Gz(t.fullPath),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var o=this,p=this.current;this.transitionTo(t,(function(t){Yz(t.fullPath),Oz(o.router,t,p,!1),e&&e(t)}),n)},e.prototype.go=function(t){window.history.go(t)},e.prototype.ensureURL=function(t){var e=this.current.fullPath;Fz()!==e&&(t?Gz(e):Yz(e))},e.prototype.getCurrentLocation=function(){return Fz()},e}(xz);function jz(){var t=Fz();return"/"===t.charAt(0)||(Yz("/"+t),!1)}function Fz(){var t=window.location.href,e=t.indexOf("#");return e<0?"":t=t.slice(e+1)}function Hz(t){var e=window.location.href,n=e.indexOf("#");return(n>=0?e.slice(0,n):e)+"#"+t}function Gz(t){Wz?vz(Hz(t)):window.location.hash=t}function Yz(t){Wz?Rz(Hz(t)):window.location.replace(Hz(t))}var $z=function(t){function e(e,n){t.call(this,e,n),this.stack=[],this.index=-1}return t&&(e.__proto__=t),e.prototype=Object.create(t&&t.prototype),e.prototype.constructor=e,e.prototype.push=function(t,e,n){var o=this;this.transitionTo(t,(function(t){o.stack=o.stack.slice(0,o.index+1).concat(t),o.index++,e&&e(t)}),n)},e.prototype.replace=function(t,e,n){var o=this;this.transitionTo(t,(function(t){o.stack=o.stack.slice(0,o.index).concat(t),e&&e(t)}),n)},e.prototype.go=function(t){var e=this,n=this.index+t;if(!(n<0||n>=this.stack.length)){var o=this.stack[n];this.confirmTransition(o,(function(){var t=e.current;e.index=n,e.updateRoute(o),e.router.afterHooks.forEach((function(e){e&&e(o,t)}))}),(function(t){Ez(t,mz.duplicated)&&(e.index=n)}))}},e.prototype.getCurrentLocation=function(){var t=this.stack[this.stack.length-1];return t?t.fullPath:"/"},e.prototype.ensureURL=function(){},e}(xz),Vz=function(t){void 0===t&&(t={}),this.app=null,this.apps=[],this.options=t,this.beforeHooks=[],this.resolveHooks=[],this.afterHooks=[],this.matcher=oz(t.routes||[],this);var e=t.mode||"hash";switch(this.fallback="history"===e&&!Wz&&!1!==t.fallback,this.fallback&&(e="hash"),Jr||(e="abstract"),this.mode=e,e){case"history":this.history=new Dz(this,t.base);break;case"hash":this.history=new Uz(this,t.base,this.fallback);break;case"abstract":this.history=new $z(this,t.base)}},Kz={currentRoute:{configurable:!0}};Vz.prototype.match=function(t,e,n){return this.matcher.match(t,e,n)},Kz.currentRoute.get=function(){return this.history&&this.history.current},Vz.prototype.init=function(t){var e=this;if(this.apps.push(t),t.$once("hook:destroyed",(function(){var n=e.apps.indexOf(t);n>-1&&e.apps.splice(n,1),e.app===t&&(e.app=e.apps[0]||null),e.app||e.history.teardown()})),!this.app){this.app=t;var n=this.history;if(n instanceof Dz||n instanceof Uz){var o=function(t){n.setupListeners(),function(t){var o=n.current,p=e.options.scrollBehavior;Wz&&p&&"fullPath"in t&&Oz(e,t,o,!1)}(t)};n.transitionTo(n.getCurrentLocation(),o,o)}n.listen((function(t){e.apps.forEach((function(e){e._route=t}))}))}},Vz.prototype.beforeEach=function(t){return Qz(this.beforeHooks,t)},Vz.prototype.beforeResolve=function(t){return Qz(this.resolveHooks,t)},Vz.prototype.afterEach=function(t){return Qz(this.afterHooks,t)},Vz.prototype.onReady=function(t,e){this.history.onReady(t,e)},Vz.prototype.onError=function(t){this.history.onError(t)},Vz.prototype.push=function(t,e,n){var o=this;if(!e&&!n&&"undefined"!=typeof Promise)return new Promise((function(e,n){o.history.push(t,e,n)}));this.history.push(t,e,n)},Vz.prototype.replace=function(t,e,n){var o=this;if(!e&&!n&&"undefined"!=typeof Promise)return new Promise((function(e,n){o.history.replace(t,e,n)}));this.history.replace(t,e,n)},Vz.prototype.go=function(t){this.history.go(t)},Vz.prototype.back=function(){this.go(-1)},Vz.prototype.forward=function(){this.go(1)},Vz.prototype.getMatchedComponents=function(t){var e=t?t.matched?t:this.resolve(t).route:this.currentRoute;return e?[].concat.apply([],e.matched.map((function(t){return Object.keys(t.components).map((function(e){return t.components[e]}))}))):[]},Vz.prototype.resolve=function(t,e,n){var o=Yr(t,e=e||this.history.current,n,this),p=this.match(o,e),M=p.redirectedFrom||p.fullPath,b=function(t,e,n){var o="hash"===n?"#"+e:e;return t?yr(t+"/"+o):o}(this.history.base,M,this.mode);return{location:o,route:p,href:b,normalizedTo:o,resolved:p}},Vz.prototype.getRoutes=function(){return this.matcher.getRoutes()},Vz.prototype.addRoute=function(t,e){this.matcher.addRoute(t,e),this.history.current!==fr&&this.history.transitionTo(this.history.getCurrentLocation())},Vz.prototype.addRoutes=function(t){this.matcher.addRoutes(t),this.history.current!==fr&&this.history.transitionTo(this.history.getCurrentLocation())},Object.defineProperties(Vz.prototype,Kz);var Zz=Vz;function Qz(t,e){return t.push(e),function(){var n=t.indexOf(e);n>-1&&t.splice(n,1)}}Vz.install=function t(e){if(!t.installed||$r!==e){t.installed=!0,$r=e;var n=function(t){return void 0!==t},o=function(t,e){var o=t.$options._parentVnode;n(o)&&n(o=o.data)&&n(o=o.registerRouteInstance)&&o(t,e)};e.mixin({beforeCreate:function(){n(this.$options.router)?(this._routerRoot=this,this._router=this.$options.router,this._router.init(this),e.util.defineReactive(this,"_route",this._router.history.current)):this._routerRoot=this.$parent&&this.$parent._routerRoot||this,o(this,this)},destroyed:function(){o(this)}}),Object.defineProperty(e.prototype,"$router",{get:function(){return this._routerRoot._router}}),Object.defineProperty(e.prototype,"$route",{get:function(){return this._routerRoot._route}}),e.component("RouterView",mr),e.component("RouterLink",Kr);var p=e.config.optionMergeStrategies;p.beforeRouteEnter=p.beforeRouteLeave=p.beforeRouteUpdate=p.created}},Vz.version="3.6.5",Vz.isNavigationFailure=Ez,Vz.NavigationFailureType=mz,Vz.START_LOCATION=fr,Jr&&window.Vue&&window.Vue.use(Vz);var Jz=n(4566),ta=n.n(Jz),ea=n(3379),na=n.n(ea),oa=n(1991),pa={insert:"head",singleton:!1};na()(oa.Z,pa);oa.Z.locals;n(3734);var Ma=document.head.querySelector('meta[name="csrf-token"]');Ma&&(pr.Z.defaults.headers.common["X-CSRF-TOKEN"]=Ma.content),no.use(Zz),window.Popper=n(8981).default,nr().tz.setDefault(Telescope.timezone),window.Telescope.basePath="/"+window.Telescope.path;var ba=window.Telescope.basePath+"/";""!==window.Telescope.path&&"/"!==window.Telescope.path||(ba="/",window.Telescope.basePath="");var ca=new Zz({routes:Mr,mode:"history",base:ba});no.component("vue-json-pretty",ta()),no.component("related-entries",n(9932).Z),no.component("index-screen",n(8106).Z),no.component("preview-screen",n(2986).Z),no.component("alert",n(4518).Z),no.component("copy-clipboard",n(7973).Z),no.mixin(or),new no({el:"#telescope",router:ca,data:function(){return{alert:{type:null,autoClose:0,message:"",confirmationProceed:null,confirmationCancel:null},autoLoadsNewEntries:"1"===localStorage.autoLoadsNewEntries,recording:Telescope.recording}},created:function(){window.addEventListener("keydown",this.keydownListener)},destroyed:function(){window.removeEventListener("keydown",this.keydownListener)},methods:{autoLoadNewEntries:function(){this.autoLoadsNewEntries?(this.autoLoadsNewEntries=!1,localStorage.autoLoadsNewEntries=0):(this.autoLoadsNewEntries=!0,localStorage.autoLoadsNewEntries=1)},toggleRecording:function(){pr.Z.post(Telescope.basePath+"/telescope-api/toggle-recording"),window.Telescope.recording=!Telescope.recording,this.recording=!this.recording},clearEntries:function(){(!(arguments.length>0&&void 0!==arguments[0])||arguments[0])&&!confirm("Are you sure you want to delete all Telescope data?")||pr.Z.delete(Telescope.basePath+"/telescope-api/entries").then((function(t){return location.reload()}))},keydownListener:function(t){t.metaKey&&"k"===t.key&&this.clearEntries(!1)}}})},601:(t,e,n)=>{"use strict";n.d(e,{Z:()=>o});const o={methods:{cacheActionTypeClass:function(t){return"hit"===t?"success":"set"===t?"info":"forget"===t?"warning":"missed"===t?"danger":void 0},composerTypeClass:function(t){return"composer"===t?"info":"creator"===t?"success":void 0},gateResultClass:function(t){return"allowed"===t?"success":"denied"===t?"danger":void 0},jobStatusClass:function(t){return"pending"===t?"secondary":"processed"===t?"success":"failed"===t?"danger":void 0},logLevelClass:function(t){return"debug"===t?"success":"info"===t?"info":"notice"===t?"secondary":"warning"===t?"warning":"error"===t||"critical"===t||"alert"===t||"emergency"===t?"danger":void 0},modelActionClass:function(t){return"created"==t?"success":"updated"==t?"info":"retrieved"==t?"secondary":"deleted"==t||"forceDeleted"==t?"danger":void 0},requestStatusClass:function(t){return t?t<300?"success":t<400?"info":t<500?"warning":t>=500?"danger":void 0:"danger"},requestMethodClass:function(t){return"GET"==t||"OPTIONS"==t?"secondary":"POST"==t||"PATCH"==t||"PUT"==t?"info":"DELETE"==t?"danger":void 0}}}},9742:(t,e)=>{"use strict";e.byteLength=function(t){var e=c(t),n=e[0],o=e[1];return 3*(n+o)/4-o},e.toByteArray=function(t){var e,n,M=c(t),b=M[0],r=M[1],z=new p(function(t,e,n){return 3*(e+n)/4-n}(0,b,r)),a=0,i=r>0?b-4:b;for(n=0;n>16&255,z[a++]=e>>8&255,z[a++]=255&e;2===r&&(e=o[t.charCodeAt(n)]<<2|o[t.charCodeAt(n+1)]>>4,z[a++]=255&e);1===r&&(e=o[t.charCodeAt(n)]<<10|o[t.charCodeAt(n+1)]<<4|o[t.charCodeAt(n+2)]>>2,z[a++]=e>>8&255,z[a++]=255&e);return z},e.fromByteArray=function(t){for(var e,o=t.length,p=o%3,M=[],b=16383,c=0,z=o-p;cz?z:c+b));1===p?(e=t[o-1],M.push(n[e>>2]+n[e<<4&63]+"==")):2===p&&(e=(t[o-2]<<8)+t[o-1],M.push(n[e>>10]+n[e>>4&63]+n[e<<2&63]+"="));return M.join("")};for(var n=[],o=[],p="undefined"!=typeof Uint8Array?Uint8Array:Array,M="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",b=0;b<64;++b)n[b]=M[b],o[M.charCodeAt(b)]=b;function c(t){var e=t.length;if(e%4>0)throw new Error("Invalid string. Length must be a multiple of 4");var n=t.indexOf("=");return-1===n&&(n=e),[n,n===e?0:4-n%4]}function r(t,e,o){for(var p,M,b=[],c=e;c>18&63]+n[M>>12&63]+n[M>>6&63]+n[63&M]);return b.join("")}o["-".charCodeAt(0)]=62,o["_".charCodeAt(0)]=63},3734:function(t,e,n){!function(t,e,n){"use strict";function o(t){return t&&"object"==typeof t&&"default"in t?t:{default:t}}var p=o(e),M=o(n);function b(t,e){for(var n=0;n=b)throw new Error("Bootstrap's JavaScript requires at least jQuery v1.9.1 but less than v4.0.0")}};f.jQueryDetection(),d();var q="alert",h="4.6.2",W="bs.alert",v="."+W,R=".data-api",m=p.default.fn[q],g="alert",L="fade",y="show",_="close"+v,N="closed"+v,E="click"+v+R,T='[data-dismiss="alert"]',B=function(){function t(t){this._element=t}var e=t.prototype;return e.close=function(t){var e=this._element;t&&(e=this._getRootElement(t)),this._triggerCloseEvent(e).isDefaultPrevented()||this._removeElement(e)},e.dispose=function(){p.default.removeData(this._element,W),this._element=null},e._getRootElement=function(t){var e=f.getSelectorFromElement(t),n=!1;return e&&(n=document.querySelector(e)),n||(n=p.default(t).closest("."+g)[0]),n},e._triggerCloseEvent=function(t){var e=p.default.Event(_);return p.default(t).trigger(e),e},e._removeElement=function(t){var e=this;if(p.default(t).removeClass(y),p.default(t).hasClass(L)){var n=f.getTransitionDurationFromElement(t);p.default(t).one(f.TRANSITION_END,(function(n){return e._destroyElement(t,n)})).emulateTransitionEnd(n)}else this._destroyElement(t)},e._destroyElement=function(t){p.default(t).detach().trigger(N).remove()},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this),o=n.data(W);o||(o=new t(this),n.data(W,o)),"close"===e&&o[e](this)}))},t._handleDismiss=function(t){return function(e){e&&e.preventDefault(),t.close(this)}},c(t,null,[{key:"VERSION",get:function(){return h}}]),t}();p.default(document).on(E,T,B._handleDismiss(new B)),p.default.fn[q]=B._jQueryInterface,p.default.fn[q].Constructor=B,p.default.fn[q].noConflict=function(){return p.default.fn[q]=m,B._jQueryInterface};var C="button",w="4.6.2",S="bs.button",X="."+S,x=".data-api",k=p.default.fn[C],I="active",D="btn",P="focus",U="click"+X+x,j="focus"+X+x+" blur"+X+x,F="load"+X+x,H='[data-toggle^="button"]',G='[data-toggle="buttons"]',Y='[data-toggle="button"]',$='[data-toggle="buttons"] .btn',V='input:not([type="hidden"])',K=".active",Z=".btn",Q=function(){function t(t){this._element=t,this.shouldAvoidTriggerChange=!1}var e=t.prototype;return e.toggle=function(){var t=!0,e=!0,n=p.default(this._element).closest(G)[0];if(n){var o=this._element.querySelector(V);if(o){if("radio"===o.type)if(o.checked&&this._element.classList.contains(I))t=!1;else{var M=n.querySelector(K);M&&p.default(M).removeClass(I)}t&&("checkbox"!==o.type&&"radio"!==o.type||(o.checked=!this._element.classList.contains(I)),this.shouldAvoidTriggerChange||p.default(o).trigger("change")),o.focus(),e=!1}}this._element.hasAttribute("disabled")||this._element.classList.contains("disabled")||(e&&this._element.setAttribute("aria-pressed",!this._element.classList.contains(I)),t&&p.default(this._element).toggleClass(I))},e.dispose=function(){p.default.removeData(this._element,S),this._element=null},t._jQueryInterface=function(e,n){return this.each((function(){var o=p.default(this),M=o.data(S);M||(M=new t(this),o.data(S,M)),M.shouldAvoidTriggerChange=n,"toggle"===e&&M[e]()}))},c(t,null,[{key:"VERSION",get:function(){return w}}]),t}();p.default(document).on(U,H,(function(t){var e=t.target,n=e;if(p.default(e).hasClass(D)||(e=p.default(e).closest(Z)[0]),!e||e.hasAttribute("disabled")||e.classList.contains("disabled"))t.preventDefault();else{var o=e.querySelector(V);if(o&&(o.hasAttribute("disabled")||o.classList.contains("disabled")))return void t.preventDefault();"INPUT"!==n.tagName&&"LABEL"===e.tagName||Q._jQueryInterface.call(p.default(e),"toggle","INPUT"===n.tagName)}})).on(j,H,(function(t){var e=p.default(t.target).closest(Z)[0];p.default(e).toggleClass(P,/^focus(in)?$/.test(t.type))})),p.default(window).on(F,(function(){for(var t=[].slice.call(document.querySelectorAll($)),e=0,n=t.length;e0,this._pointerEvent=Boolean(window.PointerEvent||window.MSPointerEvent),this._addEventListeners()}var e=t.prototype;return e.next=function(){this._isSliding||this._slide(dt)},e.nextWhenVisible=function(){var t=p.default(this._element);!document.hidden&&t.is(":visible")&&"hidden"!==t.css("visibility")&&this.next()},e.prev=function(){this._isSliding||this._slide(ft)},e.pause=function(t){t||(this._isPaused=!0),this._element.querySelector(kt)&&(f.triggerTransitionEnd(this._element),this.cycle(!0)),clearInterval(this._interval),this._interval=null},e.cycle=function(t){t||(this._isPaused=!1),this._interval&&(clearInterval(this._interval),this._interval=null),this._config.interval&&!this._isPaused&&(this._updateInterval(),this._interval=setInterval((document.visibilityState?this.nextWhenVisible:this.next).bind(this),this._config.interval))},e.to=function(t){var e=this;this._activeElement=this._element.querySelector(St);var n=this._getItemIndex(this._activeElement);if(!(t>this._items.length-1||t<0))if(this._isSliding)p.default(this._element).one(vt,(function(){return e.to(t)}));else{if(n===t)return this.pause(),void this.cycle();var o=t>n?dt:ft;this._slide(o,this._items[t])}},e.dispose=function(){p.default(this._element).off(nt),p.default.removeData(this._element,et),this._items=null,this._config=null,this._element=null,this._interval=null,this._isPaused=null,this._isSliding=null,this._activeElement=null,this._indicatorsElement=null},e._getConfig=function(t){return t=r({},Ut,t),f.typeCheckConfig(J,t,jt),t},e._handleSwipe=function(){var t=Math.abs(this.touchDeltaX);if(!(t<=rt)){var e=t/this.touchDeltaX;this.touchDeltaX=0,e>0&&this.prev(),e<0&&this.next()}},e._addEventListeners=function(){var t=this;this._config.keyboard&&p.default(this._element).on(Rt,(function(e){return t._keydown(e)})),"hover"===this._config.pause&&p.default(this._element).on(mt,(function(e){return t.pause(e)})).on(gt,(function(e){return t.cycle(e)})),this._config.touch&&this._addTouchEventListeners()},e._addTouchEventListeners=function(){var t=this;if(this._touchSupported){var e=function(e){t._pointerEvent&&Ft[e.originalEvent.pointerType.toUpperCase()]?t.touchStartX=e.originalEvent.clientX:t._pointerEvent||(t.touchStartX=e.originalEvent.touches[0].clientX)},n=function(e){t.touchDeltaX=e.originalEvent.touches&&e.originalEvent.touches.length>1?0:e.originalEvent.touches[0].clientX-t.touchStartX},o=function(e){t._pointerEvent&&Ft[e.originalEvent.pointerType.toUpperCase()]&&(t.touchDeltaX=e.originalEvent.clientX-t.touchStartX),t._handleSwipe(),"hover"===t._config.pause&&(t.pause(),t.touchTimeout&&clearTimeout(t.touchTimeout),t.touchTimeout=setTimeout((function(e){return t.cycle(e)}),ct+t._config.interval))};p.default(this._element.querySelectorAll(xt)).on(Tt,(function(t){return t.preventDefault()})),this._pointerEvent?(p.default(this._element).on(Nt,(function(t){return e(t)})),p.default(this._element).on(Et,(function(t){return o(t)})),this._element.classList.add(lt)):(p.default(this._element).on(Lt,(function(t){return e(t)})),p.default(this._element).on(yt,(function(t){return n(t)})),p.default(this._element).on(_t,(function(t){return o(t)})))}},e._keydown=function(t){if(!/input|textarea/i.test(t.target.tagName))switch(t.which){case Mt:t.preventDefault(),this.prev();break;case bt:t.preventDefault(),this.next()}},e._getItemIndex=function(t){return this._items=t&&t.parentNode?[].slice.call(t.parentNode.querySelectorAll(Xt)):[],this._items.indexOf(t)},e._getItemByDirection=function(t,e){var n=t===dt,o=t===ft,p=this._getItemIndex(e),M=this._items.length-1;if((o&&0===p||n&&p===M)&&!this._config.wrap)return e;var b=(p+(t===ft?-1:1))%this._items.length;return-1===b?this._items[this._items.length-1]:this._items[b]},e._triggerSlideEvent=function(t,e){var n=this._getItemIndex(t),o=this._getItemIndex(this._element.querySelector(St)),M=p.default.Event(Wt,{relatedTarget:t,direction:e,from:o,to:n});return p.default(this._element).trigger(M),M},e._setActiveIndicatorElement=function(t){if(this._indicatorsElement){var e=[].slice.call(this._indicatorsElement.querySelectorAll(wt));p.default(e).removeClass(at);var n=this._indicatorsElement.children[this._getItemIndex(t)];n&&p.default(n).addClass(at)}},e._updateInterval=function(){var t=this._activeElement||this._element.querySelector(St);if(t){var e=parseInt(t.getAttribute("data-interval"),10);e?(this._config.defaultInterval=this._config.defaultInterval||this._config.interval,this._config.interval=e):this._config.interval=this._config.defaultInterval||this._config.interval}},e._slide=function(t,e){var n,o,M,b=this,c=this._element.querySelector(St),r=this._getItemIndex(c),z=e||c&&this._getItemByDirection(t,c),a=this._getItemIndex(z),i=Boolean(this._interval);if(t===dt?(n=st,o=At,M=qt):(n=Ot,o=ut,M=ht),z&&p.default(z).hasClass(at))this._isSliding=!1;else if(!this._triggerSlideEvent(z,M).isDefaultPrevented()&&c&&z){this._isSliding=!0,i&&this.pause(),this._setActiveIndicatorElement(z),this._activeElement=z;var O=p.default.Event(vt,{relatedTarget:z,direction:M,from:r,to:a});if(p.default(this._element).hasClass(it)){p.default(z).addClass(o),f.reflow(z),p.default(c).addClass(n),p.default(z).addClass(n);var s=f.getTransitionDurationFromElement(c);p.default(c).one(f.TRANSITION_END,(function(){p.default(z).removeClass(n+" "+o).addClass(at),p.default(c).removeClass(at+" "+o+" "+n),b._isSliding=!1,setTimeout((function(){return p.default(b._element).trigger(O)}),0)})).emulateTransitionEnd(s)}else p.default(c).removeClass(at),p.default(z).addClass(at),this._isSliding=!1,p.default(this._element).trigger(O);i&&this.cycle()}},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this).data(et),o=r({},Ut,p.default(this).data());"object"==typeof e&&(o=r({},o,e));var M="string"==typeof e?e:o.slide;if(n||(n=new t(this,o),p.default(this).data(et,n)),"number"==typeof e)n.to(e);else if("string"==typeof M){if(void 0===n[M])throw new TypeError('No method named "'+M+'"');n[M]()}else o.interval&&o.ride&&(n.pause(),n.cycle())}))},t._dataApiClickHandler=function(e){var n=f.getSelectorFromElement(this);if(n){var o=p.default(n)[0];if(o&&p.default(o).hasClass(zt)){var M=r({},p.default(o).data(),p.default(this).data()),b=this.getAttribute("data-slide-to");b&&(M.interval=!1),t._jQueryInterface.call(p.default(o),M),b&&p.default(o).data(et).to(b),e.preventDefault()}}},c(t,null,[{key:"VERSION",get:function(){return tt}},{key:"Default",get:function(){return Ut}}]),t}();p.default(document).on(Ct,Dt,Ht._dataApiClickHandler),p.default(window).on(Bt,(function(){for(var t=[].slice.call(document.querySelectorAll(Pt)),e=0,n=t.length;e0&&(this._selector=b,this._triggerArray.push(M))}this._parent=this._config.parent?this._getParent():null,this._config.parent||this._addAriaAndCollapsedClass(this._element,this._triggerArray),this._config.toggle&&this.toggle()}var e=t.prototype;return e.toggle=function(){p.default(this._element).hasClass(Qt)?this.hide():this.show()},e.show=function(){var e,n,o=this;if(!(this._isTransitioning||p.default(this._element).hasClass(Qt)||(this._parent&&0===(e=[].slice.call(this._parent.querySelectorAll(ze)).filter((function(t){return"string"==typeof o._config.parent?t.getAttribute("data-parent")===o._config.parent:t.classList.contains(Jt)}))).length&&(e=null),e&&(n=p.default(e).not(this._selector).data($t))&&n._isTransitioning))){var M=p.default.Event(pe);if(p.default(this._element).trigger(M),!M.isDefaultPrevented()){e&&(t._jQueryInterface.call(p.default(e).not(this._selector),"hide"),n||p.default(e).data($t,null));var b=this._getDimension();p.default(this._element).removeClass(Jt).addClass(te),this._element.style[b]=0,this._triggerArray.length&&p.default(this._triggerArray).removeClass(ee).attr("aria-expanded",!0),this.setTransitioning(!0);var c=function(){p.default(o._element).removeClass(te).addClass(Jt+" "+Qt),o._element.style[b]="",o.setTransitioning(!1),p.default(o._element).trigger(Me)},r="scroll"+(b[0].toUpperCase()+b.slice(1)),z=f.getTransitionDurationFromElement(this._element);p.default(this._element).one(f.TRANSITION_END,c).emulateTransitionEnd(z),this._element.style[b]=this._element[r]+"px"}}},e.hide=function(){var t=this;if(!this._isTransitioning&&p.default(this._element).hasClass(Qt)){var e=p.default.Event(be);if(p.default(this._element).trigger(e),!e.isDefaultPrevented()){var n=this._getDimension();this._element.style[n]=this._element.getBoundingClientRect()[n]+"px",f.reflow(this._element),p.default(this._element).addClass(te).removeClass(Jt+" "+Qt);var o=this._triggerArray.length;if(o>0)for(var M=0;M0},e._getOffset=function(){var t=this,e={};return"function"==typeof this._config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t._config.offset(e.offsets,t._element)),e}:e.offset=this._config.offset,e},e._getPopperConfig=function(){var t={placement:this._getPlacement(),modifiers:{offset:this._getOffset(),flip:{enabled:this._config.flip},preventOverflow:{boundariesElement:this._config.boundary}}};return"static"===this._config.display&&(t.modifiers.applyStyle={enabled:!1}),r({},t,this._config.popperConfig)},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this).data(le);if(n||(n=new t(this,"object"==typeof e?e:null),p.default(this).data(le,n)),"string"==typeof e){if(void 0===n[e])throw new TypeError('No method named "'+e+'"');n[e]()}}))},t._clearMenus=function(e){if(!e||e.which!==ge&&("keyup"!==e.type||e.which===ve))for(var n=[].slice.call(document.querySelectorAll(Ue)),o=0,M=n.length;o0&&b--,e.which===me&&bdocument.documentElement.clientHeight;n||(this._element.style.overflowY="hidden"),this._element.classList.add(ln);var o=f.getTransitionDurationFromElement(this._dialog);p.default(this._element).off(f.TRANSITION_END),p.default(this._element).one(f.TRANSITION_END,(function(){t._element.classList.remove(ln),n||p.default(t._element).one(f.TRANSITION_END,(function(){t._element.style.overflowY=""})).emulateTransitionEnd(t._element,o)})).emulateTransitionEnd(o),this._element.focus()}},e._showElement=function(t){var e=this,n=p.default(this._element).hasClass(An),o=this._dialog?this._dialog.querySelector(En):null;this._element.parentNode&&this._element.parentNode.nodeType===Node.ELEMENT_NODE||document.body.appendChild(this._element),this._element.style.display="block",this._element.removeAttribute("aria-hidden"),this._element.setAttribute("aria-modal",!0),this._element.setAttribute("role","dialog"),p.default(this._dialog).hasClass(zn)&&o?o.scrollTop=0:this._element.scrollTop=0,n&&f.reflow(this._element),p.default(this._element).addClass(un),this._config.focus&&this._enforceFocus();var M=p.default.Event(Wn,{relatedTarget:t}),b=function(){e._config.focus&&e._element.focus(),e._isTransitioning=!1,p.default(e._element).trigger(M)};if(n){var c=f.getTransitionDurationFromElement(this._dialog);p.default(this._dialog).one(f.TRANSITION_END,b).emulateTransitionEnd(c)}else b()},e._enforceFocus=function(){var t=this;p.default(document).off(vn).on(vn,(function(e){document!==e.target&&t._element!==e.target&&0===p.default(t._element).has(e.target).length&&t._element.focus()}))},e._setEscapeEvent=function(){var t=this;this._isShown?p.default(this._element).on(gn,(function(e){t._config.keyboard&&e.which===rn?(e.preventDefault(),t.hide()):t._config.keyboard||e.which!==rn||t._triggerBackdropTransition()})):this._isShown||p.default(this._element).off(gn)},e._setResizeEvent=function(){var t=this;this._isShown?p.default(window).on(Rn,(function(e){return t.handleUpdate(e)})):p.default(window).off(Rn)},e._hideModal=function(){var t=this;this._element.style.display="none",this._element.setAttribute("aria-hidden",!0),this._element.removeAttribute("aria-modal"),this._element.removeAttribute("role"),this._isTransitioning=!1,this._showBackdrop((function(){p.default(document.body).removeClass(sn),t._resetAdjustments(),t._resetScrollbar(),p.default(t._element).trigger(qn)}))},e._removeBackdrop=function(){this._backdrop&&(p.default(this._backdrop).remove(),this._backdrop=null)},e._showBackdrop=function(t){var e=this,n=p.default(this._element).hasClass(An)?An:"";if(this._isShown&&this._config.backdrop){if(this._backdrop=document.createElement("div"),this._backdrop.className=On,n&&this._backdrop.classList.add(n),p.default(this._backdrop).appendTo(document.body),p.default(this._element).on(mn,(function(t){e._ignoreBackdropClick?e._ignoreBackdropClick=!1:t.target===t.currentTarget&&("static"===e._config.backdrop?e._triggerBackdropTransition():e.hide())})),n&&f.reflow(this._backdrop),p.default(this._backdrop).addClass(un),!t)return;if(!n)return void t();var o=f.getTransitionDurationFromElement(this._backdrop);p.default(this._backdrop).one(f.TRANSITION_END,t).emulateTransitionEnd(o)}else if(!this._isShown&&this._backdrop){p.default(this._backdrop).removeClass(un);var M=function(){e._removeBackdrop(),t&&t()};if(p.default(this._element).hasClass(An)){var b=f.getTransitionDurationFromElement(this._backdrop);p.default(this._backdrop).one(f.TRANSITION_END,M).emulateTransitionEnd(b)}else M()}else t&&t()},e._adjustDialog=function(){var t=this._element.scrollHeight>document.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},e._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},e._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=Math.round(t.left+t.right)
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",customClass:"",sanitize:!0,sanitizeFn:null,whiteList:In,popperConfig:null},ao={animation:"boolean",template:"string",title:"(string|element|function)",trigger:"string",delay:"(number|object)",html:"boolean",selector:"(string|boolean)",placement:"(string|function)",offset:"(number|string|function)",container:"(string|element|boolean)",fallbackPlacement:"(string|array)",boundary:"(string|element)",customClass:"(string|function)",sanitize:"boolean",sanitizeFn:"(null|function)",whiteList:"object",popperConfig:"(null|object)"},io={HIDE:"hide"+Yn,HIDDEN:"hidden"+Yn,SHOW:"show"+Yn,SHOWN:"shown"+Yn,INSERTED:"inserted"+Yn,CLICK:"click"+Yn,FOCUSIN:"focusin"+Yn,FOCUSOUT:"focusout"+Yn,MOUSEENTER:"mouseenter"+Yn,MOUSELEAVE:"mouseleave"+Yn},Oo=function(){function t(t,e){if(void 0===M.default)throw new TypeError("Bootstrap's tooltips require Popper (https://popper.js.org)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var e=t.prototype;return e.enable=function(){this._isEnabled=!0},e.disable=function(){this._isEnabled=!1},e.toggleEnabled=function(){this._isEnabled=!this._isEnabled},e.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=p.default(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),p.default(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(p.default(this.getTipElement()).hasClass(Jn))return void this._leave(null,this);this._enter(null,this)}},e.dispose=function(){clearTimeout(this._timeout),p.default.removeData(this.element,this.constructor.DATA_KEY),p.default(this.element).off(this.constructor.EVENT_KEY),p.default(this.element).closest(".modal").off("hide.bs.modal",this._hideModalHandler),this.tip&&p.default(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,this._activeTrigger=null,this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},e.show=function(){var t=this;if("none"===p.default(this.element).css("display"))throw new Error("Please use show on visible elements");var e=p.default.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){p.default(this.element).trigger(e);var n=f.findShadowRoot(this.element),o=p.default.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(e.isDefaultPrevented()||!o)return;var b=this.getTipElement(),c=f.getUID(this.constructor.NAME);b.setAttribute("id",c),this.element.setAttribute("aria-describedby",c),this.setContent(),this.config.animation&&p.default(b).addClass(Qn);var r="function"==typeof this.config.placement?this.config.placement.call(this,b,this.element):this.config.placement,z=this._getAttachment(r);this.addAttachmentClass(z);var a=this._getContainer();p.default(b).data(this.constructor.DATA_KEY,this),p.default.contains(this.element.ownerDocument.documentElement,this.tip)||p.default(b).appendTo(a),p.default(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new M.default(this.element,b,this._getPopperConfig(z)),p.default(b).addClass(Jn),p.default(b).addClass(this.config.customClass),"ontouchstart"in document.documentElement&&p.default(document.body).children().on("mouseover",null,p.default.noop);var i=function(){t.config.animation&&t._fixTransition();var e=t._hoverState;t._hoverState=null,p.default(t.element).trigger(t.constructor.Event.SHOWN),e===eo&&t._leave(null,t)};if(p.default(this.tip).hasClass(Qn)){var O=f.getTransitionDurationFromElement(this.tip);p.default(this.tip).one(f.TRANSITION_END,i).emulateTransitionEnd(O)}else i()}},e.hide=function(t){var e=this,n=this.getTipElement(),o=p.default.Event(this.constructor.Event.HIDE),M=function(){e._hoverState!==to&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),p.default(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(p.default(this.element).trigger(o),!o.isDefaultPrevented()){if(p.default(n).removeClass(Jn),"ontouchstart"in document.documentElement&&p.default(document.body).children().off("mouseover",null,p.default.noop),this._activeTrigger[bo]=!1,this._activeTrigger[Mo]=!1,this._activeTrigger[po]=!1,p.default(this.tip).hasClass(Qn)){var b=f.getTransitionDurationFromElement(n);p.default(n).one(f.TRANSITION_END,M).emulateTransitionEnd(b)}else M();this._hoverState=""}},e.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},e.isWithContent=function(){return Boolean(this.getTitle())},e.addAttachmentClass=function(t){p.default(this.getTipElement()).addClass(Vn+"-"+t)},e.getTipElement=function(){return this.tip=this.tip||p.default(this.config.template)[0],this.tip},e.setContent=function(){var t=this.getTipElement();this.setElementContent(p.default(t.querySelectorAll(no)),this.getTitle()),p.default(t).removeClass(Qn+" "+Jn)},e.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=jn(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?p.default(e).parent().is(t)||t.empty().append(e):t.text(p.default(e).text())},e.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},e._getPopperConfig=function(t){var e=this;return r({},{placement:t,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:oo},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}},this.config.popperConfig)},e._getOffset=function(){var t=this,e={};return"function"==typeof this.config.offset?e.fn=function(e){return e.offsets=r({},e.offsets,t.config.offset(e.offsets,t.element)),e}:e.offset=this.config.offset,e},e._getContainer=function(){return!1===this.config.container?document.body:f.isElement(this.config.container)?p.default(this.config.container):p.default(document).find(this.config.container)},e._getAttachment=function(t){return ro[t.toUpperCase()]},e._setListeners=function(){var t=this;this.config.trigger.split(" ").forEach((function(e){if("click"===e)p.default(t.element).on(t.constructor.Event.CLICK,t.config.selector,(function(e){return t.toggle(e)}));else if(e!==co){var n=e===po?t.constructor.Event.MOUSEENTER:t.constructor.Event.FOCUSIN,o=e===po?t.constructor.Event.MOUSELEAVE:t.constructor.Event.FOCUSOUT;p.default(t.element).on(n,t.config.selector,(function(e){return t._enter(e)})).on(o,t.config.selector,(function(e){return t._leave(e)}))}})),this._hideModalHandler=function(){t.element&&t.hide()},p.default(this.element).closest(".modal").on("hide.bs.modal",this._hideModalHandler),this.config.selector?this.config=r({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},e._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},e._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Mo:po]=!0),p.default(e.getTipElement()).hasClass(Jn)||e._hoverState===to?e._hoverState=to:(clearTimeout(e._timeout),e._hoverState=to,e.config.delay&&e.config.delay.show?e._timeout=setTimeout((function(){e._hoverState===to&&e.show()}),e.config.delay.show):e.show())},e._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||p.default(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),p.default(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Mo:po]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=eo,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout((function(){e._hoverState===eo&&e.hide()}),e.config.delay.hide):e.hide())},e._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},e._getConfig=function(t){var e=p.default(this.element).data();return Object.keys(e).forEach((function(t){-1!==Zn.indexOf(t)&&delete e[t]})),"number"==typeof(t=r({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),f.typeCheckConfig(Fn,t,this.constructor.DefaultType),t.sanitize&&(t.template=jn(t.template,t.whiteList,t.sanitizeFn)),t},e._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},e._cleanTipClass=function(){var t=p.default(this.getTipElement()),e=t.attr("class").match(Kn);null!==e&&e.length&&t.removeClass(e.join(""))},e._handlePopperPlacementChange=function(t){this.tip=t.instance.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},e._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(p.default(t).removeClass(Qn),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},t._jQueryInterface=function(e){return this.each((function(){var n=p.default(this),o=n.data(Gn),M="object"==typeof e&&e;if((o||!/dispose|hide/.test(e))&&(o||(o=new t(this,M),n.data(Gn,o)),"string"==typeof e)){if(void 0===o[e])throw new TypeError('No method named "'+e+'"');o[e]()}}))},c(t,null,[{key:"VERSION",get:function(){return Hn}},{key:"Default",get:function(){return zo}},{key:"NAME",get:function(){return Fn}},{key:"DATA_KEY",get:function(){return Gn}},{key:"Event",get:function(){return io}},{key:"EVENT_KEY",get:function(){return Yn}},{key:"DefaultType",get:function(){return ao}}]),t}();p.default.fn[Fn]=Oo._jQueryInterface,p.default.fn[Fn].Constructor=Oo,p.default.fn[Fn].noConflict=function(){return p.default.fn[Fn]=$n,Oo._jQueryInterface};var so="popover",Ao="4.6.2",uo="bs.popover",lo="."+uo,fo=p.default.fn[so],qo="bs-popover",ho=new RegExp("(^|\\s)"+qo+"\\S+","g"),Wo="fade",vo="show",Ro=".popover-header",mo=".popover-body",go=r({},Oo.Default,{placement:"right",trigger:"click",content:"",template:''}),Lo=r({},Oo.DefaultType,{content:"(string|element|function)"}),yo={HIDE:"hide"+lo,HIDDEN:"hidden"+lo,SHOW:"show"+lo,SHOWN:"shown"+lo,INSERTED:"inserted"+lo,CLICK:"click"+lo,FOCUSIN:"focusin"+lo,FOCUSOUT:"focusout"+lo,MOUSEENTER:"mouseenter"+lo,MOUSELEAVE:"mouseleave"+lo},_o=function(t){function e(){return t.apply(this,arguments)||this}z(e,t);var n=e.prototype;return n.isWithContent=function(){return this.getTitle()||this._getContent()},n.addAttachmentClass=function(t){p.default(this.getTipElement()).addClass(qo+"-"+t)},n.getTipElement=function(){return this.tip=this.tip||p.default(this.config.template)[0],this.tip},n.setContent=function(){var t=p.default(this.getTipElement());this.setElementContent(t.find(Ro),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(mo),e),t.removeClass(Wo+" "+vo)},n._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},n._cleanTipClass=function(){var t=p.default(this.getTipElement()),e=t.attr("class").match(ho);null!==e&&e.length>0&&t.removeClass(e.join(""))},e._jQueryInterface=function(t){return this.each((function(){var n=p.default(this).data(uo),o="object"==typeof t?t:null;if((n||!/dispose|hide/.test(t))&&(n||(n=new e(this,o),p.default(this).data(uo,n)),"string"==typeof t)){if(void 0===n[t])throw new TypeError('No method named "'+t+'"');n[t]()}}))},c(e,null,[{key:"VERSION",get:function(){return Ao}},{key:"Default",get:function(){return go}},{key:"NAME",get:function(){return so}},{key:"DATA_KEY",get:function(){return uo}},{key:"Event",get:function(){return yo}},{key:"EVENT_KEY",get:function(){return lo}},{key:"DefaultType",get:function(){return Lo}}]),e}(Oo);p.default.fn[so]=_o._jQueryInterface,p.default.fn[so].Constructor=_o,p.default.fn[so].noConflict=function(){return p.default.fn[so]=fo,_o._jQueryInterface};var No="scrollspy",Eo="4.6.2",To="bs.scrollspy",Bo="."+To,Co=".data-api",wo=p.default.fn[No],So="dropdown-item",Xo="active",xo="activate"+Bo,ko="scroll"+Bo,Io="load"+Bo+Co,Do="offset",Po="position",Uo='[data-spy="scroll"]',jo=".nav, .list-group",Fo=".nav-link",Ho=".nav-item",Go=".list-group-item",Yo=".dropdown",$o=".dropdown-item",Vo=".dropdown-toggle",Ko={offset:10,method:"auto",target:""},Zo={offset:"number",method:"string",target:"(string|element)"},Qo=function(){function t(t,e){var n=this;this._element=t,this._scrollElement="BODY"===t.tagName?window:t,this._config=this._getConfig(e),this._selector=this._config.target+" "+Fo+","+this._config.target+" "+Go+","+this._config.target+" "+$o,this._offsets=[],this._targets=[],this._activeTarget=null,this._scrollHeight=0,p.default(this._scrollElement).on(ko,(function(t){return n._process(t)})),this.refresh(),this._process()}var e=t.prototype;return e.refresh=function(){var t=this,e=this._scrollElement===this._scrollElement.window?Do:Po,n="auto"===this._config.method?e:this._config.method,o=n===Po?this._getScrollTop():0;this._offsets=[],this._targets=[],this._scrollHeight=this._getScrollHeight(),[].slice.call(document.querySelectorAll(this._selector)).map((function(t){var e,M=f.getSelectorFromElement(t);if(M&&(e=document.querySelector(M)),e){var b=e.getBoundingClientRect();if(b.width||b.height)return[p.default(e)[n]().top+o,M]}return null})).filter(Boolean).sort((function(t,e){return t[0]-e[0]})).forEach((function(e){t._offsets.push(e[0]),t._targets.push(e[1])}))},e.dispose=function(){p.default.removeData(this._element,To),p.default(this._scrollElement).off(Bo),this._element=null,this._scrollElement=null,this._config=null,this._selector=null,this._offsets=null,this._targets=null,this._activeTarget=null,this._scrollHeight=null},e._getConfig=function(t){if("string"!=typeof(t=r({},Ko,"object"==typeof t&&t?t:{})).target&&f.isElement(t.target)){var e=p.default(t.target).attr("id");e||(e=f.getUID(No),p.default(t.target).attr("id",e)),t.target="#"+e}return f.typeCheckConfig(No,t,Zo),t},e._getScrollTop=function(){return this._scrollElement===window?this._scrollElement.pageYOffset:this._scrollElement.scrollTop},e._getScrollHeight=function(){return this._scrollElement.scrollHeight||Math.max(document.body.scrollHeight,document.documentElement.scrollHeight)},e._getOffsetHeight=function(){return this._scrollElement===window?window.innerHeight:this._scrollElement.getBoundingClientRect().height},e._process=function(){var t=this._getScrollTop()+this._config.offset,e=this._getScrollHeight(),n=this._config.offset+e-this._getOffsetHeight();if(this._scrollHeight!==e&&this.refresh(),t>=n){var o=this._targets[this._targets.length-1];this._activeTarget!==o&&this._activate(o)}else{if(this._activeTarget&&t0)return this._activeTarget=null,void this._clear();for(var p=this._offsets.length;p--;)this._activeTarget!==this._targets[p]&&t>=this._offsets[p]&&(void 0===this._offsets[p+1]||t{"use strict";var o=n(9742),p=n(645),M=n(5826);function b(){return r.TYPED_ARRAY_SUPPORT?2147483647:1073741823}function c(t,e){if(b()=b())throw new RangeError("Attempt to allocate Buffer larger than maximum size: 0x"+b().toString(16)+" bytes");return 0|t}function A(t,e){if(r.isBuffer(t))return t.length;if("undefined"!=typeof ArrayBuffer&&"function"==typeof ArrayBuffer.isView&&(ArrayBuffer.isView(t)||t instanceof ArrayBuffer))return t.byteLength;"string"!=typeof t&&(t=""+t);var n=t.length;if(0===n)return 0;for(var o=!1;;)switch(e){case"ascii":case"latin1":case"binary":return n;case"utf8":case"utf-8":case void 0:return P(t).length;case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return 2*n;case"hex":return n>>>1;case"base64":return U(t).length;default:if(o)return P(t).length;e=(""+e).toLowerCase(),o=!0}}function u(t,e,n){var o=!1;if((void 0===e||e<0)&&(e=0),e>this.length)return"";if((void 0===n||n>this.length)&&(n=this.length),n<=0)return"";if((n>>>=0)<=(e>>>=0))return"";for(t||(t="utf8");;)switch(t){case"hex":return E(this,e,n);case"utf8":case"utf-8":return L(this,e,n);case"ascii":return _(this,e,n);case"latin1":case"binary":return N(this,e,n);case"base64":return g(this,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return T(this,e,n);default:if(o)throw new TypeError("Unknown encoding: "+t);t=(t+"").toLowerCase(),o=!0}}function l(t,e,n){var o=t[e];t[e]=t[n],t[n]=o}function d(t,e,n,o,p){if(0===t.length)return-1;if("string"==typeof n?(o=n,n=0):n>2147483647?n=2147483647:n<-2147483648&&(n=-2147483648),n=+n,isNaN(n)&&(n=p?0:t.length-1),n<0&&(n=t.length+n),n>=t.length){if(p)return-1;n=t.length-1}else if(n<0){if(!p)return-1;n=0}if("string"==typeof e&&(e=r.from(e,o)),r.isBuffer(e))return 0===e.length?-1:f(t,e,n,o,p);if("number"==typeof e)return e&=255,r.TYPED_ARRAY_SUPPORT&&"function"==typeof Uint8Array.prototype.indexOf?p?Uint8Array.prototype.indexOf.call(t,e,n):Uint8Array.prototype.lastIndexOf.call(t,e,n):f(t,[e],n,o,p);throw new TypeError("val must be string, number or Buffer")}function f(t,e,n,o,p){var M,b=1,c=t.length,r=e.length;if(void 0!==o&&("ucs2"===(o=String(o).toLowerCase())||"ucs-2"===o||"utf16le"===o||"utf-16le"===o)){if(t.length<2||e.length<2)return-1;b=2,c/=2,r/=2,n/=2}function z(t,e){return 1===b?t[e]:t.readUInt16BE(e*b)}if(p){var a=-1;for(M=n;Mc&&(n=c-r),M=n;M>=0;M--){for(var i=!0,O=0;Op&&(o=p):o=p;var M=e.length;if(M%2!=0)throw new TypeError("Invalid hex string");o>M/2&&(o=M/2);for(var b=0;b>8,p=n%256,M.push(p),M.push(o);return M}(e,t.length-n),t,n,o)}function g(t,e,n){return 0===e&&n===t.length?o.fromByteArray(t):o.fromByteArray(t.slice(e,n))}function L(t,e,n){n=Math.min(t.length,n);for(var o=[],p=e;p239?4:z>223?3:z>191?2:1;if(p+i<=n)switch(i){case 1:z<128&&(a=z);break;case 2:128==(192&(M=t[p+1]))&&(r=(31&z)<<6|63&M)>127&&(a=r);break;case 3:M=t[p+1],b=t[p+2],128==(192&M)&&128==(192&b)&&(r=(15&z)<<12|(63&M)<<6|63&b)>2047&&(r<55296||r>57343)&&(a=r);break;case 4:M=t[p+1],b=t[p+2],c=t[p+3],128==(192&M)&&128==(192&b)&&128==(192&c)&&(r=(15&z)<<18|(63&M)<<12|(63&b)<<6|63&c)>65535&&r<1114112&&(a=r)}null===a?(a=65533,i=1):a>65535&&(a-=65536,o.push(a>>>10&1023|55296),a=56320|1023&a),o.push(a),p+=i}return function(t){var e=t.length;if(e<=y)return String.fromCharCode.apply(String,t);var n="",o=0;for(;o0&&(t=this.toString("hex",0,n).match(/.{2}/g).join(" "),this.length>n&&(t+=" ... ")),""},r.prototype.compare=function(t,e,n,o,p){if(!r.isBuffer(t))throw new TypeError("Argument must be a Buffer");if(void 0===e&&(e=0),void 0===n&&(n=t?t.length:0),void 0===o&&(o=0),void 0===p&&(p=this.length),e<0||n>t.length||o<0||p>this.length)throw new RangeError("out of range index");if(o>=p&&e>=n)return 0;if(o>=p)return-1;if(e>=n)return 1;if(this===t)return 0;for(var M=(p>>>=0)-(o>>>=0),b=(n>>>=0)-(e>>>=0),c=Math.min(M,b),z=this.slice(o,p),a=t.slice(e,n),i=0;ip)&&(n=p),t.length>0&&(n<0||e<0)||e>this.length)throw new RangeError("Attempt to write outside buffer bounds");o||(o="utf8");for(var M=!1;;)switch(o){case"hex":return q(this,t,e,n);case"utf8":case"utf-8":return h(this,t,e,n);case"ascii":return W(this,t,e,n);case"latin1":case"binary":return v(this,t,e,n);case"base64":return R(this,t,e,n);case"ucs2":case"ucs-2":case"utf16le":case"utf-16le":return m(this,t,e,n);default:if(M)throw new TypeError("Unknown encoding: "+o);o=(""+o).toLowerCase(),M=!0}},r.prototype.toJSON=function(){return{type:"Buffer",data:Array.prototype.slice.call(this._arr||this,0)}};var y=4096;function _(t,e,n){var o="";n=Math.min(t.length,n);for(var p=e;po)&&(n=o);for(var p="",M=e;Mn)throw new RangeError("Trying to access beyond buffer length")}function C(t,e,n,o,p,M){if(!r.isBuffer(t))throw new TypeError('"buffer" argument must be a Buffer instance');if(e>p||et.length)throw new RangeError("Index out of range")}function w(t,e,n,o){e<0&&(e=65535+e+1);for(var p=0,M=Math.min(t.length-n,2);p>>8*(o?p:1-p)}function S(t,e,n,o){e<0&&(e=4294967295+e+1);for(var p=0,M=Math.min(t.length-n,4);p>>8*(o?p:3-p)&255}function X(t,e,n,o,p,M){if(n+o>t.length)throw new RangeError("Index out of range");if(n<0)throw new RangeError("Index out of range")}function x(t,e,n,o,M){return M||X(t,0,n,4),p.write(t,e,n,o,23,4),n+4}function k(t,e,n,o,M){return M||X(t,0,n,8),p.write(t,e,n,o,52,8),n+8}r.prototype.slice=function(t,e){var n,o=this.length;if((t=~~t)<0?(t+=o)<0&&(t=0):t>o&&(t=o),(e=void 0===e?o:~~e)<0?(e+=o)<0&&(e=0):e>o&&(e=o),e0&&(p*=256);)o+=this[t+--e]*p;return o},r.prototype.readUInt8=function(t,e){return e||B(t,1,this.length),this[t]},r.prototype.readUInt16LE=function(t,e){return e||B(t,2,this.length),this[t]|this[t+1]<<8},r.prototype.readUInt16BE=function(t,e){return e||B(t,2,this.length),this[t]<<8|this[t+1]},r.prototype.readUInt32LE=function(t,e){return e||B(t,4,this.length),(this[t]|this[t+1]<<8|this[t+2]<<16)+16777216*this[t+3]},r.prototype.readUInt32BE=function(t,e){return e||B(t,4,this.length),16777216*this[t]+(this[t+1]<<16|this[t+2]<<8|this[t+3])},r.prototype.readIntLE=function(t,e,n){t|=0,e|=0,n||B(t,e,this.length);for(var o=this[t],p=1,M=0;++M=(p*=128)&&(o-=Math.pow(2,8*e)),o},r.prototype.readIntBE=function(t,e,n){t|=0,e|=0,n||B(t,e,this.length);for(var o=e,p=1,M=this[t+--o];o>0&&(p*=256);)M+=this[t+--o]*p;return M>=(p*=128)&&(M-=Math.pow(2,8*e)),M},r.prototype.readInt8=function(t,e){return e||B(t,1,this.length),128&this[t]?-1*(255-this[t]+1):this[t]},r.prototype.readInt16LE=function(t,e){e||B(t,2,this.length);var n=this[t]|this[t+1]<<8;return 32768&n?4294901760|n:n},r.prototype.readInt16BE=function(t,e){e||B(t,2,this.length);var n=this[t+1]|this[t]<<8;return 32768&n?4294901760|n:n},r.prototype.readInt32LE=function(t,e){return e||B(t,4,this.length),this[t]|this[t+1]<<8|this[t+2]<<16|this[t+3]<<24},r.prototype.readInt32BE=function(t,e){return e||B(t,4,this.length),this[t]<<24|this[t+1]<<16|this[t+2]<<8|this[t+3]},r.prototype.readFloatLE=function(t,e){return e||B(t,4,this.length),p.read(this,t,!0,23,4)},r.prototype.readFloatBE=function(t,e){return e||B(t,4,this.length),p.read(this,t,!1,23,4)},r.prototype.readDoubleLE=function(t,e){return e||B(t,8,this.length),p.read(this,t,!0,52,8)},r.prototype.readDoubleBE=function(t,e){return e||B(t,8,this.length),p.read(this,t,!1,52,8)},r.prototype.writeUIntLE=function(t,e,n,o){(t=+t,e|=0,n|=0,o)||C(this,t,e,n,Math.pow(2,8*n)-1,0);var p=1,M=0;for(this[e]=255&t;++M=0&&(M*=256);)this[e+p]=t/M&255;return e+n},r.prototype.writeUInt8=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,1,255,0),r.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),this[e]=255&t,e+1},r.prototype.writeUInt16LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,65535,0),r.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):w(this,t,e,!0),e+2},r.prototype.writeUInt16BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,65535,0),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):w(this,t,e,!1),e+2},r.prototype.writeUInt32LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,4294967295,0),r.TYPED_ARRAY_SUPPORT?(this[e+3]=t>>>24,this[e+2]=t>>>16,this[e+1]=t>>>8,this[e]=255&t):S(this,t,e,!0),e+4},r.prototype.writeUInt32BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,4294967295,0),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):S(this,t,e,!1),e+4},r.prototype.writeIntLE=function(t,e,n,o){if(t=+t,e|=0,!o){var p=Math.pow(2,8*n-1);C(this,t,e,n,p-1,-p)}var M=0,b=1,c=0;for(this[e]=255&t;++M>0)-c&255;return e+n},r.prototype.writeIntBE=function(t,e,n,o){if(t=+t,e|=0,!o){var p=Math.pow(2,8*n-1);C(this,t,e,n,p-1,-p)}var M=n-1,b=1,c=0;for(this[e+M]=255&t;--M>=0&&(b*=256);)t<0&&0===c&&0!==this[e+M+1]&&(c=1),this[e+M]=(t/b>>0)-c&255;return e+n},r.prototype.writeInt8=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,1,127,-128),r.TYPED_ARRAY_SUPPORT||(t=Math.floor(t)),t<0&&(t=255+t+1),this[e]=255&t,e+1},r.prototype.writeInt16LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,32767,-32768),r.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8):w(this,t,e,!0),e+2},r.prototype.writeInt16BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,2,32767,-32768),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>8,this[e+1]=255&t):w(this,t,e,!1),e+2},r.prototype.writeInt32LE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,2147483647,-2147483648),r.TYPED_ARRAY_SUPPORT?(this[e]=255&t,this[e+1]=t>>>8,this[e+2]=t>>>16,this[e+3]=t>>>24):S(this,t,e,!0),e+4},r.prototype.writeInt32BE=function(t,e,n){return t=+t,e|=0,n||C(this,t,e,4,2147483647,-2147483648),t<0&&(t=4294967295+t+1),r.TYPED_ARRAY_SUPPORT?(this[e]=t>>>24,this[e+1]=t>>>16,this[e+2]=t>>>8,this[e+3]=255&t):S(this,t,e,!1),e+4},r.prototype.writeFloatLE=function(t,e,n){return x(this,t,e,!0,n)},r.prototype.writeFloatBE=function(t,e,n){return x(this,t,e,!1,n)},r.prototype.writeDoubleLE=function(t,e,n){return k(this,t,e,!0,n)},r.prototype.writeDoubleBE=function(t,e,n){return k(this,t,e,!1,n)},r.prototype.copy=function(t,e,n,o){if(n||(n=0),o||0===o||(o=this.length),e>=t.length&&(e=t.length),e||(e=0),o>0&&o=this.length)throw new RangeError("sourceStart out of bounds");if(o<0)throw new RangeError("sourceEnd out of bounds");o>this.length&&(o=this.length),t.length-e=0;--p)t[p+e]=this[p+n];else if(M<1e3||!r.TYPED_ARRAY_SUPPORT)for(p=0;p>>=0,n=void 0===n?this.length:n>>>0,t||(t=0),"number"==typeof t)for(M=e;M55295&&n<57344){if(!p){if(n>56319){(e-=3)>-1&&M.push(239,191,189);continue}if(b+1===o){(e-=3)>-1&&M.push(239,191,189);continue}p=n;continue}if(n<56320){(e-=3)>-1&&M.push(239,191,189),p=n;continue}n=65536+(p-55296<<10|n-56320)}else p&&(e-=3)>-1&&M.push(239,191,189);if(p=null,n<128){if((e-=1)<0)break;M.push(n)}else if(n<2048){if((e-=2)<0)break;M.push(n>>6|192,63&n|128)}else if(n<65536){if((e-=3)<0)break;M.push(n>>12|224,n>>6&63|128,63&n|128)}else{if(!(n<1114112))throw new Error("Invalid code point");if((e-=4)<0)break;M.push(n>>18|240,n>>12&63|128,n>>6&63|128,63&n|128)}}return M}function U(t){return o.toByteArray(function(t){if((t=function(t){return t.trim?t.trim():t.replace(/^\s+|\s+$/g,"")}(t).replace(I,"")).length<2)return"";for(;t.length%4!=0;)t+="=";return t}(t))}function j(t,e,n,o){for(var p=0;p=e.length||p>=t.length);++p)e[p+n]=t[p];return p}},640:(t,e,n)=>{"use strict";var o=n(1742),p={"text/plain":"Text","text/html":"Url",default:"Text"};t.exports=function(t,e){var n,M,b,c,r,z=!1;e||(e={}),e.debug;try{if(M=o(),b=document.createRange(),c=document.getSelection(),(r=document.createElement("span")).textContent=t,r.ariaHidden="true",r.style.all="unset",r.style.position="fixed",r.style.top=0,r.style.clip="rect(0, 0, 0, 0)",r.style.whiteSpace="pre",r.style.webkitUserSelect="text",r.style.MozUserSelect="text",r.style.msUserSelect="text",r.style.userSelect="text",r.addEventListener("copy",(function(n){if(n.stopPropagation(),e.format)if(n.preventDefault(),void 0===n.clipboardData){window.clipboardData.clearData();var o=p[e.format]||p.default;window.clipboardData.setData(o,t)}else n.clipboardData.clearData(),n.clipboardData.setData(e.format,t);e.onCopy&&(n.preventDefault(),e.onCopy(n.clipboardData))})),document.body.appendChild(r),b.selectNodeContents(r),c.addRange(b),!document.execCommand("copy"))throw new Error("copy command was unsuccessful");z=!0}catch(o){try{window.clipboardData.setData(e.format||"text",t),e.onCopy&&e.onCopy(window.clipboardData),z=!0}catch(o){n=function(t){var e=(/mac os x/i.test(navigator.userAgent)?"⌘":"Ctrl")+"+C";return t.replace(/#{\s*key\s*}/g,e)}("message"in e?e.message:"Copy to clipboard: #{key}, Enter"),window.prompt(n,t)}}finally{c&&("function"==typeof c.removeRange?c.removeRange(b):c.removeAllRanges()),r&&document.body.removeChild(r),M()}return z}},645:(t,e)=>{e.read=function(t,e,n,o,p){var M,b,c=8*p-o-1,r=(1<>1,a=-7,i=n?p-1:0,O=n?-1:1,s=t[e+i];for(i+=O,M=s&(1<<-a)-1,s>>=-a,a+=c;a>0;M=256*M+t[e+i],i+=O,a-=8);for(b=M&(1<<-a)-1,M>>=-a,a+=o;a>0;b=256*b+t[e+i],i+=O,a-=8);if(0===M)M=1-z;else{if(M===r)return b?NaN:1/0*(s?-1:1);b+=Math.pow(2,o),M-=z}return(s?-1:1)*b*Math.pow(2,M-o)},e.write=function(t,e,n,o,p,M){var b,c,r,z=8*M-p-1,a=(1<>1,O=23===p?Math.pow(2,-24)-Math.pow(2,-77):0,s=o?0:M-1,A=o?1:-1,u=e<0||0===e&&1/e<0?1:0;for(e=Math.abs(e),isNaN(e)||e===1/0?(c=isNaN(e)?1:0,b=a):(b=Math.floor(Math.log(e)/Math.LN2),e*(r=Math.pow(2,-b))<1&&(b--,r*=2),(e+=b+i>=1?O/r:O*Math.pow(2,1-i))*r>=2&&(b++,r/=2),b+i>=a?(c=0,b=a):b+i>=1?(c=(e*r-1)*Math.pow(2,p),b+=i):(c=e*Math.pow(2,i-1)*Math.pow(2,p),b=0));p>=8;t[n+s]=255&c,s+=A,c/=256,p-=8);for(b=b<0;t[n+s]=255&b,s+=A,b/=256,z-=8);t[n+s-A]|=128*u}},5826:t=>{var e={}.toString;t.exports=Array.isArray||function(t){return"[object Array]"==e.call(t)}},9755:function(t,e){var n;!function(e,n){"use strict";"object"==typeof t.exports?t.exports=e.document?n(e,!0):function(t){if(!t.document)throw new Error("jQuery requires a window with a document");return n(t)}:n(e)}("undefined"!=typeof window?window:this,(function(o,p){"use strict";var M=[],b=Object.getPrototypeOf,c=M.slice,r=M.flat?function(t){return M.flat.call(t)}:function(t){return M.concat.apply([],t)},z=M.push,a=M.indexOf,i={},O=i.toString,s=i.hasOwnProperty,A=s.toString,u=A.call(Object),l={},d=function(t){return"function"==typeof t&&"number"!=typeof t.nodeType&&"function"!=typeof t.item},f=function(t){return null!=t&&t===t.window},q=o.document,h={type:!0,src:!0,nonce:!0,noModule:!0};function W(t,e,n){var o,p,M=(n=n||q).createElement("script");if(M.text=t,e)for(o in h)(p=e[o]||e.getAttribute&&e.getAttribute(o))&&M.setAttribute(o,p);n.head.appendChild(M).parentNode.removeChild(M)}function v(t){return null==t?t+"":"object"==typeof t||"function"==typeof t?i[O.call(t)]||"object":typeof t}var R="3.7.1",m=/HTML$/i,g=function(t,e){return new g.fn.init(t,e)};function L(t){var e=!!t&&"length"in t&&t.length,n=v(t);return!d(t)&&!f(t)&&("array"===n||0===e||"number"==typeof e&&e>0&&e-1 in t)}function y(t,e){return t.nodeName&&t.nodeName.toLowerCase()===e.toLowerCase()}g.fn=g.prototype={jquery:R,constructor:g,length:0,toArray:function(){return c.call(this)},get:function(t){return null==t?c.call(this):t<0?this[t+this.length]:this[t]},pushStack:function(t){var e=g.merge(this.constructor(),t);return e.prevObject=this,e},each:function(t){return g.each(this,t)},map:function(t){return this.pushStack(g.map(this,(function(e,n){return t.call(e,n,e)})))},slice:function(){return this.pushStack(c.apply(this,arguments))},first:function(){return this.eq(0)},last:function(){return this.eq(-1)},even:function(){return this.pushStack(g.grep(this,(function(t,e){return(e+1)%2})))},odd:function(){return this.pushStack(g.grep(this,(function(t,e){return e%2})))},eq:function(t){var e=this.length,n=+t+(t<0?e:0);return this.pushStack(n>=0&&n+~]|"+T+")"+T+"*"),P=new RegExp(T+"|>"),U=new RegExp(x),j=new RegExp("^"+C+"$"),F={ID:new RegExp("^#("+C+")"),CLASS:new RegExp("^\\.("+C+")"),TAG:new RegExp("^("+C+"|[*])"),ATTR:new RegExp("^"+w),PSEUDO:new RegExp("^"+x),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+T+"*(even|odd|(([+-]|)(\\d*)n|)"+T+"*(?:([+-]|)"+T+"*(\\d+)|))"+T+"*\\)|)","i"),bool:new RegExp("^(?:"+L+")$","i"),needsContext:new RegExp("^"+T+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+T+"*((?:-\\d)?\\d*)"+T+"*\\)|)(?=[^-]|$)","i")},H=/^(?:input|select|textarea|button)$/i,G=/^h\d$/i,Y=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,$=/[+~]/,V=new RegExp("\\\\[\\da-fA-F]{1,6}"+T+"?|\\\\([^\\r\\n\\f])","g"),K=function(t,e){var n="0x"+t.slice(1)-65536;return e||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},Z=function(){rt()},Q=Ot((function(t){return!0===t.disabled&&y(t,"fieldset")}),{dir:"parentNode",next:"legend"});try{u.apply(M=c.call(S.childNodes),S.childNodes),M[S.childNodes.length].nodeType}catch(t){u={apply:function(t,e){X.apply(t,c.call(e))},call:function(t){X.apply(t,c.call(arguments,1))}}}function J(t,e,n,o){var p,M,b,c,z,a,s,A=e&&e.ownerDocument,f=e?e.nodeType:9;if(n=n||[],"string"!=typeof t||!t||1!==f&&9!==f&&11!==f)return n;if(!o&&(rt(e),e=e||r,i)){if(11!==f&&(z=Y.exec(t)))if(p=z[1]){if(9===f){if(!(b=e.getElementById(p)))return n;if(b.id===p)return u.call(n,b),n}else if(A&&(b=A.getElementById(p))&&J.contains(e,b)&&b.id===p)return u.call(n,b),n}else{if(z[2])return u.apply(n,e.getElementsByTagName(t)),n;if((p=z[3])&&e.getElementsByClassName)return u.apply(n,e.getElementsByClassName(p)),n}if(!(R[t+" "]||O&&O.test(t))){if(s=t,A=e,1===f&&(P.test(t)||D.test(t))){for((A=$.test(t)&&ct(e.parentNode)||e)==e&&l.scope||((c=e.getAttribute("id"))?c=g.escapeSelector(c):e.setAttribute("id",c=d)),M=(a=at(t)).length;M--;)a[M]=(c?"#"+c:":scope")+" "+it(a[M]);s=a.join(",")}try{return u.apply(n,A.querySelectorAll(s)),n}catch(e){R(t,!0)}finally{c===d&&e.removeAttribute("id")}}}return ft(t.replace(B,"$1"),e,n,o)}function tt(){var t=[];return function n(o,p){return t.push(o+" ")>e.cacheLength&&delete n[t.shift()],n[o+" "]=p}}function et(t){return t[d]=!0,t}function nt(t){var e=r.createElement("fieldset");try{return!!t(e)}catch(t){return!1}finally{e.parentNode&&e.parentNode.removeChild(e),e=null}}function ot(t){return function(e){return y(e,"input")&&e.type===t}}function pt(t){return function(e){return(y(e,"input")||y(e,"button"))&&e.type===t}}function Mt(t){return function(e){return"form"in e?e.parentNode&&!1===e.disabled?"label"in e?"label"in e.parentNode?e.parentNode.disabled===t:e.disabled===t:e.isDisabled===t||e.isDisabled!==!t&&Q(e)===t:e.disabled===t:"label"in e&&e.disabled===t}}function bt(t){return et((function(e){return e=+e,et((function(n,o){for(var p,M=t([],n.length,e),b=M.length;b--;)n[p=M[b]]&&(n[p]=!(o[p]=n[p]))}))}))}function ct(t){return t&&void 0!==t.getElementsByTagName&&t}function rt(t){var n,o=t?t.ownerDocument||t:S;return o!=r&&9===o.nodeType&&o.documentElement?(z=(r=o).documentElement,i=!g.isXMLDoc(r),A=z.matches||z.webkitMatchesSelector||z.msMatchesSelector,z.msMatchesSelector&&S!=r&&(n=r.defaultView)&&n.top!==n&&n.addEventListener("unload",Z),l.getById=nt((function(t){return z.appendChild(t).id=g.expando,!r.getElementsByName||!r.getElementsByName(g.expando).length})),l.disconnectedMatch=nt((function(t){return A.call(t,"*")})),l.scope=nt((function(){return r.querySelectorAll(":scope")})),l.cssHas=nt((function(){try{return r.querySelector(":has(*,:jqfake)"),!1}catch(t){return!0}})),l.getById?(e.filter.ID=function(t){var e=t.replace(V,K);return function(t){return t.getAttribute("id")===e}},e.find.ID=function(t,e){if(void 0!==e.getElementById&&i){var n=e.getElementById(t);return n?[n]:[]}}):(e.filter.ID=function(t){var e=t.replace(V,K);return function(t){var n=void 0!==t.getAttributeNode&&t.getAttributeNode("id");return n&&n.value===e}},e.find.ID=function(t,e){if(void 0!==e.getElementById&&i){var n,o,p,M=e.getElementById(t);if(M){if((n=M.getAttributeNode("id"))&&n.value===t)return[M];for(p=e.getElementsByName(t),o=0;M=p[o++];)if((n=M.getAttributeNode("id"))&&n.value===t)return[M]}return[]}}),e.find.TAG=function(t,e){return void 0!==e.getElementsByTagName?e.getElementsByTagName(t):e.querySelectorAll(t)},e.find.CLASS=function(t,e){if(void 0!==e.getElementsByClassName&&i)return e.getElementsByClassName(t)},O=[],nt((function(t){var e;z.appendChild(t).innerHTML="",t.querySelectorAll("[selected]").length||O.push("\\["+T+"*(?:value|"+L+")"),t.querySelectorAll("[id~="+d+"-]").length||O.push("~="),t.querySelectorAll("a#"+d+"+*").length||O.push(".#.+[+~]"),t.querySelectorAll(":checked").length||O.push(":checked"),(e=r.createElement("input")).setAttribute("type","hidden"),t.appendChild(e).setAttribute("name","D"),z.appendChild(t).disabled=!0,2!==t.querySelectorAll(":disabled").length&&O.push(":enabled",":disabled"),(e=r.createElement("input")).setAttribute("name",""),t.appendChild(e),t.querySelectorAll("[name='']").length||O.push("\\["+T+"*name"+T+"*="+T+"*(?:''|\"\")")})),l.cssHas||O.push(":has"),O=O.length&&new RegExp(O.join("|")),m=function(t,e){if(t===e)return b=!0,0;var n=!t.compareDocumentPosition-!e.compareDocumentPosition;return n||(1&(n=(t.ownerDocument||t)==(e.ownerDocument||e)?t.compareDocumentPosition(e):1)||!l.sortDetached&&e.compareDocumentPosition(t)===n?t===r||t.ownerDocument==S&&J.contains(S,t)?-1:e===r||e.ownerDocument==S&&J.contains(S,e)?1:p?a.call(p,t)-a.call(p,e):0:4&n?-1:1)},r):r}for(t in J.matches=function(t,e){return J(t,null,null,e)},J.matchesSelector=function(t,e){if(rt(t),i&&!R[e+" "]&&(!O||!O.test(e)))try{var n=A.call(t,e);if(n||l.disconnectedMatch||t.document&&11!==t.document.nodeType)return n}catch(t){R(e,!0)}return J(e,r,null,[t]).length>0},J.contains=function(t,e){return(t.ownerDocument||t)!=r&&rt(t),g.contains(t,e)},J.attr=function(t,n){(t.ownerDocument||t)!=r&&rt(t);var o=e.attrHandle[n.toLowerCase()],p=o&&s.call(e.attrHandle,n.toLowerCase())?o(t,n,!i):void 0;return void 0!==p?p:t.getAttribute(n)},J.error=function(t){throw new Error("Syntax error, unrecognized expression: "+t)},g.uniqueSort=function(t){var e,n=[],o=0,M=0;if(b=!l.sortStable,p=!l.sortStable&&c.call(t,0),N.call(t,m),b){for(;e=t[M++];)e===t[M]&&(o=n.push(M));for(;o--;)E.call(t,n[o],1)}return p=null,t},g.fn.uniqueSort=function(){return this.pushStack(g.uniqueSort(c.apply(this)))},e=g.expr={cacheLength:50,createPseudo:et,match:F,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(t){return t[1]=t[1].replace(V,K),t[3]=(t[3]||t[4]||t[5]||"").replace(V,K),"~="===t[2]&&(t[3]=" "+t[3]+" "),t.slice(0,4)},CHILD:function(t){return t[1]=t[1].toLowerCase(),"nth"===t[1].slice(0,3)?(t[3]||J.error(t[0]),t[4]=+(t[4]?t[5]+(t[6]||1):2*("even"===t[3]||"odd"===t[3])),t[5]=+(t[7]+t[8]||"odd"===t[3])):t[3]&&J.error(t[0]),t},PSEUDO:function(t){var e,n=!t[6]&&t[2];return F.CHILD.test(t[0])?null:(t[3]?t[2]=t[4]||t[5]||"":n&&U.test(n)&&(e=at(n,!0))&&(e=n.indexOf(")",n.length-e)-n.length)&&(t[0]=t[0].slice(0,e),t[2]=n.slice(0,e)),t.slice(0,3))}},filter:{TAG:function(t){var e=t.replace(V,K).toLowerCase();return"*"===t?function(){return!0}:function(t){return y(t,e)}},CLASS:function(t){var e=h[t+" "];return e||(e=new RegExp("(^|"+T+")"+t+"("+T+"|$)"))&&h(t,(function(t){return e.test("string"==typeof t.className&&t.className||void 0!==t.getAttribute&&t.getAttribute("class")||"")}))},ATTR:function(t,e,n){return function(o){var p=J.attr(o,t);return null==p?"!="===e:!e||(p+="","="===e?p===n:"!="===e?p!==n:"^="===e?n&&0===p.indexOf(n):"*="===e?n&&p.indexOf(n)>-1:"$="===e?n&&p.slice(-n.length)===n:"~="===e?(" "+p.replace(k," ")+" ").indexOf(n)>-1:"|="===e&&(p===n||p.slice(0,n.length+1)===n+"-"))}},CHILD:function(t,e,n,o,p){var M="nth"!==t.slice(0,3),b="last"!==t.slice(-4),c="of-type"===e;return 1===o&&0===p?function(t){return!!t.parentNode}:function(e,n,r){var z,a,i,O,s,A=M!==b?"nextSibling":"previousSibling",u=e.parentNode,l=c&&e.nodeName.toLowerCase(),q=!r&&!c,h=!1;if(u){if(M){for(;A;){for(i=e;i=i[A];)if(c?y(i,l):1===i.nodeType)return!1;s=A="only"===t&&!s&&"nextSibling"}return!0}if(s=[b?u.firstChild:u.lastChild],b&&q){for(h=(O=(z=(a=u[d]||(u[d]={}))[t]||[])[0]===f&&z[1])&&z[2],i=O&&u.childNodes[O];i=++O&&i&&i[A]||(h=O=0)||s.pop();)if(1===i.nodeType&&++h&&i===e){a[t]=[f,O,h];break}}else if(q&&(h=O=(z=(a=e[d]||(e[d]={}))[t]||[])[0]===f&&z[1]),!1===h)for(;(i=++O&&i&&i[A]||(h=O=0)||s.pop())&&(!(c?y(i,l):1===i.nodeType)||!++h||(q&&((a=i[d]||(i[d]={}))[t]=[f,h]),i!==e)););return(h-=p)===o||h%o==0&&h/o>=0}}},PSEUDO:function(t,n){var o,p=e.pseudos[t]||e.setFilters[t.toLowerCase()]||J.error("unsupported pseudo: "+t);return p[d]?p(n):p.length>1?(o=[t,t,"",n],e.setFilters.hasOwnProperty(t.toLowerCase())?et((function(t,e){for(var o,M=p(t,n),b=M.length;b--;)t[o=a.call(t,M[b])]=!(e[o]=M[b])})):function(t){return p(t,0,o)}):p}},pseudos:{not:et((function(t){var e=[],n=[],o=dt(t.replace(B,"$1"));return o[d]?et((function(t,e,n,p){for(var M,b=o(t,null,p,[]),c=t.length;c--;)(M=b[c])&&(t[c]=!(e[c]=M))})):function(t,p,M){return e[0]=t,o(e,null,M,n),e[0]=null,!n.pop()}})),has:et((function(t){return function(e){return J(t,e).length>0}})),contains:et((function(t){return t=t.replace(V,K),function(e){return(e.textContent||g.text(e)).indexOf(t)>-1}})),lang:et((function(t){return j.test(t||"")||J.error("unsupported lang: "+t),t=t.replace(V,K).toLowerCase(),function(e){var n;do{if(n=i?e.lang:e.getAttribute("xml:lang")||e.getAttribute("lang"))return(n=n.toLowerCase())===t||0===n.indexOf(t+"-")}while((e=e.parentNode)&&1===e.nodeType);return!1}})),target:function(t){var e=o.location&&o.location.hash;return e&&e.slice(1)===t.id},root:function(t){return t===z},focus:function(t){return t===function(){try{return r.activeElement}catch(t){}}()&&r.hasFocus()&&!!(t.type||t.href||~t.tabIndex)},enabled:Mt(!1),disabled:Mt(!0),checked:function(t){return y(t,"input")&&!!t.checked||y(t,"option")&&!!t.selected},selected:function(t){return t.parentNode&&t.parentNode.selectedIndex,!0===t.selected},empty:function(t){for(t=t.firstChild;t;t=t.nextSibling)if(t.nodeType<6)return!1;return!0},parent:function(t){return!e.pseudos.empty(t)},header:function(t){return G.test(t.nodeName)},input:function(t){return H.test(t.nodeName)},button:function(t){return y(t,"input")&&"button"===t.type||y(t,"button")},text:function(t){var e;return y(t,"input")&&"text"===t.type&&(null==(e=t.getAttribute("type"))||"text"===e.toLowerCase())},first:bt((function(){return[0]})),last:bt((function(t,e){return[e-1]})),eq:bt((function(t,e,n){return[n<0?n+e:n]})),even:bt((function(t,e){for(var n=0;ne?e:n;--o>=0;)t.push(o);return t})),gt:bt((function(t,e,n){for(var o=n<0?n+e:n;++o1?function(e,n,o){for(var p=t.length;p--;)if(!t[p](e,n,o))return!1;return!0}:t[0]}function At(t,e,n,o,p){for(var M,b=[],c=0,r=t.length,z=null!=e;c-1&&(M[z]=!(b[z]=O))}}else s=At(s===b?s.splice(d,s.length):s),p?p(null,b,s,r):u.apply(b,s)}))}function lt(t){for(var o,p,M,b=t.length,c=e.relative[t[0].type],r=c||e.relative[" "],z=c?1:0,i=Ot((function(t){return t===o}),r,!0),O=Ot((function(t){return a.call(o,t)>-1}),r,!0),s=[function(t,e,p){var M=!c&&(p||e!=n)||((o=e).nodeType?i(t,e,p):O(t,e,p));return o=null,M}];z1&&st(s),z>1&&it(t.slice(0,z-1).concat({value:" "===t[z-2].type?"*":""})).replace(B,"$1"),p,z0,M=t.length>0,b=function(b,c,z,a,O){var s,A,l,d=0,q="0",h=b&&[],W=[],v=n,R=b||M&&e.find.TAG("*",O),m=f+=null==v?1:Math.random()||.1,L=R.length;for(O&&(n=c==r||c||O);q!==L&&null!=(s=R[q]);q++){if(M&&s){for(A=0,c||s.ownerDocument==r||(rt(s),z=!i);l=t[A++];)if(l(s,c||r,z)){u.call(a,s);break}O&&(f=m)}p&&((s=!l&&s)&&d--,b&&h.push(s))}if(d+=q,p&&q!==d){for(A=0;l=o[A++];)l(h,W,c,z);if(b){if(d>0)for(;q--;)h[q]||W[q]||(W[q]=_.call(a));W=At(W)}u.apply(a,W),O&&!b&&W.length>0&&d+o.length>1&&g.uniqueSort(a)}return O&&(f=m,n=v),h};return p?et(b):b}(b,M)),c.selector=t}return c}function ft(t,n,o,p){var M,b,c,r,z,a="function"==typeof t&&t,O=!p&&at(t=a.selector||t);if(o=o||[],1===O.length){if((b=O[0]=O[0].slice(0)).length>2&&"ID"===(c=b[0]).type&&9===n.nodeType&&i&&e.relative[b[1].type]){if(!(n=(e.find.ID(c.matches[0].replace(V,K),n)||[])[0]))return o;a&&(n=n.parentNode),t=t.slice(b.shift().value.length)}for(M=F.needsContext.test(t)?0:b.length;M--&&(c=b[M],!e.relative[r=c.type]);)if((z=e.find[r])&&(p=z(c.matches[0].replace(V,K),$.test(b[0].type)&&ct(n.parentNode)||n))){if(b.splice(M,1),!(t=p.length&&it(b)))return u.apply(o,p),o;break}}return(a||dt(t,O))(p,n,!i,o,!n||$.test(t)&&ct(n.parentNode)||n),o}zt.prototype=e.filters=e.pseudos,e.setFilters=new zt,l.sortStable=d.split("").sort(m).join("")===d,rt(),l.sortDetached=nt((function(t){return 1&t.compareDocumentPosition(r.createElement("fieldset"))})),g.find=J,g.expr[":"]=g.expr.pseudos,g.unique=g.uniqueSort,J.compile=dt,J.select=ft,J.setDocument=rt,J.tokenize=at,J.escape=g.escapeSelector,J.getText=g.text,J.isXML=g.isXMLDoc,J.selectors=g.expr,J.support=g.support,J.uniqueSort=g.uniqueSort}();var x=function(t,e,n){for(var o=[],p=void 0!==n;(t=t[e])&&9!==t.nodeType;)if(1===t.nodeType){if(p&&g(t).is(n))break;o.push(t)}return o},k=function(t,e){for(var n=[];t;t=t.nextSibling)1===t.nodeType&&t!==e&&n.push(t);return n},I=g.expr.match.needsContext,D=/^<([a-z][^\/\0>:\x20\t\r\n\f]*)[\x20\t\r\n\f]*\/?>(?:<\/\1>|)$/i;function P(t,e,n){return d(e)?g.grep(t,(function(t,o){return!!e.call(t,o,t)!==n})):e.nodeType?g.grep(t,(function(t){return t===e!==n})):"string"!=typeof e?g.grep(t,(function(t){return a.call(e,t)>-1!==n})):g.filter(e,t,n)}g.filter=function(t,e,n){var o=e[0];return n&&(t=":not("+t+")"),1===e.length&&1===o.nodeType?g.find.matchesSelector(o,t)?[o]:[]:g.find.matches(t,g.grep(e,(function(t){return 1===t.nodeType})))},g.fn.extend({find:function(t){var e,n,o=this.length,p=this;if("string"!=typeof t)return this.pushStack(g(t).filter((function(){for(e=0;e1?g.uniqueSort(n):n},filter:function(t){return this.pushStack(P(this,t||[],!1))},not:function(t){return this.pushStack(P(this,t||[],!0))},is:function(t){return!!P(this,"string"==typeof t&&I.test(t)?g(t):t||[],!1).length}});var U,j=/^(?:\s*(<[\w\W]+>)[^>]*|#([\w-]+))$/;(g.fn.init=function(t,e,n){var o,p;if(!t)return this;if(n=n||U,"string"==typeof t){if(!(o="<"===t[0]&&">"===t[t.length-1]&&t.length>=3?[null,t,null]:j.exec(t))||!o[1]&&e)return!e||e.jquery?(e||n).find(t):this.constructor(e).find(t);if(o[1]){if(e=e instanceof g?e[0]:e,g.merge(this,g.parseHTML(o[1],e&&e.nodeType?e.ownerDocument||e:q,!0)),D.test(o[1])&&g.isPlainObject(e))for(o in e)d(this[o])?this[o](e[o]):this.attr(o,e[o]);return this}return(p=q.getElementById(o[2]))&&(this[0]=p,this.length=1),this}return t.nodeType?(this[0]=t,this.length=1,this):d(t)?void 0!==n.ready?n.ready(t):t(g):g.makeArray(t,this)}).prototype=g.fn,U=g(q);var F=/^(?:parents|prev(?:Until|All))/,H={children:!0,contents:!0,next:!0,prev:!0};function G(t,e){for(;(t=t[e])&&1!==t.nodeType;);return t}g.fn.extend({has:function(t){var e=g(t,this),n=e.length;return this.filter((function(){for(var t=0;t-1:1===n.nodeType&&g.find.matchesSelector(n,t))){M.push(n);break}return this.pushStack(M.length>1?g.uniqueSort(M):M)},index:function(t){return t?"string"==typeof t?a.call(g(t),this[0]):a.call(this,t.jquery?t[0]:t):this[0]&&this[0].parentNode?this.first().prevAll().length:-1},add:function(t,e){return this.pushStack(g.uniqueSort(g.merge(this.get(),g(t,e))))},addBack:function(t){return this.add(null==t?this.prevObject:this.prevObject.filter(t))}}),g.each({parent:function(t){var e=t.parentNode;return e&&11!==e.nodeType?e:null},parents:function(t){return x(t,"parentNode")},parentsUntil:function(t,e,n){return x(t,"parentNode",n)},next:function(t){return G(t,"nextSibling")},prev:function(t){return G(t,"previousSibling")},nextAll:function(t){return x(t,"nextSibling")},prevAll:function(t){return x(t,"previousSibling")},nextUntil:function(t,e,n){return x(t,"nextSibling",n)},prevUntil:function(t,e,n){return x(t,"previousSibling",n)},siblings:function(t){return k((t.parentNode||{}).firstChild,t)},children:function(t){return k(t.firstChild)},contents:function(t){return null!=t.contentDocument&&b(t.contentDocument)?t.contentDocument:(y(t,"template")&&(t=t.content||t),g.merge([],t.childNodes))}},(function(t,e){g.fn[t]=function(n,o){var p=g.map(this,e,n);return"Until"!==t.slice(-5)&&(o=n),o&&"string"==typeof o&&(p=g.filter(o,p)),this.length>1&&(H[t]||g.uniqueSort(p),F.test(t)&&p.reverse()),this.pushStack(p)}}));var Y=/[^\x20\t\r\n\f]+/g;function $(t){return t}function V(t){throw t}function K(t,e,n,o){var p;try{t&&d(p=t.promise)?p.call(t).done(e).fail(n):t&&d(p=t.then)?p.call(t,e,n):e.apply(void 0,[t].slice(o))}catch(t){n.apply(void 0,[t])}}g.Callbacks=function(t){t="string"==typeof t?function(t){var e={};return g.each(t.match(Y)||[],(function(t,n){e[n]=!0})),e}(t):g.extend({},t);var e,n,o,p,M=[],b=[],c=-1,r=function(){for(p=p||t.once,o=e=!0;b.length;c=-1)for(n=b.shift();++c-1;)M.splice(n,1),n<=c&&c--})),this},has:function(t){return t?g.inArray(t,M)>-1:M.length>0},empty:function(){return M&&(M=[]),this},disable:function(){return p=b=[],M=n="",this},disabled:function(){return!M},lock:function(){return p=b=[],n||e||(M=n=""),this},locked:function(){return!!p},fireWith:function(t,n){return p||(n=[t,(n=n||[]).slice?n.slice():n],b.push(n),e||r()),this},fire:function(){return z.fireWith(this,arguments),this},fired:function(){return!!o}};return z},g.extend({Deferred:function(t){var e=[["notify","progress",g.Callbacks("memory"),g.Callbacks("memory"),2],["resolve","done",g.Callbacks("once memory"),g.Callbacks("once memory"),0,"resolved"],["reject","fail",g.Callbacks("once memory"),g.Callbacks("once memory"),1,"rejected"]],n="pending",p={state:function(){return n},always:function(){return M.done(arguments).fail(arguments),this},catch:function(t){return p.then(null,t)},pipe:function(){var t=arguments;return g.Deferred((function(n){g.each(e,(function(e,o){var p=d(t[o[4]])&&t[o[4]];M[o[1]]((function(){var t=p&&p.apply(this,arguments);t&&d(t.promise)?t.promise().progress(n.notify).done(n.resolve).fail(n.reject):n[o[0]+"With"](this,p?[t]:arguments)}))})),t=null})).promise()},then:function(t,n,p){var M=0;function b(t,e,n,p){return function(){var c=this,r=arguments,z=function(){var o,z;if(!(t=M&&(n!==V&&(c=void 0,r=[o]),e.rejectWith(c,r))}};t?a():(g.Deferred.getErrorHook?a.error=g.Deferred.getErrorHook():g.Deferred.getStackHook&&(a.error=g.Deferred.getStackHook()),o.setTimeout(a))}}return g.Deferred((function(o){e[0][3].add(b(0,o,d(p)?p:$,o.notifyWith)),e[1][3].add(b(0,o,d(t)?t:$)),e[2][3].add(b(0,o,d(n)?n:V))})).promise()},promise:function(t){return null!=t?g.extend(t,p):p}},M={};return g.each(e,(function(t,o){var b=o[2],c=o[5];p[o[1]]=b.add,c&&b.add((function(){n=c}),e[3-t][2].disable,e[3-t][3].disable,e[0][2].lock,e[0][3].lock),b.add(o[3].fire),M[o[0]]=function(){return M[o[0]+"With"](this===M?void 0:this,arguments),this},M[o[0]+"With"]=b.fireWith})),p.promise(M),t&&t.call(M,M),M},when:function(t){var e=arguments.length,n=e,o=Array(n),p=c.call(arguments),M=g.Deferred(),b=function(t){return function(n){o[t]=this,p[t]=arguments.length>1?c.call(arguments):n,--e||M.resolveWith(o,p)}};if(e<=1&&(K(t,M.done(b(n)).resolve,M.reject,!e),"pending"===M.state()||d(p[n]&&p[n].then)))return M.then();for(;n--;)K(p[n],b(n),M.reject);return M.promise()}});var Z=/^(Eval|Internal|Range|Reference|Syntax|Type|URI)Error$/;g.Deferred.exceptionHook=function(t,e){o.console&&o.console.warn&&t&&Z.test(t.name)&&o.console.warn("jQuery.Deferred exception: "+t.message,t.stack,e)},g.readyException=function(t){o.setTimeout((function(){throw t}))};var Q=g.Deferred();function J(){q.removeEventListener("DOMContentLoaded",J),o.removeEventListener("load",J),g.ready()}g.fn.ready=function(t){return Q.then(t).catch((function(t){g.readyException(t)})),this},g.extend({isReady:!1,readyWait:1,ready:function(t){(!0===t?--g.readyWait:g.isReady)||(g.isReady=!0,!0!==t&&--g.readyWait>0||Q.resolveWith(q,[g]))}}),g.ready.then=Q.then,"complete"===q.readyState||"loading"!==q.readyState&&!q.documentElement.doScroll?o.setTimeout(g.ready):(q.addEventListener("DOMContentLoaded",J),o.addEventListener("load",J));var tt=function(t,e,n,o,p,M,b){var c=0,r=t.length,z=null==n;if("object"===v(n))for(c in p=!0,n)tt(t,e,c,n[c],!0,M,b);else if(void 0!==o&&(p=!0,d(o)||(b=!0),z&&(b?(e.call(t,o),e=null):(z=e,e=function(t,e,n){return z.call(g(t),n)})),e))for(;c1,null,!0)},removeData:function(t){return this.each((function(){rt.remove(this,t)}))}}),g.extend({queue:function(t,e,n){var o;if(t)return e=(e||"fx")+"queue",o=ct.get(t,e),n&&(!o||Array.isArray(n)?o=ct.access(t,e,g.makeArray(n)):o.push(n)),o||[]},dequeue:function(t,e){e=e||"fx";var n=g.queue(t,e),o=n.length,p=n.shift(),M=g._queueHooks(t,e);"inprogress"===p&&(p=n.shift(),o--),p&&("fx"===e&&n.unshift("inprogress"),delete M.stop,p.call(t,(function(){g.dequeue(t,e)}),M)),!o&&M&&M.empty.fire()},_queueHooks:function(t,e){var n=e+"queueHooks";return ct.get(t,n)||ct.access(t,n,{empty:g.Callbacks("once memory").add((function(){ct.remove(t,[e+"queue",n])}))})}}),g.fn.extend({queue:function(t,e){var n=2;return"string"!=typeof t&&(e=t,t="fx",n--),arguments.length\x20\t\r\n\f]*)/i,yt=/^$|^module$|\/(?:java|ecma)script/i;Rt=q.createDocumentFragment().appendChild(q.createElement("div")),(mt=q.createElement("input")).setAttribute("type","radio"),mt.setAttribute("checked","checked"),mt.setAttribute("name","t"),Rt.appendChild(mt),l.checkClone=Rt.cloneNode(!0).cloneNode(!0).lastChild.checked,Rt.innerHTML="",l.noCloneChecked=!!Rt.cloneNode(!0).lastChild.defaultValue,Rt.innerHTML="",l.option=!!Rt.lastChild;var _t={thead:[1,"","
"],col:[2,"","
"],tr:[2,"","
"],td:[3,"","
"],_default:[0,"",""]};function Nt(t,e){var n;return n=void 0!==t.getElementsByTagName?t.getElementsByTagName(e||"*"):void 0!==t.querySelectorAll?t.querySelectorAll(e||"*"):[],void 0===e||e&&y(t,e)?g.merge([t],n):n}function Et(t,e){for(var n=0,o=t.length;n",""]);var Tt=/<|&#?\w+;/;function Bt(t,e,n,o,p){for(var M,b,c,r,z,a,i=e.createDocumentFragment(),O=[],s=0,A=t.length;s-1)p&&p.push(M);else if(z=lt(M),b=Nt(i.appendChild(M),"script"),z&&Et(b),n)for(a=0;M=b[a++];)yt.test(M.type||"")&&n.push(M);return i}var Ct=/^([^.]*)(?:\.(.+)|)/;function wt(){return!0}function St(){return!1}function Xt(t,e,n,o,p,M){var b,c;if("object"==typeof e){for(c in"string"!=typeof n&&(o=o||n,n=void 0),e)Xt(t,c,n,o,e[c],M);return t}if(null==o&&null==p?(p=n,o=n=void 0):null==p&&("string"==typeof n?(p=o,o=void 0):(p=o,o=n,n=void 0)),!1===p)p=St;else if(!p)return t;return 1===M&&(b=p,p=function(t){return g().off(t),b.apply(this,arguments)},p.guid=b.guid||(b.guid=g.guid++)),t.each((function(){g.event.add(this,e,p,o,n)}))}function xt(t,e,n){n?(ct.set(t,e,!1),g.event.add(t,e,{namespace:!1,handler:function(t){var n,o=ct.get(this,e);if(1&t.isTrigger&&this[e]){if(o)(g.event.special[e]||{}).delegateType&&t.stopPropagation();else if(o=c.call(arguments),ct.set(this,e,o),this[e](),n=ct.get(this,e),ct.set(this,e,!1),o!==n)return t.stopImmediatePropagation(),t.preventDefault(),n}else o&&(ct.set(this,e,g.event.trigger(o[0],o.slice(1),this)),t.stopPropagation(),t.isImmediatePropagationStopped=wt)}})):void 0===ct.get(t,e)&&g.event.add(t,e,wt)}g.event={global:{},add:function(t,e,n,o,p){var M,b,c,r,z,a,i,O,s,A,u,l=ct.get(t);if(Mt(t))for(n.handler&&(n=(M=n).handler,p=M.selector),p&&g.find.matchesSelector(ut,p),n.guid||(n.guid=g.guid++),(r=l.events)||(r=l.events=Object.create(null)),(b=l.handle)||(b=l.handle=function(e){return void 0!==g&&g.event.triggered!==e.type?g.event.dispatch.apply(t,arguments):void 0}),z=(e=(e||"").match(Y)||[""]).length;z--;)s=u=(c=Ct.exec(e[z])||[])[1],A=(c[2]||"").split(".").sort(),s&&(i=g.event.special[s]||{},s=(p?i.delegateType:i.bindType)||s,i=g.event.special[s]||{},a=g.extend({type:s,origType:u,data:o,handler:n,guid:n.guid,selector:p,needsContext:p&&g.expr.match.needsContext.test(p),namespace:A.join(".")},M),(O=r[s])||((O=r[s]=[]).delegateCount=0,i.setup&&!1!==i.setup.call(t,o,A,b)||t.addEventListener&&t.addEventListener(s,b)),i.add&&(i.add.call(t,a),a.handler.guid||(a.handler.guid=n.guid)),p?O.splice(O.delegateCount++,0,a):O.push(a),g.event.global[s]=!0)},remove:function(t,e,n,o,p){var M,b,c,r,z,a,i,O,s,A,u,l=ct.hasData(t)&&ct.get(t);if(l&&(r=l.events)){for(z=(e=(e||"").match(Y)||[""]).length;z--;)if(s=u=(c=Ct.exec(e[z])||[])[1],A=(c[2]||"").split(".").sort(),s){for(i=g.event.special[s]||{},O=r[s=(o?i.delegateType:i.bindType)||s]||[],c=c[2]&&new RegExp("(^|\\.)"+A.join("\\.(?:.*\\.|)")+"(\\.|$)"),b=M=O.length;M--;)a=O[M],!p&&u!==a.origType||n&&n.guid!==a.guid||c&&!c.test(a.namespace)||o&&o!==a.selector&&("**"!==o||!a.selector)||(O.splice(M,1),a.selector&&O.delegateCount--,i.remove&&i.remove.call(t,a));b&&!O.length&&(i.teardown&&!1!==i.teardown.call(t,A,l.handle)||g.removeEvent(t,s,l.handle),delete r[s])}else for(s in r)g.event.remove(t,s+e[z],n,o,!0);g.isEmptyObject(r)&&ct.remove(t,"handle events")}},dispatch:function(t){var e,n,o,p,M,b,c=new Array(arguments.length),r=g.event.fix(t),z=(ct.get(this,"events")||Object.create(null))[r.type]||[],a=g.event.special[r.type]||{};for(c[0]=r,e=1;e=1))for(;z!==this;z=z.parentNode||this)if(1===z.nodeType&&("click"!==t.type||!0!==z.disabled)){for(M=[],b={},n=0;n-1:g.find(p,this,null,[z]).length),b[p]&&M.push(o);M.length&&c.push({elem:z,handlers:M})}return z=this,r\s*$/g;function Pt(t,e){return y(t,"table")&&y(11!==e.nodeType?e:e.firstChild,"tr")&&g(t).children("tbody")[0]||t}function Ut(t){return t.type=(null!==t.getAttribute("type"))+"/"+t.type,t}function jt(t){return"true/"===(t.type||"").slice(0,5)?t.type=t.type.slice(5):t.removeAttribute("type"),t}function Ft(t,e){var n,o,p,M,b,c;if(1===e.nodeType){if(ct.hasData(t)&&(c=ct.get(t).events))for(p in ct.remove(e,"handle events"),c)for(n=0,o=c[p].length;n1&&"string"==typeof A&&!l.checkClone&&It.test(A))return t.each((function(p){var M=t.eq(p);u&&(e[0]=A.call(this,p,M.html())),Gt(M,e,n,o)}));if(O&&(M=(p=Bt(e,t[0].ownerDocument,!1,t,o)).firstChild,1===p.childNodes.length&&(p=M),M||o)){for(c=(b=g.map(Nt(p,"script"),Ut)).length;i0&&Et(b,!r&&Nt(t,"script")),c},cleanData:function(t){for(var e,n,o,p=g.event.special,M=0;void 0!==(n=t[M]);M++)if(Mt(n)){if(e=n[ct.expando]){if(e.events)for(o in e.events)p[o]?g.event.remove(n,o):g.removeEvent(n,o,e.handle);n[ct.expando]=void 0}n[rt.expando]&&(n[rt.expando]=void 0)}}}),g.fn.extend({detach:function(t){return Yt(this,t,!0)},remove:function(t){return Yt(this,t)},text:function(t){return tt(this,(function(t){return void 0===t?g.text(this):this.empty().each((function(){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||(this.textContent=t)}))}),null,t,arguments.length)},append:function(){return Gt(this,arguments,(function(t){1!==this.nodeType&&11!==this.nodeType&&9!==this.nodeType||Pt(this,t).appendChild(t)}))},prepend:function(){return Gt(this,arguments,(function(t){if(1===this.nodeType||11===this.nodeType||9===this.nodeType){var e=Pt(this,t);e.insertBefore(t,e.firstChild)}}))},before:function(){return Gt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this)}))},after:function(){return Gt(this,arguments,(function(t){this.parentNode&&this.parentNode.insertBefore(t,this.nextSibling)}))},empty:function(){for(var t,e=0;null!=(t=this[e]);e++)1===t.nodeType&&(g.cleanData(Nt(t,!1)),t.textContent="");return this},clone:function(t,e){return t=null!=t&&t,e=null==e?t:e,this.map((function(){return g.clone(this,t,e)}))},html:function(t){return tt(this,(function(t){var e=this[0]||{},n=0,o=this.length;if(void 0===t&&1===e.nodeType)return e.innerHTML;if("string"==typeof t&&!kt.test(t)&&!_t[(Lt.exec(t)||["",""])[1].toLowerCase()]){t=g.htmlPrefilter(t);try{for(;n=0&&(r+=Math.max(0,Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-M-r-c-.5))||0),r+z}function ae(t,e,n){var o=Kt(t),p=(!l.boxSizingReliable()||n)&&"border-box"===g.css(t,"boxSizing",!1,o),M=p,b=Jt(t,e,o),c="offset"+e[0].toUpperCase()+e.slice(1);if($t.test(b)){if(!n)return b;b="auto"}return(!l.boxSizingReliable()&&p||!l.reliableTrDimensions()&&y(t,"tr")||"auto"===b||!parseFloat(b)&&"inline"===g.css(t,"display",!1,o))&&t.getClientRects().length&&(p="border-box"===g.css(t,"boxSizing",!1,o),(M=c in t)&&(b=t[c])),(b=parseFloat(b)||0)+ze(t,e,n||(p?"border":"content"),M,o,b)+"px"}function ie(t,e,n,o,p){return new ie.prototype.init(t,e,n,o,p)}g.extend({cssHooks:{opacity:{get:function(t,e){if(e){var n=Jt(t,"opacity");return""===n?"1":n}}}},cssNumber:{animationIterationCount:!0,aspectRatio:!0,borderImageSlice:!0,columnCount:!0,flexGrow:!0,flexShrink:!0,fontWeight:!0,gridArea:!0,gridColumn:!0,gridColumnEnd:!0,gridColumnStart:!0,gridRow:!0,gridRowEnd:!0,gridRowStart:!0,lineHeight:!0,opacity:!0,order:!0,orphans:!0,scale:!0,widows:!0,zIndex:!0,zoom:!0,fillOpacity:!0,floodOpacity:!0,stopOpacity:!0,strokeMiterlimit:!0,strokeOpacity:!0},cssProps:{},style:function(t,e,n,o){if(t&&3!==t.nodeType&&8!==t.nodeType&&t.style){var p,M,b,c=pt(e),r=Vt.test(e),z=t.style;if(r||(e=pe(c)),b=g.cssHooks[e]||g.cssHooks[c],void 0===n)return b&&"get"in b&&void 0!==(p=b.get(t,!1,o))?p:z[e];"string"===(M=typeof n)&&(p=st.exec(n))&&p[1]&&(n=qt(t,e,p),M="number"),null!=n&&n==n&&("number"!==M||r||(n+=p&&p[3]||(g.cssNumber[c]?"":"px")),l.clearCloneStyle||""!==n||0!==e.indexOf("background")||(z[e]="inherit"),b&&"set"in b&&void 0===(n=b.set(t,n,o))||(r?z.setProperty(e,n):z[e]=n))}},css:function(t,e,n,o){var p,M,b,c=pt(e);return Vt.test(e)||(e=pe(c)),(b=g.cssHooks[e]||g.cssHooks[c])&&"get"in b&&(p=b.get(t,!0,n)),void 0===p&&(p=Jt(t,e,o)),"normal"===p&&e in ce&&(p=ce[e]),""===n||n?(M=parseFloat(p),!0===n||isFinite(M)?M||0:p):p}}),g.each(["height","width"],(function(t,e){g.cssHooks[e]={get:function(t,n,o){if(n)return!Me.test(g.css(t,"display"))||t.getClientRects().length&&t.getBoundingClientRect().width?ae(t,e,o):Zt(t,be,(function(){return ae(t,e,o)}))},set:function(t,n,o){var p,M=Kt(t),b=!l.scrollboxSize()&&"absolute"===M.position,c=(b||o)&&"border-box"===g.css(t,"boxSizing",!1,M),r=o?ze(t,e,o,c,M):0;return c&&b&&(r-=Math.ceil(t["offset"+e[0].toUpperCase()+e.slice(1)]-parseFloat(M[e])-ze(t,e,"border",!1,M)-.5)),r&&(p=st.exec(n))&&"px"!==(p[3]||"px")&&(t.style[e]=n,n=g.css(t,e)),re(0,n,r)}}})),g.cssHooks.marginLeft=te(l.reliableMarginLeft,(function(t,e){if(e)return(parseFloat(Jt(t,"marginLeft"))||t.getBoundingClientRect().left-Zt(t,{marginLeft:0},(function(){return t.getBoundingClientRect().left})))+"px"})),g.each({margin:"",padding:"",border:"Width"},(function(t,e){g.cssHooks[t+e]={expand:function(n){for(var o=0,p={},M="string"==typeof n?n.split(" "):[n];o<4;o++)p[t+At[o]+e]=M[o]||M[o-2]||M[0];return p}},"margin"!==t&&(g.cssHooks[t+e].set=re)})),g.fn.extend({css:function(t,e){return tt(this,(function(t,e,n){var o,p,M={},b=0;if(Array.isArray(e)){for(o=Kt(t),p=e.length;b1)}}),g.Tween=ie,ie.prototype={constructor:ie,init:function(t,e,n,o,p,M){this.elem=t,this.prop=n,this.easing=p||g.easing._default,this.options=e,this.start=this.now=this.cur(),this.end=o,this.unit=M||(g.cssNumber[n]?"":"px")},cur:function(){var t=ie.propHooks[this.prop];return t&&t.get?t.get(this):ie.propHooks._default.get(this)},run:function(t){var e,n=ie.propHooks[this.prop];return this.options.duration?this.pos=e=g.easing[this.easing](t,this.options.duration*t,0,1,this.options.duration):this.pos=e=t,this.now=(this.end-this.start)*e+this.start,this.options.step&&this.options.step.call(this.elem,this.now,this),n&&n.set?n.set(this):ie.propHooks._default.set(this),this}},ie.prototype.init.prototype=ie.prototype,ie.propHooks={_default:{get:function(t){var e;return 1!==t.elem.nodeType||null!=t.elem[t.prop]&&null==t.elem.style[t.prop]?t.elem[t.prop]:(e=g.css(t.elem,t.prop,""))&&"auto"!==e?e:0},set:function(t){g.fx.step[t.prop]?g.fx.step[t.prop](t):1!==t.elem.nodeType||!g.cssHooks[t.prop]&&null==t.elem.style[pe(t.prop)]?t.elem[t.prop]=t.now:g.style(t.elem,t.prop,t.now+t.unit)}}},ie.propHooks.scrollTop=ie.propHooks.scrollLeft={set:function(t){t.elem.nodeType&&t.elem.parentNode&&(t.elem[t.prop]=t.now)}},g.easing={linear:function(t){return t},swing:function(t){return.5-Math.cos(t*Math.PI)/2},_default:"swing"},g.fx=ie.prototype.init,g.fx.step={};var Oe,se,Ae=/^(?:toggle|show|hide)$/,ue=/queueHooks$/;function le(){se&&(!1===q.hidden&&o.requestAnimationFrame?o.requestAnimationFrame(le):o.setTimeout(le,g.fx.interval),g.fx.tick())}function de(){return o.setTimeout((function(){Oe=void 0})),Oe=Date.now()}function fe(t,e){var n,o=0,p={height:t};for(e=e?1:0;o<4;o+=2-e)p["margin"+(n=At[o])]=p["padding"+n]=t;return e&&(p.opacity=p.width=t),p}function qe(t,e,n){for(var o,p=(he.tweeners[e]||[]).concat(he.tweeners["*"]),M=0,b=p.length;M1)},removeAttr:function(t){return this.each((function(){g.removeAttr(this,t)}))}}),g.extend({attr:function(t,e,n){var o,p,M=t.nodeType;if(3!==M&&8!==M&&2!==M)return void 0===t.getAttribute?g.prop(t,e,n):(1===M&&g.isXMLDoc(t)||(p=g.attrHooks[e.toLowerCase()]||(g.expr.match.bool.test(e)?We:void 0)),void 0!==n?null===n?void g.removeAttr(t,e):p&&"set"in p&&void 0!==(o=p.set(t,n,e))?o:(t.setAttribute(e,n+""),n):p&&"get"in p&&null!==(o=p.get(t,e))?o:null==(o=g.find.attr(t,e))?void 0:o)},attrHooks:{type:{set:function(t,e){if(!l.radioValue&&"radio"===e&&y(t,"input")){var n=t.value;return t.setAttribute("type",e),n&&(t.value=n),e}}}},removeAttr:function(t,e){var n,o=0,p=e&&e.match(Y);if(p&&1===t.nodeType)for(;n=p[o++];)t.removeAttribute(n)}}),We={set:function(t,e,n){return!1===e?g.removeAttr(t,n):t.setAttribute(n,n),n}},g.each(g.expr.match.bool.source.match(/\w+/g),(function(t,e){var n=ve[e]||g.find.attr;ve[e]=function(t,e,o){var p,M,b=e.toLowerCase();return o||(M=ve[b],ve[b]=p,p=null!=n(t,e,o)?b:null,ve[b]=M),p}}));var Re=/^(?:input|select|textarea|button)$/i,me=/^(?:a|area)$/i;function ge(t){return(t.match(Y)||[]).join(" ")}function Le(t){return t.getAttribute&&t.getAttribute("class")||""}function ye(t){return Array.isArray(t)?t:"string"==typeof t&&t.match(Y)||[]}g.fn.extend({prop:function(t,e){return tt(this,g.prop,t,e,arguments.length>1)},removeProp:function(t){return this.each((function(){delete this[g.propFix[t]||t]}))}}),g.extend({prop:function(t,e,n){var o,p,M=t.nodeType;if(3!==M&&8!==M&&2!==M)return 1===M&&g.isXMLDoc(t)||(e=g.propFix[e]||e,p=g.propHooks[e]),void 0!==n?p&&"set"in p&&void 0!==(o=p.set(t,n,e))?o:t[e]=n:p&&"get"in p&&null!==(o=p.get(t,e))?o:t[e]},propHooks:{tabIndex:{get:function(t){var e=g.find.attr(t,"tabindex");return e?parseInt(e,10):Re.test(t.nodeName)||me.test(t.nodeName)&&t.href?0:-1}}},propFix:{for:"htmlFor",class:"className"}}),l.optSelected||(g.propHooks.selected={get:function(t){var e=t.parentNode;return e&&e.parentNode&&e.parentNode.selectedIndex,null},set:function(t){var e=t.parentNode;e&&(e.selectedIndex,e.parentNode&&e.parentNode.selectedIndex)}}),g.each(["tabIndex","readOnly","maxLength","cellSpacing","cellPadding","rowSpan","colSpan","useMap","frameBorder","contentEditable"],(function(){g.propFix[this.toLowerCase()]=this})),g.fn.extend({addClass:function(t){var e,n,o,p,M,b;return d(t)?this.each((function(e){g(this).addClass(t.call(this,e,Le(this)))})):(e=ye(t)).length?this.each((function(){if(o=Le(this),n=1===this.nodeType&&" "+ge(o)+" "){for(M=0;M-1;)n=n.replace(" "+p+" "," ");b=ge(n),o!==b&&this.setAttribute("class",b)}})):this:this.attr("class","")},toggleClass:function(t,e){var n,o,p,M,b=typeof t,c="string"===b||Array.isArray(t);return d(t)?this.each((function(n){g(this).toggleClass(t.call(this,n,Le(this),e),e)})):"boolean"==typeof e&&c?e?this.addClass(t):this.removeClass(t):(n=ye(t),this.each((function(){if(c)for(M=g(this),p=0;p-1)return!0;return!1}});var _e=/\r/g;g.fn.extend({val:function(t){var e,n,o,p=this[0];return arguments.length?(o=d(t),this.each((function(n){var p;1===this.nodeType&&(null==(p=o?t.call(this,n,g(this).val()):t)?p="":"number"==typeof p?p+="":Array.isArray(p)&&(p=g.map(p,(function(t){return null==t?"":t+""}))),(e=g.valHooks[this.type]||g.valHooks[this.nodeName.toLowerCase()])&&"set"in e&&void 0!==e.set(this,p,"value")||(this.value=p))}))):p?(e=g.valHooks[p.type]||g.valHooks[p.nodeName.toLowerCase()])&&"get"in e&&void 0!==(n=e.get(p,"value"))?n:"string"==typeof(n=p.value)?n.replace(_e,""):null==n?"":n:void 0}}),g.extend({valHooks:{option:{get:function(t){var e=g.find.attr(t,"value");return null!=e?e:ge(g.text(t))}},select:{get:function(t){var e,n,o,p=t.options,M=t.selectedIndex,b="select-one"===t.type,c=b?null:[],r=b?M+1:p.length;for(o=M<0?r:b?M:0;o-1)&&(n=!0);return n||(t.selectedIndex=-1),M}}}}),g.each(["radio","checkbox"],(function(){g.valHooks[this]={set:function(t,e){if(Array.isArray(e))return t.checked=g.inArray(g(t).val(),e)>-1}},l.checkOn||(g.valHooks[this].get=function(t){return null===t.getAttribute("value")?"on":t.value})}));var Ne=o.location,Ee={guid:Date.now()},Te=/\?/;g.parseXML=function(t){var e,n;if(!t||"string"!=typeof t)return null;try{e=(new o.DOMParser).parseFromString(t,"text/xml")}catch(t){}return n=e&&e.getElementsByTagName("parsererror")[0],e&&!n||g.error("Invalid XML: "+(n?g.map(n.childNodes,(function(t){return t.textContent})).join("\n"):t)),e};var Be=/^(?:focusinfocus|focusoutblur)$/,Ce=function(t){t.stopPropagation()};g.extend(g.event,{trigger:function(t,e,n,p){var M,b,c,r,z,a,i,O,A=[n||q],u=s.call(t,"type")?t.type:t,l=s.call(t,"namespace")?t.namespace.split("."):[];if(b=O=c=n=n||q,3!==n.nodeType&&8!==n.nodeType&&!Be.test(u+g.event.triggered)&&(u.indexOf(".")>-1&&(l=u.split("."),u=l.shift(),l.sort()),z=u.indexOf(":")<0&&"on"+u,(t=t[g.expando]?t:new g.Event(u,"object"==typeof t&&t)).isTrigger=p?2:3,t.namespace=l.join("."),t.rnamespace=t.namespace?new RegExp("(^|\\.)"+l.join("\\.(?:.*\\.|)")+"(\\.|$)"):null,t.result=void 0,t.target||(t.target=n),e=null==e?[t]:g.makeArray(e,[t]),i=g.event.special[u]||{},p||!i.trigger||!1!==i.trigger.apply(n,e))){if(!p&&!i.noBubble&&!f(n)){for(r=i.delegateType||u,Be.test(r+u)||(b=b.parentNode);b;b=b.parentNode)A.push(b),c=b;c===(n.ownerDocument||q)&&A.push(c.defaultView||c.parentWindow||o)}for(M=0;(b=A[M++])&&!t.isPropagationStopped();)O=b,t.type=M>1?r:i.bindType||u,(a=(ct.get(b,"events")||Object.create(null))[t.type]&&ct.get(b,"handle"))&&a.apply(b,e),(a=z&&b[z])&&a.apply&&Mt(b)&&(t.result=a.apply(b,e),!1===t.result&&t.preventDefault());return t.type=u,p||t.isDefaultPrevented()||i._default&&!1!==i._default.apply(A.pop(),e)||!Mt(n)||z&&d(n[u])&&!f(n)&&((c=n[z])&&(n[z]=null),g.event.triggered=u,t.isPropagationStopped()&&O.addEventListener(u,Ce),n[u](),t.isPropagationStopped()&&O.removeEventListener(u,Ce),g.event.triggered=void 0,c&&(n[z]=c)),t.result}},simulate:function(t,e,n){var o=g.extend(new g.Event,n,{type:t,isSimulated:!0});g.event.trigger(o,null,e)}}),g.fn.extend({trigger:function(t,e){return this.each((function(){g.event.trigger(t,e,this)}))},triggerHandler:function(t,e){var n=this[0];if(n)return g.event.trigger(t,e,n,!0)}});var we=/\[\]$/,Se=/\r?\n/g,Xe=/^(?:submit|button|image|reset|file)$/i,xe=/^(?:input|select|textarea|keygen)/i;function ke(t,e,n,o){var p;if(Array.isArray(e))g.each(e,(function(e,p){n||we.test(t)?o(t,p):ke(t+"["+("object"==typeof p&&null!=p?e:"")+"]",p,n,o)}));else if(n||"object"!==v(e))o(t,e);else for(p in e)ke(t+"["+p+"]",e[p],n,o)}g.param=function(t,e){var n,o=[],p=function(t,e){var n=d(e)?e():e;o[o.length]=encodeURIComponent(t)+"="+encodeURIComponent(null==n?"":n)};if(null==t)return"";if(Array.isArray(t)||t.jquery&&!g.isPlainObject(t))g.each(t,(function(){p(this.name,this.value)}));else for(n in t)ke(n,t[n],e,p);return o.join("&")},g.fn.extend({serialize:function(){return g.param(this.serializeArray())},serializeArray:function(){return this.map((function(){var t=g.prop(this,"elements");return t?g.makeArray(t):this})).filter((function(){var t=this.type;return this.name&&!g(this).is(":disabled")&&xe.test(this.nodeName)&&!Xe.test(t)&&(this.checked||!gt.test(t))})).map((function(t,e){var n=g(this).val();return null==n?null:Array.isArray(n)?g.map(n,(function(t){return{name:e.name,value:t.replace(Se,"\r\n")}})):{name:e.name,value:n.replace(Se,"\r\n")}})).get()}});var Ie=/%20/g,De=/#.*$/,Pe=/([?&])_=[^&]*/,Ue=/^(.*?):[ \t]*([^\r\n]*)$/gm,je=/^(?:GET|HEAD)$/,Fe=/^\/\//,He={},Ge={},Ye="*/".concat("*"),$e=q.createElement("a");function Ve(t){return function(e,n){"string"!=typeof e&&(n=e,e="*");var o,p=0,M=e.toLowerCase().match(Y)||[];if(d(n))for(;o=M[p++];)"+"===o[0]?(o=o.slice(1)||"*",(t[o]=t[o]||[]).unshift(n)):(t[o]=t[o]||[]).push(n)}}function Ke(t,e,n,o){var p={},M=t===Ge;function b(c){var r;return p[c]=!0,g.each(t[c]||[],(function(t,c){var z=c(e,n,o);return"string"!=typeof z||M||p[z]?M?!(r=z):void 0:(e.dataTypes.unshift(z),b(z),!1)})),r}return b(e.dataTypes[0])||!p["*"]&&b("*")}function Ze(t,e){var n,o,p=g.ajaxSettings.flatOptions||{};for(n in e)void 0!==e[n]&&((p[n]?t:o||(o={}))[n]=e[n]);return o&&g.extend(!0,t,o),t}$e.href=Ne.href,g.extend({active:0,lastModified:{},etag:{},ajaxSettings:{url:Ne.href,type:"GET",isLocal:/^(?:about|app|app-storage|.+-extension|file|res|widget):$/.test(Ne.protocol),global:!0,processData:!0,async:!0,contentType:"application/x-www-form-urlencoded; charset=UTF-8",accepts:{"*":Ye,text:"text/plain",html:"text/html",xml:"application/xml, text/xml",json:"application/json, text/javascript"},contents:{xml:/\bxml\b/,html:/\bhtml/,json:/\bjson\b/},responseFields:{xml:"responseXML",text:"responseText",json:"responseJSON"},converters:{"* text":String,"text html":!0,"text json":JSON.parse,"text xml":g.parseXML},flatOptions:{url:!0,context:!0}},ajaxSetup:function(t,e){return e?Ze(Ze(t,g.ajaxSettings),e):Ze(g.ajaxSettings,t)},ajaxPrefilter:Ve(He),ajaxTransport:Ve(Ge),ajax:function(t,e){"object"==typeof t&&(e=t,t=void 0),e=e||{};var n,p,M,b,c,r,z,a,i,O,s=g.ajaxSetup({},e),A=s.context||s,u=s.context&&(A.nodeType||A.jquery)?g(A):g.event,l=g.Deferred(),d=g.Callbacks("once memory"),f=s.statusCode||{},h={},W={},v="canceled",R={readyState:0,getResponseHeader:function(t){var e;if(z){if(!b)for(b={};e=Ue.exec(M);)b[e[1].toLowerCase()+" "]=(b[e[1].toLowerCase()+" "]||[]).concat(e[2]);e=b[t.toLowerCase()+" "]}return null==e?null:e.join(", ")},getAllResponseHeaders:function(){return z?M:null},setRequestHeader:function(t,e){return null==z&&(t=W[t.toLowerCase()]=W[t.toLowerCase()]||t,h[t]=e),this},overrideMimeType:function(t){return null==z&&(s.mimeType=t),this},statusCode:function(t){var e;if(t)if(z)R.always(t[R.status]);else for(e in t)f[e]=[f[e],t[e]];return this},abort:function(t){var e=t||v;return n&&n.abort(e),m(0,e),this}};if(l.promise(R),s.url=((t||s.url||Ne.href)+"").replace(Fe,Ne.protocol+"//"),s.type=e.method||e.type||s.method||s.type,s.dataTypes=(s.dataType||"*").toLowerCase().match(Y)||[""],null==s.crossDomain){r=q.createElement("a");try{r.href=s.url,r.href=r.href,s.crossDomain=$e.protocol+"//"+$e.host!=r.protocol+"//"+r.host}catch(t){s.crossDomain=!0}}if(s.data&&s.processData&&"string"!=typeof s.data&&(s.data=g.param(s.data,s.traditional)),Ke(He,s,e,R),z)return R;for(i in(a=g.event&&s.global)&&0==g.active++&&g.event.trigger("ajaxStart"),s.type=s.type.toUpperCase(),s.hasContent=!je.test(s.type),p=s.url.replace(De,""),s.hasContent?s.data&&s.processData&&0===(s.contentType||"").indexOf("application/x-www-form-urlencoded")&&(s.data=s.data.replace(Ie,"+")):(O=s.url.slice(p.length),s.data&&(s.processData||"string"==typeof s.data)&&(p+=(Te.test(p)?"&":"?")+s.data,delete s.data),!1===s.cache&&(p=p.replace(Pe,"$1"),O=(Te.test(p)?"&":"?")+"_="+Ee.guid+++O),s.url=p+O),s.ifModified&&(g.lastModified[p]&&R.setRequestHeader("If-Modified-Since",g.lastModified[p]),g.etag[p]&&R.setRequestHeader("If-None-Match",g.etag[p])),(s.data&&s.hasContent&&!1!==s.contentType||e.contentType)&&R.setRequestHeader("Content-Type",s.contentType),R.setRequestHeader("Accept",s.dataTypes[0]&&s.accepts[s.dataTypes[0]]?s.accepts[s.dataTypes[0]]+("*"!==s.dataTypes[0]?", "+Ye+"; q=0.01":""):s.accepts["*"]),s.headers)R.setRequestHeader(i,s.headers[i]);if(s.beforeSend&&(!1===s.beforeSend.call(A,R,s)||z))return R.abort();if(v="abort",d.add(s.complete),R.done(s.success),R.fail(s.error),n=Ke(Ge,s,e,R)){if(R.readyState=1,a&&u.trigger("ajaxSend",[R,s]),z)return R;s.async&&s.timeout>0&&(c=o.setTimeout((function(){R.abort("timeout")}),s.timeout));try{z=!1,n.send(h,m)}catch(t){if(z)throw t;m(-1,t)}}else m(-1,"No Transport");function m(t,e,b,r){var i,O,q,h,W,v=e;z||(z=!0,c&&o.clearTimeout(c),n=void 0,M=r||"",R.readyState=t>0?4:0,i=t>=200&&t<300||304===t,b&&(h=function(t,e,n){for(var o,p,M,b,c=t.contents,r=t.dataTypes;"*"===r[0];)r.shift(),void 0===o&&(o=t.mimeType||e.getResponseHeader("Content-Type"));if(o)for(p in c)if(c[p]&&c[p].test(o)){r.unshift(p);break}if(r[0]in n)M=r[0];else{for(p in n){if(!r[0]||t.converters[p+" "+r[0]]){M=p;break}b||(b=p)}M=M||b}if(M)return M!==r[0]&&r.unshift(M),n[M]}(s,R,b)),!i&&g.inArray("script",s.dataTypes)>-1&&g.inArray("json",s.dataTypes)<0&&(s.converters["text script"]=function(){}),h=function(t,e,n,o){var p,M,b,c,r,z={},a=t.dataTypes.slice();if(a[1])for(b in t.converters)z[b.toLowerCase()]=t.converters[b];for(M=a.shift();M;)if(t.responseFields[M]&&(n[t.responseFields[M]]=e),!r&&o&&t.dataFilter&&(e=t.dataFilter(e,t.dataType)),r=M,M=a.shift())if("*"===M)M=r;else if("*"!==r&&r!==M){if(!(b=z[r+" "+M]||z["* "+M]))for(p in z)if((c=p.split(" "))[1]===M&&(b=z[r+" "+c[0]]||z["* "+c[0]])){!0===b?b=z[p]:!0!==z[p]&&(M=c[0],a.unshift(c[1]));break}if(!0!==b)if(b&&t.throws)e=b(e);else try{e=b(e)}catch(t){return{state:"parsererror",error:b?t:"No conversion from "+r+" to "+M}}}return{state:"success",data:e}}(s,h,R,i),i?(s.ifModified&&((W=R.getResponseHeader("Last-Modified"))&&(g.lastModified[p]=W),(W=R.getResponseHeader("etag"))&&(g.etag[p]=W)),204===t||"HEAD"===s.type?v="nocontent":304===t?v="notmodified":(v=h.state,O=h.data,i=!(q=h.error))):(q=v,!t&&v||(v="error",t<0&&(t=0))),R.status=t,R.statusText=(e||v)+"",i?l.resolveWith(A,[O,v,R]):l.rejectWith(A,[R,v,q]),R.statusCode(f),f=void 0,a&&u.trigger(i?"ajaxSuccess":"ajaxError",[R,s,i?O:q]),d.fireWith(A,[R,v]),a&&(u.trigger("ajaxComplete",[R,s]),--g.active||g.event.trigger("ajaxStop")))}return R},getJSON:function(t,e,n){return g.get(t,e,n,"json")},getScript:function(t,e){return g.get(t,void 0,e,"script")}}),g.each(["get","post"],(function(t,e){g[e]=function(t,n,o,p){return d(n)&&(p=p||o,o=n,n=void 0),g.ajax(g.extend({url:t,type:e,dataType:p,data:n,success:o},g.isPlainObject(t)&&t))}})),g.ajaxPrefilter((function(t){var e;for(e in t.headers)"content-type"===e.toLowerCase()&&(t.contentType=t.headers[e]||"")})),g._evalUrl=function(t,e,n){return g.ajax({url:t,type:"GET",dataType:"script",cache:!0,async:!1,global:!1,converters:{"text script":function(){}},dataFilter:function(t){g.globalEval(t,e,n)}})},g.fn.extend({wrapAll:function(t){var e;return this[0]&&(d(t)&&(t=t.call(this[0])),e=g(t,this[0].ownerDocument).eq(0).clone(!0),this[0].parentNode&&e.insertBefore(this[0]),e.map((function(){for(var t=this;t.firstElementChild;)t=t.firstElementChild;return t})).append(this)),this},wrapInner:function(t){return d(t)?this.each((function(e){g(this).wrapInner(t.call(this,e))})):this.each((function(){var e=g(this),n=e.contents();n.length?n.wrapAll(t):e.append(t)}))},wrap:function(t){var e=d(t);return this.each((function(n){g(this).wrapAll(e?t.call(this,n):t)}))},unwrap:function(t){return this.parent(t).not("body").each((function(){g(this).replaceWith(this.childNodes)})),this}}),g.expr.pseudos.hidden=function(t){return!g.expr.pseudos.visible(t)},g.expr.pseudos.visible=function(t){return!!(t.offsetWidth||t.offsetHeight||t.getClientRects().length)},g.ajaxSettings.xhr=function(){try{return new o.XMLHttpRequest}catch(t){}};var Qe={0:200,1223:204},Je=g.ajaxSettings.xhr();l.cors=!!Je&&"withCredentials"in Je,l.ajax=Je=!!Je,g.ajaxTransport((function(t){var e,n;if(l.cors||Je&&!t.crossDomain)return{send:function(p,M){var b,c=t.xhr();if(c.open(t.type,t.url,t.async,t.username,t.password),t.xhrFields)for(b in t.xhrFields)c[b]=t.xhrFields[b];for(b in t.mimeType&&c.overrideMimeType&&c.overrideMimeType(t.mimeType),t.crossDomain||p["X-Requested-With"]||(p["X-Requested-With"]="XMLHttpRequest"),p)c.setRequestHeader(b,p[b]);e=function(t){return function(){e&&(e=n=c.onload=c.onerror=c.onabort=c.ontimeout=c.onreadystatechange=null,"abort"===t?c.abort():"error"===t?"number"!=typeof c.status?M(0,"error"):M(c.status,c.statusText):M(Qe[c.status]||c.status,c.statusText,"text"!==(c.responseType||"text")||"string"!=typeof c.responseText?{binary:c.response}:{text:c.responseText},c.getAllResponseHeaders()))}},c.onload=e(),n=c.onerror=c.ontimeout=e("error"),void 0!==c.onabort?c.onabort=n:c.onreadystatechange=function(){4===c.readyState&&o.setTimeout((function(){e&&n()}))},e=e("abort");try{c.send(t.hasContent&&t.data||null)}catch(t){if(e)throw t}},abort:function(){e&&e()}}})),g.ajaxPrefilter((function(t){t.crossDomain&&(t.contents.script=!1)})),g.ajaxSetup({accepts:{script:"text/javascript, application/javascript, application/ecmascript, application/x-ecmascript"},contents:{script:/\b(?:java|ecma)script\b/},converters:{"text script":function(t){return g.globalEval(t),t}}}),g.ajaxPrefilter("script",(function(t){void 0===t.cache&&(t.cache=!1),t.crossDomain&&(t.type="GET")})),g.ajaxTransport("script",(function(t){var e,n;if(t.crossDomain||t.scriptAttrs)return{send:function(o,p){e=g(" - {{-- Get IPTABLES --}} - - diff --git a/resources/views/livewire/destination/form.blade.php b/resources/views/livewire/destination/form.blade.php deleted file mode 100644 index 0b9097f2d7..0000000000 --- a/resources/views/livewire/destination/form.blade.php +++ /dev/null @@ -1,29 +0,0 @@ -
-
-
-

Destination

- - Save - - @if ($destination->network !== 'coolify') - - @endif -
- - @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') -
A Docker network in a non-swarm environment.
- @else -
Your swarm docker network. WIP
- @endif -
- - - @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') - - @endif -
-
-
diff --git a/resources/views/livewire/destination/index.blade.php b/resources/views/livewire/destination/index.blade.php new file mode 100644 index 0000000000..df64e953e6 --- /dev/null +++ b/resources/views/livewire/destination/index.blade.php @@ -0,0 +1,44 @@ +
+ + Destinations | Coolify + +
+

Destinations

+ @if ($servers->count() > 0) + + + + @endif +
+
Network endpoints to deploy your resources.
+
+ @forelse ($servers as $server) + @forelse ($server->destinations() as $destination) + @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') + +
+
{{ $destination->name }}
+
Server: {{ $destination->server->name }}
+
+
+ @endif + @if ($destination->getMorphClass() === 'App\Models\SwarmDocker') + +
+
{{ $destination->name }}
+
server: {{ $destination->server->name }}
+
+
+ @endif + @empty +
No destinations found.
+ @endforelse + @empty +
No servers found.
+ @endforelse +
+
diff --git a/resources/views/livewire/destination/new/docker.blade.php b/resources/views/livewire/destination/new/docker.blade.php index a6da63c6c5..1502f70af8 100644 --- a/resources/views/livewire/destination/new/docker.blade.php +++ b/resources/views/livewire/destination/new/docker.blade.php @@ -5,7 +5,7 @@ - + @foreach ($servers as $server) diff --git a/resources/views/livewire/destination/show.blade.php b/resources/views/livewire/destination/show.blade.php index ecc68dc5c7..6c3a9d9dd8 100644 --- a/resources/views/livewire/destination/show.blade.php +++ b/resources/views/livewire/destination/show.blade.php @@ -1,42 +1,29 @@
- @if ($server->isFunctional()) -
-

Destinations

- - - - Scan Destinations -
-
Destinations are used to segregate resources by network.
-
- Available for using: - @forelse ($server->standaloneDockers as $docker) - - - - @empty - @endforelse - @forelse ($server->swarmDockers as $docker) - - - - @empty - @endforelse +
+
+

Destination

+ + Save + + @if ($network !== 'coolify') + + @endif
-
- @if (count($networks) > 0) -

Found Destinations

+ + @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') +
A simple Docker network.
+ @else +
A swarm Docker network. WIP
+ @endif +
+ + + @if ($destination->getMorphClass() === 'App\Models\StandaloneDocker') + @endif -
- @foreach ($networks as $network) -
- Add - {{ data_get($network, 'Name') }} -
- @endforeach -
- @else -
Server is not validated. Validate first.
- @endif +
diff --git a/resources/views/livewire/help.blade.php b/resources/views/livewire/help.blade.php index 0b79d3bd62..dea6ca46c9 100644 --- a/resources/views/livewire/help.blade.php +++ b/resources/views/livewire/help.blade.php @@ -1,10 +1,11 @@
Your feedback helps us to improve Coolify. Thank you! 💜
- - + +
- Send + Send
diff --git a/resources/views/livewire/notifications/discord.blade.php b/resources/views/livewire/notifications/discord.blade.php index 1dbdf241e9..af6f98b0ab 100644 --- a/resources/views/livewire/notifications/discord.blade.php +++ b/resources/views/livewire/notifications/discord.blade.php @@ -9,7 +9,7 @@ Save - @if ($team->discord_enabled) + @if ($discordEnabled) Send Test Notifications @@ -17,26 +17,27 @@ @endif
- +
+ id="discordWebhookUrl" label="Webhook" /> - @if (data_get($team, 'discord_enabled')) + @if ($discordEnabled)

Subscribe to events

@if (isDev()) - + @endif - - - - + +
@endif
diff --git a/resources/views/livewire/notifications/email.blade.php b/resources/views/livewire/notifications/email.blade.php index 594cf427bb..a2e5326c69 100644 --- a/resources/views/livewire/notifications/email.blade.php +++ b/resources/views/livewire/notifications/email.blade.php @@ -9,7 +9,7 @@ Save - @if (isInstanceAdmin() && !$team->use_instance_email_settings) + @if (isInstanceAdmin() && !$useInstanceEmailSettings) Copy from Instance Settings @@ -25,97 +25,90 @@ @endif - - @if (isCloud()) - @if ($this->sharedEmailEnabled) -
- + @if (!isCloud()) +
+
- @else -
- + @endif + @if (!$useInstanceEmailSettings) +
+ +
@endif - @else -
- + + @if (isCloud()) +
+
@endif - @if (!$team->use_instance_email_settings) -
- - - - Save - - + @if (!$useInstanceEmailSettings)
-
-

SMTP Server

+
+
+

SMTP Server

+ + Save + +
- +
- +
- - - + +
- - - + +
-
- - Save - -
- -
-
-

Resend

+
+ +
+
+

Resend

+ + Save + +
- +
- +
-
-
- - Save - -
- -
+
+
@endif - @if (isEmailEnabled($team) || data_get($team, 'use_instance_email_settings')) + @if (isEmailEnabled($team) || $useInstanceEmailSettings)

Subscribe to events

@if (isDev()) - + @endif - - - - + +
@endif
diff --git a/resources/views/livewire/notifications/telegram.blade.php b/resources/views/livewire/notifications/telegram.blade.php index 3f57ff4711..76378ada1a 100644 --- a/resources/views/livewire/notifications/telegram.blade.php +++ b/resources/views/livewire/notifications/telegram.blade.php @@ -9,7 +9,7 @@ Save - @if ($team->telegram_enabled) + @if ($telegramEnabled) Send Test Notifications @@ -17,61 +17,63 @@ @endif
- +
- + required id="telegramToken" label="Token" /> + id="telegramChatId" label="Chat ID" />
- @if (data_get($team, 'telegram_enabled')) + @if ($telegramEnabled)

Subscribe to events

@if (isDev())

Test Notification

- + + id="telegramNotificationsTestMessageThreadId" label="Custom Topic ID" />
@endif

Container Status Changes

- + + id="telegramNotificationsStatusChangesMessageThreadId" label="Custom Topic ID" />

Application Deployments

- + + id="telegramNotificationsDeploymentsMessageThreadId" label="Custom Topic ID" />

Database Backup Status

- + id="telegramNotificationsDatabaseBackupsMessageThreadId" label="Custom Topic ID" />

Scheduled Tasks Status

- + + id="telegramNotificationsScheduledTasksMessageThreadId" label="Custom Topic ID" /> +
+
+

Server Disk Usage

+
-
@endif diff --git a/resources/views/livewire/profile/index.blade.php b/resources/views/livewire/profile/index.blade.php index 0648016b70..fc367e6f25 100644 --- a/resources/views/livewire/profile/index.blade.php +++ b/resources/views/livewire/profile/index.blade.php @@ -33,20 +33,66 @@ Please finish configuring two factor authentication below. Read the QR code or enter the secret key manually.
-
+
@csrf - + Validate 2FA -
-
{!! request()->user()->twoFactorQrCodeSvg() !!}
-
- - Show secret key to manually - enter +
+
+ {!! request()->user()->twoFactorQrCodeSvg() !!} +
+
+
+
+ + +
+
+ + +
+
+ + +
diff --git a/resources/views/livewire/project/add-empty.blade.php b/resources/views/livewire/project/add-empty.blade.php index 67953f2197..e5bc083539 100644 --- a/resources/views/livewire/project/add-empty.blade.php +++ b/resources/views/livewire/project/add-empty.blade.php @@ -1,8 +1,9 @@
-
New project will have a default production environment.
- +
New project will have a default production + environment.
+ Continue diff --git a/resources/views/livewire/project/add-environment.blade.php b/resources/views/livewire/project/add-environment.blade.php deleted file mode 100644 index f1511b18ff..0000000000 --- a/resources/views/livewire/project/add-environment.blade.php +++ /dev/null @@ -1,6 +0,0 @@ -
- - - Save - - diff --git a/resources/views/livewire/project/application/advanced.blade.php b/resources/views/livewire/project/application/advanced.blade.php index dc3f135dbc..6658c0ed2d 100644 --- a/resources/views/livewire/project/application/advanced.blade.php +++ b/resources/views/livewire/project/application/advanced.blade.php @@ -8,90 +8,86 @@

General

@if ($application->git_based()) + id="isAutoDeployEnabled" label="Auto Deploy" /> + instantSave id="isPreviewDeploymentsEnabled" label="Preview Deployments" /> @endif + instantSave id="isForceHttpsEnabled" label="Force Https" /> + instantSave id="isGzipEnabled" /> + instantSave id="isStripprefixEnabled" label="Strip Prefixes" /> @if ($application->build_pack === 'dockercompose')

Docker Compose

- @endif -

Container Names

+

Container Names

- @if (!$application->settings->is_consistent_container_name_enabled) -
+ instantSave id="isConsistentContainerNameEnabled" label="Consistent Container Names" /> + @if ($isConsistentContainerNameEnabled === false) + + instantSave id="customInternalName" label="Custom Container Name" /> Save @endif @if ($application->build_pack === 'dockercompose') -

Network

- Network + @endif - @if (!$application->settings->is_raw_compose_deployment_enabled) -

Logs

+ @if ($isLogDrainEnabled === false) +

Logs

+ instantSave id="isLogDrainEnabled" label="Drain Logs" /> @endif @if ($application->git_based())

Git

- - @endif - {{-- - - --}}
+ +
+
@if ($application->build_pack !== 'dockercompose') -

GPU

+
+

GPU

+ @if ($isGpuEnabled) + Save + @endif +
@endif - - @if ($application->build_pack !== 'dockercompose') + @if ($application->build_pack !== 'dockercompose') +
- @if ($application->settings->is_gpu_enabled) -
GPU Settings
- - Save - @endif - @endif - @if ($application->settings->is_gpu_enabled) -
- - - - -
-
- - + instantSave id="isGpuEnabled" label="Enable GPU" /> +
+ @endif + @if ($isGpuEnabled) +
+
+ + +
- @endif - -
+ + + +
+ @endif +
diff --git a/resources/views/livewire/project/application/general.blade.php b/resources/views/livewire/project/application/general.blade.php index 6c93d25b74..ed50fd2308 100644 --- a/resources/views/livewire/project/application/general.blade.php +++ b/resources/views/livewire/project/application/general.blade.php @@ -5,7 +5,7 @@ Save - {{-- + {{-- Download Config @@ -70,7 +70,7 @@
+ helper="You need to add both the www and non-www A DNS records pointing to your server."> @@ -238,9 +238,9 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" @if ($application->build_pack !== 'dockercompose')
+ label="Use a Build Server?" />
@endif @if ($application->could_set_build_commands()) @@ -312,7 +312,7 @@ class="underline" href="https://coolify.io/docs/knowledge-base/docker/registry" id="application.settings.is_container_label_readonly_enabled" instantSave>
- +
- - - + +
+ + @if ($database->started_at) +
+ +
+ @else +
Please verify these values. You can only modify them before the initial + start. After that, you need to modify it in the database. +
+
+ +
+ @endif + id="customDockerRunOptions" label="Custom Docker Options" />

Network

-
- @if ($db_url_public) + type="password" readonly wire:model="dbUrl" /> + @if ($dbUrlPublic) + type="password" readonly wire:model="dbUrlPublic" /> + @else + @endif
-

Proxy

-
- - - Proxy Logs - - - - Proxy Logs - - +
+
+
+

Proxy

+ +
+ @if ($isPublic) + + Proxy Logs + + + + Logs + + @endif +
+
+
-

Advanced

-
- -
+ label="Custom KeyDB Configuration" rows="10" id="keydbConf" /> +

Advanced

+
+ +
diff --git a/resources/views/livewire/project/database/mariadb/general.blade.php b/resources/views/livewire/project/database/mariadb/general.blade.php index 7c0a6bcba5..4cad817490 100644 --- a/resources/views/livewire/project/database/mariadb/general.blade.php +++ b/resources/views/livewire/project/database/mariadb/general.blade.php @@ -66,21 +66,28 @@ @endif
-

Proxy

-
- - - Proxy Logs - - - - Proxy Logs - +
+
+
+

Proxy

+ +
+ @if (data_get($database, 'is_public')) + + Proxy Logs + + + + Logs + + @endif +
+

Advanced

diff --git a/resources/views/livewire/project/database/mongodb/general.blade.php b/resources/views/livewire/project/database/mongodb/general.blade.php index 8fc86ae1cb..72fd2f75d6 100644 --- a/resources/views/livewire/project/database/mongodb/general.blade.php +++ b/resources/views/livewire/project/database/mongodb/general.blade.php @@ -56,21 +56,28 @@ @endif
-

Proxy

-
- - - Proxy Logs - - - - Proxy Logs - +
+
+
+

Proxy

+ +
+ @if (data_get($database, 'is_public')) + + Proxy Logs + + + + Logs + + @endif +
+

Advanced

diff --git a/resources/views/livewire/project/database/mysql/general.blade.php b/resources/views/livewire/project/database/mysql/general.blade.php index 40fcca1e86..c4ac7221a5 100644 --- a/resources/views/livewire/project/database/mysql/general.blade.php +++ b/resources/views/livewire/project/database/mysql/general.blade.php @@ -66,21 +66,28 @@ @endif
-

Proxy

-
- - - Proxy Logs - - - - Proxy Logs - +
+
+
+

Proxy

+ +
+ @if (data_get($database, 'is_public')) + + Proxy Logs + + + + Logs + + @endif +
+

Advanced

diff --git a/resources/views/livewire/project/database/postgresql/general.blade.php b/resources/views/livewire/project/database/postgresql/general.blade.php index 73f8e4313d..5abde2b010 100644 --- a/resources/views/livewire/project/database/postgresql/general.blade.php +++ b/resources/views/livewire/project/database/postgresql/general.blade.php @@ -74,21 +74,28 @@ @endif
-

Proxy

-
- - - Proxy Logs - - - - Proxy Logs - +
+
+
+

Proxy

+ +
+ @if (data_get($database, 'is_public')) + + Proxy Logs + + + + Logs + + @endif +
+
@@ -102,8 +109,7 @@ class="w-28">Proxy Logs

Initialization scripts

- + diff --git a/resources/views/livewire/project/database/redis/general.blade.php b/resources/views/livewire/project/database/redis/general.blade.php index 7d4de27cda..a274fa62ed 100644 --- a/resources/views/livewire/project/database/redis/general.blade.php +++ b/resources/views/livewire/project/database/redis/general.blade.php @@ -12,6 +12,24 @@
+
+ @if (version_compare($redis_version, '6.0', '>=')) + + @endif + +
-

Proxy

-
- - - Proxy Logs - - - - Proxy Logs - +
+
+
+

Proxy

+ +
+ @if (data_get($database, 'is_public')) + + Proxy Logs + + + + Logs + + @endif +
+
Proxy Logs
+ diff --git a/resources/views/livewire/project/edit.blade.php b/resources/views/livewire/project/edit.blade.php index ec9304da99..58680f5628 100644 --- a/resources/views/livewire/project/edit.blade.php +++ b/resources/views/livewire/project/edit.blade.php @@ -7,14 +7,13 @@

Project: {{ data_get($project, 'name') }}

Save - +
Edit project details here.
-
- - + +
diff --git a/resources/views/livewire/project/environment-edit.blade.php b/resources/views/livewire/project/environment-edit.blade.php index 2035259595..af5e686c48 100644 --- a/resources/views/livewire/project/environment-edit.blade.php +++ b/resources/views/livewire/project/environment-edit.blade.php @@ -13,7 +13,7 @@
  • {{ data_get($parameters, 'environment_name') }} + href="{{ route('project.resource.index', ['environment_name' => $environment->name, 'project_uuid' => $project->uuid]) }}"> + {{ $environment->name }} +
  • @@ -43,8 +45,8 @@
    - - + +
    diff --git a/resources/views/livewire/project/index.blade.php b/resources/views/livewire/project/index.blade.php index 10719456e0..cb8e1bbede 100644 --- a/resources/views/livewire/project/index.blade.php +++ b/resources/views/livewire/project/index.blade.php @@ -24,6 +24,12 @@
    + diff --git a/resources/views/livewire/project/resource/index.blade.php b/resources/views/livewire/project/resource/index.blade.php index f6502762a7..0e16b7266d 100644 --- a/resources/views/livewire/project/resource/index.blade.php +++ b/resources/views/livewire/project/resource/index.blade.php @@ -7,15 +7,15 @@

    Resources

    @if ($environment->isEmpty()) + href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }}"> Clone @else - + New + href="{{ route('project.clone-me', ['project_uuid' => data_get($project, 'uuid'), 'environment_name' => data_get($parameters, 'environment_name')]) }}"> Clone @endif @@ -25,7 +25,7 @@ class="button">+
    1. + href="{{ route('project.show', ['project_uuid' => data_get($parameters, 'project_uuid')]) }}"> {{ $project->name }}
    2. @@ -44,7 +44,7 @@ class="button">+ @if ($environment->isEmpty()) - + Add New Resource @else
      diff --git a/resources/views/livewire/project/service/configuration.blade.php b/resources/views/livewire/project/service/configuration.blade.php index d7d9d21d39..ed2a6dec99 100644 --- a/resources/views/livewire/project/service/configuration.blade.php +++ b/resources/views/livewire/project/service/configuration.blade.php @@ -1,4 +1,4 @@ -
      +
      {{ data_get_str($service, 'name')->limit(10) }} > Configuration | Coolify diff --git a/resources/views/livewire/project/service/database.blade.php b/resources/views/livewire/project/service/database.blade.php index de8b522be4..c18473a142 100644 --- a/resources/views/livewire/project/service/database.blade.php +++ b/resources/views/livewire/project/service/database.blade.php @@ -17,7 +17,6 @@ label="Image Tag" id="database.image">
      - diff --git a/resources/views/livewire/project/service/index.blade.php b/resources/views/livewire/project/service/index.blade.php index 5fcfb6b29a..fb3ed5636b 100644 --- a/resources/views/livewire/project/service/index.blade.php +++ b/resources/views/livewire/project/service/index.blade.php @@ -38,7 +38,7 @@ class="{{ request()->routeIs('project.service.configuration') ? 'menu-item-activ

      Scheduled Backups

      - +
      diff --git a/resources/views/livewire/project/service/navbar.blade.php b/resources/views/livewire/project/service/navbar.blade.php index 6ff297c61c..342c071d4f 100644 --- a/resources/views/livewire/project/service/navbar.blade.php +++ b/resources/views/livewire/project/service/navbar.blade.php @@ -20,127 +20,143 @@ -
      - @if (str($service->status())->contains('running')) - - - Advanced - - + Deploy + + @endif +
      + @else + + @endif
      @script - -
      -

      Memory (%)

      -
      - - + const serverCpuChart = new ApexCharts(document.getElementById(`{!! $chartId !!}-cpu`), + optionsServerCpu); + serverCpuChart.render(); + document.addEventListener('livewire:init', () => { + Livewire.on('refreshChartData-{!! $chartId !!}-cpu', (chartData) => { + checkTheme(); + serverCpuChart.updateOptions({ + series: [{ + data: chartData[0].seriesData, + }], + colors: [baseColor], + xaxis: { + type: 'datetime', + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + yaxis: { + show: true, + labels: { + show: true, + style: { + colors: textColor, + } + } + }, + noData: { + text: 'Loading...', + style: { + color: textColor, + } + } + }); + }); + }); + + +
      +

      Memory (%)

      +
      + + +
      +
      + @else +
      Metrics are disabled for this server.
      + @endif +
      diff --git a/resources/views/livewire/server/cloudflare-tunnels.blade.php b/resources/views/livewire/server/cloudflare-tunnels.blade.php new file mode 100644 index 0000000000..4cb7fc2ec4 --- /dev/null +++ b/resources/views/livewire/server/cloudflare-tunnels.blade.php @@ -0,0 +1,54 @@ +
      + + {{ data_get_str($server, 'name')->limit(10) }} > Cloudflare Tunnels | Coolify + + +
      + +
      +
      +
      +

      Cloudflare Tunnels

      + +
      +
      Secure your servers with Cloudflare Tunnels
      + +
      +
      + @if ($isCloudflareTunnelsEnabled) +
      + +
      + @elseif (!$server->isFunctional()) +
      + To automatically configure Cloudflare Tunnels, please + validate your server first. Then you will need a Cloudflare token and an SSH + domain configured. +
      + To manually configure Cloudflare Tunnels, please + click here, + then you should validate the server. +

      + For more information, please read our documentation. +
      + @endif + @if (!$isCloudflareTunnelsEnabled && $server->isFunctional()) +

      Configuration

      +
      + + + + + Manual + +
      + @endif +
      +
      +
      +
      diff --git a/resources/views/livewire/server/delete.blade.php b/resources/views/livewire/server/delete.blade.php index 360e1e0c6c..1b56b35c95 100644 --- a/resources/views/livewire/server/delete.blade.php +++ b/resources/views/livewire/server/delete.blade.php @@ -1,22 +1,31 @@
      - @if ($server->id !== 0) -

      Danger Zone

      -
      Woah. I hope you know what are you doing.
      -

      Delete Server

      -
      This will remove this server from Coolify. Beware! There is no coming - back! + + {{ data_get_str($server, 'name')->limit(10) }} > Delete Server | Coolify + + +
      + +
      + @if ($server->id !== 0) +

      Danger Zone

      +
      Woah. I hope you know what are you doing.
      +

      Delete Server

      +
      This will remove this server from Coolify. Beware! There is no coming + back! +
      + @if ($server->definedResources()->count() > 0) +
      You need to delete all resources before deleting this server.
      + + @else + + @endif + @endif
      - @if ($server->definedResources()->count() > 0) -
      You need to delete all resources before deleting this server.
      - - @else - - @endif - @endif +
      diff --git a/resources/views/livewire/server/destination/show.blade.php b/resources/views/livewire/server/destination/show.blade.php deleted file mode 100644 index 1a1bbeb1b3..0000000000 --- a/resources/views/livewire/server/destination/show.blade.php +++ /dev/null @@ -1,7 +0,0 @@ -
      - - {{ data_get_str($server, 'name')->limit(10) }} > Server Destinations | Coolify - - - -
      diff --git a/resources/views/livewire/server/destinations.blade.php b/resources/views/livewire/server/destinations.blade.php new file mode 100644 index 0000000000..88503f62db --- /dev/null +++ b/resources/views/livewire/server/destinations.blade.php @@ -0,0 +1,49 @@ +
      + + {{ data_get_str($server, 'name')->limit(10) }} > Destinations | Coolify + + +
      + +
      + @if ($server->isFunctional()) +
      +

      Destinations

      + + + + Scan for Destinations +
      +
      Destinations are used to segregate resources by network.
      +

      Available Destinations

      +
      + @foreach ($server->standaloneDockers as $docker) + + {{ data_get($docker, 'network') }} + + @endforeach + @foreach ($server->swarmDockers as $docker) + + {{ data_get($docker, 'network') }} + + @endforeach +
      + @if ($networks->count() > 0) +
      +

      Found Destinations

      +
      + @foreach ($networks as $network) +
      + Add + {{ data_get($network, 'Name') }} +
      + @endforeach +
      +
      + @endif + @else +
      Server is not validated. Validate first.
      + @endif +
      +
      +
      diff --git a/resources/views/livewire/server/form.blade.php b/resources/views/livewire/server/form.blade.php deleted file mode 100644 index 48c16051ee..0000000000 --- a/resources/views/livewire/server/form.blade.php +++ /dev/null @@ -1,289 +0,0 @@ -
      -
      -
      -

      General

      - @if ($server->id === 0) - - @else - Save - @if ($server->isFunctional()) - - Validate & configure - - - - - Revalidate server - - - @endif - @endif -
      - @if ($server->isFunctional()) - Server is reachable and validated. - @else - You can't use this server until it is validated. - @endif - @if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id !== 0) - - Validate & configure - - - - - Validate Server & Install Docker Engine - - - @if ($server->validation_logs) -

      Previous Validation Logs

      -
      - {!! $server->validation_logs !!} -
      - @endif - @endif - @if ((!$server->settings->is_reachable || !$server->settings->is_usable) && $server->id === 0) - - Validate Server - - @endif - @if ($server->isForceDisabled() && isCloud()) -
      The system has disabled the server because you have exceeded the - number of servers for which you have paid.
      - @endif -
      -
      - - - @if (!$server->settings->is_swarm_worker && !$server->settings->is_build_server) - - @endif - -
      -
      - -
      - - -
      -
      -
      -
      - - -
      -
      -
      - - - - -
      -
      - -
      -
      -
      - -
      - @if (!$server->isLocalhost()) - -
      -
      -

      Cloudflare Tunnels

      - -
      - @if ($server->settings->is_cloudflare_tunnel) -
      - -
      - @elseif (!$server->isFunctional()) -
      - To automatically configure Cloudflare Tunnels, please validate your server first. Then you will need a Cloudflare token and an SSH domain configured. -
      - To manually configure Cloudflare Tunnels, please click here, then you should validate the server. -

      - For more information, please read our documentation. -
      - @endif - @if (!$server->settings->is_cloudflare_tunnel && $server->isFunctional()) - - - - @endif - @if ($server->isFunctional() &&!$server->settings->is_cloudflare_tunnel) -
      - I have configured Cloudflare Tunnels manually -
      - @endif - -
      - @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel) -

      Swarm (experimental)

      -
      Read the docs here. -
      - @if ($server->settings->is_swarm_worker) - - @else - - @endif - - @if ($server->settings->is_swarm_manager) - - @else - - @endif - @endif - @endif -
      -
      - - @if ($server->isFunctional()) -

      Settings

      -
      -
      -
      -
      - -
      - -
      - @if ($server->settings->force_docker_cleanup) - - @else - - @endif -
      - -
      -

      Warning: Enable these options only if you fully understand their implications and consequences!
      Improper use will result in data loss and could cause functional issues.

      - - -
      -
      -
      - -
      - - -
      -
      -
      -

      Sentinel

      - {{-- @if ($server->isSentinelEnabled()) --}} - {{-- Restart --}} - {{-- @endif --}} -
      -
      Metrics are disabled until a few bugs are fixed.
      - {{--
      - -
      -
      -
      - - - -
      -
      --}} - @endif -
      -
      diff --git a/resources/views/livewire/server/log-drains.blade.php b/resources/views/livewire/server/log-drains.blade.php index 1c19e3662a..a16993e960 100644 --- a/resources/views/livewire/server/log-drains.blade.php +++ b/resources/views/livewire/server/log-drains.blade.php @@ -1,120 +1,118 @@
      - {{ data_get_str($server, 'name')->limit(10) }} > Server LogDrains | Coolify + {{ data_get_str($server, 'name')->limit(10) }} > Log Drains | Coolify - - @if ($server->isFunctional()) -

      Log Drains

      -
      Sends service logs to 3rd party tools.
      -
      -
      -
      -

      New Relic

      -
      - -
      -
      -
      - @if ($server->isLogDrainEnabled()) - - + +
      + +
      + @if ($server->isFunctional()) +
      +

      Log Drains

      + +
      +
      Sends service logs to 3rd party tools.
      +
      +
      + +

      New Relic

      +
      + @if ($isLogDrainAxiomEnabled || $isLogDrainCustomEnabled) + + @else + + @endif +
      +
      +
      + @if ($server->isLogDrainEnabled()) + + + @else + + + @endif +
      +
      +
      + + Save + +
      + + +

      Axiom

      +
      + @if ($isLogDrainNewRelicEnabled || $isLogDrainCustomEnabled) + @else - - + @endif
      -
      -
      - - Save - -
      - - -

      Axiom

      -
      - -
      -
      -
      -
      - @if ($server->isLogDrainEnabled()) - - + +
      +
      + @if ($server->isLogDrainEnabled()) + + + @else + + + @endif +
      +
      +
      + + Save + +
      + +

      Custom FluentBit

      +
      + @if ($isLogDrainNewRelicEnabled || $isLogDrainAxiomEnabled) + @else - - + @endif
      -
      -
      - - Save - -
      - - {{--

      Highlight.io

      -
      - -
      -
      -
      -
      - -
      -
      -
      - - Save - -
      -
      --}} -

      Custom FluentBit configuration

      -
      - -
      -
      -
      - @if ($server->isLogDrainEnabled()) - - - @else - - - @endif + +
      + @if ($server->isLogDrainEnabled()) + + + @else + + + @endif -
      -
      - - Save - -
      - +
      +
      + + Save + +
      + -
      +
      +
      + @else +
      Server is not validated. Validate first.
      + @endif
      - @else -
      Server is not validated. Validate first.
      - @endif +
      diff --git a/resources/views/livewire/server/private-key/show.blade.php b/resources/views/livewire/server/private-key/show.blade.php index 3cf190bcac..53e9ed002b 100644 --- a/resources/views/livewire/server/private-key/show.blade.php +++ b/resources/views/livewire/server/private-key/show.blade.php @@ -1,7 +1,43 @@
      - Server Connection | Coolify + {{ data_get_str($server, 'name')->limit(10) }} > Private Key | Coolify - - + +
      + +
      +
      +

      Private Key

      + + + + + Check connection + +
      +
      Change your server's private key.
      +
      + @forelse ($privateKeys as $private_key) +
      +
      +
      {{ $private_key->name }}
      +
      {{ $private_key->description }}
      +
      + @if (data_get($server, 'privateKey.uuid') !== $private_key->uuid) + + Use this key + + @else + + Currently used + + @endif +
      + @empty +
      No private keys found.
      + @endforelse +
      +
      +
      diff --git a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php index a8192cdb16..ec63f451b2 100644 --- a/resources/views/livewire/server/proxy/dynamic-configurations.blade.php +++ b/resources/views/livewire/server/proxy/dynamic-configurations.blade.php @@ -3,8 +3,8 @@ Proxy Dynamic Configuration | Coolify -
      - +
      +
      @if ($server->isFunctional())
      diff --git a/resources/views/livewire/server/proxy/logs.blade.php b/resources/views/livewire/server/proxy/logs.blade.php index d5dc488d4d..4556d67bd3 100644 --- a/resources/views/livewire/server/proxy/logs.blade.php +++ b/resources/views/livewire/server/proxy/logs.blade.php @@ -3,8 +3,8 @@ Proxy Logs | Coolify -
      - +
      +

      Logs

      diff --git a/resources/views/livewire/server/proxy/modal.blade.php b/resources/views/livewire/server/proxy/modal.blade.php deleted file mode 100644 index 3dfb2d31cb..0000000000 --- a/resources/views/livewire/server/proxy/modal.blade.php +++ /dev/null @@ -1,12 +0,0 @@ -
      - - - - - - - Close - - - -
      diff --git a/resources/views/livewire/server/proxy/show.blade.php b/resources/views/livewire/server/proxy/show.blade.php index 381e7f8583..2370ab797c 100644 --- a/resources/views/livewire/server/proxy/show.blade.php +++ b/resources/views/livewire/server/proxy/show.blade.php @@ -4,13 +4,13 @@ @if ($server->isFunctional()) -
      - +
      +
      - @else + @else
      Server is not validated. Validate first.
      @endif
      diff --git a/resources/views/livewire/server/resources.blade.php b/resources/views/livewire/server/resources.blade.php index 1e361728ca..5968b53f05 100644 --- a/resources/views/livewire/server/resources.blade.php +++ b/resources/views/livewire/server/resources.blade.php @@ -3,23 +3,37 @@ {{ data_get_str($server, 'name')->limit(10) }} > Server Resources | Coolify -
      - +
      -
      -
      -
      -

      Resources

      - Refresh +
      +
      +

      Resources

      + Refresh +
      +
      Here you can find all resources that are managed by Coolify.
      +
      +
      $activeTab === 'managed', + ]) wire:click="loadManagedContainers"> + Managed +
      + +
      +
      +
      $activeTab === 'unmanaged', + ]) wire:click="loadUnmanagedContainers"> + Unmanaged +
      + +
      -
      Here you can find all resources that are managed by Coolify.
      - @if ($server->definedResources()->count() > 0) +
      + @if ($containers->count() > 0) + @if ($activeTab === 'managed')
      @@ -78,19 +92,7 @@
      - @else -
      No resources found.
      - @endif -
      -
      -
      -
      -

      Resources

      - Refresh -
      -
      Here you can find all other containers running on the server.
      -
      - @if ($unmanagedContainers->count() > 0) + @elseif ($activeTab === 'unmanaged')
      @@ -114,7 +116,7 @@ - @forelse ($unmanagedContainers->sortBy('name',SORT_NATURAL) as $resource) + @forelse ($containers->sortBy('name',SORT_NATURAL) as $resource) {{ data_get($resource, 'Names') }} @@ -152,11 +154,14 @@
      -
      - @else -
      No resources found.
      @endif -
      + @else + @if ($activeTab === 'managed') +
      No managed resources found.
      + @elseif ($activeTab === 'unmanaged') +
      No unmanaged resources found.
      + @endif + @endif
      diff --git a/resources/views/livewire/server/show-private-key.blade.php b/resources/views/livewire/server/show-private-key.blade.php deleted file mode 100644 index 86bf2568eb..0000000000 --- a/resources/views/livewire/server/show-private-key.blade.php +++ /dev/null @@ -1,40 +0,0 @@ -
      -
      -

      Private Key

      - - - - - Check connection - -
      - -
      - @if (data_get($server, 'privateKey.uuid')) -
      - Currently attached Private Key: - - - -
      - @else -
      No private key attached.
      - @endif - -
      -

      Choose another Key

      -
      - @forelse ($privateKeys as $private_key) -
      -
      -
      {{ $private_key->name }}
      -
      {{ $private_key->description }}
      -
      -
      - @empty -
      No private keys found.
      - @endforelse -
      -
      diff --git a/resources/views/livewire/server/show.blade.php b/resources/views/livewire/server/show.blade.php index 4a2729d3c9..de12d0e5bc 100644 --- a/resources/views/livewire/server/show.blade.php +++ b/resources/views/livewire/server/show.blade.php @@ -1,13 +1,238 @@
      - {{ data_get_str($server, 'name')->limit(10) }} > Server Configurations | Coolify + {{ data_get_str($server, 'name')->limit(10) }} > General | Coolify - - - @if ($server->isFunctional() && $server->isMetricsEnabled()) -
      - + +
      + +
      +
      +
      +

      General

      + @if ($server->id === 0) + + @else + Save + @if ($server->isFunctional()) + + Validate & configure + + + + + Revalidate server + + + @endif + @endif +
      + @if ($server->isFunctional()) + Server is reachable and validated. + @else + You can't use this server until it is validated. + @endif + @if ((!$isReachable || !$isUsable) && $server->id !== 0) + + Validate & configure + + + + + Validate Server & Install Docker Engine + + + @if ($server->validation_logs) +

      Previous Validation Logs

      +
      + {!! $server->validation_logs !!} +
      + @endif + @endif + @if ((!$isReachable || !$isUsable) && $server->id === 0) + + Validate Server + + @endif + @if ($server->isForceDisabled() && isCloud()) +
      The system has disabled the server because you have + exceeded the + number of servers for which you have paid.
      + @endif +
      +
      + + + @if (!$isSwarmWorker && !$isBuildServer) + + @endif + +
      +
      + +
      + + +
      +
      +
      +
      + + +
      +
      +
      + + + + +
      +
      + +
      +
      +
      + +
      + @if (!$server->isLocalhost()) +
      + +
      + + @if (!$server->isBuildServer() && !$server->settings->is_cloudflare_tunnel) +

      Swarm (experimental) +

      +
      Read the docs here. +
      +
      + @if ($server->settings->is_swarm_worker) + + @else + + @endif + + @if ($server->settings->is_swarm_manager) + + @else + + @endif +
      + @endif + @endif +
      +
      +
      + @if ($server->isFunctional() && !$server->isSwarm() && !$server->isBuildServer()) +
      +
      +

      Sentinel

      + @if ($server->isSentinelEnabled()) +
      + @if ($server->isSentinelLive()) + + Save + Restart + @else + + Save + Sync + @endif +
      + @endif +
      +
      +
      Experimental feature +
      +
      + + @if ($server->isSentinelEnabled()) + + + @else + + + @endif +
      + @if ($server->isSentinelEnabled()) +
      + + Regenerate +
      + + + +
      +
      + + + +
      +
      + @endif +
      +
      + @endif
      - @endif - +
      diff --git a/resources/views/livewire/settings-backup.blade.php b/resources/views/livewire/settings-backup.blade.php index 9eb34e8b73..9760c173d3 100644 --- a/resources/views/livewire/settings-backup.blade.php +++ b/resources/views/livewire/settings-backup.blade.php @@ -17,13 +17,13 @@ @if (isset($database) && isset($backup))
      - - - + + +
      - - + +
      @@ -33,9 +33,8 @@ @else To configure automatic backup for your Coolify instance, you first need to add a database resource into Coolify. - Add Database + Configure Backup @endif
      -
      diff --git a/resources/views/livewire/settings-email.blade.php b/resources/views/livewire/settings-email.blade.php index 37d395cd8c..ff3e4bfb81 100644 --- a/resources/views/livewire/settings-email.blade.php +++ b/resources/views/livewire/settings-email.blade.php @@ -1,19 +1,21 @@
      - Settings | Coolify + Transactional Email | Coolify -
      -

      Transactional Email

      -
      -
      Email settings for password resets, invitations, etc.
      -
      - - - - Save - + +
      +

      Transactional Email

      + + Save + +
      +
      Email settings for password resets, invitations, etc.
      +
      + + +
      +
      @@ -25,26 +27,26 @@
      - +
      - - - + + +
      - - - + + +
      -
      +

      Resend

      @@ -52,12 +54,12 @@
      - +
      - +
      diff --git a/resources/views/livewire/settings-oauth.blade.php b/resources/views/livewire/settings-oauth.blade.php index 9a94d3c2b1..eefd10c7c9 100644 --- a/resources/views/livewire/settings-oauth.blade.php +++ b/resources/views/livewire/settings-oauth.blade.php @@ -25,7 +25,7 @@ + type="password" label="Client Secret" autocomplete="new-password" /> @if ($oauth_setting->provider == 'azure') diff --git a/resources/views/livewire/settings/index.blade.php b/resources/views/livewire/settings/index.blade.php index f9293e7d7f..c41b0e641f 100644 --- a/resources/views/livewire/settings/index.blade.php +++ b/resources/views/livewire/settings/index.blade.php @@ -16,10 +16,10 @@

      Instance Settings

      - - +
      -
      - + wire:model.debounce.300ms="instance_timezone"> @@ -58,35 +59,43 @@ class="absolute z-50 w-full mt-1 bg-white dark:bg-coolgray-100 border dark:bord
      -
      +
      +
      + + +

      DNS Validation

      -
      {{--
      - - + +
      --}}

      API

      -
      +
      - @@ -95,7 +104,7 @@ class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-coolgray-300 tex
      -
      Update
      +

      Update

      @if (!is_null(env('AUTOUPDATE', null)))
      @@ -119,6 +128,37 @@ class="px-4 py-2 cursor-pointer hover:bg-gray-100 dark:hover:bg-coolgray-300 tex helper="Cron expression for auto update frequency (automatically update coolify).
      You can use every_minute, hourly, daily, weekly, monthly, yearly.

      Default is every day at 00:00" /> @endif
      - +

      Advanced

      +
      + + +
      + +
      Confirmation Settings
      + @if ($disable_two_step_confirmation) +
      + +
      + @else +
      + +
      +
      +

      Warning!

      +

      Disabling two step confirmation reduces security (as anyone can easily delete anything) and increases + the + risk of accidental actions. This is not recommended for production servers.

      +
      + @endif +
      diff --git a/resources/views/livewire/subscription/pricing-plans.blade.php b/resources/views/livewire/subscription/pricing-plans.blade.php index 384a09369e..82b491eb18 100644 --- a/resources/views/livewire/subscription/pricing-plans.blade.php +++ b/resources/views/livewire/subscription/pricing-plans.blade.php @@ -14,21 +14,18 @@ class="sr-only"> :class="selected === 'yearly' ? 'dark:bg-coollabs-100 bg-warning dark:text-white' : ''"> - Annually (save ~20%) + Annually (save ~20%)
      - {{--
      For the detailed list of features, please visit our landing page: coolify.io
      --}}

      Pay-as-you-go

      Dynamic pricing based on the number of servers you connect. -

      +

      $5 @@ -43,43 +40,38 @@ class="grid max-w-sm grid-cols-1 -mt-16 divide-y divide-neutral-200 dark:divide-

      $3 - per additional servers billed monthly (+VAT) + per additional servers billed monthly (+VAT) $2.7 - per additional servers billed annually (+VAT) + per additional servers billed annually (+VAT)

      - + + + +
      -
      - You need to bring your own servers from any cloud provider (such as Hetzner, DigitalOcean, AWS, etc.) -
      -
      - (You can connect your RPi, old laptop, or any other device that runs - the supported operating systems.) -
      +
      + You need to bring your own servers from any cloud provider (such as Hetzner, DigitalOcean, AWS, + etc.) +
      +
      + (You can connect your RPi, old laptop, or any other device that runs + the supported operating systems.) +
      -
      +
      Subscribe @@ -90,120 +82,72 @@ class="w-full h-10 buyme" wire:click="subscribeStripe('dynamic-yearly')">
      • - - Connect - unlimited servers + + Connect + unlimited servers
      • - - Deploy - unlimited applications per server + + Deploy + unlimited applications per server
      • - - Free email notifications + + Free email notifications
      • - - Support by email + + Support by email
      • - - - - - - - + All Upcoming Features + + + + + + + + All Upcoming Features
      • - + + + + + - Do you require official support for your self-hosted instance?Contact Us + Do you require official support for your self-hosted instance?Contact Us
      • -
      +
      diff --git a/resources/views/livewire/tags/deployments.blade.php b/resources/views/livewire/tags/deployments.blade.php index 03da021f9a..8f23d994d4 100644 --- a/resources/views/livewire/tags/deployments.blade.php +++ b/resources/views/livewire/tags/deployments.blade.php @@ -1,5 +1,5 @@ -
      - @forelse ($deployments_per_tag_per_server as $server_name => $deployments) +
      + @forelse ($deploymentsPerTagPerServer as $server_name => $deployments)

      {{ $server_name }}

      @foreach ($deployments as $deployment) diff --git a/resources/views/livewire/tags/index.blade.php b/resources/views/livewire/tags/index.blade.php deleted file mode 100644 index 287e1da55a..0000000000 --- a/resources/views/livewire/tags/index.blade.php +++ /dev/null @@ -1,67 +0,0 @@ -
      - - Tags | Coolify - -

      Tags

      -
      -
      Tags help you to perform actions on multiple resources.
      -
      - @if ($tags->count() === 0) -
      No tags yet defined yet. Go to a resource and add a tag there.
      - @else - - @foreach ($tags as $oneTag) - - @endforeach - - @if ($tag) -
      -
      -
      - -
      - -
      - -
      -

      Deployments

      - @if (count($deployments_per_tag_per_server) > 0) - - @endif -
      - -
      - @endif - @endif -
      -
      -
      diff --git a/resources/views/livewire/tags/show.blade.php b/resources/views/livewire/tags/show.blade.php index 869b56daeb..9127278ef8 100644 --- a/resources/views/livewire/tags/show.blade.php +++ b/resources/views/livewire/tags/show.blade.php @@ -1,90 +1,96 @@
      - - Tag | Coolify - -
      +

      Tags

      +
      Tags help you to perform actions on multiple resources.
      -
      -
      Available tags
      -
      - @forelse ($tags as $oneTag) - {{ $oneTag->name }} - @empty -
      No tags yet defined yet. Go to a resource and add a tag there.
      - @endforelse -
      +
      + @forelse ($tags as $oneTag) + {{ $oneTag->name }} + @empty +
      No tags yet defined yet. Go to a resource and add a tag there.
      + @endforelse
      -
      -

      Details

      -
      -
      - + @if (isset($tag)) +
      +

      Details

      +
      +
      + +
      +
      - -
      - -
      -

      Deployments

      - @if (count($deployments_per_tag_per_server) > 0) - - @endif -
      -
      - @forelse ($deployments_per_tag_per_server as $server_name => $deployments) -

      {{ $server_name }}

      - diff --git a/resources/views/livewire/team/invite-link.blade.php b/resources/views/livewire/team/invite-link.blade.php index 739c062672..2e0f020785 100644 --- a/resources/views/livewire/team/invite-link.blade.php +++ b/resources/views/livewire/team/invite-link.blade.php @@ -1,8 +1,10 @@
      - + - + @if (auth()->user()->role() === 'owner') + + @endif diff --git a/resources/views/livewire/team/member.blade.php b/resources/views/livewire/team/member.blade.php index 654fd03e22..fac93e1c7a 100644 --- a/resources/views/livewire/team/member.blade.php +++ b/resources/views/livewire/team/member.blade.php @@ -1,6 +1,6 @@ $member->id == auth()->user()->id, + 'dark:bg-coolgray-100 bg-neutral-200' => $member->id == Auth::id(), ])> {{ $member->name }} @@ -12,9 +12,9 @@ {{ data_get($member, 'pivot.role') }} - @if (auth()->user()->isAdminFromSession()) - @if ($member->id !== auth()->user()->id) - @if (auth()->user()->isOwner()) + @if (Auth::user()->isAdminFromSession()) + @if ($member->id !== Auth::id()) + @if (Auth::user()->isOwner()) @if (data_get($member, 'pivot.role') === 'owner') To Admin To Member @@ -30,7 +30,7 @@ To Admin Remove @endif - @elseif (auth()->user()->isAdmin()) + @elseif (Auth::user()->isAdmin()) @if (data_get($member, 'pivot.role') === 'admin') To Member Remove diff --git a/routes/api.php b/routes/api.php index 57f45be5da..e8425aeb1c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -13,6 +13,8 @@ use App\Http\Middleware\ApiAllowed; use App\Http\Middleware\IgnoreReadOnlyApiToken; use App\Http\Middleware\OnlyRootApiToken; +use App\Jobs\PushServerUpdateJob; +use App\Models\Server; use Illuminate\Support\Facades\Route; Route::get('/health', [OtherController::class, 'healthcheck']); @@ -126,7 +128,34 @@ Route::match(['get', 'post'], '/services/{uuid}/start', [ServicesController::class, 'action_deploy'])->middleware([IgnoreReadOnlyApiToken::class]); Route::match(['get', 'post'], '/services/{uuid}/restart', [ServicesController::class, 'action_restart'])->middleware([IgnoreReadOnlyApiToken::class]); Route::match(['get', 'post'], '/services/{uuid}/stop', [ServicesController::class, 'action_stop'])->middleware([IgnoreReadOnlyApiToken::class]); +}); +Route::group([ + 'prefix' => 'v1', +], function () { + Route::post('/sentinel/push', function () { + $token = request()->header('Authorization'); + if (! $token) { + return response()->json(['message' => 'Unauthorized'], 401); + } + $naked_token = str_replace('Bearer ', '', $token); + $decrypted = decrypt($naked_token); + $decrypted_token = json_decode($decrypted, true); + $server_uuid = data_get($decrypted_token, 'server_uuid'); + $server = Server::where('uuid', $server_uuid)->first(); + if (! $server) { + return response()->json(['message' => 'Server not found'], 404); + } + if ($server->settings->sentinel_token !== $naked_token) { + return response()->json(['message' => 'Unauthorized'], 401); + } + $data = request()->all(); + + // \App\Jobs\ServerCheckNewJob::dispatch($server, $data); + PushServerUpdateJob::dispatch($server, $data); + + return response()->json(['message' => 'ok'], 200); + }); }); Route::any('/{any}', function () { diff --git a/routes/channels.php b/routes/channels.php index d60b9590a6..f75c30d0f1 100644 --- a/routes/channels.php +++ b/routes/channels.php @@ -11,8 +11,8 @@ | */ -use App\Models\Application; use App\Models\User; +use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\Broadcast; Broadcast::channel('team.{teamId}', function (User $user, int $teamId) { @@ -24,7 +24,7 @@ }); Broadcast::channel('user.{userId}', function (User $user) { - if ($user->id === auth()->user()->id) { + if ($user->id === Auth::id()) { return true; } diff --git a/routes/web.php b/routes/web.php index 339987bc90..afe3920524 100644 --- a/routes/web.php +++ b/routes/web.php @@ -7,6 +7,8 @@ use App\Livewire\Admin\Index as AdminIndex; use App\Livewire\Boarding\Index as BoardingIndex; use App\Livewire\Dashboard; +use App\Livewire\Destination\Index as DestinationIndex; +use App\Livewire\Destination\Show as DestinationShow; use App\Livewire\Dev\Compose as Compose; use App\Livewire\ForcePasswordReset; use App\Livewire\Notifications\Discord as NotificationDiscord; @@ -34,7 +36,11 @@ use App\Livewire\Security\ApiTokens; use App\Livewire\Security\PrivateKey\Index as SecurityPrivateKeyIndex; use App\Livewire\Security\PrivateKey\Show as SecurityPrivateKeyShow; -use App\Livewire\Server\Destination\Show as DestinationShow; +use App\Livewire\Server\Advanced as ServerAdvanced; +use App\Livewire\Server\Charts as ServerCharts; +use App\Livewire\Server\CloudflareTunnels; +use App\Livewire\Server\Delete as DeleteServer; +use App\Livewire\Server\Destinations as ServerDestinations; use App\Livewire\Server\Index as ServerIndex; use App\Livewire\Server\LogDrains; use App\Livewire\Server\PrivateKey\Show as PrivateKeyShow; @@ -59,18 +65,14 @@ use App\Livewire\Storage\Show as StorageShow; use App\Livewire\Subscription\Index as SubscriptionIndex; use App\Livewire\Subscription\Show as SubscriptionShow; -use App\Livewire\Tags\Index as TagsIndex; use App\Livewire\Tags\Show as TagsShow; use App\Livewire\Team\AdminView as TeamAdminView; use App\Livewire\Team\Index as TeamIndex; use App\Livewire\Team\Member\Index as TeamMemberIndex; use App\Livewire\Terminal\Index as TerminalIndex; -use App\Livewire\Waitlist\Index as WaitlistIndex; use App\Models\GitlabApp; use App\Models\ScheduledDatabaseBackupExecution; use App\Models\Server; -use App\Models\StandaloneDocker; -use App\Models\SwarmDocker; use App\Providers\RouteServiceProvider; use Illuminate\Http\Request; use Illuminate\Support\Facades\Route; @@ -83,9 +85,9 @@ Route::get('/admin', AdminIndex::class)->name('admin.index'); -Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot'); +Route::post('/forgot-password', [Controller::class, 'forgot_password'])->name('password.forgot')->middleware('throttle:forgot-password'); Route::get('/realtime', [Controller::class, 'realtime_test'])->middleware('auth'); -Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); +// Route::get('/waitlist', WaitlistIndex::class)->name('waitlist.index'); Route::get('/verify', [Controller::class, 'verify'])->middleware('auth')->name('verify.email'); Route::get('/email/verify/{id}/{hash}', [Controller::class, 'email_verify'])->middleware(['auth'])->name('verify.verify'); Route::middleware(['throttle:login'])->group(function () { @@ -95,14 +97,14 @@ Route::get('/auth/{provider}/redirect', [OauthController::class, 'redirect'])->name('auth.redirect'); Route::get('/auth/{provider}/callback', [OauthController::class, 'callback'])->name('auth.callback'); -Route::prefix('magic')->middleware(['auth'])->group(function () { - Route::get('/servers', [MagicController::class, 'servers']); - Route::get('/destinations', [MagicController::class, 'destinations']); - Route::get('/projects', [MagicController::class, 'projects']); - Route::get('/environments', [MagicController::class, 'environments']); - Route::get('/project/new', [MagicController::class, 'newProject']); - Route::get('/environment/new', [MagicController::class, 'newEnvironment']); -}); +// Route::prefix('magic')->middleware(['auth'])->group(function () { +// Route::get('/servers', [MagicController::class, 'servers']); +// Route::get('/destinations', [MagicController::class, 'destinations']); +// Route::get('/projects', [MagicController::class, 'projects']); +// Route::get('/environments', [MagicController::class, 'environments']); +// Route::get('/project/new', [MagicController::class, 'newProject']); +// Route::get('/environment/new', [MagicController::class, 'newEnvironment']); +// }); Route::middleware(['auth', 'verified'])->group(function () { Route::middleware(['throttle:force-password-reset'])->group(function () { @@ -124,8 +126,7 @@ Route::get('/profile', ProfileIndex::class)->name('profile'); Route::prefix('tags')->group(function () { - Route::get('/', TagsIndex::class)->name('tags.index'); - Route::get('/{tag_name}', TagsShow::class)->name('tags.show'); + Route::get('/{tagName?}', TagsShow::class)->name('tags.show'); }); Route::prefix('notifications')->group(function () { @@ -163,7 +164,7 @@ })->name('terminal.auth'); Route::prefix('invitations')->group(function () { - Route::get('/{uuid}', [Controller::class, 'accept_invitation'])->name('team.invitation.accept'); + Route::get('/{uuid}', [Controller::class, 'acceptInvitation'])->name('team.invitation.accept'); Route::get('/{uuid}/revoke', [Controller::class, 'revoke_invitation'])->name('team.invitation.revoke'); }); @@ -205,14 +206,21 @@ Route::prefix('server/{server_uuid}')->group(function () { Route::get('/', ServerShow::class)->name('server.show'); + Route::get('/advanced', ServerAdvanced::class)->name('server.advanced'); + Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key'); Route::get('/resources', ResourcesShow::class)->name('server.resources'); + Route::get('/cloudflare-tunnels', CloudflareTunnels::class)->name('server.cloudflare-tunnels'); + Route::get('/destinations', ServerDestinations::class)->name('server.destinations'); + Route::get('/log-drains', LogDrains::class)->name('server.log-drains'); + Route::get('/metrics', ServerCharts::class)->name('server.charts'); + Route::get('/danger', DeleteServer::class)->name('server.delete'); Route::get('/proxy', ProxyShow::class)->name('server.proxy'); Route::get('/proxy/dynamic', ProxyDynamicConfigurations::class)->name('server.proxy.dynamic-confs'); Route::get('/proxy/logs', ProxyLogs::class)->name('server.proxy.logs'); - Route::get('/private-key', PrivateKeyShow::class)->name('server.private-key'); - Route::get('/destinations', DestinationShow::class)->name('server.destinations'); - Route::get('/log-drains', LogDrains::class)->name('server.log-drains'); + Route::get('/terminal', ExecuteContainerCommand::class)->name('server.command'); }); + Route::get('/destinations', DestinationIndex::class)->name('destination.index'); + Route::get('/destination/{destination_uuid}', DestinationShow::class)->name('destination.show'); // Route::get('/security', fn () => view('security.index'))->name('security.index'); Route::get('/security/private-key', SecurityPrivateKeyIndex::class)->name('security.private-key.index'); @@ -232,7 +240,7 @@ })->name('source.all'); Route::get('/source/github/{github_app_uuid}', GitHubChange::class)->name('source.github.show'); Route::get('/source/gitlab/{gitlab_app_uuid}', function (Request $request) { - $gitlab_app = GitlabApp::where('uuid', request()->gitlab_app_uuid)->first(); + $gitlab_app = GitlabApp::ownedByCurrentTeam()->where('uuid', request()->gitlab_app_uuid)->firstOrFail(); return view('source.gitlab.show', [ 'gitlab_app' => $gitlab_app, @@ -244,7 +252,6 @@ Route::post('/upload/backup/{databaseUuid}', [UploadController::class, 'upload'])->name('upload.backup'); Route::get('/download/backup/{executionId}', function () { try { - ray()->clearAll(); $team = auth()->user()->currentTeam(); if (is_null($team)) { return response()->json(['message' => 'Team not found.'], 404); @@ -264,7 +271,7 @@ } } $filename = data_get($execution, 'filename'); - if ($execution->scheduledDatabaseBackup->database->getMorphClass() === 'App\Models\ServiceDatabase') { + if ($execution->scheduledDatabaseBackup->database->getMorphClass() === \App\Models\ServiceDatabase::class) { $server = $execution->scheduledDatabaseBackup->database->service->destination->server; } else { $server = $execution->scheduledDatabaseBackup->database->destination->server; @@ -305,52 +312,7 @@ return response()->json(['message' => $e->getMessage()], 500); } })->name('download.backup'); - Route::get('/destinations', function () { - $servers = Server::isUsable()->get(); - $destinations = collect([]); - foreach ($servers as $server) { - $destinations = $destinations->merge($server->destinations()); - } - $pre_selected_server_uuid = data_get(request()->query(), 'server'); - if ($pre_selected_server_uuid) { - $server = $servers->firstWhere('uuid', $pre_selected_server_uuid); - if ($server) { - $server_id = $server->id; - } - } - - return view('destination.all', [ - 'destinations' => $destinations, - 'servers' => $servers, - 'server_id' => $server_id ?? null, - ]); - })->name('destination.all'); - // Route::get('/destination/new', function () { - // $servers = Server::isUsable()->get(); - // $pre_selected_server_uuid = data_get(request()->query(), 'server'); - // if ($pre_selected_server_uuid) { - // $server = $servers->firstWhere('uuid', $pre_selected_server_uuid); - // if ($server) { - // $server_id = $server->id; - // } - // } - // return view('destination.new', [ - // "servers" => $servers, - // "server_id" => $server_id ?? null, - // ]); - // })->name('destination.new'); - Route::get('/destination/{destination_uuid}', function () { - $standalone_dockers = StandaloneDocker::where('uuid', request()->destination_uuid)->first(); - $swarm_dockers = SwarmDocker::where('uuid', request()->destination_uuid)->first(); - if (! $standalone_dockers && ! $swarm_dockers) { - abort(404); - } - $destination = $standalone_dockers ? $standalone_dockers : $swarm_dockers; - return view('destination.show', [ - 'destination' => $destination->load(['server']), - ]); - })->name('destination.show'); }); Route::any('/{any}', function () { diff --git a/scripts/install.sh b/scripts/install.sh index 76a369e62a..6712e2de25 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -13,7 +13,7 @@ DOCKER_VERSION="26.0" # TODO: Ask for a user CURRENT_USER=$USER -mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,metrics,logs} +mkdir -p /data/coolify/{source,ssh,applications,databases,backups,services,proxy,webhooks-during-maintenance,sentinel} mkdir -p /data/coolify/ssh/{keys,mux} mkdir -p /data/coolify/proxy/dynamic @@ -164,7 +164,6 @@ sles | opensuse-leap | opensuse-tumbleweed) esac - echo -e "2. Check OpenSSH server configuration. " # Detect OpenSSH server @@ -186,11 +185,51 @@ elif [ -x "$(command -v service)" ]; then SSH_DETECTED=true fi fi + + if [ "$SSH_DETECTED" = "false" ]; then - echo "###############################################################################" - echo "WARNING: Could not detect if OpenSSH server is installed and running - this does not mean that it is not installed, just that we could not detect it." - echo -e "Please make sure it is set, otherwise Coolify cannot connect to the host system. \n" - echo "###############################################################################" + echo " - OpenSSH server not detected. Installing OpenSSH server." + case "$OS_TYPE" in + arch) + pacman -Sy --noconfirm openssh >/dev/null + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + alpine) + apk add openssh >/dev/null + rc-update add sshd default >/dev/null 2>&1 + service sshd start >/dev/null 2>&1 + ;; + ubuntu | debian | raspbian) + apt-get update -y >/dev/null + apt-get install -y openssh-server >/dev/null + systemctl enable ssh >/dev/null 2>&1 + systemctl start ssh >/dev/null 2>&1 + ;; + centos | fedora | rhel | ol | rocky | almalinux | amzn) + if [ "$OS_TYPE" = "amzn" ]; then + dnf install -y openssh-server >/dev/null + else + dnf install -y openssh-server >/dev/null + fi + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + sles | opensuse-leap | opensuse-tumbleweed) + zypper install -y openssh >/dev/null + systemctl enable sshd >/dev/null 2>&1 + systemctl start sshd >/dev/null 2>&1 + ;; + *) + echo "###############################################################################" + echo "WARNING: Could not detect and install OpenSSH server - this does not mean that it is not installed or not running, just that we could not detect it." + echo -e "Please make sure it is installed and running, otherwise Coolify cannot connect to the host system. \n" + echo "###############################################################################" + exit 1 + ;; + esac + echo " - OpenSSH server installed successfully." + SSH_DETECTED=true fi # Detect SSH PermitRootLogin @@ -262,9 +301,14 @@ if ! [ -x "$(command -v docker)" ]; then fi ;; *) - curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh >/dev/null 2>&1 + if [ "$OS_TYPE" = "ubuntu" ] && [ "$OS_VERSION" = "24.10" ]; then + echo "Docker automated installation is not supported on Ubuntu 24.10 (non-LTS release)." + echo "Please install Docker manually." + exit 1 + fi + curl -s https://releases.rancher.com/install-docker/${DOCKER_VERSION}.sh | sh 2>&1 if ! [ -x "$(command -v docker)" ]; then - curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} >/dev/null 2>&1 + curl -s https://get.docker.com | sh -s -- --version ${DOCKER_VERSION} 2>&1 if ! [ -x "$(command -v docker)" ]; then echo " - Docker installation failed." echo " Maybe your OS is not supported?" @@ -287,7 +331,10 @@ test -s /etc/docker/daemon.json && cp /etc/docker/daemon.json /etc/docker/daemon "log-opts": { "max-size": "10m", "max-file": "3" - } + }, + "default-address-pools": [ + {"base":"10.0.0.0/8","size":24} + ] } EOL cat >/etc/docker/daemon.json.coolify </etc/docker/daemon.json.coolify < /dev/tcp/127.0.0.1/3010' || exit 1"] + interval: 5s + timeout: 20s + retries: 3 + + redis: + image: redis + volumes: + - affine-redis-data:/data + healthcheck: + test: + - CMD + - redis-cli + - '--raw' + - incr + - ping + interval: 10s + timeout: 5s + retries: 5 + postgres: + image: postgres:16 + volumes: + - affine-postgres-data:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - 'pg_isready -U affine' + interval: 10s + timeout: 5s + retries: 5 + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-affine} + - PGDATA=/var/lib/postgresql/data/pgdata diff --git a/templates/compose/appwrite.yaml b/templates/compose/appwrite.yaml index 8622ff2b72..5eaa7bf8ff 100644 --- a/templates/compose/appwrite.yaml +++ b/templates/compose/appwrite.yaml @@ -13,7 +13,7 @@ x-logging: &x-logging services: appwrite: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 container_name: appwrite <<: *x-logging volumes: @@ -120,7 +120,7 @@ services: - _APP_ASSISTANT_OPENAI_API_KEY=${_APP_ASSISTANT_OPENAI_API_KEY} appwrite-realtime: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: realtime <<: *x-logging depends_on: @@ -146,7 +146,7 @@ services: - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} appwrite-worker-audits: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-audits <<: *x-logging container_name: appwrite-worker-audits @@ -170,7 +170,7 @@ services: - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} appwrite-worker-webhooks: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-webhooks <<: *x-logging container_name: appwrite-worker-webhooks @@ -190,7 +190,7 @@ services: - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} appwrite-worker-deletes: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-deletes <<: *x-logging container_name: appwrite-worker-deletes @@ -243,7 +243,7 @@ services: - _APP_EXECUTOR_HOST=${_APP_EXECUTOR_HOST:-http://appwrite-executor/v1} appwrite-worker-databases: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-databases <<: *x-logging container_name: appwrite-worker-databases @@ -267,7 +267,7 @@ services: - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} appwrite-worker-builds: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-builds <<: *x-logging container_name: appwrite-worker-builds @@ -326,7 +326,7 @@ services: - _APP_STORAGE_WASABI_BUCKET=${_APP_STORAGE_WASABI_BUCKET} appwrite-worker-certificates: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-certificates <<: *x-logging container_name: appwrite-worker-certificates @@ -357,7 +357,7 @@ services: - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} appwrite-worker-functions: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-functions <<: *x-logging container_name: appwrite-worker-functions @@ -392,7 +392,7 @@ services: - _APP_LOGGING_PROVIDER=${_APP_LOGGING_PROVIDER} appwrite-worker-mails: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-mails <<: *x-logging container_name: appwrite-worker-mails @@ -417,7 +417,7 @@ services: - _APP_LOGGING_CONFIG=${_APP_LOGGING_CONFIG} appwrite-worker-messaging: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-messaging <<: *x-logging container_name: appwrite-worker-messaging @@ -442,7 +442,7 @@ services: - _APP_SMS_PROVIDER=${_APP_SMS_PROVIDER} appwrite-worker-migrations: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-migrations <<: *x-logging container_name: appwrite-worker-migrations @@ -470,7 +470,7 @@ services: - _APP_MIGRATIONS_FIREBASE_CLIENT_SECRET=${_APP_MIGRATIONS_FIREBASE_CLIENT_SECRET} appwrite-maintenance: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: maintenance <<: *x-logging container_name: appwrite-maintenance @@ -501,7 +501,7 @@ services: - _APP_MAINTENANCE_RETENTION_SCHEDULES=${_APP_MAINTENANCE_RETENTION_SCHEDULES:-86400} appwrite-worker-usage: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-usage container_name: appwrite-worker-usage <<: *x-logging @@ -528,7 +528,7 @@ services: - _APP_USAGE_AGGREGATION_INTERVAL=${_APP_USAGE_AGGREGATION_INTERVAL:-30} appwrite-worker-usage-dump: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: worker-usage-dump <<: *x-logging container_name: appwrite-worker-usage-dump @@ -554,7 +554,7 @@ services: - _APP_USAGE_AGGREGATION_INTERVAL=${_APP_USAGE_AGGREGATION_INTERVAL:-30} appwrite-scheduler-functions: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: schedule-functions container_name: appwrite-scheduler-functions <<: *x-logging @@ -577,7 +577,7 @@ services: - _APP_DB_PASS=$SERVICE_PASSWORD_MARIADB appwrite-scheduler-messages: - image: appwrite/appwrite:1.5 + image: appwrite/appwrite:1.6 entrypoint: schedule-messages container_name: appwrite-scheduler-messages <<: *x-logging diff --git a/templates/compose/authentik.yaml b/templates/compose/authentik.yaml index 85281e175d..87128f6c4c 100644 --- a/templates/compose/authentik.yaml +++ b/templates/compose/authentik.yaml @@ -72,7 +72,7 @@ services: redis: condition: service_healthy postgresql: - image: docker.io/library/postgres:16-alpine + image: postgres:16-alpine restart: unless-stopped healthcheck: test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"] @@ -86,7 +86,7 @@ services: - POSTGRES_USER=${SERVICE_USER_POSTGRESQL} - POSTGRES_DB=authentik redis: - image: docker.io/library/redis:alpine + image: redis:alpine command: --save 60 1 --loglevel warning restart: unless-stopped healthcheck: diff --git a/templates/compose/azimutt.yaml b/templates/compose/azimutt.yaml index 314d4479a6..4b498e4239 100644 --- a/templates/compose/azimutt.yaml +++ b/templates/compose/azimutt.yaml @@ -9,9 +9,9 @@ services: postgres: image: postgres:15 environment: - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - - POSTGRES_USER=$SERVICE_USER_POSTGRES - - POSTGRES_DB=azimutt + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-azimutt} volumes: - azimutt-postgres-data:/var/lib/postgresql/data healthcheck: @@ -80,8 +80,8 @@ services: - PHX_SERVER=true - PHX_HOST=$SERVICE_URL_AZIMUTT - PORT=${PORT:-4000} - - DATABASE_URL=ecto://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgres/azimutt - - SECRET_KEY_BASE=$SERVICE_BASE64_64_AZIMUTT + - DATABASE_URL=ecto://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres/${POSTGRES_DB:-azimutt} + - SECRET_KEY_BASE=${SERVICE_BASE64_64_AZIMUTT} - FILE_STORAGE_ADAPTER=${FILE_STORAGE_ADAPTER:-s3} - AUTH_PASSWORD=${AUTH_PASSWORD:-true} - SKIP_ONBOARDING_FUNNEL=${SKIP_ONBOARDING_FUNNEL:-true} diff --git a/templates/compose/bitcoin-core.yaml b/templates/compose/bitcoin-core.yaml index f1a4136f1b..a33eb48510 100644 --- a/templates/compose/bitcoin-core.yaml +++ b/templates/compose/bitcoin-core.yaml @@ -9,9 +9,19 @@ services: environment: - BITCOIN_RPCUSER=${BITCOIN_RPCUSER:-bitcoinuser} - BITCOIN_RPCPASSWORD=${SERVICE_PASSWORD_PASSWORD64} - - BITCOIN_NETWORK=${BITCOIN_NETWORK:-mainnet} - BITCOIN_PRINTTOCONSOLE=${BITCOIN_PRINTTOCONSOLE:-1} - BITCOIN_TXINDEX=${BITCOIN_TXINDEX:-1} + - BITCOIN_SERVER=${BITCOIN_SERVER:-1} volumes: - - bitcoin_data:/home/bitcoin/.bitcoin - + - blockchain-data:/home/bitcoin/.bitcoin + command: + [ + "-datadir=/home/bitcoin/.bitcoin", + "-rpcbind=127.0.0.1", # only allow local connections + "-rpcallowip=127.0.0.1", + "-rpcuser=${BITCOIN_RPCUSER}", + "-rpcpassword=${SERVICE_PASSWORD_PASSWORD64}", + "-printtoconsole=${BITCOIN_PRINTTOCONSOLE}", + "-txindex=${BITCOIN_TXINDEX}", + "-server=${BITCOIN_SERVER}" + ] diff --git a/templates/compose/bookstack.yaml b/templates/compose/bookstack.yaml index 0bfe4f8e92..dd9719471f 100644 --- a/templates/compose/bookstack.yaml +++ b/templates/compose/bookstack.yaml @@ -10,24 +10,34 @@ services: environment: - SERVICE_FQDN_BOOKSTACK_80 - APP_URL=${SERVICE_FQDN_BOOKSTACK} + - APP_KEY=${SERVICE_PASSWORD_APPKEY} - PUID=1000 - PGID=1000 - TZ=${TZ:-Europe/Berlin} - DB_HOST=mariadb - DB_PORT=3306 - - DB_USER=${SERVICE_USER_MYSQL} - - DB_PASS=${SERVICE_PASSWORD_MYSQL} + - DB_USERNAME=${SERVICE_USER_MYSQL} + - DB_PASSWORD=${SERVICE_PASSWORD_MYSQL} - DB_DATABASE=${MYSQL_DATABASE:-bookstackapp} - QUEUE_CONNECTION=${QUEUE_CONNECTION} # You will need to set up an authentication provider as described at https://www.bookstackapp.com/docs/admin/third-party-auth/. - GITHUB_APP_ID=${GITHUB_APP_ID} - GITHUB_APP_SECRET=${GITHUB_APP_SECRET} + # SMTP Mail variables as per https://www.bookstackapp.com/docs/admin/email-webhooks/#email-configuration/. + - MAIL_DRIVER=${MAIL_DRIVER:-smtp} + - MAIL_HOST=${MAIL_HOST} + - MAIL_PORT=${MAIL_PORT:-587} + - MAIL_ENCRYPTION=${MAIL_ENCRYPTION:-tls} + - MAIL_USERNAME=${MAIL_USERNAME} + - MAIL_PASSWORD=${MAIL_PASSWORD} + - MAIL_FROM=${MAIL_FROM} + - MAIL_FROM_NAME=${MAIL_FROM_NAME:-BookStack} volumes: - 'bookstack-data:/config' healthcheck: test: - CMD-SHELL - - 'wget -qO- http://127.0.0.1:80/' + - 'curl -f http://127.0.0.1:80/' interval: 5s timeout: 20s retries: 10 diff --git a/templates/compose/calcom.yaml b/templates/compose/calcom.yaml new file mode 100644 index 0000000000..89f3376eef --- /dev/null +++ b/templates/compose/calcom.yaml @@ -0,0 +1,65 @@ +# documentation: https://cal.com/docs +# slogan: Scheduling infrastructure for everyone. +# tags: calcom,calendso,scheduling,open,source +# logo: svgs/calcom.svg +# port: 3000 + +services: + calcom: + image: calcom.docker.scarf.sh/calcom/cal.com + platform: linux/amd64 + environment: + # Some variables still uses Calcom previous name, Calendso + # + # Full list https://github.com/calcom/cal.com/blob/main/.env.example + - SERVICE_FQDN_CALCOM_3000 + - NEXT_PUBLIC_LICENSE_CONSENT=agree + - NODE_ENV=production + - NEXT_PUBLIC_WEBAPP_URL=${SERVICE_FQDN_CALCOM} + - NEXT_PUBLIC_API_V2_URL=${SERVICE_FQDN_CALCOM}/api/v2 + # https://next-auth.js.org/configuration/options#nextauth_url + # From https://github.com/calcom/docker?tab=readme-ov-file#important-run-time-variables, it should be ${NEXT_PUBLIC_WEBAPP_URL}/api/auth + - NEXTAUTH_URL=${SERVICE_FQDN_CALCOM}/api/auth + # It is highly recommended that the NEXTAUTH_SECRET must be overridden and very unique + # Use `openssl rand -base64 32` to generate a key + - NEXTAUTH_SECRET=${SERVICE_BASE64_CALCOMSECRET} + # Encryption key that will be used to encrypt CalDAV credentials, choose a random string, for example with `dd if=/dev/urandom bs=1K count=1 | md5sum` + - CALENDSO_ENCRYPTION_KEY=${SERVICE_BASE64_CALCOMKEY} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-calendso} + - DATABASE_HOST=postgresql + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@${DATABASE_HOST:-postgresql}/${POSTGRES_DB:-calendso} + # Needed to run migrations while using a connection pooler like PgBouncer + # Use the same one as DATABASE_URL if you are not using a connection pooler + - DATABASE_DIRECT_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@${DATABASE_HOST:-postgresql}/${POSTGRES_DB:-calendso} + # GOOGLE_API_CREDENTIALS={} + # Set this to 1 if you don't want Cal to collect anonymous usage + - CALCOM_TELEMETRY_DISABLED=1 + # E-mail settings + # Configures the global From: header whilst sending emails. + - EMAIL_FROM=${EMAIL_FROM} + - EMAIL_FROM_NAME=${EMAIL_FROM_NAME} + # Configure SMTP settings (@see https://nodemailer.com/smtp/). + - EMAIL_SERVER_HOST=${EMAIL_SERVER_HOST} + - EMAIL_SERVER_PORT=${EMAIL_SERVER_PORT} + - EMAIL_SERVER_USER=${EMAIL_SERVER_USER} + - EMAIL_SERVER_PASSWORD=${EMAIL_SERVER_PASSWORD} + - NEXT_PUBLIC_APP_NAME="Cal.com" + # More info on ALLOWED_HOSTNAMES https://github.com/calcom/cal.com/issues/12201 + - ALLOWED_HOSTNAMES=["${SERVICE_FQDN_CALCOM}"] + depends_on: + - postgresql + postgresql: + image: postgres:16-alpine + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-calendso} + volumes: + - calcom-postgresql-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/cloudbeaver.yaml b/templates/compose/cloudbeaver.yaml new file mode 100644 index 0000000000..a21b004530 --- /dev/null +++ b/templates/compose/cloudbeaver.yaml @@ -0,0 +1,18 @@ +# documentation: https://dbeaver.com/docs/cloudbeaver/ +# slogan: CloudBeaver is a lightweight web application designed for comprehensive data management. +# tags: dbeaver, data management, data, database, mysql, postgres, sqlite, sql, mongodb +# logo: svgs/cloudbeaver.svg +# port: 8978 + +services: + cloudbeaver: + image: dbeaver/cloudbeaver:24 + volumes: + - cloudbeaver-data:/opt/cloudbeaver/workspace + environment: + - SERVICE_FQDN_CLOUDBEAVER_8978 + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:8978/"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/coder.yaml b/templates/compose/coder.yaml new file mode 100644 index 0000000000..fec22b5bfb --- /dev/null +++ b/templates/compose/coder.yaml @@ -0,0 +1,45 @@ +# documentation: https://coder.com/docs +# slogan: Coder is an open-source platform for creating and managing cloud development environments on your infrastructure, with the tools and IDEs your developers already love. +# tags: coder,development,environment,self-hosted,postgres +# logo: svgs/coder.svg +# port: 7080 + +services: + coder: + image: ghcr.io/coder/coder:latest + environment: + - SERVICE_FQDN_CODER_7080 + - CODER_PG_CONNECTION_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@coder-database/${POSTGRES_DB:-coder-db}?sslmode=disable + - CODER_HTTP_ADDRESS=0.0.0.0:7080 + - CODER_ACCESS_URL=${SERVICE_FQDN_CODER} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + depends_on: + coder-database: + condition: service_healthy + healthcheck: + test: + - CMD + - wget + - "-q" + - "--spider" + - "http://localhost:7080" + interval: 5s + timeout: 20s + retries: 10 + + coder-database: + image: postgres:16.4-alpine + environment: + POSTGRES_USER: "${SERVICE_USER_POSTGRES}" + POSTGRES_PASSWORD: "${SERVICE_PASSWORD_POSTGRES}" + POSTGRES_DB: "${POSTGRES_DB:-coder-db}" + volumes: + - coder-postgres-data:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - "pg_isready -U ${POSTGRES_USER:-username} -d ${POSTGRES_DB:-coder}" + interval: 5s + timeout: 5s + retries: 5 diff --git a/templates/compose/cryptgeon.yaml b/templates/compose/cryptgeon.yaml new file mode 100644 index 0000000000..942b1601c6 --- /dev/null +++ b/templates/compose/cryptgeon.yaml @@ -0,0 +1,41 @@ +# documentation: https://github.com/cupcakearmy/cryptgeon +# slogan: Secure note / file sharing service inspired by PrivNote. +# tags: cryptgeon, secure, note, sharing, privnote, file, sharing +# logo: svgs/cryptgeon.png +# port: 8000 + +services: + app: + image: cupcakearmy/cryptgeon:latest + environment: + - SERVICE_FQDN_CRYPTGEON_8000 + - SIZE_LIMIT=${SIZE_LIMIT:-4 MiB} + - MAX_VIEWS=${MAX_VIEWS:-100} + - MAX_EXPIRATION=${MAX_EXPIRATION:-360} + - ALLOW_ADVANCED=${ALLOW_ADVANCED:-true} + - ALLOW_FILES=${ALLOW_FILES:-true} + depends_on: + redis: + condition: service_healthy + healthcheck: + test: + - CMD + - curl + - "--fail" + - "http://127.0.0.1:8000/api/live/" + interval: 1m + timeout: 3s + retries: 2 + start_period: 5s + + redis: + image: redis:7-alpine + command: "redis-server --maxmemory 200mb --maxmemory-policy allkeys-lru" + healthcheck: + test: + - CMD + - redis-cli + - PING + interval: 5s + timeout: 10s + retries: 2 diff --git a/templates/compose/dashboard.yaml b/templates/compose/dashboard.yaml deleted file mode 100644 index f977e3876b..0000000000 --- a/templates/compose/dashboard.yaml +++ /dev/null @@ -1,17 +0,0 @@ -# documentation: https://github.com/phntxx/dashboard?tab=readme-ov-file#dashboard -# slogan: A dashboard, inspired by SUI. -# tags: dashboard, web, search, bookmarks -# port: 8080 - -services: - dashboard: - image: phntxx/dashboard:latest - environment: - - SERVICE_FQDN_DASHBOARD_8080 - volumes: - - dashboard-data:/app/data - healthcheck: - test: ["CMD", "curl", "-f", "http://127.0.0.1:8080"] - interval: 2s - timeout: 10s - retries: 15 diff --git a/templates/compose/dify.yaml b/templates/compose/dify.yaml new file mode 100644 index 0000000000..fb4e971b58 --- /dev/null +++ b/templates/compose/dify.yaml @@ -0,0 +1,640 @@ +# ignore: true +# documentation: https://docs.dify.ai +# slogan: Dify is an open-source LLM app development platform. Dify's intuitive interface combines AI workflow, RAG pipeline, agent capabilities, model management, observability features and more, letting you quickly go from prototype to production. +# tags: ai, weaviate, openai, gpt, llm, lmops, dify, redis, postgres, qdrant, RAG, agent +# logo: svgs/dify.png +# port: 3000 + +x-shared-env: &shared-api-worker-env + LOG_LEVEL: ${LOG_LEVEL:-INFO} + DEBUG: ${DEBUG:-false} + FLASK_DEBUG: ${FLASK_DEBUG:-false} + CONSOLE_WEB_URL: ${CONSOLE_WEB_URL:-} + CONSOLE_API_URL: ${CONSOLE_API_URL:-} + SERVICE_API_URL: + APP_WEB_URL: ${APP_WEB_URL:-} + CHECK_UPDATE_URL: ${CHECK_UPDATE_URL:-https://updates.dify.ai} + OPENAI_API_BASE: ${OPENAI_API_BASE:-https://api.openai.com/v1} + FILES_URL: ${FILES_URL:-} + FILES_ACCESS_TIMEOUT: ${FILES_ACCESS_TIMEOUT:-300} + APP_MAX_ACTIVE_REQUESTS: ${APP_MAX_ACTIVE_REQUESTS:-0} + MIGRATION_ENABLED: ${MIGRATION_ENABLED:-true} + DEPLOY_ENV: ${DEPLOY_ENV:-PRODUCTION} + DIFY_BIND_ADDRESS: ${DIFY_BIND_ADDRESS:-0.0.0.0} + DIFY_PORT: ${DIFY_PORT:-5001} + SERVER_WORKER_AMOUNT: ${SERVER_WORKER_AMOUNT:-} + SERVER_WORKER_CLASS: ${SERVER_WORKER_CLASS:-} + CELERY_WORKER_CLASS: ${CELERY_WORKER_CLASS:-} + GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-360} + CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-} + CELERY_AUTO_SCALE: ${CELERY_AUTO_SCALE:-false} + CELERY_MAX_WORKERS: ${CELERY_MAX_WORKERS:-} + CELERY_MIN_WORKERS: ${CELERY_MIN_WORKERS:-} + API_TOOL_DEFAULT_CONNECT_TIMEOUT: ${API_TOOL_DEFAULT_CONNECT_TIMEOUT:-10} + API_TOOL_DEFAULT_READ_TIMEOUT: ${API_TOOL_DEFAULT_READ_TIMEOUT:-60} + DB_USERNAME: $SERVICE_USER_POSTGRES + DB_PASSWORD: $SERVICE_PASSWORD_POSTGRES + DB_HOST: ${DB_HOST:-db} + DB_PORT: ${DB_PORT:-5432} + DB_DATABASE: dify + SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30} + SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600} + SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false} + POSTGRES_MAX_CONNECTIONS: ${POSTGRES_MAX_CONNECTIONS:-100} + POSTGRES_SHARED_BUFFERS: ${POSTGRES_SHARED_BUFFERS:-128MB} + POSTGRES_WORK_MEM: ${POSTGRES_WORK_MEM:-4MB} + POSTGRES_MAINTENANCE_WORK_MEM: ${POSTGRES_MAINTENANCE_WORK_MEM:-64MB} + POSTGRES_EFFECTIVE_CACHE_SIZE: ${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB} + REDIS_HOST: ${REDIS_HOST:-redis} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_USERNAME: ${REDIS_USERNAME:-} + REDIS_PASSWORD: $SERVICE_PASSWORD_REDIS + REDIS_USE_SSL: ${REDIS_USE_SSL:-false} + REDIS_DB: 0 + CELERY_BROKER_URL: redis://:$SERVICE_PASSWORD_REDIS@redis:6379/1 + BROKER_USE_SSL: ${BROKER_USE_SSL:-false} + WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*} + CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*} + STORAGE_TYPE: ${STORAGE_TYPE:-local} + STORAGE_LOCAL_PATH: storage + S3_USE_AWS_MANAGED_IAM: ${S3_USE_AWS_MANAGED_IAM:-false} + S3_ENDPOINT: ${S3_ENDPOINT:-} + S3_BUCKET_NAME: ${S3_BUCKET_NAME:-} + S3_ACCESS_KEY: ${S3_ACCESS_KEY:-} + S3_SECRET_KEY: ${S3_SECRET_KEY:-} + S3_REGION: ${S3_REGION:-us-east-1} + AZURE_BLOB_ACCOUNT_NAME: ${AZURE_BLOB_ACCOUNT_NAME:-} + AZURE_BLOB_ACCOUNT_KEY: ${AZURE_BLOB_ACCOUNT_KEY:-} + AZURE_BLOB_CONTAINER_NAME: ${AZURE_BLOB_CONTAINER_NAME:-} + AZURE_BLOB_ACCOUNT_URL: ${AZURE_BLOB_ACCOUNT_URL:-} + GOOGLE_STORAGE_BUCKET_NAME: ${GOOGLE_STORAGE_BUCKET_NAME:-} + GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: ${GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64:-} + ALIYUN_OSS_BUCKET_NAME: ${ALIYUN_OSS_BUCKET_NAME:-} + ALIYUN_OSS_ACCESS_KEY: ${ALIYUN_OSS_ACCESS_KEY:-} + ALIYUN_OSS_SECRET_KEY: ${ALIYUN_OSS_SECRET_KEY:-} + ALIYUN_OSS_ENDPOINT: ${ALIYUN_OSS_ENDPOINT:-} + ALIYUN_OSS_REGION: ${ALIYUN_OSS_REGION:-} + ALIYUN_OSS_AUTH_VERSION: ${ALIYUN_OSS_AUTH_VERSION:-v4} + TENCENT_COS_BUCKET_NAME: ${TENCENT_COS_BUCKET_NAME:-} + TENCENT_COS_SECRET_KEY: ${TENCENT_COS_SECRET_KEY:-} + TENCENT_COS_SECRET_ID: ${TENCENT_COS_SECRET_ID:-} + TENCENT_COS_REGION: ${TENCENT_COS_REGION:-} + TENCENT_COS_SCHEME: ${TENCENT_COS_SCHEME:-} + OCI_ENDPOINT: ${OCI_ENDPOINT:-} + OCI_BUCKET_NAME: ${OCI_BUCKET_NAME:-} + OCI_ACCESS_KEY: ${OCI_ACCESS_KEY:-} + OCI_SECRET_KEY: ${OCI_SECRET_KEY:-} + OCI_REGION: ${OCI_REGION:-} + VECTOR_STORE: ${VECTOR_STORE:-weaviate} + WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080} + WEAVIATE_API_KEY: $SERVICE_PASSWORD_WEAVIATE + RELYT_HOST: ${RELYT_HOST:-db} + RELYT_PORT: ${RELYT_PORT:-5432} + RELYT_USER: $SERVICE_USER_RELYT + RELYT_PASSWORD: $SERVICE_PASSWORD_RELYT + RELYT_DATABASE: ${RELYT_DATABASE:-postgres} + TIDB_VECTOR_HOST: ${TIDB_VECTOR_HOST:-tidb} + TIDB_VECTOR_PORT: ${TIDB_VECTOR_PORT:-4000} + TIDB_VECTOR_USER: $SERVICE_USER_TIDB + TIDB_VECTOR_PASSWORD: $SERVICE_PASSWORD_TIDB + TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify} + # AnalyticDB configuration + ANALYTICDB_KEY_ID: ${ANALYTICDB_KEY_ID:-} + ANALYTICDB_KEY_SECRET: ${ANALYTICDB_KEY_SECRET:-} + ANALYTICDB_REGION_ID: ${ANALYTICDB_REGION_ID:-} + ANALYTICDB_INSTANCE_ID: ${ANALYTICDB_INSTANCE_ID:-} + ANALYTICDB_ACCOUNT: ${ANALYTICDB_ACCOUNT:-} + ANALYTICDB_PASSWORD: ${ANALYTICDB_PASSWORD:-} + ANALYTICDB_NAMESPACE: ${ANALYTICDB_NAMESPACE:-dify} + ANALYTICDB_NAMESPACE_PASSWORD: ${ANALYTICDB_NAMESPACE_PASSWORD:-} + TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1} + TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify} + TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30} + TENCENT_VECTOR_DB_USERNAME: ${TENCENT_VECTOR_DB_USERNAME:-dify} + TENCENT_VECTOR_DB_DATABASE: ${TENCENT_VECTOR_DB_DATABASE:-dify} + TENCENT_VECTOR_DB_SHARD: ${TENCENT_VECTOR_DB_SHARD:-1} + TENCENT_VECTOR_DB_REPLICAS: ${TENCENT_VECTOR_DB_REPLICAS:-2} + UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15} + UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5} + ETL_TYPE: ${ETL_TYPE:-dify} + MULTIMODAL_SEND_IMAGE_FORMAT: ${MULTIMODAL_SEND_IMAGE_FORMAT:-base64} + UPLOAD_IMAGE_FILE_SIZE_LIMIT: ${UPLOAD_IMAGE_FILE_SIZE_LIMIT:-10} + SENTRY_DSN: ${API_SENTRY_DSN:-} + SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0} + SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0} + NOTION_INTEGRATION_TYPE: ${NOTION_INTEGRATION_TYPE:-public} + NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-} + NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-} + NOTION_INTERNAL_SECRET: ${NOTION_INTERNAL_SECRET:-} + MAIL_TYPE: ${MAIL_TYPE:-resend} + MAIL_DEFAULT_SEND_FROM: ${MAIL_DEFAULT_SEND_FROM:-} + SMTP_SERVER: ${SMTP_SERVER:-} + SMTP_PORT: ${SMTP_PORT:-465} + SMTP_USERNAME: ${SMTP_USERNAME:-} + SMTP_PASSWORD: ${SMTP_PASSWORD:-} + SMTP_USE_TLS: ${SMTP_USE_TLS:-true} + SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false} + RESEND_API_KEY: ${RESEND_API_KEY:-your-resend-api-key} + RESEND_API_URL: https://api.resend.com + INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-1000} + INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72} + RESET_PASSWORD_TOKEN_EXPIRY_HOURS: ${RESET_PASSWORD_TOKEN_EXPIRY_HOURS:-24} + CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194} + CODE_EXECUTION_API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807} + CODE_MIN_NUMBER: ${CODE_MIN_NUMBER:--9223372036854775808} + CODE_MAX_STRING_LENGTH: ${CODE_MAX_STRING_LENGTH:-80000} + TEMPLATE_TRANSFORM_MAX_LENGTH: ${TEMPLATE_TRANSFORM_MAX_LENGTH:-80000} + CODE_MAX_STRING_ARRAY_LENGTH: ${CODE_MAX_STRING_ARRAY_LENGTH:-30} + CODE_MAX_OBJECT_ARRAY_LENGTH: ${CODE_MAX_OBJECT_ARRAY_LENGTH:-30} + CODE_MAX_NUMBER_ARRAY_LENGTH: ${CODE_MAX_NUMBER_ARRAY_LENGTH:-1000} + SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128} + SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128} + +services: + api: + image: langgenius/dify-api:latest + environment: + SECRET_KEY: $SERVICE_PASSWORD_64_SECRETKEY + INIT_PASSWORD: $SERVICE_USER_INITPASSWORD + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'api' starts the API server. + MODE: api + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + # Mount the storage directory to the container, for storing user files. + - dify-storage:/app/api/storage + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5001/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # worker service + # The Celery worker for processing the queue. + worker: + image: langgenius/dify-api:latest + environment: + # Use the shared environment variables. + <<: *shared-api-worker-env + # Startup mode, 'worker' starts the Celery worker for processing the queue. + MODE: worker + depends_on: + db: + condition: service_healthy + redis: + condition: service_healthy + volumes: + # Mount the storage directory to the container, for storing user files. + - dify-storage:/app/api/storage + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD-SHELL", "celery inspect ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # Frontend web application. + web: + image: langgenius/dify-web:latest + environment: + - SERVICE_FQDN_WEB_3000 + - CONSOLE_API_URL=${SERVICE_FQDN_WEB} + - APP_API_URL=${SERVICE_FQDN_API} + - SENTRY_DSN=${WEB_SENTRY_DSN:-} + - NEXT_TELEMETRY_DISABLED=${NEXT_TELEMETRY_DISABLED:-0} + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://web:3000"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # The postgres database. + db: + image: postgres:15-alpine + environment: + POSTGRES_USER: $SERVICE_USER_POSTGRES + POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES + POSTGRES_DB: dify + PGDATA: /var/lib/postgresql/data/pgdata + command: > + postgres -c 'max_connections=${POSTGRES_MAX_CONNECTIONS:-100}' + -c 'shared_buffers=${POSTGRES_SHARED_BUFFERS:-128MB}' + -c 'work_mem=${POSTGRES_WORK_MEM:-4MB}' + -c 'maintenance_work_mem=${POSTGRES_MAINTENANCE_WORK_MEM:-64MB}' + -c 'effective_cache_size=${POSTGRES_EFFECTIVE_CACHE_SIZE:-4096MB}' + volumes: + - dify-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD", "pg_isready", "-U", "$SERVICE_USER_POSTGRES", "-d", "dify"] + interval: 10s + timeout: 5s + retries: 5 + + # The redis cache. + redis: + image: redis:6-alpine + environment: + REDIS_PASSWORD: $SERVICE_PASSWORD_REDIS + volumes: + - dify-redis-data:/data + # Set the redis password when startup redis server. + command: redis-server --requirepass "$SERVICE_PASSWORD_REDIS" + healthcheck: + test: [ "CMD", "redis-cli", "-a", "$SERVICE_PASSWORD_REDIS", "ping" ] + + # The DifySandbox + sandbox: + image: langgenius/dify-sandbox:latest + restart: always + environment: + # The DifySandbox configurations + # Make sure you are changing this key for your deployment with a strong key. + # You can generate a strong key using `openssl rand -base64 42`. + API_KEY: ${SANDBOX_API_KEY:-dify-sandbox} + GIN_MODE: ${SANDBOX_GIN_MODE:-release} + WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15} + ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true} + HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128} + HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} + volumes: + - './volumes/sandbox/dependencies:/dependencies' + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/8194' || exit 1"] + interval: 5s + timeout: 20s + retries: 3 + + # ssrf_proxy server + # for more information, please refer to + # https://docs.dify.ai/learn-more/faq/self-host-faq#id-18.-why-is-ssrf_proxy-needed + ssrf_proxy: + image: ubuntu/squid:latest + volumes: + - type: bind + source: ./ssrf_proxy/squid.conf.template + target: /etc/squid/squid.conf.template + read_only: true + content: | + acl localnet src 0.0.0.1-0.255.255.255 # RFC 1122 "this" network (LAN) + acl localnet src 10.0.0.0/8 # RFC 1918 local private network (LAN) + acl localnet src 100.64.0.0/10 # RFC 6598 shared address space (CGN) + acl localnet src 169.254.0.0/16 # RFC 3927 link-local (directly plugged) machines + acl localnet src 172.16.0.0/12 # RFC 1918 local private network (LAN) + acl localnet src 192.168.0.0/16 # RFC 1918 local private network (LAN) + acl localnet src fc00::/7 # RFC 4193 local private network range + acl localnet src fe80::/10 # RFC 4291 link-local (directly plugged) machines + acl SSL_ports port 443 + acl Safe_ports port 80 # http + acl Safe_ports port 21 # ftp + acl Safe_ports port 443 # https + acl Safe_ports port 70 # gopher + acl Safe_ports port 210 # wais + acl Safe_ports port 1025-65535 # unregistered ports + acl Safe_ports port 280 # http-mgmt + acl Safe_ports port 488 # gss-http + acl Safe_ports port 591 # filemaker + acl Safe_ports port 777 # multiling http + acl CONNECT method CONNECT + http_access deny !Safe_ports + http_access deny CONNECT !SSL_ports + http_access allow localhost manager + http_access deny manager + http_access allow localhost + include /etc/squid/conf.d/*.conf + http_access deny all + + ################################## Proxy Server ################################ + http_port 3128 + coredump_dir ${COREDUMP_DIR} + refresh_pattern ^ftp: 1440 20% 10080 + refresh_pattern ^gopher: 1440 0% 1440 + refresh_pattern -i (/cgi-bin/|\?) 0 0% 0 + refresh_pattern \/(Packages|Sources)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims + refresh_pattern \/Release(|\.gpg)$ 0 0% 0 refresh-ims + refresh_pattern \/InRelease$ 0 0% 0 refresh-ims + refresh_pattern \/(Translation-.*)(|\.bz2|\.gz|\.xz)$ 0 0% 0 refresh-ims + refresh_pattern . 0 20% 4320 + + + # cache_dir ufs /var/spool/squid 100 16 256 + # upstream proxy, set to your own upstream proxy IP to avoid SSRF attacks + # cache_peer 172.1.1.1 parent 3128 0 no-query no-digest no-netdb-exchange default + + ################################## Reverse Proxy To Sandbox ################################ + http_port 3129 accel vhost + cache_peer ${SANDBOX_HOST} parent ${SANDBOX_PORT} 0 no-query originserver + acl src_all src all + http_access allow src_all + - type: bind + source: ./ssrf_proxy/docker-entrypoint.sh + target: /docker-entrypoint.sh + read_only: true + content: | + #!/bin/bash + + # Modified based on Squid OCI image entrypoint + + # This entrypoint aims to forward the squid logs to stdout to assist users of + # common container related tooling (e.g., kubernetes, docker-compose, etc) to + # access the service logs. + + # Moreover, it invokes the squid binary, leaving all the desired parameters to + # be provided by the "command" passed to the spawned container. If no command + # is provided by the user, the default behavior (as per the CMD statement in + # the Dockerfile) will be to use Ubuntu's default configuration [1] and run + # squid with the "-NYC" options to mimic the behavior of the Ubuntu provided + # systemd unit. + + # [1] The default configuration is changed in the Dockerfile to allow local + # network connections. See the Dockerfile for further information. + + echo "[ENTRYPOINT] re-create snakeoil self-signed certificate removed in the build process" + if [ ! -f /etc/ssl/private/ssl-cert-snakeoil.key ]; then + /usr/sbin/make-ssl-cert generate-default-snakeoil --force-overwrite > /dev/null 2>&1 + fi + + tail -F /var/log/squid/access.log 2>/dev/null & + tail -F /var/log/squid/error.log 2>/dev/null & + tail -F /var/log/squid/store.log 2>/dev/null & + tail -F /var/log/squid/cache.log 2>/dev/null & + + # Replace environment variables in the template and output to the squid.conf + echo "[ENTRYPOINT] replacing environment variables in the template" + awk '{ + while(match($0, /\${[A-Za-z_][A-Za-z_0-9]*}/)) { + var = substr($0, RSTART+2, RLENGTH-3) + val = ENVIRON[var] + $0 = substr($0, 1, RSTART-1) val substr($0, RSTART+RLENGTH) + } + print + }' /etc/squid/squid.conf.template > /etc/squid/squid.conf + + /usr/sbin/squid -Nz + echo "[ENTRYPOINT] starting squid" + /usr/sbin/squid -f /etc/squid/squid.conf -NYC 1 + - ssrf_proxy_var_log_squid:/var/log/squid + - ssrf_proxy_var_spool_squid:/var/spool/squid + entrypoint: ["/bin/sh", "/docker-entrypoint.sh"] + environment: + # pls clearly modify the squid env vars to fit your network environment. + HTTP_PORT: ${SSRF_HTTP_PORT:-3128} + COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid} + REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194} + SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox} + SANDBOX_PORT: ${SANDBOX_PORT:-8194} + networks: + - ssrf_proxy_network + - default + healthcheck: + test: ["CMD", "squid", "-k", "check"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + # The nginx reverse proxy. + # used for reverse proxying the API service and Web service. + nginx: + image: nginx:latest + volumes: + - type: bind + source: ./nginx/nginx.conf.template + target: /etc/nginx/nginx.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + + user nginx; + worker_processes ${NGINX_WORKER_PROCESSES}; + + error_log /var/log/nginx/error.log notice; + pid /var/run/nginx.pid; + + + events { + worker_connections 1024; + } + + + http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /var/log/nginx/access.log main; + + sendfile on; + #tcp_nopush on; + + keepalive_timeout ${NGINX_KEEPALIVE_TIMEOUT}; + + #gzip on; + client_max_body_size ${NGINX_CLIENT_MAX_BODY_SIZE}; + + include /etc/nginx/conf.d/*.conf; + } + - type: bind + source: ./nginx/proxy.conf.template + target: /etc/nginx/proxy.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_buffering off; + proxy_read_timeout ${NGINX_PROXY_READ_TIMEOUT}; + proxy_send_timeout ${NGINX_PROXY_SEND_TIMEOUT}; + - type: bind + source: ./nginx/https.conf.template + target: /etc/nginx/https.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + + listen ${NGINX_SSL_PORT} ssl; + ssl_certificate ${SSL_CERTIFICATE_PATH}; + ssl_certificate_key ${SSL_CERTIFICATE_KEY_PATH}; + ssl_protocols ${NGINX_SSL_PROTOCOLS}; + ssl_prefer_server_ciphers on; + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + - type: bind + source: ./nginx/docker-entrypoint.sh + target: /docker-entrypoint-mount.sh + read_only: true + content: | + #!/bin/bash + + if [ "${NGINX_HTTPS_ENABLED}" = "true" ]; then + # Check if the certificate and key files for the specified domain exist + if [ -n "${CERTBOT_DOMAIN}" ] && \ + [ -f "/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_FILENAME}" ] && \ + [ -f "/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_KEY_FILENAME}" ]; then + SSL_CERTIFICATE_PATH="/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_FILENAME}" + SSL_CERTIFICATE_KEY_PATH="/etc/letsencrypt/live/${CERTBOT_DOMAIN}/${NGINX_SSL_CERT_KEY_FILENAME}" + else + SSL_CERTIFICATE_PATH="/etc/ssl/${NGINX_SSL_CERT_FILENAME}" + SSL_CERTIFICATE_KEY_PATH="/etc/ssl/${NGINX_SSL_CERT_KEY_FILENAME}" + fi + export SSL_CERTIFICATE_PATH + export SSL_CERTIFICATE_KEY_PATH + + # set the HTTPS_CONFIG environment variable to the content of the https.conf.template + HTTPS_CONFIG=$(envsubst < /etc/nginx/https.conf.template) + export HTTPS_CONFIG + # Substitute the HTTPS_CONFIG in the default.conf.template with content from https.conf.template + envsubst '${HTTPS_CONFIG}' < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + fi + + if [ "${NGINX_ENABLE_CERTBOT_CHALLENGE}" = "true" ]; then + ACME_CHALLENGE_LOCATION='location /.well-known/acme-challenge/ { root /var/www/html; }' + else + ACME_CHALLENGE_LOCATION='' + fi + export ACME_CHALLENGE_LOCATION + + env_vars=$(printenv | cut -d= -f1 | sed 's/^/$/g' | paste -sd, -) + + envsubst "$env_vars" < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + envsubst "$env_vars" < /etc/nginx/proxy.conf.template > /etc/nginx/proxy.conf + + envsubst < /etc/nginx/conf.d/default.conf.template > /etc/nginx/conf.d/default.conf + + # Start Nginx using the default entrypoint + exec nginx -g 'daemon off;' + - type: bind + source: ./nginx/default.conf.template + target: /etc/nginx/conf.d/default.conf.template + read_only: true + content: | + # Please do not directly edit this file. Instead, modify the .env variables related to NGINX configuration. + + server { + listen ${NGINX_PORT}; + server_name ${NGINX_SERVER_NAME}; + + location /console/api { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /api { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /v1 { + proxy_pass http://api:5001; + include proxy.conf; + } + + location /files { + proxy_pass http://api:5001; + include proxy.conf; + } + + location / { + proxy_pass http://web:3000; + include proxy.conf; + } + + # placeholder for acme challenge location + ${ACME_CHALLENGE_LOCATION} + + # placeholder for https config defined in https.conf.template + ${HTTPS_CONFIG} + } + - './nginx/ssl:/etc/ssl' + - './volumes/certbot/conf/live:/etc/letsencrypt/live' + - './volumes/certbot/conf:/etc/letsencrypt' + - './volumes/certbot/www:/var/www/html' + entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ] + environment: + NGINX_SERVER_NAME: $SERVICE_FQDN_NGINX + NGINX_HTTPS_ENABLED: ${NGINX_HTTPS_ENABLED:-false} + NGINX_SSL_PORT: ${NGINX_SSL_PORT:-443} + NGINX_PORT: ${NGINX_PORT:-80} + # You're required to add your own SSL certificates/keys to the `./nginx/ssl` directory + # and modify the env vars below in .env if HTTPS_ENABLED is true. + NGINX_SSL_CERT_FILENAME: ${NGINX_SSL_CERT_FILENAME:-dify.crt} + NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key} + NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3} + NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto} + NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M} + NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65} + NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s} + NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s} + NGINX_ENABLE_CERTBOT_CHALLENGE: ${NGINX_ENABLE_CERTBOT_CHALLENGE:-false} + CERTBOT_DOMAIN: ${CERTBOT_DOMAIN:-} + depends_on: + - api + - web + healthcheck: + test: ["CMD", "nginx", "-t"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + + + # The Weaviate vector store. + weaviate: + image: semitechnologies/weaviate:1.19.0 + profiles: + - '' + - weaviate + volumes: + - dify-weaviate-data:/var/lib/weaviate + environment: + # The Weaviate configurations + # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information. + PERSISTENCE_DATA_PATH: ${WEAVIATE_PERSISTENCE_DATA_PATH:-/var/lib/weaviate} + QUERY_DEFAULTS_LIMIT: ${WEAVIATE_QUERY_DEFAULTS_LIMIT:-25} + AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: ${WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED:-false} + DEFAULT_VECTORIZER_MODULE: ${WEAVIATE_DEFAULT_VECTORIZER_MODULE:-none} + CLUSTER_HOSTNAME: ${WEAVIATE_CLUSTER_HOSTNAME:-node1} + AUTHENTICATION_APIKEY_ENABLED: ${WEAVIATE_AUTHENTICATION_APIKEY_ENABLED:-true} + AUTHENTICATION_APIKEY_ALLOWED_KEYS: $SERVICE_PASSWORD_WEAVIATE + AUTHENTICATION_APIKEY_USERS: $SERVICE_USER_WEAVIATE + AUTHORIZATION_ADMINLIST_ENABLED: ${WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED:-true} + AUTHORIZATION_ADMINLIST_USERS: $SERVICE_USER_WEAVIATE + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/v1/.well-known/live"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + +networks: + # create a network between sandbox, api and ssrf_proxy, and can not access outside. + ssrf_proxy_network: + driver: bridge + internal: true + +volumes: + ssrf_proxy_var_log_squid: + ssrf_proxy_var_spool_squid: diff --git a/templates/compose/dozzle-with-auth.yaml b/templates/compose/dozzle-with-auth.yaml index 0b0e5b9a33..8521f824bf 100644 --- a/templates/compose/dozzle-with-auth.yaml +++ b/templates/compose/dozzle-with-auth.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://dozzle.dev/ # slogan: Dozzle is a simple and lightweight web UI for Docker logs. # tags: dozzle,docker,logs,web-ui @@ -14,19 +13,19 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - type: bind - source: /data/users.yml - target: /data/users.yml + source: ./data/users.yml + target: /data/users.yml:ro content: | users: - # "admin" here is username + # "admin" is the username admin: - name: "Admin" - # Just sha-256 which can be computed with "echo -n password | shasum -a 256" - password: "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8" - email: me@email.net + email: test@email.com + name: Admin + # A sha-256 hash of the password you want to use. Can be computed with "echo -n password | shasum -a 256". Default password is "Test". + password: $2a$11$viucCvFLlHWvBNOOI6uypuVU.D09UWb.zswRxEg0MkDPi1q/bKbdG + healthcheck: test: ["CMD", "/dozzle", "healthcheck"] interval: 3s timeout: 30s retries: 5 - start_period: 30s diff --git a/templates/compose/edgedb.yaml b/templates/compose/edgedb.yaml new file mode 100644 index 0000000000..c305895ee2 --- /dev/null +++ b/templates/compose/edgedb.yaml @@ -0,0 +1,41 @@ +# ignore: true +# documentation: https://www.edgedb.com +# slogan: An open-source database designed as a spiritual successor to SQL and the relational paradigm. Powered by the Postgres query engine under the hood. +# tags: db database sql +# logo: svgs/edgedb.svg +# port: 5656 + +services: + edgedb: + image: edgedb/edgedb + environment: + - SERVICE_FQDN_EDGEDB_5656 + - EDGEDB_SERVER_ADMIN_UI=${EDGEDB_SERVER_ADMIN_UI:-enabled} + - EDGEDB_SERVER_BACKEND_DSN=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql:5432/${POSTGRES_DB:-edgedb} + - EDGEDB_SERVER_SECURITY=${EDGEDB_SERVER_SECURITY:-strict} + - EDGEDB_SERVER_PASSWORD=${SERVICE_PASSWORD_EDGEDB} + - POSTGRES_DB=${POSTGRES_DB:-edgedb} + depends_on: + postgresql: + condition: service_healthy + volumes: + - edgedb-data:/dbschema + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5656/server/status/alive"] + interval: 5s + timeout: 20s + retries: 10 + + postgresql: + image: postgres:16-alpine + volumes: + - edgedb-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-edgedb} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/firefly.yaml b/templates/compose/firefly.yaml index 4dd8dda965..1b1c6bf65a 100644 --- a/templates/compose/firefly.yaml +++ b/templates/compose/firefly.yaml @@ -29,7 +29,7 @@ services: mysql: condition: service_healthy mysql: - image: mariadb:lts + image: mariadb:11 environment: - MYSQL_USER=${SERVICE_USER_MYSQL} - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} diff --git a/templates/compose/flowise-with-databases.yaml b/templates/compose/flowise-with-databases.yaml new file mode 100644 index 0000000000..f2e26839e0 --- /dev/null +++ b/templates/compose/flowise-with-databases.yaml @@ -0,0 +1,79 @@ +# documentation: https://docs.flowiseai.com/ +# slogan: Flowise is an open source low-code tool for developers to build customized LLM orchestration flows & AI agents. Also deploys Redis, Postgres and other services. +# tags: lowcode, nocode, ai, llm, openai, anthropic, machine-learning, rag, agents, chatbot, api, team, bot, flows +# logo: svgs/flowise.png +# port: 3001 + +services: + flowise: + image: flowiseai/flowise:latest + depends_on: + pg-record-manager: + condition: service_healthy + redis-cache: + condition: service_healthy + qdrant: + condition: service_healthy + environment: + - SERVICE_FQDN_FLOWISE_3001 + - DEBUG=${DEBUG:-false} + - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY:-true} + - PORT=${PORT:-3001} + - DATABASE_PATH=/root/.flowise + - APIKEY_PATH=/root/.flowise + - SECRETKEY_PATH=/root/.flowise + - LOG_PATH=/root/.flowise/logs + - BLOB_STORAGE_PATH=/root/.flowise/storage + - FLOWISE_USERNAME=${SERVICE_USER_FLOWISE} + - FLOWISE_PASSWORD=${SERVICE_PASSWORD_FLOWISE} + volumes: + - flowise-data:/root/.flowise + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001 || exit 1"] + interval: 5s + timeout: 5s + retries: 3 + + pg-record-manager: + image: postgres:16 + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-pg-record-manager} + volumes: + - pg-record-manager-data:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - "pg_isready -h localhost -U $${POSTGRES_USER} -d $${POSTGRES_DB}" + interval: 5s + timeout: 5s + retries: 10 + start_period: 20s + + redis-cache: + image: redis:7 + volumes: + - flowise-redis-cache-data:/data + healthcheck: + test: + - CMD-SHELL + - "redis-cli -h localhost -p 6379 ping" + interval: 5s + timeout: 5s + retries: 3 + + qdrant: + image: qdrant/qdrant:latest + environment: + - SERVICE_FQDN_QDRANT_6333 + - QDRANT__SERVICE__API_KEY=${SERVICE_PASSWORD_QDRANTAPIKEY} + volumes: + - flowise-qdrant-data:/qdrant/storage + healthcheck: + test: + - CMD-SHELL + - bash -c ':> /dev/tcp/127.0.0.1/6333' || exit 1 + interval: 5s + timeout: 5s + retries: 3 diff --git a/templates/compose/flowise.yaml b/templates/compose/flowise.yaml new file mode 100644 index 0000000000..796d0cc4ed --- /dev/null +++ b/templates/compose/flowise.yaml @@ -0,0 +1,28 @@ +# documentation: https://docs.flowiseai.com/ +# slogan: Flowise is an open source low-code tool for developers to build customized LLM orchestration flows & AI agents. +# tags: lowcode, nocode, ai, llm, openai, anthropic, machine-learning, rag, agents, chatbot, api, team, bot, flows +# logo: svgs/flowise.png +# port: 3001 + +services: + flowise: + image: flowiseai/flowise:latest + environment: + - SERVICE_FQDN_FLOWISE_3001 + - DEBUG=${DEBUG:-false} + - DISABLE_FLOWISE_TELEMETRY=${DISABLE_FLOWISE_TELEMETRY:-true} + - PORT=${PORT:-3001} + - DATABASE_PATH=/root/.flowise + - APIKEY_PATH=/root/.flowise + - SECRETKEY_PATH=/root/.flowise + - LOG_PATH=/root/.flowise/logs + - BLOB_STORAGE_PATH=/root/.flowise/storage + - FLOWISE_USERNAME=${SERVICE_USER_FLOWISE} + - FLOWISE_PASSWORD=${SERVICE_PASSWORD_FLOWISE} + volumes: + - flowise-data:/root/.flowise + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:3001 || exit 1"] + interval: 5s + timeout: 5s + retries: 3 diff --git a/templates/compose/forgejo-with-mariadb.yaml b/templates/compose/forgejo-with-mariadb.yaml index 1a7bc12e12..b359884cd8 100644 --- a/templates/compose/forgejo-with-mariadb.yaml +++ b/templates/compose/forgejo-with-mariadb.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://forgejo.org/docs # slogan: Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job. # tags: version control, collaboration, code, hosting, lightweight, mariadb diff --git a/templates/compose/forgejo-with-mysql.yaml b/templates/compose/forgejo-with-mysql.yaml index 94c6034de3..9a4cd45264 100644 --- a/templates/compose/forgejo-with-mysql.yaml +++ b/templates/compose/forgejo-with-mysql.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://forgejo.org/docs # slogan: Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job. # tags: version control, collaboration, code, hosting, lightweight, mysql diff --git a/templates/compose/forgejo-with-postgresql.yaml b/templates/compose/forgejo-with-postgresql.yaml index 19eb18862c..83c5d85db9 100644 --- a/templates/compose/forgejo-with-postgresql.yaml +++ b/templates/compose/forgejo-with-postgresql.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://forgejo.org/docs # slogan: Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job. # tags: version control, collaboration, code, hosting, lightweight, postgresql diff --git a/templates/compose/forgejo.yaml b/templates/compose/forgejo.yaml index f981b3ad5f..3e7d5647f9 100644 --- a/templates/compose/forgejo.yaml +++ b/templates/compose/forgejo.yaml @@ -1,4 +1,3 @@ -# ignore: true # documentation: https://forgejo.org/docs # slogan: Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job. # tags: version control, collaboration, code, hosting, lightweight diff --git a/templates/compose/foundryvtt.yaml b/templates/compose/foundryvtt.yaml new file mode 100644 index 0000000000..5cf961a379 --- /dev/null +++ b/templates/compose/foundryvtt.yaml @@ -0,0 +1,52 @@ +# documentation: https://foundryvtt.com/kb/ +# slogan: Foundry Virtual Tabletop is a self-hosted & modern roleplaying platform +# tags: foundryvtt,foundry,vtt,ttrpg,roleplaying +# logo: svgs/foundryvtt.png +# port: 30000 + +services: + foundryvtt: + image: felddy/foundryvtt:release + expose: + - 30000 + environment: + - SERVICE_FQDN_FOUNDRY_30000 + # Account username or email address for foundryvtt.com. Required for downloading an application distribution. + - FOUNDRY_USERNAME=${FOUNDRY_USERNAME} + # Account password for foundryvtt.com. Required for downloading an application distribution. + - FOUNDRY_PASSWORD=${FOUNDRY_PASSWORD} + # The presigned URL generate from the user's profile. Required for downloading an application distribution if username/password are not provided. + - FOUNDRY_RELEASE_URL=${FOUNDRY_RELEASE_URL} + # The license key to install. e.g.; AAAA-BBBB-CCCC-DDDD-EEEE-FFFF If left unset, a license key will be fetched when using account authentication. + - FOUNDRY_LICENSE_KEY=${FOUNDRY_LICENSE_KEY} + # Admin password to be applied at startup. If omitted the admin password will be cleared. + - FOUNDRY_ADMIN_KEY=${FOUNDRY_ADMIN:-atropos} + # A custom hostname to use in place of the host machine's public IP address when displaying the address of the game session. This allows for reverse proxies or DNS servers to modify the public address. Example: foundry.example.com + - FOUNDRY_HOSTNAME=${FOUNDRY_HOSTNAME} + # A string path which is appended to the base hostname to serve Foundry VTT content from a specific namespace. For example setting this to demo will result in data being served from http://x.x.x.x/demo/. + - FOUNDRY_ROUTE_PREFIX=${FOUNDRY_ROUTE_PREFIX} + # Inform the Foundry server that the software is running behind a reverse proxy on some other port. This allows the invitation links created to the game to include the correct external port. + - FOUNDRY_PROXY_PORT=${FOUNDRY_PROXY_PORT:-80} + # Indicates whether the software is running behind a reverse proxy that uses SSL. This allows invitation links and A/V functionality to work as if the Foundry server had SSL configured directly. + - FOUNDRY_PROXY_SSL=${FOUNDRY_PROXY_SSL:-true} + # An absolute or relative path that points to the awsConfig.json⁠ or true for AWS environment variable credentials evaluation⁠ usage. + - FOUNDRY_AWS_CONFIG=${FOUNDRY_AWS_CONFIG} + # The default application language and module which provides the core translation files. + - FOUNDRY_LANGUAGE=${FOUNDRY_LANGUAGE:-en.core} + # Choose the CSS theme for the setup page. Choose from foundry, fantasy, or scifi. + - FOUNDRY_CSS_THEME=${FOUNDRY_CSS_THEME:-foundry} + # Set to true to reduce network traffic by serving minified static JavaScript and CSS files. Enabling this setting is recommended for most users, but module developers may wish to disable it. + - FOUNDRY_MINIFY_STATIC_FILES=${FOUNDRY_MINIFY_STATIC_FILES:-true} + # The world ID to startup at system start. + - FOUNDRY_WORLD=${FOUNDRY_WORLD} + - FOUNDRY_TELEMETRY=${FOUNDRY_TELEMETRY:-false} + - TIMEZONE=${TIMEZONE:-UTC} + # Set a path to cache downloads of the Foundry distribution archive and speed up subsequent container startups. + - CONTAINER_CACHE=/data/container_cache + volumes: + - foundryvtt-data:/data + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:30000"] + timeout: 5s + interval: 30s + retries: 3 diff --git a/templates/compose/freshrss-with-mariadb.yaml b/templates/compose/freshrss-with-mariadb.yaml new file mode 100644 index 0000000000..fe066ffb60 --- /dev/null +++ b/templates/compose/freshrss-with-mariadb.yaml @@ -0,0 +1,41 @@ +# documentation: https://freshrss.org/index.html +# slogan: A free, self-hostable feed aggregator. +# tags: rss, feed +# logo: svgs/freshrss.png +# port: 80 + +services: + freshrss: + image: freshrss/freshrss:latest + environment: + - SERVICE_FQDN_FRESHRSS_80 + - CRON_MIN=${CRON_MIN:-1,31} + - MARIADB_DB=${MARIADB_DATABASE:-freshrss} + - MARIADB_USER=${SERVICE_USER_MARIADB} + - MARIADB_PASSWORD=${SERVICE_PASSWORD_MARIADB} + volumes: + - freshrss-data:/var/www/FreshRSS/data + - freshrss-extensions:/var/www/FreshRSS/extensions + depends_on: + freshrss-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/80' || exit 1"] + interval: 5s + timeout: 20s + retries: 3 + + freshrss-db: + image: mariadb:11 + volumes: + - mariadb-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=$SERVICE_PASSWORD_ROOT + - MYSQL_DATABASE=${MARIADB_DATABASE:-freshrss} + - MYSQL_USER=${SERVICE_USER_MARIADB} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MARIADB} + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/freshrss-with-mysql.yaml b/templates/compose/freshrss-with-mysql.yaml new file mode 100644 index 0000000000..ca3726a124 --- /dev/null +++ b/templates/compose/freshrss-with-mysql.yaml @@ -0,0 +1,41 @@ +# documentation: https://freshrss.org/index.html +# slogan: A free, self-hostable feed aggregator. +# tags: rss, feed +# logo: svgs/freshrss.png +# port: 80 + +services: + freshrss: + image: freshrss/freshrss:latest + environment: + - SERVICE_FQDN_FRESHRSS_80 + - CRON_MIN=${CRON_MIN:-1,31} + - MYSQL_DB=${MYSQL_DATABASE:-freshrss} + - MYSQL_USER=${SERVICE_USER_MYSQL} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} + volumes: + - freshrss-data:/var/www/FreshRSS/data + - freshrss-extensions:/var/www/FreshRSS/extensions + depends_on: + freshrss-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/80' || exit 1"] + interval: 5s + timeout: 20s + retries: 3 + + freshrss-db: + image: mysql:8 + volumes: + - mysql-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=$SERVICE_PASSWORD_ROOT + - MYSQL_DATABASE=${MYSQL_DATABASE:-freshrss} + - MYSQL_USER=$SERVICE_USER_MYSQL + - MYSQL_PASSWORD=$SERVICE_PASSWORD_MYSQL + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/freshrss-with-postgresql.yaml b/templates/compose/freshrss-with-postgresql.yaml new file mode 100644 index 0000000000..8928dfd562 --- /dev/null +++ b/templates/compose/freshrss-with-postgresql.yaml @@ -0,0 +1,41 @@ +# documentation: https://freshrss.org/index.html +# slogan: A free, self-hostable feed aggregator. +# tags: rss, feed +# logo: svgs/freshrss.png +# port: 80 + +services: + freshrss: + image: freshrss/freshrss:latest + environment: + - SERVICE_FQDN_FRESHRSS_80 + - CRON_MIN=${CRON_MIN:-1,31} + - POSTGRES_DB=${POSTGRESQL_DATABASE:-freshrss} + - POSTGRES_USER=${SERVICE_USER_POSTGRESQL} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL} + - POSTGRES_HOST=postgresql + volumes: + - freshrss-data:/var/www/FreshRSS/data + - freshrss-extensions:/var/www/FreshRSS/extensions + depends_on: + freshrss-db: + condition: service_healthy + healthcheck: + test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/80' || exit 1"] + interval: 5s + timeout: 20s + retries: 3 + + freshrss-db: + image: postgres:16 + volumes: + - freshrss-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRESQL} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL} + - POSTGRES_DB=${POSTGRESQL_DATABASE:-freshrss} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/freshrss.yaml b/templates/compose/freshrss.yaml new file mode 100644 index 0000000000..c481b96d4c --- /dev/null +++ b/templates/compose/freshrss.yaml @@ -0,0 +1,20 @@ +# documentation: https://freshrss.org/index.html +# slogan: A free, self-hostable feed aggregator. +# tags: rss, feed +# logo: svgs/freshrss.png +# port: 80 + +services: + freshrss: + image: freshrss/freshrss:latest + environment: + - SERVICE_FQDN_FRESHRSS_80 + - CRON_MIN=${CRON_MIN:-1,31} + volumes: + - freshrss-data:/var/www/FreshRSS/data + - freshrss-extensions:/var/www/FreshRSS/extensions + healthcheck: + test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/80' || exit 1"] + interval: 5s + timeout: 20s + retries: 3 diff --git a/templates/compose/glitchtip.yaml b/templates/compose/glitchtip.yaml index a8e4848b09..2f0b0100c3 100644 --- a/templates/compose/glitchtip.yaml +++ b/templates/compose/glitchtip.yaml @@ -12,12 +12,13 @@ services: - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRESQL} - POSTGRES_DB=${POSTGRESQL_DATABASE:-glitchtip} volumes: - - pg-data:/var/lib/postgresql/data + - glitchtip-postgres-data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 20s retries: 10 + redis: image: redis healthcheck: @@ -25,11 +26,14 @@ services: interval: 5s timeout: 20s retries: 10 + web: image: glitchtip/glitchtip depends_on: - - postgres - - redis + postgres: + condition: service_healthy + redis: + condition: service_healthy environment: - SERVICE_FQDN_GLITCHTIP_8080 - DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip} @@ -46,14 +50,16 @@ services: interval: 5s timeout: 20s retries: 10 + worker: image: glitchtip/glitchtip command: ./bin/run-celery-with-beat.sh depends_on: - - postgres - - redis + postgres: + condition: service_healthy + redis: + condition: service_healthy environment: - - SERVICE_FQDN_GLITCHTIP - DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip} - SECRET_KEY=$SERVICE_BASE64_64_ENCRYPTION - EMAIL_URL=${EMAIL_URL:-consolemail://} @@ -68,12 +74,15 @@ services: interval: 5s timeout: 20s retries: 10 + migrate: image: glitchtip/glitchtip restart: "no" depends_on: - - postgres - - redis + postgres: + condition: service_healthy + redis: + condition: service_healthy command: "./manage.py migrate" environment: - DATABASE_URL=postgres://$SERVICE_USER_POSTGRESQL:$SERVICE_PASSWORD_POSTGRESQL@postgres:5432/${POSTGRESQL_DATABASE:-glitchtip} diff --git a/templates/compose/grafana-with-postgresql.yaml b/templates/compose/grafana-with-postgresql.yaml index 25276468e4..0ccdd235d9 100644 --- a/templates/compose/grafana-with-postgresql.yaml +++ b/templates/compose/grafana-with-postgresql.yaml @@ -39,4 +39,3 @@ services: interval: 5s timeout: 20s retries: 10 - diff --git a/templates/compose/heyform.yaml b/templates/compose/heyform.yaml new file mode 100644 index 0000000000..a92f141090 --- /dev/null +++ b/templates/compose/heyform.yaml @@ -0,0 +1,54 @@ +# documentation: https://docs.heyform.net/open-source/self-hosting +# slogan: Allows anyone to create engaging conversational forms for surveys, questionnaires, quizzes, and polls. No coding skills required. +# tags: form, builder, forms, survey, quiz, open source, self-hosted, docker +# logo: svgs/heyform.svg +# port: 8000 + +services: + heyform: + image: heyform/community-edition:latest + volumes: + - heyform-assets:/app/static/upload + depends_on: + mongo: + condition: service_healthy + keydb: + condition: service_healthy + environment: + - SERVICE_FQDN_HEYFORM_8000 + - APP_HOMEPAGE_URL=${SERVICE_FQDN_HEYFORM} + - SESSION_KEY=${SERVICE_BASE64_64_SESSION} + - FORM_ENCRYPTION_KEY=${SERVICE_BASE64_64_FORM} + - MONGO_URI=mongodb://mongo:27017/heyform + - REDIS_HOST=keydb + - REDIS_PORT=6379 + healthcheck: + test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8000 || exit 1"] + interval: 5s + timeout: 5s + retries: 3 + + mongo: + image: percona/percona-server-mongodb:latest + volumes: + - heyform-mongo-data:/data/db + healthcheck: + test: ["CMD-SHELL", "echo 'ok' > /dev/null 2>&1"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s + + keydb: + image: eqalpha/keydb:latest + command: keydb-server --appendonly yes + environment: + - KEYDB_PASSWORD=${SERVICE_PASSWORD_KEYDB} + volumes: + - heyform-keydb-data:/data + healthcheck: + test: ["CMD-SHELL", "keydb-cli", "--pass", "${SERVICE_PASSWORD_KEYDB}", "ping"] + interval: 5s + timeout: 5s + retries: 10 + start_period: 5s diff --git a/templates/compose/immich.yaml b/templates/compose/immich.yaml new file mode 100644 index 0000000000..b3525cc585 --- /dev/null +++ b/templates/compose/immich.yaml @@ -0,0 +1,76 @@ +# documentation: https://immich.app/docs/overview/introduction +# slogan: Self-hosted photo and video management solution. +# tags: photo,video,management,server,cloud,storage,sharing,metadata,face,recognition +# logo: svgs/immich.svg +# port: 2283 + +services: + immich: + image: ghcr.io/immich-app/immich-server:release + # extends: + # file: hwaccel.transcoding.yml + # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding + volumes: + - immich-uploads:/usr/src/app/upload + - /etc/localtime:/etc/localtime:ro + environment: + - SERVICE_FQDN_IMMICH_3001 + - UPLOAD_LOCATION=./library + - DB_DATA_LOCATION=./postgres + - DB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - DB_USERNAME=$SERVICE_USER_POSTGRES + - DB_DATABASE_NAME=${DB_DATABASE_NAME:-immich} + - TZ=${TZ:-Etc/UTC} + depends_on: + redis: + condition: service_healthy + database: + condition: service_healthy + healthcheck: + disable: false + + immich-machine-learning: + container_name: immich_machine_learning + # For hardware acceleration, add one of -[armnn, cuda, openvino] to the image tag. + # Example tag: ${IMMICH_VERSION:-release}-cuda + image: ghcr.io/immich-app/immich-machine-learning:release + # extends: # uncomment this section for hardware acceleration - see https://immich.app/docs/features/ml-hardware-acceleration + # file: hwaccel.ml.yml + # service: cpu # set to one of [armnn, cuda, openvino, openvino-wsl] for accelerated inference - use the `-wsl` version for WSL2 where applicable + volumes: + - immich-model-cache:/cache + environment: + - UPLOAD_LOCATION=./library + - DB_DATA_LOCATION=./postgres + - DB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - DB_USERNAME=$SERVICE_USER_POSTGRES + - DB_DATABASE_NAME=${DB_DATABASE_NAME:-immich} + - TZ=${TZ:-Etc/UTC} + healthcheck: + disable: false + + redis: + image: redis:7.4-alpine + healthcheck: + test: + - CMD + - redis-cli + - PING + interval: 5s + timeout: 10s + retries: 20 + + database: + image: docker.io/tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0 + environment: + POSTGRES_PASSWORD: ${SERVICE_PASSWORD_POSTGRES} + POSTGRES_USER: ${SERVICE_USER_POSTGRES} + POSTGRES_DB: ${DB_DATABASE_NAME:-immich} + POSTGRES_INITDB_ARGS: '--data-checksums' + volumes: + - immich-postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/jenkins.yaml b/templates/compose/jenkins.yaml new file mode 100644 index 0000000000..0606bb049c --- /dev/null +++ b/templates/compose/jenkins.yaml @@ -0,0 +1,20 @@ +# documentation: https://www.jenkins.io/doc/ +# slogan: Jenkins is an open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project. +# tags: jenkins, automation, open-source +# logo: svgs/jenkins.svg +# port: 8080 + +services: + jenkins: + image: jenkins/jenkins:latest + environment: + - SERVICE_FQDN_JENKINS_8080 + volumes: + - jenkins-home:/var/jenkins_home + - /var/run/docker.sock:/var/run/docker.sock + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/login"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/templates/compose/jitsi.yaml b/templates/compose/jitsi.yaml new file mode 100644 index 0000000000..957661c1d9 --- /dev/null +++ b/templates/compose/jitsi.yaml @@ -0,0 +1,125 @@ +# documentation: https://jitsi.github.io/handbook/docs/intro +# slogan: World's easiest way to add meetings to your apps +# logo: svgs/jitsi.svg +# tags: video, conferencing, meetings, communication, open-source + +services: + jitsi-web: + image: "jitsi/web:${JITSI_IMAGE_VERSION:-unstable}" + container_name: jitsi-web + restart: unless-stopped + ports: + - "8001:80" + - "8443:443" + volumes: + - ~/.jitsi-meet-cfg/web:/config:Z + - ~/.jitsi-meet-cfg/web/crontabs:/var/spool/cron/crontabs:Z + - ~/.jitsi-meet-cfg/transcripts:/usr/share/jitsi-meet/transcripts:Z + environment: + - SERVICE_FQDN_JITSI + - PUBLIC_URL=$SERVICE_FQDN_JITSI + - JITSI_IMAGE_VERSION=unstable + - JIBRI_RECORDER_PASSWORD=$SERVICE_PASSWORD_JITSI + - JIBRI_XMPP_PASSWORD=$SERVICE_PASSWORD_JITSI + - JICOFO_AUTH_PASSWORD=$SERVICE_PASSWORD_JITSI + - JIGASI_XMPP_PASSWORD=$SERVICE_PASSWORD_JITSI + - JVB_AUTH_PASSWORD=$SERVICE_PASSWORD_JITSI + - TZ=UTC + networks: + meet.jitsi: + aliases: + - meet.jitsi + depends_on: + - jvb + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost"] + interval: 2s + timeout: 10s + retries: 15 + + prosody: + image: "jitsi/prosody:${JITSI_IMAGE_VERSION:-unstable}" + expose: + - '5222' + - '5347' + - '5280' + container_name: jitsi-xmpp + restart: unless-stopped + volumes: + - ~/.jitsi-meet-cfg/prosody/config:/config:Z + - ~/.jitsi-meet-cfg/prosody/prosody-plugins-custom:/prosody-plugins-custom:Z + environment: + - JICOFO_AUTH_PASSWORD + - JVB_AUTH_PASSWORD + - PUBLIC_URL=$SERVICE_FQDN_JITSI + - TZ + networks: + meet.jitsi: + aliases: + - xmpp.meet.jitsi + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5280/http-bind"] + interval: 2s + timeout: 10s + retries: 15 + + jicofo: + image: "jitsi/jicofo:${JITSI_IMAGE_VERSION:-unstable}" + container_name: jitsi-jicofo + restart: unless-stopped + volumes: + - ~/.jitsi-meet-cfg/jicofo:/config:Z + environment: + - XMPP_SERVER=prosody + - JICOFO_AUTH_PASSWORD + - TZ + - JICOFO_ENABLE_HEALTH_CHECKS=1 + depends_on: + - prosody + networks: + meet.jitsi: + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8888/about/health"] + interval: 2s + timeout: 10s + retries: 15 + + jvb: + image: "jitsi/jvb:${JITSI_IMAGE_VERSION:-unstable}" + container_name: jitsi-jvb + restart: unless-stopped + expose: + - '10000:10000/udp' + - '8080:8080' + - '10000' + volumes: + - ~/.jitsi-meet-cfg/jvb:/config:Z + environment: + - JVB_ADVERTISE_IPS + - JVB_AUTH_PASSWORD + - PUBLIC_URL=$SERVICE_FQDN_JITSI + - TZ + - XMPP_SERVER=prosody + depends_on: + - prosody + networks: + meet.jitsi: + labels: + - "traefik.enable=true" + - "traefik.udp.routers.my-udp-router.entrypoints=video" + - "traefik.udp.routers.my-udp-router.service=my-udp-service" + - "traefik.udp.services.my-udp-service.loadbalancer.server.port=10000" + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8080/about/health"] + interval: 2s + timeout: 10s + retries: 15 + +networks: + meet.jitsi: + +volumes: + jitsi-web: + jitsi-xmpp: + jitsi-jicofo: + jitsi-jvb: diff --git a/templates/compose/keycloak-with-postgres.yaml b/templates/compose/keycloak-with-postgres.yaml index eaa48f74d7..9f9a395a0c 100644 --- a/templates/compose/keycloak-with-postgres.yaml +++ b/templates/compose/keycloak-with-postgres.yaml @@ -6,7 +6,7 @@ services: keycloak: - image: quay.io/keycloak/keycloak:25.0.2 + image: quay.io/keycloak/keycloak:26.0 command: - start environment: @@ -32,7 +32,7 @@ services: test: [ "CMD-SHELL", - "exec 3<>/dev/tcp/127.0.0.1/9000;echo -e 'GET /health/ready HTTP/1.1\r\nhost: http://localhost\r\nConnection: close\r\n\r\n' >&3;if [ $? -eq 0 ]; then echo 'Healthcheck Successful';exit 0;else echo 'Healthcheck Failed';exit 1;fi;", + "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost:9000\r\nConnection: close\r\n\r\n' >&3;cat <&3 | grep -q '\"status\": \"UP\"' && exit 0 || exit 1", ] interval: 5s timeout: 20s diff --git a/templates/compose/keycloak.yaml b/templates/compose/keycloak.yaml index aebe83b58f..b3e7ecf079 100644 --- a/templates/compose/keycloak.yaml +++ b/templates/compose/keycloak.yaml @@ -6,7 +6,7 @@ services: keycloak: - image: quay.io/keycloak/keycloak:25.0.2 + image: quay.io/keycloak/keycloak:26.0 command: - start environment: @@ -24,7 +24,7 @@ services: test: [ "CMD-SHELL", - "exec 3<>/dev/tcp/127.0.0.1/9000;echo -e 'GET /health/ready HTTP/1.1\r\nhost: http://localhost\r\nConnection: close\r\n\r\n' >&3;if [ $? -eq 0 ]; then echo 'Healthcheck Successful';exit 0;else echo 'Healthcheck Failed';exit 1;fi;", + "exec 3<>/dev/tcp/127.0.0.1/9000; echo -e 'GET /health/ready HTTP/1.1\r\nHost: localhost:9000\r\nConnection: close\r\n\r\n' >&3;cat <&3 | grep -q '\"status\": \"UP\"' && exit 0 || exit 1", ] interval: 5s timeout: 20s diff --git a/templates/compose/kimai.yaml b/templates/compose/kimai.yaml new file mode 100644 index 0000000000..ba73ba9805 --- /dev/null +++ b/templates/compose/kimai.yaml @@ -0,0 +1,44 @@ +# documentation: https://www.kimai.org/ +# slogan: Open source time-tracking app. +# tags: time-tracking, open-source +# logo: svgs/kimai.svg +# port: 8001 + +services: + mysql: + image: mysql:8 + volumes: + - kimai-mysql-data:/var/lib/mysql + environment: + - MYSQL_DATABASE=${MYSQL_DATABASE:-kimai} + - MYSQL_USER=${SERVICE_USER_MYSQL} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT} + command: --default-storage-engine innodb + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"] + interval: 5s + timeout: 20s + retries: 10 + kimai: + image: kimai/kimai2:apache-latest + container_name: kimai + depends_on: + mysql: + condition: service_healthy + volumes: + - kimai-data:/opt/kimai/var/data + environment: + - SERVICE_FQDN_KIMAI_8001 + - APP_SECRET=${SERVICE_PASSWORD_APPSECRET} + - MAILER_FROM=${MAILER_FROM:-kimai@example.com} + - MAILER_URL=${MAILER_URL:-null://null} + - ADMINMAIL=${ADMINMAIL:-admin@kimai.local} + - ADMINPASS=${SERVICE_PASSWORD_ADMINPASS} + - DATABASE_URL=mysql://${SERVICE_USER_MYSQL}:${SERVICE_PASSWORD_MYSQL}@mysql/${MYSQL_DATABASE}?charset=utf8mb4&serverVersion=8.3.0 + - TRUSTED_HOSTS=localhost + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:8001"] + interval: 2s + timeout: 10s + retries: 15 diff --git a/templates/compose/libretranslate.yaml b/templates/compose/libretranslate.yaml new file mode 100644 index 0000000000..6b387b63fd --- /dev/null +++ b/templates/compose/libretranslate.yaml @@ -0,0 +1,19 @@ +# documentation: https://libretranslate.com/docs/ +# slogan: Free and open-source machine translation API, entirely self-hosted. +# tags: translate, api +# logo: svgs/libretranslate.svg +# port: 5000 + +services: + libretranslate: + image: "libretranslate/libretranslate:latest" + environment: + - SERVICE_FQDN_LIBRETRANSLATE_5000 + - LT_SSL=${LT_SSL:-true} + - LT_UPDATE_MODELS=${LT_UPDATE_MODELS:-true} + - LT_LOAD_ONLY=${LT_LOAD_ONLY:-en,es,fr,de,ja} + volumes: + - libretranslate-api-keys:/app/db + - libretranslate-models:/home/libretranslate/.local + healthcheck: + test: ["CMD-SHELL", "./venv/bin/python scripts/healthcheck.py"] diff --git a/templates/compose/litequeen.yaml b/templates/compose/litequeen.yaml new file mode 100644 index 0000000000..01530125b4 --- /dev/null +++ b/templates/compose/litequeen.yaml @@ -0,0 +1,24 @@ +# documentation: https://litequeen.com/ +# slogan: Lite Queen is an open-source SQLite database management software that runs on your server. +# tags: sqlite, sqlite-database-management, self-hosted, VPS, database +# logo: svgs/litequeen.svg +# port: 8000 + +services: + litequeen: + image: kivsegrob/lite-queen:latest + environment: + - SERVICE_FQDN_LITEQUEEN_8000 + volumes: + - litequeen-data:/home/litequeen/data + - type: bind + source: ./databases + target: /srv + is_directory: true + healthcheck: + test: + - CMD-SHELL + - bash -c ':> /dev/tcp/127.0.0.1/8000' || exit 1 + interval: 5s + timeout: 5s + retries: 3 diff --git a/templates/compose/martin.yaml b/templates/compose/martin.yaml new file mode 100644 index 0000000000..a56ebe12ce --- /dev/null +++ b/templates/compose/martin.yaml @@ -0,0 +1,36 @@ +# documentation: https://maplibre.org/martin/introduction.html/ +# slogan: Martin is a tile server able to generate and serve vector tiles on the fly from large PostGIS databases, PMTiles (local or remote), and MBTiles files, allowing multiple tile sources to be dynamically combined into one. +# tags: postgis, vector, tiles +# logo: svgs/martin.png +# port: 3000 + +services: + martin: + image: ghcr.io/maplibre/martin:v0.13.0 + environment: + - SERVICE_FQDN_MARTIN_3000 + - HOST=${SERVICE_FQDN_MARTIN} + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql:5432/${POSTGRES_DB:-martin-db} + depends_on: + postgresql: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000"] + interval: 5s + timeout: 20s + retries: 10 + + postgresql: + image: postgis/postgis:16-3.4-alpine + platform: linux/amd64 + volumes: + - martin-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-martin-db} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/mediawiki.yaml b/templates/compose/mediawiki.yaml index 28c1bbd96a..12d9ec5111 100644 --- a/templates/compose/mediawiki.yaml +++ b/templates/compose/mediawiki.yaml @@ -12,7 +12,8 @@ services: volumes: - mediawiki-images:/var/www/html/images - mediawiki-sqlite:/var/www/html/data - - ./LocalSettings.php:/var/www/html/LocalSettings.php + # Follow the instructions in https://coolify.io/docs/services/mediawiki#installation-steps for the following line + # - ./LocalSettings.php:/var/www/html/LocalSettings.php healthcheck: test: ["CMD", "curl", "-f", "http://localhost:80"] interval: 5s diff --git a/templates/compose/mindsdb.yaml b/templates/compose/mindsdb.yaml new file mode 100644 index 0000000000..72dc5a2d06 --- /dev/null +++ b/templates/compose/mindsdb.yaml @@ -0,0 +1,48 @@ +# documentation: https://docs.mindsdb.com/what-is-mindsdb +# slogan: MindsDB is the platform for building AI from enterprise data, enabling smarter organizations. +# tags: mysql, postgresdb, machine-learning, ai +# logo: svgs/mindsdb.svg +# port: 47334 + +services: + mindsdb: + image: mindsdb/mindsdb:latest + environment: + - SERVICE_FQDN_MINDSDB_47334 + - SERVICE_FQDN_API_47335=/api + - MINDSDB_DOCKER_ENV=true + - MINDSDB_STORAGE_DIR=/mindsdb/var + - FLASK_DEBUG=${FLASK_DEBUG:-1} # This will make sure http requests are logged regardless of log level + - OPENAI_API_KEY=${OPENAI_API_KEY} + - LANGFUSE_HOST=${LANGFUSE_HOST} + - LANGFUSE_PUBLIC_KEY=${LANGFUSE_PUBLIC_KEY} + - LANGFUSE_SECRET_KEY=${LANGFUSE_SECRET_KEY} + - LANGFUSE_RELEASE=${LANGFUSE_RELEASE:-local} + - LANGFUSE_DEBUG=${LANGFUSE_DEBUG:-False} + - LANGFUSE_TIMEOUT=${LANGFUSE_TIMEOUT:-10} + - LANGFUSE_SAMPLE_RATE=${LANGFUSE_SAMPLE_RATE:-1.0} + - MINDSDB_DB_CON=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql/${POSTGRES_DB:-mindsdb-db} + volumes: + - mindsdb-data:/mindsdb/var + depends_on: + postgresql: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:47334/api/util/ping"] + interval: 30s + timeout: 5s + retries: 15 + + postgresql: + image: postgres:16-alpine + volumes: + - mindsdb-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-mindsdb-db} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 15 diff --git a/templates/compose/mosquitto.yaml b/templates/compose/mosquitto.yaml new file mode 100644 index 0000000000..475d7cf394 --- /dev/null +++ b/templates/compose/mosquitto.yaml @@ -0,0 +1,48 @@ +# documentation: https://mosquitto.org/documentation/ +# slogan: Mosquitto is lightweight and suitable for use on all devices, from low-power single-board computers to full servers. +# tags: mosquitto, mqtt, open-source +# logo: svgs/mosquitto.svg +# port: 1883 + +services: + mosquitto: + image: eclipse-mosquitto + environment: + - SERVICE_FQDN_MOSQUITTO_1883 + - MQTT_USERNAME=${SERVICE_USER_MOSQUITTO} + - MQTT_PASSWORD=${SERVICE_PASSWORD_MOSQUITTO} + - REQUIRE_CERTIFICATE=${REQUIRE_CERTIFICATE:-false} + - ALLOW_ANONYMOUS=${ALLOW_ANONYMOUS:-true} + volumes: + - mosquitto-config:/mosquitto/config + - mosquitto-certs:/certs + healthcheck: + test: ["CMD-SHELL", "exit 0"] + interval: 30s + timeout: 10s + retries: 3 + + entrypoint: 'sh -c " + if [ ''$REQUIRE_CERTIFICATE'' = ''true'' ]; then + echo ''listener 8883'' > /mosquitto/config/mosquitto.conf && + echo ''cafile /certs/ca.crt'' >> /mosquitto/config/mosquitto.conf && + echo ''certfile /certs/server.crt'' >> /mosquitto/config/mosquitto.conf && + echo ''keyfile /certs/server.key'' >> /mosquitto/config/mosquitto.conf; + else + echo ''listener 1883'' > /mosquitto/config/mosquitto.conf; + fi && + echo ''require_certificate ''$REQUIRE_CERTIFICATE >> /mosquitto/config/mosquitto.conf && + echo ''allow_anonymous ''$ALLOW_ANONYMOUS >> /mosquitto/config/mosquitto.conf; + if [ -n ''$SERVICE_USER_MOSQUITTO''] && [ -n ''$SERVICE_PASSWORD_MOSQUITTO'' ]; then + echo ''password_file /mosquitto/config/passwords'' >> /mosquitto/config/mosquitto.conf && + touch /mosquitto/config/passwords && + chmod 0700 /mosquitto/config/passwords && + chown root:root /mosquitto/config/passwords && + mosquitto_passwd -b -c /mosquitto/config/passwords $SERVICE_USER_MOSQUITTO $SERVICE_PASSWORD_MOSQUITTO && + chown mosquitto:mosquitto /mosquitto/config/passwords; + fi && + exec mosquitto -c /mosquitto/config/mosquitto.conf + "' + labels: + - traefik.tcp.routers.mqtt.entrypoints=mqtt + - traefik.tcp.routers.mqtts.entrypoints=mqtts diff --git a/templates/compose/nextcloud-with-mariadb.yaml b/templates/compose/nextcloud-with-mariadb.yaml new file mode 100644 index 0000000000..5cab4f0bbc --- /dev/null +++ b/templates/compose/nextcloud-with-mariadb.yaml @@ -0,0 +1,61 @@ +# documentation: https://docs.nextcloud.com +# slogan: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management. +# tags: cloud, collaboration, communication, filestorage, data +# logo: svgs/nextcloud.svg +# port: 80 + +services: + nextcloud: + image: lscr.io/linuxserver/nextcloud:latest + environment: + - SERVICE_FQDN_NEXTCLOUD_80 + - PUID=1000 + - PGID=1000 + - TZ=${TZ:-Europe/Paris} + - MYSQL_DATABASE=${MARIADB_DATABASE:-nextcloud} + - MYSQL_USER=${SERVICE_USER_MARIADB} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MARIADB} + - MYSQL_HOST=nextcloud-db + - REDIS_HOST=redis + - REDIS_PORT=6379 + volumes: + - nextcloud-config:/config + - nextcloud-data:/data + depends_on: + nextcloud-db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:80"] + interval: 2s + timeout: 10s + retries: 15 + + nextcloud-db: + image: mariadb:11 + volumes: + - nextcloud-mariadb-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT} + - MYSQL_DATABASE=${MARIADB_DATABASE:-nextcloud} + - MYSQL_USER=${SERVICE_USER_MARIADB} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MARIADB} + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 20s + retries: 10 + + redis: + image: "redis:7.4-alpine" + volumes: + - "nextcloud-redis-data:/data" + healthcheck: + test: + - CMD + - redis-cli + - PING + interval: 5s + timeout: 10s + retries: 20 diff --git a/templates/compose/nextcloud-with-mysql.yaml b/templates/compose/nextcloud-with-mysql.yaml new file mode 100644 index 0000000000..f8f6b972fa --- /dev/null +++ b/templates/compose/nextcloud-with-mysql.yaml @@ -0,0 +1,61 @@ +# documentation: https://docs.nextcloud.com +# slogan: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management. +# tags: cloud, collaboration, communication, filestorage, data +# logo: svgs/nextcloud.svg +# port: 80 + +services: + nextcloud: + image: lscr.io/linuxserver/nextcloud:latest + environment: + - SERVICE_FQDN_NEXTCLOUD_80 + - PUID=1000 + - PGID=1000 + - TZ=${TZ:-Europe/Paris} + - MYSQL_DATABASE=${MYSQL_DATABASE:-nextcloud} + - MYSQL_USER=${SERVICE_USER_MYSQL} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} + - MYSQL_HOST=nextcloud-db + - REDIS_HOST=redis + - REDIS_PORT=6379 + volumes: + - nextcloud-config:/config + - nextcloud-data:/data + depends_on: + nextcloud-db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:80"] + interval: 2s + timeout: 10s + retries: 15 + + nextcloud-db: + image: mysql:8.4.2 + volumes: + - nextcloud-mysql-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT} + - MYSQL_DATABASE=${MYSQL_DATABASE:-nextcloud} + - MYSQL_USER=${SERVICE_USER_MYSQL} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"] + interval: 5s + timeout: 20s + retries: 10 + + redis: + image: "redis:7.4-alpine" + volumes: + - "nextcloud-redis-data:/data" + healthcheck: + test: + - CMD + - redis-cli + - PING + interval: 5s + timeout: 10s + retries: 20 diff --git a/templates/compose/nextcloud-with-postgres.yaml b/templates/compose/nextcloud-with-postgres.yaml new file mode 100644 index 0000000000..503fb4b826 --- /dev/null +++ b/templates/compose/nextcloud-with-postgres.yaml @@ -0,0 +1,60 @@ +# documentation: https://docs.nextcloud.com +# slogan: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management. +# tags: cloud, collaboration, communication, filestorage, data +# logo: svgs/nextcloud.svg +# port: 80 + +services: + nextcloud: + image: lscr.io/linuxserver/nextcloud:latest + environment: + - SERVICE_FQDN_NEXTCLOUD_80 + - PUID=1000 + - PGID=1000 + - TZ=${TZ:-Europe/Paris} + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_HOST=nextcloud-db + - REDIS_HOST=redis + - REDIS_PORT=6379 + volumes: + - nextcloud-config:/config + - nextcloud-data:/data + depends_on: + nextcloud-db: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:80"] + interval: 2s + timeout: 10s + retries: 15 + + nextcloud-db: + image: postgres:16-alpine + volumes: + - nextcloud-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-nextcloud} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 + + redis: + image: "redis:7.4-alpine" + volumes: + - "nextcloud-redis-data:/data" + healthcheck: + test: + - CMD + - redis-cli + - PING + interval: 5s + timeout: 10s + retries: 20 diff --git a/templates/compose/nextcloud.yaml b/templates/compose/nextcloud.yaml index 33858ee15a..d0b2c2a593 100644 --- a/templates/compose/nextcloud.yaml +++ b/templates/compose/nextcloud.yaml @@ -2,15 +2,16 @@ # slogan: NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management. # tags: cloud, collaboration, communication, filestorage, data # logo: svgs/nextcloud.svg +# port: 80 services: nextcloud: image: lscr.io/linuxserver/nextcloud:latest environment: - - SERVICE_FQDN_NEXTCLOUD + - SERVICE_FQDN_NEXTCLOUD_80 - PUID=1000 - PGID=1000 - - TZ=Europe/Madrid + - TZ=${TZ:-Europe/Madrid} volumes: - nextcloud-config:/config - nextcloud-data:/data diff --git a/templates/compose/ntfy.yaml b/templates/compose/ntfy.yaml new file mode 100644 index 0000000000..47b66a1247 --- /dev/null +++ b/templates/compose/ntfy.yaml @@ -0,0 +1,46 @@ +# documentation: https://docs.ntfy.sh/ +# slogan: ntfy is a simple HTTP-based pub-sub notification service. It allows you to send notifications to your phone or desktop via scripts from any computer, and/or using a REST API. +# tags: ntfy, notification, push notification, pub-sub, notify +# logo: svgs/ntfy.svg +# port: 80 + +services: + ntfy: + image: binwiederhier/ntfy + command: + - serve + environment: + - SERVICE_FQDN_NTFY_80 + - NTFY_BASE_URL=${SERVICE_FQDN_NTFY} + - TZ=${TZ:-UTC} + - NTFY_CACHE_FILE=/var/cache/ntfy/cache.db + - NTFY_AUTH_FILE=/var/lib/ntfy/auth.db + - NTFY_UPSTREAM_BASE_URL=${UPSTREAM_BASE_URL:-https://ntfy.sh} + - NTFY_ENABLE_SIGNUP=${NTFY_ENABLE_SIGNUP:-true} + - NTFY_ENABLE_LOGIN=${NTFY_ENABLE_LOGIN:-true} + - NTFY_CACHE_DURATION=${NTFY_CACHE_DURATION:-24h} + - NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT=${NTFY_ATTACHMENT_TOTAL_SIZE_LIMIT:-1G} + - NTFY_ATTACHMENT_FILE_SIZE_LIMIT=${NTFY_ATTACHMENT_FILE_SIZE_LIMIT:-10M} + - NTFY_ATTACHMENT_EXPIRY_DURATION=${NTFY_ATTACHMENT_EXPIRY_DURATION:-24h} + - NTFY_SMTP_SENDER_ADDR=${NTFY_SMTP_SENDER_ADDR:-smtp.your-domain.de} + - NTFY_SMTP_SENDER_USER=${NTFY_SMTP_SENDER_USER:-no-reply@de} + - NTFY_SMTP_SENDER_PASS=${NTFY_SMTP_SENDER_PASS:-password} + - NTFY_SMTP_SENDER_FROM=${NTFY_SMTP_SENDER_FROM:-no-reply@de} + - NTFY_KEEPALIVE_INTERVAL=${NTFY_KEEPALIVE_INTERVAL:-5m} + - NTFY_MANAGER_INTERVAL=${NTFY_MANAGER_INTERVAL:-5m} + - NTFY_VISITOR_MESSAGE_DAILY_LIMIT=${NTFY_VISITOR_MESSAGE_DAILY_LIMIT:-100} + - NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT=${NTFY_VISITOR_ATTACHMENT_DAILY_BANDWIDTH_LIMIT:-1G} + - NTFY_UPSTREAM_ACCESS_TOKEN=${UPSTREAM_ACCESS_TOKEN} + - NTFY_AUTH_DEFAULT_ACCESS=${NTFY_AUTH_DEFAULT_ACCESS:-read-write} + - NTFY_WEB_PUSH_PUBLIC_KEY=${NTFY_WEB_PUSH_PUBLIC_KEY} + - NTFY_WEB_PUSH_PRIVATE_KEY=${NTFY_WEB_PUSH_PRIVATE_KEY} + - NTFY_WEB_PUSH_EMAIL_ADDRESS=${NTFY_WEB_PUSH_EMAIL_ADDRESS} + volumes: + - ntfy-cache:/var/cache/ntfy + - ntfy-db:/var/lib/ntfy/ + healthcheck: + test: ["CMD-SHELL", "wget -q --tries=1 http://localhost:80/v1/health -O - | grep -Eo '\"healthy\"\\s*:\\s*true' || exit 1"] + interval: 60s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/templates/compose/osticket.yaml b/templates/compose/osticket.yaml new file mode 100644 index 0000000000..7e2fbcbcfa --- /dev/null +++ b/templates/compose/osticket.yaml @@ -0,0 +1,53 @@ +# documentation: https://docs.osticket.com/en/latest/ +# slogan: osTicket is a widely-used open source support ticket system. +# tags: helpdesk, ticketing, support, open-source +# logo: svgs/osticket.png +# port: 80 + +services: + osticket: + image: tiredofit/osticket:latest + environment: + - SERVICE_FQDN_OSTICKET_80 + - APP_URL=${SERVICE_FQDN_OSTICKET} + - CRON_INTERVAL=${CRON_INTERVAL:-10} + - DB_HOST=mariadb + - DB_NAME=${OSTICKET_DATABASE:-osticket-db} + - DB_USER=${SERVICE_USER_MARIADB} + - DB_PASS=${SERVICE_PASSWORD_MARIADB} + - INSTALL_SECRET=${SERVICE_PASSWORD_OSTICKETSECRET} + - ADMIN_FIRSTNAME=${OSTICKET_FIRSTNAME:-Admin} + - ADMIN_LASTNAME=${OSTICKET_LASTNAME:-istrator} + - ADMIN_EMAIL=${OSTICKET_ADMIN_EMAIL:-admin@example.com} + - ADMIN_USER=${SERVICE_USER_OSTICKETADMIN} + - ADMIN_PASS=${SERVICE_PASSWORD_OSTICKETADMINPASS} + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1/"] + start_period: 10s + interval: 10s + timeout: 5s + retries: 3 + depends_on: + mariadb: + condition: service_healthy + volumes: + - osticket-data:/www/osticket + mariadb: + image: mariadb:11 + environment: + MARIADB_ROOT_PASSWORD: ${SERVICE_PASSWORD_MARIADBROOT} + MARIADB_DATABASE: ${OSTICKET_DATABASE:-osticket-db} + MARIADB_USER: ${SERVICE_USER_MARIADB} + MARIADB_PASSWORD: ${SERVICE_PASSWORD_MARIADB} + healthcheck: + test: + - CMD + - healthcheck.sh + - '--connect' + - '--innodb_initialized' + start_period: 10s + interval: 10s + timeout: 5s + retries: 3 + volumes: + - osticket-mariadb-data:/var/lib/mysql diff --git a/templates/compose/owncloud.yaml b/templates/compose/owncloud.yaml new file mode 100644 index 0000000000..8d65f6c604 --- /dev/null +++ b/templates/compose/owncloud.yaml @@ -0,0 +1,72 @@ +# documentation: https://owncloud.com/docs +# slogan: OwnCloud with Open Web UI integrates file management with a powerful, user-friendly interface. +# tags: owncloud,file-management,open-web-ui,integration,cloud +# logo: svgs/owncloud.svg +# port: 8080 + +services: + owncloud: + image: owncloud/server:latest + depends_on: + mariadb: + condition: service_healthy + redis: + condition: service_healthy + environment: + - SERVICE_FQDN_OWNCLOUD_8080 + - OWNCLOUD_DOMAIN=${SERVICE_FQDN_OWNCLOUD} + - OWNCLOUD_TRUSTED_DOMAINS=${SERVICE_URL_OWNCLOUD} + - OWNCLOUD_DB_TYPE=mysql + - OWNCLOUD_DB_HOST=mariadb + - OWNCLOUD_DB_NAME=${DB_NAME:-owncloud} + - OWNCLOUD_DB_USERNAME=${SERVICE_USER_MARIADB} + - OWNCLOUD_DB_PASSWORD=${SERVICE_PASSWORD_MARIADB} + - OWNCLOUD_ADMIN_USERNAME=${SERVICE_USER_OWNCLOUD} + - OWNCLOUD_ADMIN_PASSWORD=${SERVICE_PASSWORD_OWNCLOUD} + - OWNCLOUD_MYSQL_UTF8MB4=${MYSQL_UTF8MB4:-true} + - OWNCLOUD_REDIS_ENABLED=${REDIS_ENABLED:-true} + - OWNCLOUD_REDIS_HOST=redis + healthcheck: + test: + - CMD + - /usr/bin/healthcheck + interval: 30s + timeout: 10s + retries: 5 + volumes: + - owncloud-data:/mnt/data + + mariadb: + image: mariadb:latest + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MARIADBROOT} + - MYSQL_USER=${SERVICE_USER_MARIADB} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_MARIADB} + - MYSQL_DATABASE=${DB_NAME:-owncloud} + - TZ=auto + command: + - "--character-set-server=utf8mb4" + - "--collation-server=utf8mb4_bin" + - "--max-allowed-packet=128M" + - "--innodb-log-file-size=64M" + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 20s + retries: 10 + volumes: + - owncloud-mysql-data:/var/lib/mysql + + redis: + image: redis:6 + command: + - "--databases" + - "1" + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 10s + timeout: 5s + retries: 5 diff --git a/templates/compose/paperless.yaml b/templates/compose/paperless.yaml index af74b5ea1e..76c7c5b55f 100644 --- a/templates/compose/paperless.yaml +++ b/templates/compose/paperless.yaml @@ -5,7 +5,7 @@ services: redis: - image: docker.io/library/redis:7.4 + image: redis:7.4 volumes: - paperless-redis:/data healthcheck: diff --git a/templates/compose/peppermint.yaml b/templates/compose/peppermint.yaml new file mode 100644 index 0000000000..228389eb49 --- /dev/null +++ b/templates/compose/peppermint.yaml @@ -0,0 +1,42 @@ +# ignore: true +# documentation: https://docs.peppermint.sh/ +# slogan: Open source helpdesk solution designed to enhance the user experience for teams currently utilizing costly software alternatives +# tags: helpdesk, open-source, peppermint, postgres +# logo: svgs/peppermint.png +# port: 3000 + +services: + postgres: + image: postgres:16-alpine + volumes: + - peppermint-postgresql-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-peppermint-db} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 + + peppermint: + image: pepperlabs/peppermint:latest + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1:3000"] + interval: 2s + timeout: 10s + retries: 15 + environment: + - SERVICE_FQDN_PEPPERMINT_3000 + - SERVICE_FQDN_PEPPERMINT_5003 + - DB_USERNAME=${SERVICE_USER_POSTGRES} + - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - DB_HOST=postgres + - DB_NAME=${POSTGRES_DB:-peppermint-db} + - SECRET=${SERVICE_PASSWORD_PEPPERMINT} + - API_URL=${SERVICE_FQDN_PEPPERMINT_5003} + # The default login is "admin@admin.com" with the password "1234" diff --git a/templates/compose/plane.yaml b/templates/compose/plane.yaml index d3ff156176..fc62cb1224 100644 --- a/templates/compose/plane.yaml +++ b/templates/compose/plane.yaml @@ -23,6 +23,15 @@ x-app-env: &app-env - REDIS_HOST=plane-redis - REDIS_PORT=6379 - REDIS_URL=${REDIS_URL:-redis://plane-redis:6379/} + + # RabbitMQ Settings + - RABBITMQ_HOST=plane-mq + - RABBITMQ_PORT=${RABBITMQ_PORT:-5672} + - RABBITMQ_DEFAULT_USER=${SERVICE_USER_RABBITMQ:-plane} + - RABBITMQ_DEFAULT_PASS=${SERVICE_PASSWORD_RABBITMQ:-plane} + - RABBITMQ_DEFAULT_VHOST=${RABBITMQ_VHOST:-plane} + - RABBITMQ_VHOST=${RABBITMQ_VHOST:-plane} + - 'AMQP_URL=amqp://${SERVICE_USER_RABBITMQ}:${SERVICE_PASSWORD_RABBITMQ}@plane-mq:${RABBITMQ_PORT}/plane' # Application secret - SECRET_KEY=$SERVICE_PASSWORD_64_SECRETKEY # DATA STORE SETTINGS @@ -36,10 +45,8 @@ x-app-env: &app-env - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO - BUCKET_NAME=${BUCKET_NAME:-uploads} - FILE_SIZE_LIMIT=${FILE_SIZE_LIMIT:-5242880} - # Admin and Space URLs - - ADMIN_BASE_URL=${ADMIN_BASE_URL} - - SPACE_BASE_URL=${SPACE_BASE_URL} - - APP_BASE_URL=${SERVICE_FQDN_PLANE} + # Live server env + - API_BASE_URL=${API_BASE_URL:-http://api:8000} services: proxy: @@ -97,6 +104,19 @@ services: timeout: 10s retries: 15 + live: + <<: *app-env + image: makeplane/plane-live:stable + command: node live/dist/server.js live + depends_on: + - api + - web + healthcheck: + test: ["CMD", "echo", "hey whats up"] + interval: 2s + timeout: 10s + retries: 15 + api: <<: *app-env image: makeplane/plane-backend:stable @@ -157,7 +177,7 @@ services: plane-db: <<: *app-env - image: postgres:15.5-alpine + image: postgres:15.7-alpine command: postgres -c 'max_connections=1000' volumes: - pgdata:/var/lib/postgresql/data @@ -178,6 +198,18 @@ services: timeout: 20s retries: 10 + plane-mq: + <<: *app-env + image: rabbitmq:3.13.6-management-alpine + restart: always + volumes: + - rabbitmq_data:/var/lib/rabbitmq + healthcheck: + test: rabbitmq-diagnostics -q ping + interval: 30s + timeout: 30s + retries: 3 + plane-minio: <<: *app-env image: minio/minio:latest diff --git a/templates/compose/plausible.yaml b/templates/compose/plausible.yaml index 7db12cb005..a37dcaf6e1 100644 --- a/templates/compose/plausible.yaml +++ b/templates/compose/plausible.yaml @@ -6,35 +6,64 @@ services: plausible: - image: "ghcr.io/plausible/community-edition:v2.1" + image: "ghcr.io/plausible/community-edition:v2.1.4" command: 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"' environment: - SERVICE_FQDN_PLAUSIBLE - - "DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@plausible_db/plausible" - - BASE_URL=$SERVICE_FQDN_PLAUSIBLE - - SECRET_KEY_BASE=$SERVICE_BASE64_64_PLAUSIBLE - - TOTP_VAULT_KEY=$SERVICE_REALBASE64_32_TOTP + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@plausible-db:5432/${POSTGRES_DB:-plausible-db} + - CLICKHOUSE_DATABASE_URL=http://plausible-events-db:8123/plausible_events_db + - BASE_URL=${SERVICE_FQDN_PLAUSIBLE} + - SECRET_KEY_BASE=${SERVICE_BASE64_64_PLAUSIBLE} + - TOTP_VAULT_KEY=${SERVICE_REALBASE64_32_TOTP} depends_on: - - plausible_db - - plausible_events_db - - mail + plausible-db: + condition: service_healthy + plausible-events-db: + condition: service_healthy + mail: + condition: service_healthy + healthcheck: + test: + [ + "CMD", + "wget", + "--no-verbose", + "--tries=1", + "--spider", + "http://127.0.0.1:8000/api/health", + ] + interval: 10s + timeout: 5s + retries: 5 + start_period: 45s + mail: image: bytemark/smtp + platform: linux/amd64 + healthcheck: + test: ["CMD-SHELL", "bash -c ':> /dev/tcp/127.0.0.1/25' || exit 1"] + interval: 5s + timeout: 10s + retries: 20 - plausible_db: - image: "postgres:14-alpine" + plausible-db: + image: "postgres:16-alpine" volumes: - - "db-data:/var/lib/postgresql/data" + - plausible-postgres-data:/var/lib/postgresql/data environment: - - POSTGRES_DB=plausible - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - POSTGRES_DB=${POSTGRES_DB:-plausible-db} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 - plausible_events_db: + plausible-events-db: image: "clickhouse/clickhouse-server:24.3.3.102-alpine" volumes: - - type: volume - source: event-data - target: /var/lib/clickhouse + - plausible-events-data:/var/lib/clickhouse - type: bind source: ./clickhouse/clickhouse-config.xml target: /etc/clickhouse-server/config.d/logging.xml @@ -49,3 +78,10 @@ services: nofile: soft: 262144 hard: 262144 + healthcheck: + test: + [ + "CMD-SHELL", + "wget --no-verbose --tries=1 -O - http://127.0.0.1:8123/ping || exit 1", + ] + start_period: 30s diff --git a/templates/compose/plunk.yaml b/templates/compose/plunk.yaml index cc1616c42d..4b356720a6 100644 --- a/templates/compose/plunk.yaml +++ b/templates/compose/plunk.yaml @@ -4,10 +4,9 @@ # logo: svgs/plunk.svg # port: 3000 -version: '3' services: plunk: - image: driaug/plunk + image: driaug/plunk:latest depends_on: postgresql: condition: service_healthy @@ -16,39 +15,41 @@ services: environment: - SERVICE_FQDN_PLUNK_3000 - REDIS_URL=redis://redis:6379 - - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql/plunk?schema=public - - JWT_SECRET=${SERVICE_PASSWORD_JWT_SECRET} - - AWS_REGION=${AWS_REGION} - - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} - - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} - - AWS_SES_CONFIGURATION_SET=${AWS_SES_CONFIGURATION_SET} + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgresql/plunk-db?schema=public + - JWT_SECRET=${SERVICE_PASSWORD_JWTSECRET} + - AWS_REGION=${AWS_REGION:?} + - AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:?} + - AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:?} + - AWS_SES_CONFIGURATION_SET=${AWS_SES_CONFIGURATION_SET:?} - NEXT_PUBLIC_API_URI=${SERVICE_FQDN_PLUNK}/api - APP_URI=${SERVICE_FQDN_PLUNK} - API_URI=${SERVICE_FQDN_PLUNK}/api - - DISABLE_SIGNUPS=False + - DISABLE_SIGNUPS=${DISABLE_SIGNUPS:-False} entrypoint: [ "/app/entry.sh" ] healthcheck: test: ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000"] interval: 2s timeout: 10s retries: 15 + postgresql: image: postgres:16-alpine environment: - - POSTGRES_USER=$SERVICE_USER_POSTGRES - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - - POSTGRES_DB=${POSTGRES_DB:-plunk} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-plunk-db} volumes: - - postgresql-data:/var/lib/postgresql/data + - plunk-postgresql-data:/var/lib/postgresql/data healthcheck: - test: [ "CMD-SHELL", "pg_isready -U postgres -d postgres" ] + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s - timeout: 10s - retries: 20 + timeout: 20s + retries: 10 + redis: - image: "redis:7.4-alpine" + image: redis:7.4-alpine volumes: - - "redis-data:/data" + - plunk-redis-data:/data healthcheck: test: - CMD diff --git a/templates/compose/qbittorrent.yaml b/templates/compose/qbittorrent.yaml new file mode 100644 index 0000000000..f7a4ad878f --- /dev/null +++ b/templates/compose/qbittorrent.yaml @@ -0,0 +1,48 @@ +# documentation: https://docs.linuxserver.io/images/docker-qbittorrent/ +# slogan: The qBittorrent project aims to provide an open-source software alternative to μTorrent. +# tags: torrent, streaming, webui +# logo: svgs/qbittorrent.svg +# port: 8080 + +services: + qbit: + image: "lscr.io/linuxserver/qbittorrent:latest" + environment: + - WEBUI_PORT=${WEBUI_PORT:-8080} + - PUID=1000 + - PGID=1000 + volumes: + - qbittorrent-config:/config + - qbittorrent-downloads:/downloads + - qbittorrent-torrents:/torrents + healthcheck: + test: + - CMD + - wget + - "-q" + - "--spider" + - "http://127.0.0.1:8080/" + interval: 5s + timeout: 20s + retries: 10 + + vuetorrent-backend: + image: "ghcr.io/vuetorrent/vuetorrent-backend:latest" + environment: + - SERVICE_FQDN_QBITORRENT_8080 + - PORT=${WEBUI_PORT:-8080} + - QBIT_BASE=${SERVICE_FQDN_QBITORRENT} + - RELEASE_TYPE=${RELEASE_TYPE:-stable} + - UPDATE_VT_CRON=${UPDATE_VT_CRON:-"0 * * * *"} + volumes: + - vuetorrent-config:/config + healthcheck: + test: + - CMD + - wget + - "-q" + - "--spider" + - "http://127.0.0.1:8080/" + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/reactive-resume.yaml b/templates/compose/reactive-resume.yaml index e2d18e8e40..0cf8ed6b97 100644 --- a/templates/compose/reactive-resume.yaml +++ b/templates/compose/reactive-resume.yaml @@ -45,10 +45,11 @@ services: retries: 10 minio: - image: minio/minio + image: quay.io/minio/minio:latest command: server /data --console-address ":9001" environment: - - SERVICE_FQDN_MINIO_9000 + - MINIO_SERVER_URL=$MINIO_SERVER_URL + - MINIO_BROWSER_REDIRECT_URL=$MINIO_BROWSER_REDIRECT_URL - MINIO_ROOT_USER=$SERVICE_USER_MINIO - MINIO_ROOT_PASSWORD=$SERVICE_PASSWORD_MINIO volumes: @@ -61,6 +62,7 @@ services: chrome: image: ghcr.io/browserless/chrome:latest + platform: linux/amd64 environment: - HEALTH=true - TIMEOUT=10000 @@ -68,7 +70,7 @@ services: - TOKEN=$SERVICE_PASSWORD_CHROMETOKEN redis: - image: redis:alpine + image: redis:7-alpine command: redis-server volumes: - redis_data:/data diff --git a/templates/compose/stirling-pdf.yaml b/templates/compose/stirling-pdf.yaml index db0393a3ff..246303b4c8 100644 --- a/templates/compose/stirling-pdf.yaml +++ b/templates/compose/stirling-pdf.yaml @@ -16,7 +16,7 @@ services: - SERVICE_FQDN_SPDF_8080 - DOCKER_ENABLE_SECURITY=false healthcheck: - test: 'curl --fail -I http://127.0.0.1:8080 || exit 1' + test: 'curl --fail --silent http://127.0.0.1:8080/api/v1/info/status | grep -q "UP" || exit 1' interval: 5s timeout: 20s retries: 10 diff --git a/templates/compose/supabase.yaml b/templates/compose/supabase.yaml index 5eb707d938..5884052895 100644 --- a/templates/compose/supabase.yaml +++ b/templates/compose/supabase.yaml @@ -14,7 +14,7 @@ services: supabase-analytics: condition: service_healthy environment: - - SERVICE_FQDN_SUPABASEKONG + - SERVICE_FQDN_SUPABASEKONG_8000 - JWT_SECRET=${SERVICE_PASSWORD_JWT} - KONG_DATABASE=off - KONG_DECLARATIVE_CONFIG=/home/kong/kong.yml @@ -278,7 +278,7 @@ services: config: hide_credentials: true supabase-studio: - image: supabase/studio:20240729-ce42139 + image: supabase/studio:20240923-2e3e90c healthcheck: test: [ @@ -301,7 +301,7 @@ services: - DEFAULT_ORGANIZATION_NAME=${STUDIO_DEFAULT_ORGANIZATION:-Default Organization} - DEFAULT_PROJECT_NAME=${STUDIO_DEFAULT_PROJECT:-Default Project} - - SUPABASE_URL=${SERVICE_FQDN_SUPABASEKONG} + - 'SUPABASE_URL=http://supabase-kong:8000' - SUPABASE_PUBLIC_URL=${SERVICE_FQDN_SUPABASEKONG} - SUPABASE_ANON_KEY=${SERVICE_SUPABASEANON_KEY} - SUPABASE_SERVICE_KEY=${SERVICE_SUPABASESERVICE_KEY} @@ -309,6 +309,7 @@ services: - LOGFLARE_API_KEY=${SERVICE_PASSWORD_LOGFLARE} - LOGFLARE_URL=http://supabase-analytics:4000 + - 'SUPABASE_PUBLIC_API=${SERVICE_FQDN_SUPABASEKONG}' - NEXT_PUBLIC_ENABLE_LOGS=true # Comment to use Big Query backend for analytics - NEXT_ANALYTICS_BACKEND_PROVIDER=postgres @@ -330,7 +331,6 @@ services: - config_file=/etc/postgresql/postgresql.conf - -c - log_min_messages=fatal - restart: unless-stopped environment: - POSTGRES_HOST=/var/run/postgresql - PGPORT=${POSTGRES_PORT:-5432} @@ -351,6 +351,21 @@ services: create schema if not exists _realtime; alter schema _realtime owner to :pguser; + - type: bind + source: ./volumes/db/_supabase.sql + target: /docker-entrypoint-initdb.d/migrations/97-_supabase.sql + content: | + \set pguser `echo "$POSTGRES_USER"` + + CREATE DATABASE _supabase WITH OWNER :pguser; + - type: bind + source: ./volumes/db/pooler.sql + target: /docker-entrypoint-initdb.d/migrations/99-pooler.sql + content: | + \set pguser `echo "supabase_admin"` + \c _supabase + create schema if not exists _supavisor; + alter schema _supavisor owner to :pguser; - type: bind source: ./volumes/db/webhooks.sql target: /docker-entrypoint-initdb.d/init-scripts/98-webhooks.sql @@ -591,7 +606,7 @@ services: target: /docker-entrypoint-initdb.d/migrations/99-logs.sql content: | \set pguser `echo "supabase_admin"` - + \c _supabase create schema if not exists _analytics; alter schema _analytics owner to :pguser; # Use named volume to persist pgsodium decryption key between restarts @@ -604,7 +619,6 @@ services: timeout: 5s interval: 5s retries: 10 - restart: unless-stopped depends_on: supabase-db: condition: service_healthy @@ -616,7 +630,7 @@ services: environment: - LOGFLARE_NODE_HOST=127.0.0.1 - DB_USERNAME=supabase_admin - - DB_DATABASE=${POSTGRES_DB:-postgres} + - DB_DATABASE=_supabase - DB_HOSTNAME=${POSTGRES_HOSTNAME:-supabase-db} - DB_PORT=${POSTGRES_PORT:-5432} - DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} @@ -628,7 +642,7 @@ services: - LOGFLARE_MIN_CLUSTER_SIZE=1 # Comment variables to use Big Query backend for analytics - - POSTGRES_BACKEND_URL=postgresql://supabase_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} + - POSTGRES_BACKEND_URL=postgresql://supabase_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/_supabase - POSTGRES_BACKEND_SCHEMA=_analytics - LOGFLARE_FEATURE_FLAG_OVERRIDE=multibackend=true @@ -902,10 +916,9 @@ services: condition: service_healthy supabase-analytics: condition: service_healthy - restart: unless-stopped environment: - PGRST_DB_URI=postgres://authenticator:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/${POSTGRES_DB:-postgres} - - PGRST_DB_SCHEMAS=${PGRST_DB_SCHEMAS:-public} + - 'PGRST_DB_SCHEMAS=${PGRST_DB_SCHEMAS:-public,storage,graphql_public}' - PGRST_DB_ANON_ROLE=anon - PGRST_JWT_SECRET=${SERVICE_PASSWORD_JWT} - PGRST_DB_USE_LEGACY_GUCS=false @@ -914,7 +927,7 @@ services: command: "postgrest" exclude_from_hc: true supabase-auth: - image: supabase/gotrue:v2.151.0 + image: supabase/gotrue:v2.158.1 depends_on: supabase-db: # Disable this if you are using an external Postgres database @@ -992,7 +1005,7 @@ services: # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_ENABLED="true" # GOTRUE_HOOK_PASSWORD_VERIFICATION_ATTEMPT_URI="pg-functions://postgres/public/password_verification_attempt" - + # Uncomment to enable common OAuth Variables #- 'GOTRUE_EXTERNAL_GITHUB_CLIENT_ID=${GOTRUE_EXTERNAL_GITHUB_CLIENT_ID}' #- 'GOTRUE_EXTERNAL_GITHUB_ENABLED=${GOTRUE_EXTERNAL_GITHUB_ENABLED}' @@ -1005,7 +1018,7 @@ services: realtime-dev: # This container name looks inconsistent but is correct because realtime constructs tenant id by parsing the subdomain - image: supabase/realtime:v2.30.23 + image: supabase/realtime:v2.30.34 container_name: realtime-dev.supabase-realtime depends_on: supabase-db: @@ -1085,7 +1098,7 @@ services: exit 0 supabase-storage: - image: supabase/storage-api:v1.0.6 + image: supabase/storage-api:v1.10.1 depends_on: supabase-db: # Disable this if you are using an external Postgres database @@ -1185,7 +1198,7 @@ services: - PG_META_DB_PASSWORD=${SERVICE_PASSWORD_POSTGRES} supabase-edge-functions: - image: supabase/edge-runtime:v1.53.3 + image: supabase/edge-runtime:v1.58.3 depends_on: supabase-analytics: condition: service_healthy @@ -1327,3 +1340,81 @@ services: - start - --main-service - /home/deno/functions/main + + supabase-supavisor: + image: 'supabase/supavisor:1.1.56' + healthcheck: + test: + - CMD + - curl + - "-sSfL" + - "-o" + - /dev/null + - "http://127.0.0.1:4000/api/health" + timeout: 5s + interval: 5s + retries: 10 + depends_on: + supabase-db: + condition: service_healthy + supabase-analytics: + condition: service_healthy + environment: + - POOLER_TENANT_ID=dev_tenant + - POOLER_POOL_MODE=transaction + - POOLER_DEFAULT_POOL_SIZE=${POOLER_DEFAULT_POOL_SIZE:-20} + - POOLER_MAX_CLIENT_CONN=${POOLER_MAX_CLIENT_CONN:-100} + - PORT=4000 + - 'POSTGRES_PORT=${POSTGRES_PORT:-5432}' + - 'POSTGRES_HOSTNAME=${POSTGRES_HOSTNAME:-supabase-db}' + - 'POSTGRES_DB=${POSTGRES_DB:-postgres}' + - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}' + - 'DATABASE_URL=ecto://supabase_admin:${SERVICE_PASSWORD_POSTGRES}@${POSTGRES_HOSTNAME:-supabase-db}:${POSTGRES_PORT:-5432}/_supabase' + - CLUSTER_POSTGRES=true + - 'SECRET_KEY_BASE=${SERVICE_PASSWORD_SUPAVISORSECRET}' + - 'VAULT_ENC_KEY=${SERVICE_PASSWORD_VAULTENC}' + - 'API_JWT_SECRET=${SERVICE_PASSWORD_JWT}' + - 'METRICS_JWT_SECRET=${SERVICE_PASSWORD_JWT}' + - REGION=local + - 'ERL_AFLAGS=-proto_dist inet_tcp' + command: + - /bin/sh + - "-c" + - '/app/bin/migrate && /app/bin/supavisor eval "$$(cat /etc/pooler/pooler.exs)" && /app/bin/server' + volumes: + - type: bind + source: ./volumes/pooler/pooler.exs + target: /etc/pooler/pooler.exs + content: | + {:ok, _} = Application.ensure_all_started(:supavisor) + {:ok, version} = + case Supavisor.Repo.query!("select version()") do + %{rows: [[ver]]} -> Supavisor.Helpers.parse_pg_version(ver) + _ -> nil + end + params = %{ + "external_id" => System.get_env("POOLER_TENANT_ID"), + "db_host" => System.get_env("POSTGRES_HOSTNAME"), + "db_port" => System.get_env("POSTGRES_PORT") |> String.to_integer(), + "db_database" => System.get_env("POSTGRES_DB"), + "require_user" => false, + "auth_query" => "SELECT * FROM pgbouncer.get_auth($1)", + "default_max_clients" => System.get_env("POOLER_MAX_CLIENT_CONN"), + "default_pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"), + "default_parameter_status" => %{"server_version" => version}, + "users" => [%{ + "db_user" => "pgbouncer", + "db_password" => System.get_env("POSTGRES_PASSWORD"), + "mode_type" => System.get_env("POOLER_POOL_MODE"), + "pool_size" => System.get_env("POOLER_DEFAULT_POOL_SIZE"), + "is_manager" => true + }] + } + + tenant = Supavisor.Tenants.get_tenant_by_external_id(params["external_id"]) + + if tenant do + {:ok, _} = Supavisor.Tenants.update_tenant(tenant, params) + else + {:ok, _} = Supavisor.Tenants.create_tenant(params) + end diff --git a/templates/compose/traccar.yaml b/templates/compose/traccar.yaml new file mode 100644 index 0000000000..5aa0887fe9 --- /dev/null +++ b/templates/compose/traccar.yaml @@ -0,0 +1,50 @@ +# documentation: https://www.traccar.org/documentation/ +# slogan: Traccar is a free and open source modern GPS tracking system. +# tags: traccar,gps,tracking,open,source +# logo: svgs/traccar.png +# port: 8082 + +services: + traccar: + image: traccar/traccar:latest + environment: + - SERVICE_FQDN_TRACCAR_8082 + - SERVICE_FQDN_TRACCARAPI_5159 + - CONFIG_USE_ENVIRONMENT_VARIABLES=${CONFIG_USE_ENVIRONMENT_VARIABLES:-true} + - DATABASE_USER=${SERVICE_USER_POSTGRES} + - DATABASE_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + volumes: + - type: bind + source: ./srv/traccar/conf/traccar.xml + target: /opt/traccar/conf/traccar.xml + content: | + + + + ./conf/default.xml + org.postgresql.Driver + jdbc:postgresql://postgres:5432/traccar + + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:8082/ping"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 15s + + postgres: + image: postgres:16-alpine + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRESQL_DATABASE:-traccar} + volumes: + - traccar-postgresql-data:/var/lib/postgresql/data/ + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/transmission.yaml b/templates/compose/transmission.yaml new file mode 100644 index 0000000000..1e5da78eae --- /dev/null +++ b/templates/compose/transmission.yaml @@ -0,0 +1,31 @@ +# documentation: https://docs.linuxserver.io/images/docker-transmission/ +# slogan: Fast, easy, and free BitTorrent client. +# tags: bittorrent, torrent, peer-to-peer +# logo: svgs/transmission.svg +# port: 9091 + +services: + transmission: + image: lscr.io/linuxserver/transmission:latest + environment: + - SERVICE_FQDN_TRANSMISSION_9091 + - PUID=1000 + - PGID=1000 + - USER=${SERVICE_USER_ADMIN} + - PASS=${SERVICE_PASSWORD_ADMIN} + volumes: + - transmission-config:/config + - transmission-downloads:/downloads + - transmission-watch:/watch + healthcheck: + test: [ + "CMD", + "curl", + "-sSfL", + "-u", + "${SERVICE_USER_ADMIN}:${SERVICE_PASSWORD_ADMIN}", + "http://localhost:9091/" + ] + interval: 30s + timeout: 10s + retries: 3 diff --git a/templates/compose/trigger-with-external-database.yaml b/templates/compose/trigger-with-external-database.yaml index dcd3e2b971..82c4594302 100644 --- a/templates/compose/trigger-with-external-database.yaml +++ b/templates/compose/trigger-with-external-database.yaml @@ -6,7 +6,7 @@ services: trigger: - image: ghcr.io/triggerdotdev/trigger.dev:latest + image: ghcr.io/triggerdotdev/trigger.dev:main environment: - SERVICE_FQDN_TRIGGER_3000 - LOGIN_ORIGIN=$SERVICE_FQDN_TRIGGER @@ -14,8 +14,8 @@ services: - MAGIC_LINK_SECRET=$SERVICE_PASSWORD_64_MAGIC - ENCRYPTION_KEY=$SERVICE_PASSWORD_64_ENCRYPTION - SESSION_SECRET=$SERVICE_PASSWORD_64_SESSION - - DATABASE_URL=${DATABASE_URL} - - DIRECT_URL=${DATABASE_URL} + - DATABASE_URL=${DATABASE_URL:?} + - DIRECT_URL=${DATABASE_URL:?} - RUNTIME_PLATFORM=docker-compose - NODE_ENV=production - AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID} @@ -24,4 +24,7 @@ services: - FROM_EMAIL=${FROM_EMAIL} - REPLY_TO_EMAIL=${REPLY_TO_EMAIL} healthcheck: - test: ["NONE"] + test: "timeout 10s bash -c ':> /dev/tcp/127.0.0.1/3000' || exit 1" + interval: 10s + timeout: 5s + retries: 5 \ No newline at end of file diff --git a/templates/compose/trigger.yaml b/templates/compose/trigger.yaml index 6181a69259..9353336b2a 100644 --- a/templates/compose/trigger.yaml +++ b/templates/compose/trigger.yaml @@ -4,45 +4,139 @@ # logo: svgs/trigger.png # port: 3000 +x-common-env: &common-env + PORT: 3030 + REMIX_APP_PORT: 3000 + NODE_ENV: production + RUNTIME_PLATFORM: docker-compose + V3_ENABLED: true + INTERNAL_OTEL_TRACE_DISABLED: 1 + INTERNAL_OTEL_TRACE_LOGGING_ENABLED: 0 + POSTGRES_USER: $SERVICE_USER_POSTGRES + POSTGRES_PASSWORD: $SERVICE_PASSWORD_POSTGRES + POSTGRES_DB: ${POSTGRES_DB:-trigger} + MAGIC_LINK_SECRET: $SERVICE_PASSWORD_64_MAGIC + SESSION_SECRET: $SERVICE_PASSWORD_64_SESSION + ENCRYPTION_KEY: $SERVICE_PASSWORD_64_ENCRYPTION + PROVIDER_SECRET: $SERVICE_PASSWORD_64_PROVIDER + COORDINATOR_SECRET: $SERVICE_PASSWORD_64_COORDINATOR + DATABASE_HOST: postgresql + DATABASE_URL: postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB + DIRECT_URL: postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB + REDIS_HOST: redis + REDIS_PORT: 6379 + REDIS_TLS_DISABLED: true + COORDINATOR_HOST: 127.0.0.1 + COORDINATOR_PORT: 9020 + WHITELISTED_EMAILS: "" + ADMIN_EMAILS: "" + DEFAULT_ORG_EXECUTION_CONCURRENCY_LIMIT: 300 + DEFAULT_ENV_EXECUTION_CONCURRENCY_LIMIT: 100 + DEPLOY_REGISTRY_HOST: docker.io + DEPLOY_REGISTRY_NAMESPACE: trigger + REGISTRY_HOST: ${DEPLOY_REGISTRY_HOST} + REGISTRY_NAMESPACE: ${DEPLOY_REGISTRY_NAMESPACE} + AUTH_GITHUB_CLIENT_ID: ${AUTH_GITHUB_CLIENT_ID} + AUTH_GITHUB_CLIENT_SECRET: ${AUTH_GITHUB_CLIENT_SECRET} + RESEND_API_KEY: ${RESEND_API_KEY} + FROM_EMAIL: ${FROM_EMAIL} + REPLY_TO_EMAIL: ${REPLY_TO_EMAIL} + LOGIN_ORIGIN: $SERVICE_FQDN_TRIGGER_3000 + APP_ORIGIN: $SERVICE_FQDN_TRIGGER_3000 + DEV_OTEL_EXPORTER_OTLP_ENDPOINT: $SERVICE_FQDN_TRIGGER_3000/otel + OTEL_EXPORTER_OTLP_ENDPOINT: "http://trigger:3040/otel" + ELECTRIC_ORIGIN: http://electric:3000 + services: trigger: - image: ghcr.io/triggerdotdev/trigger.dev:latest + image: ghcr.io/triggerdotdev/trigger.dev:v3 + environment: + SERVICE_FQDN_TRIGGER_3000: "" + <<: *common-env + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + electric: + condition: service_healthy + healthcheck: + test: "timeout 10s bash -c ':> /dev/tcp/127.0.0.1/3000' || exit 1" + interval: 10s + timeout: 5s + retries: 5 + + electric: + image: electricsql/electric environment: - - SERVICE_FQDN_TRIGGER_3000 - - LOGIN_ORIGIN=$SERVICE_FQDN_TRIGGER - - APP_ORIGIN=$SERVICE_FQDN_TRIGGER - - MAGIC_LINK_SECRET=$SERVICE_PASSWORD_64_MAGIC - - ENCRYPTION_KEY=$SERVICE_PASSWORD_64_ENCRYPTION - - SESSION_SECRET=$SERVICE_PASSWORD_64_SESSION - - POSTGRES_USER=$SERVICE_USER_POSTGRES - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - - POSTGRES_DB=${POSTGRES_DB:-trigger} - - POSTGRES_HOST=postgres - - DATABASE_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB - - DIRECT_URL=postgres://$SERVICE_USER_POSTGRES:$SERVICE_PASSWORD_POSTGRES@postgresql:5432/$POSTGRES_DB - - RUNTIME_PLATFORM=docker-compose - - NODE_ENV=production - - AUTH_GITHUB_CLIENT_ID=${AUTH_GITHUB_CLIENT_ID} - - AUTH_GITHUB_CLIENT_SECRET=${AUTH_GITHUB_CLIENT_SECRET} - - RESEND_API_KEY=${RESEND_API_KEY} - - FROM_EMAIL=${FROM_EMAIL} - - REPLY_TO_EMAIL=${REPLY_TO_EMAIL} + <<: *common-env depends_on: postgresql: condition: service_healthy healthcheck: - test: ["NONE"] + test: + - CMD-SHELL + - pwd + + redis: + image: "redis:7" + environment: + - ALLOW_EMPTY_PASSWORD=yes + healthcheck: + test: + - CMD-SHELL + - "redis-cli -h localhost -p 6379 ping" + interval: 5s + timeout: 5s + retries: 3 + volumes: + - redis-data:/data postgresql: image: postgres:16-alpine volumes: - postgresql-data:/var/lib/postgresql/data environment: - - POSTGRES_USER=$SERVICE_USER_POSTGRES - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - - POSTGRES_DB=${POSTGRES_DB:-trigger} + <<: *common-env + command: + - -c + - wal_level=logical healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 5s timeout: 20s retries: 10 + docker-provider: + image: ghcr.io/triggerdotdev/provider/docker:v3 + platform: linux/amd64 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + user: root + depends_on: + trigger: + condition: service_healthy + environment: + <<: *common-env + PLATFORM_HOST: trigger + PLATFORM_WS_PORT: 3030 + SECURE_CONNECTION: "false" + PLATFORM_SECRET: $PROVIDER_SECRET + coordinator: + image: ghcr.io/triggerdotdev/coordinator:v3 + platform: linux/amd64 + volumes: + - /var/run/docker.sock:/var/run/docker.sock + user: root + depends_on: + trigger: + condition: service_healthy + environment: + <<: *common-env + PLATFORM_HOST: trigger + PLATFORM_WS_PORT: 3030 + SECURE_CONNECTION: "false" + PLATFORM_SECRET: $COORDINATOR_SECRET + healthcheck: + test: + - CMD-SHELL + - pwd diff --git a/templates/compose/unsend.yaml b/templates/compose/unsend.yaml new file mode 100644 index 0000000000..649b7f7047 --- /dev/null +++ b/templates/compose/unsend.yaml @@ -0,0 +1,60 @@ +# documentation: https://docs.unsend.dev/get-started/self-hosting +# slogan: Unsend is an open-source alternative to Resend, Sendgrid, Mailgun and Postmark etc. +# tags: resend, mailer, marketing emails, transaction emails, self-hosting, postmark +# logo: svgs/unsend.svg +# port: 3000 + +services: + postgres: + image: postgres:16 + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${SERVICE_DB_POSTGRES:-unsend} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 + volumes: + - unsend-postgres-data:/var/lib/postgresql/data + + redis: + image: redis:7 + volumes: + - unsend-redis-data:/data + command: ["redis-server", "--maxmemory-policy", "noeviction"] + healthcheck: + test: + - CMD + - redis-cli + - PING + interval: 5s + timeout: 10s + retries: 20 + + unsend: + image: unsend/unsend:latest + environment: + - SERVICE_FQDN_UNSEND_3000 + - DATABASE_URL=postgresql://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/${SERVICE_DB_POSTGRES:-unsend} + - NEXTAUTH_URL=${SERVICE_FQDN_UNSEND} + - NEXTAUTH_SECRET=${SERVICE_BASE64_64_NEXTAUTHSECRET} + - AWS_ACCESS_KEY=${AWS_ACCESS_KEY:?} + - AWS_SECRET_KEY=${AWS_SECRET_KEY:?} + - AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION:?} + - GITHUB_ID=${GITHUB_ID} + - GITHUB_SECRET=${GITHUB_SECRET} + - REDIS_URL=redis://redis:6379 + - NEXT_PUBLIC_IS_CLOUD=${NEXT_PUBLIC_IS_CLOUD:-false} + - API_RATE_LIMIT=${API_RATE_LIMIT:-1} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: [ "CMD-SHELL", "wget -qO- http://127.0.0.1:3000 || exit 1" ] + interval: 5s + retries: 10 + timeout: 2s diff --git a/templates/compose/vvveb-with-mariadb.yaml b/templates/compose/vvveb-with-mariadb.yaml new file mode 100644 index 0000000000..a20c70a46f --- /dev/null +++ b/templates/compose/vvveb-with-mariadb.yaml @@ -0,0 +1,41 @@ +# documentation: https://docs.vvveb.com +# slogan: Powerful and easy to use cms to build websites, blogs or ecommerce stores. +# tags: cms, blog, content, management, ecommerce, page-builder, nocode, mysql, sqlite, pgsql +# logo: svgs/vvveb.svg +# port: 80 + +services: + vvveb: + image: vvveb/vvvebcms:latest + volumes: + - vvveb-data:/var/www/html + environment: + - SERVICE_FQDN_VVVEB_80 + - DB_ENGINE=mysqli + - DB_HOST=mariadb + - DB_USER=${SERVICE_USER_VVVEB} + - DB_PASSWORD=${SERVICE_PASSWORD_VVVEB} + - DB_NAME=${MARIADB_DATABASE:-vvveb} + depends_on: + mariadb: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1"] + interval: 2s + timeout: 10s + retries: 10 + + mariadb: + image: mariadb:11 + volumes: + - vvveb-mariadb-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT} + - MYSQL_DATABASE=${MARIADB_DATABASE:-vvveb} + - MYSQL_USER=${SERVICE_USER_VVVEB} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_VVVEB} + healthcheck: + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/vvveb-with-mysql.yaml b/templates/compose/vvveb-with-mysql.yaml new file mode 100644 index 0000000000..64b4b264ad --- /dev/null +++ b/templates/compose/vvveb-with-mysql.yaml @@ -0,0 +1,41 @@ +# documentation: https://docs.vvveb.com +# slogan: Powerful and easy to use cms to build websites, blogs or ecommerce stores. +# tags: cms, blog, content, management, ecommerce, page-builder, nocode, mysql, sqlite, pgsql +# logo: svgs/vvveb.svg +# port: 80 + +services: + vvveb: + image: vvveb/vvvebcms:latest + volumes: + - vvveb-data:/var/www/html + environment: + - SERVICE_FQDN_VVVEB_80 + - DB_ENGINE=mysqli + - DB_HOST=mysql + - DB_USER=${SERVICE_USER_VVVEB} + - DB_PASSWORD=${SERVICE_PASSWORD_VVVEB} + - DB_NAME=${MYSQL_DATABASE:-vvveb} + depends_on: + mysql: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1"] + interval: 2s + timeout: 10s + retries: 10 + + mysql: + image: mysql:8.4.2 + volumes: + - vvveb-mysql-data:/var/lib/mysql + environment: + - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_ROOT} + - MYSQL_DATABASE=${MYSQL_DATABASE:-vvveb} + - MYSQL_USER=${SERVICE_USER_VVVEB} + - MYSQL_PASSWORD=${SERVICE_PASSWORD_VVVEB} + healthcheck: + test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/compose/vvveb.yaml b/templates/compose/vvveb.yaml new file mode 100644 index 0000000000..11b71a7e9a --- /dev/null +++ b/templates/compose/vvveb.yaml @@ -0,0 +1,18 @@ +# documentation: https://docs.vvveb.com +# slogan: Powerful and easy to use cms to build websites, blogs or ecommerce stores. +# tags: cms, blog, content, management, ecommerce, page-builder, nocode, mysql, sqlite, pgsql +# logo: svgs/vvveb.svg +# port: 80 + +services: + vvveb: + image: vvveb/vvvebcms:latest + volumes: + - vvveb-data:/var/www/html + environment: + - SERVICE_FQDN_VVVEB_80 + healthcheck: + test: ["CMD", "curl", "-f", "http://127.0.0.1"] + interval: 2s + timeout: 10s + retries: 10 diff --git a/templates/compose/weird.yaml b/templates/compose/weird.yaml deleted file mode 100644 index 85a3afe196..0000000000 --- a/templates/compose/weird.yaml +++ /dev/null @@ -1,77 +0,0 @@ -# ignore: true -services: - ghost: - image: ghost:5 - volumes: - - ~/configs:/etc/configs/:ro - - ./var/lib/ghost/content:/tmp/ghost2/content:ro - - /var/lib/ghost/content:/tmp/ghost/content:rw - - ghost-content-data:/var/lib/ghost/content - - type: volume - source: mydata - target: /data - volume: - nocopy: true - - type: bind - source: ./var/lib/ghost/data - target: /data - - type: bind - source: /tmp - target: /tmp - labels: - - "test.label=true" - ports: - - "3000" - - "3000-3005" - - "8000:8000" - - "9090-9091:8080-8081" - - "49100:22" - - "127.0.0.1:8001:8001" - - "127.0.0.1:5000-5010:5000-5010" - - "127.0.0.1::5000" - - "6060:6060/udp" - - "12400-12500:1240" - - target: 80 - published: 8080 - protocol: tcp - mode: host - networks: - - some-network - - other-network - environment: - - database__client=${DATABASE_CLIENT:-mysql} - - database__connection__database=${MYSQL_DATABASE:-ghost} - - database__connection__host=${DATABASE_CONNECTION_HOST:-mysql} - - test=${TEST:?true} - - url=$SERVICE_FQDN_GHOST - - database__connection__user=$SERVICE_USER_MYSQL - - database__connection__password=$SERVICE_PASSWORD_MYSQL - depends_on: - - mysql - mysql: - image: mysql:8.0 - volumes: - - ghost-mysql-data:/var/lib/mysql - environment: - - MYSQL_USER=${SERVICE_USER_MYSQL} - - MYSQL_PASSWORD=${SERVICE_PASSWORD_MYSQL} - - MYSQL_DATABASE=$MYSQL_DATABASE - - MYSQL_ROOT_PASSWORD=${SERVICE_PASSWORD_MYSQLROOT} - - SESSION_SECRET - minio: - image: minio/minio - environment: - RACK_ENV: development - A: $A - SHOW: ${SHOW} - SHOW1: ${SHOW2-show1} - SHOW2: ${SHOW3:-show2} - SHOW3: ${SHOW4?show3} - SHOW4: ${SHOW5:?show4} - SHOW5: ${SERVICE_USER_MINIO} - SHOW6: ${SERVICE_PASSWORD_MINIO} - SHOW7: ${SERVICE_PASSWORD_64_MINIO} - SHOW8: ${SERVICE_BASE64_64_MINIO} - SHOW9: ${SERVICE_BASE64_128_MINIO} - SHOW10: ${SERVICE_BASE64_MINIO} - SHOW11: diff --git a/templates/compose/windmill.yaml b/templates/compose/windmill.yaml index a14854ba0e..1ce3a46524 100644 --- a/templates/compose/windmill.yaml +++ b/templates/compose/windmill.yaml @@ -11,10 +11,11 @@ services: volumes: - db-data:/var/lib/postgresql/data environment: - - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES - - POSTGRES_DB=${POSTGRES_DB:-windmill} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-windmill-db} healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 5 @@ -23,16 +24,16 @@ services: image: ghcr.io/windmill-labs/windmill:main environment: - SERVICE_FQDN_WINDMILL_8000 - - DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill - - MODE=${MODE:-server} - - BASE_URL=$SERVICE_FQDN_WINDMILL + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db} + - MODE=server + - BASE_URL=${SERVICE_FQDN_WINDMILL} depends_on: db: condition: service_healthy volumes: - worker-logs:/tmp/windmill/logs healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api/version || exit 1"] + test: ["CMD", "curl", "-f", "http://localhost:8000/health"] interval: 30s timeout: 10s retries: 3 @@ -40,9 +41,9 @@ services: windmill-worker-1: image: ghcr.io/windmill-labs/windmill:main environment: - - DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill - - MODE=${MODE:-worker} - - WORKER_GROUP=${WORKER_GROUP:-default} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db} + - MODE=worker + - WORKER_GROUP=default depends_on: db: condition: service_healthy @@ -51,7 +52,7 @@ services: - worker-dependency-cache:/tmp/windmill/cache - worker-logs:/tmp/windmill/logs healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api/version || exit 1"] + test: ["CMD-SHELL", "exit 0"] interval: 30s timeout: 10s retries: 3 @@ -59,9 +60,9 @@ services: windmill-worker-2: image: ghcr.io/windmill-labs/windmill:main environment: - - DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill - - MODE=${MODE:-worker} - - WORKER_GROUP=${WORKER_GROUP:-default} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db} + - MODE=worker + - WORKER_GROUP=default depends_on: db: condition: service_healthy @@ -70,7 +71,7 @@ services: - worker-dependency-cache:/tmp/windmill/cache - worker-logs:/tmp/windmill/logs healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api/version || exit 1"] + test: ["CMD-SHELL", "exit 0"] interval: 30s timeout: 10s retries: 3 @@ -78,9 +79,9 @@ services: windmill-worker-3: image: ghcr.io/windmill-labs/windmill:main environment: - - DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill - - MODE=${MODE:-worker} - - WORKER_GROUP=${WORKER_GROUP:-default} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db} + - MODE=worker + - WORKER_GROUP=default depends_on: db: condition: service_healthy @@ -89,7 +90,7 @@ services: - worker-dependency-cache:/tmp/windmill/cache - worker-logs:/tmp/windmill/logs healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api/version || exit 1"] + test: ["CMD-SHELL", "exit 0"] interval: 30s timeout: 10s retries: 3 @@ -97,18 +98,18 @@ services: windmill-worker-native: image: ghcr.io/windmill-labs/windmill:main environment: - - DATABASE_URL=postgres://postgres:$SERVICE_PASSWORD_POSTGRES@db/windmill - - MODE=${MODE:-worker} - - WORKER_GROUP=${WORKER_GROUP:-native} - - NUM_WORKERS=${NUM_WORKERS:-8} - - SLEEP_QUEUE=${SLEEP_QUEUE:-200} + - DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@db/${POSTGRES_DB:-windmill-db} + - MODE=worker + - WORKER_GROUP=native + - NUM_WORKERS=8 + - SLEEP_QUEUE=200 depends_on: db: condition: service_healthy volumes: - worker-logs:/tmp/windmill/logs healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:8000/api/version || exit 1"] + test: ["CMD-SHELL", "exit 0"] interval: 30s timeout: 10s retries: 3 @@ -122,3 +123,4 @@ services: interval: 30s timeout: 10s retries: 3 + start_period: 20s \ No newline at end of file diff --git a/templates/compose/wireguard-easy.yaml b/templates/compose/wireguard-easy.yaml new file mode 100644 index 0000000000..7ccf605548 --- /dev/null +++ b/templates/compose/wireguard-easy.yaml @@ -0,0 +1,25 @@ +# documentation: https://github.com/wg-easy/wg-easy +# slogan: The easiest way to run WireGuard VPN + Web-based Admin UI. +# tags: wireguard,vpn,web,admin +# logo: svgs/wireguard.svg +# port: 8000 + +services: + wg-easy: + image: ghcr.io/wg-easy/wg-easy:latest + environment: + - SERVICE_FQDN_WIREGUARDEASY_8000 + - WG_HOST=${SERVICE_FQDN_WIREGUARDEASY} + - LANG=${LANG:-en} + - PORT=8000 + - WG_PORT=51820 + volumes: + - wg-easy:/etc/wireguard + ports: + - 51820:51820/udp + cap_add: + - NET_ADMIN + - SYS_MODULE + sysctls: + - net.ipv4.conf.all.src_valid_mark=1 + - net.ipv4.ip_forward=1 diff --git a/templates/compose/zep.yaml b/templates/compose/zep.yaml new file mode 100644 index 0000000000..1bc0912a3a --- /dev/null +++ b/templates/compose/zep.yaml @@ -0,0 +1,188 @@ +# ignore: true +# documentation: https://help.getzep.com/concepts +# slogan: Zep enhances your AI agent's knowledge through continuous learning from user interactions, enabling personalized experiences and improved accuracy. +# tags: lowcode, nocode, ai, llm, openai, anthropic, machine-learning, rag, agents, chatbot, api, team, bot, flows, memory +# logo: svgs/zep.png +# port: 8000 + +services: + postgres: + image: ghcr.io/getzep/postgres:postgres-15 + shm_size: 128mb + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + volumes: + - pg_data:/var/lib/postgresql/data + healthcheck: + test: + - CMD-SHELL + - "pg_isready -h localhost -U $${POSTGRES_USER} -d postgres" + interval: 5s + timeout: 5s + retries: 5 + nlp: + image: ghcr.io/getzep/zep-nlp-server:0.4 + environment: + - SERVICE_FQDN_NLP_5557 + - ZEP_OPENAI_API_KEY=${OPENAI_API_KEY} + - ZEP_AUTH_SECRET=${SERVICE_PASSWORD_AUTHSECRET} + - ZEP_SERVER_WEB_ENABLED=${ZEP_SERVER_WEB_ENABLED:-false} + healthcheck: + test: "timeout 10s bash -c ':> /dev/tcp/127.0.0.1/5557' || exit 1" + interval: 10s + timeout: 5s + retries: 5 + start_period: 45s + zep: + image: ghcr.io/getzep/zep:latest + depends_on: + postgres: + condition: service_healthy + nlp: + condition: service_healthy + environment: + - SERVICE_FQDN_ZEP_8000 + - ZEP_STORE_POSTGRES_DSN=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres:5432/postgres?sslmode=disable + - ZEP_NLP_SERVER_URL=http://nlp:5557 + - ZEP_EXTRACTORS_DOCUMENTS_EMBEDDINGS_SERVICE=${EXTRACTORS_DOCUMENTS_EMBEDDINGS_SERVICE:-openai} + - ZEP_EXTRACTORS_DOCUMENTS_EMBEDDINGS_DIMENSIONS=${EXTRACTORS_DOCUMENTS_EMBEDDINGS_DIMENSIONS:-1536} + - ZEP_EXTRACTORS_MESSAGES_EMBEDDINGS_SERVICE=${EXTRACTORS_MESSAGES_EMBEDDINGS_SERVICE:-openai} + - ZEP_EXTRACTORS_MESSAGES_EMBEDDINGS_DIMENSIONS=${EXTRACTORS_MESSAGES_EMBEDDINGS_DIMENSIONS:-1536} + - ZEP_EXTRACTORS_MESSAGES_SUMMARIZER_EMBEDDINGS_SERVICE=${EXTRACTORS_MESSAGES_SUMMARIZER_EMBEDDINGS_SERVICE:-openai} + - ZEP_EXTRACTORS_MESSAGES_SUMMARIZER_EMBEDDINGS_DIMENSIONS=${EXTRACTORS_MESSAGES_SUMMARIZER_EMBEDDINGS_DIMENSIONS:-1536} + - ZEP_OPENAI_API_KEY=${OPENAI_API_KEY} + - ZEP_AUTH_SECRET=${SERVICE_PASSWORD_AUTHSECRET} + - ZEP_SERVER_WEB_ENABLED=${ZEP_SERVER_WEB_ENABLED:-false} + volumes: + - type: bind + source: ./config.yaml + target: /app/config.yaml + content: | + llm: + # openai or anthropic + service: "openai" + # OpenAI: gpt-3.5-turbo, gpt-4, gpt-3.5-turbo-1106, gpt-3.5-turbo-16k, gpt-4-32k, gpt-4o-mini, gpt-4o-mini-2024-07-18; Anthropic: claude-instant-1 or claude-2 + model: "gpt-4o-mini" + ## OpenAI-specific settings + # Only used for Azure OpenAI API + azure_openai_endpoint: + # for Azure OpenAI API deployment, the model may be deployed with custom deployment names + # set the deployment names if you encounter in logs HTTP 404 errors: + # "The API deployment for this resource does not exist." + azure_openai: + # llm.model name is used as deployment name as reasonable default if not set + # assuming base model is deployed with deployment name matching model name + # llm_deployment: "gpt-4o-mini-customname" + # embeddings deployment is required when Zep is configured to use OpenAI embeddings + # embedding_deployment: "text-embedding-ada-002-customname" + # Use only with an alternate OpenAI-compatible API endpoint + llm_deployment: + embedding_deployment: + openai_endpoint: + openai_org_id: + nlp: + server_url: "http://localhost:5557" + memory: + message_window: 12 + extractors: + documents: + embeddings: + enabled: true + chunk_size: 1000 + dimensions: 384 + service: "local" + # dimensions: 1536 + # service: "openai" + messages: + summarizer: + enabled: true + entities: + enabled: true + embeddings: + enabled: true + dimensions: 384 + service: "local" + entities: + enabled: true + intent: + enabled: true + embeddings: + enabled: true + dimensions: 384 + service: "local" + # dimensions: 1536 + # service: "openai" + store: + type: "postgres" + postgres: + dsn: "postgres://postgres:postgres@localhost:5432/?sslmode=disable" + server: + # Specify the host to listen on. Defaults to 0.0.0.0 + host: 0.0.0.0 + port: 8000 + # Is the Web UI enabled? + # Warning: The Web UI is not secured by authentication and should not be enabled if + # Zep is exposed to the public internet. + web_enabled: true + # The maximum size of a request body, in bytes. Defaults to 5MB. + max_request_size: 5242880 + auth: + # Set to true to enable authentication + required: true + # Do not use this secret in production. The ZEP_AUTH_SECRET environment variable should be + # set to a cryptographically secure secret. See the Zep docs for details. + secret: "do-not-use-this-secret-in-production" + data: + # PurgeEvery is the period between hard deletes, in minutes. + # If set to 0 or undefined, hard deletes will not be performed. + purge_every: 60 + log: + level: "info" + opentelemetry: + enabled: false + # Custom Prompts Configuration + # Allows customization of extractor prompts. + custom_prompts: + summarizer_prompts: + # Anthropic Guidelines: + # - Use XML-style tags like as element identifiers. + # - Include {{.PrevSummary}} and {{.MessagesJoined}} as template variables. + # - Clearly explain model instructions, e.g., "Review content inside tags". + # - Provide a clear example within the prompt. + # + # Example format: + # anthropic: | + # + # + # + # + # {{.PrevSummary}} + # {{.MessagesJoined}} + # Response without preamble. + # + # If left empty, the default Anthropic summary prompt from zep/pkg/extractors/prompts.go will be used. + anthropic: | + + # OpenAI summarizer prompt configuration. + # Guidelines: + # - Include {{.PrevSummary}} and {{.MessagesJoined}} as template variables. + # - Provide a clear example within the prompt. + # + # Example format: + # openai: | + # + # Example: + # + # Current summary: {{.PrevSummary}} + # New lines of conversation: {{.MessagesJoined}} + # New summary:` + # + # If left empty, the default OpenAI summary prompt from zep/pkg/extractors/prompts.go will be used. + openai: | + healthcheck: + test: "timeout 10s bash -c ':> /dev/tcp/127.0.0.1/8000' || exit 1" + interval: 5s + timeout: 10s + retries: 3 + start_period: 40s diff --git a/templates/compose/zipline.yaml b/templates/compose/zipline.yaml new file mode 100644 index 0000000000..c5efc4058b --- /dev/null +++ b/templates/compose/zipline.yaml @@ -0,0 +1,42 @@ +# documentation: https://github.com/diced/zipline +# slogan: A ShareX/file upload server that is easy to use, packed with features, and with an easy setup! +# tags: zipline,file-sharing,upload,sharing +# logo: svgs/zipline.png +# port: 3000 + +services: + zipline: + image: ghcr.io/diced/zipline:latest + environment: + - SERVICE_FQDN_ZIPLINE_3000 + - CORE_RETURN_HTTPS=${CORE_RETURN_HTTPS:-false} + - CORE_SECRET=${SERVICE_PASSWORD_64_ZIPLINE} + - CORE_DATABASE_URL=postgres://${SERVICE_USER_POSTGRES}:${SERVICE_PASSWORD_POSTGRES}@postgres/${POSTGRES_DB:-zipline-db} + - CORE_LOGGER=${CORE_LOGGER:-true} + # Default credentials are "administrator" and "password" + volumes: + - zipline-uploads:/zipline/uploads + - zipline-public:/zipline/public + depends_on: + postgres: + condition: service_healthy + healthcheck: + test: + ["CMD", "wget", "-q", "--spider", "http://127.0.0.1:3000/auth/login"] + interval: 5s + timeout: 20s + retries: 10 + + postgres: + image: postgres:16-alpine + volumes: + - zipline-postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_USER=${SERVICE_USER_POSTGRES} + - POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES} + - POSTGRES_DB=${POSTGRES_DB:-zipline-db} + healthcheck: + test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] + interval: 5s + timeout: 20s + retries: 10 diff --git a/templates/service-templates.json b/templates/service-templates.json index 7a019e4c5e..ad71e06918 100644 --- a/templates/service-templates.json +++ b/templates/service-templates.json @@ -13,6 +13,20 @@ "minversion": "0.0.0", "port": "80" }, + "affine": { + "documentation": "https://docs.affine.pro/docs/self-host-affine?utm_source=coolify.io", + "slogan": "Affine is an open-source, all-in-one workspace and OS for knowledge management, a Notion/Miro alternative.", + "compose": "c2VydmljZXM6CiAgYWZmaW5lOgogICAgaW1hZ2U6ICdnaGNyLmlvL3RvZXZlcnl0aGluZy9hZmZpbmUtZ3JhcGhxbDpzdGFibGUnCiAgICBjb21tYW5kOgogICAgICAtIHNoCiAgICAgIC0gJy1jJwogICAgICAtICdub2RlIC4vc2NyaXB0cy9zZWxmLWhvc3QtcHJlZGVwbG95ICYmIG5vZGUgLi9kaXN0L2luZGV4LmpzJwogICAgZGVwZW5kc19vbjoKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FmZmluZS1jb25maWc6L3Jvb3QvLmFmZmluZS9jb25maWcnCiAgICAgIC0gJ2FmZmluZS1zdG9yYWdlOi9yb290Ly5hZmZpbmUvc3RvcmFnZScKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LXNpemU6IDEwMDBtCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQUZGSU5FXzMwMTAKICAgICAgLSBOT0RFX09QVElPTlM9LS1pbXBvcnQ9Li9zY3JpcHRzL3JlZ2lzdGVyLmpzCiAgICAgIC0gQUZGSU5FX0NPTkZJR19QQVRIPS9yb290Ly5hZmZpbmUvY29uZmlnCiAgICAgIC0gUkVESVNfU0VSVkVSX0hPU1Q9cmVkaXMKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1hZmZpbmV9JwogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSBBRkZJTkVfU0VSVkVSX0hPU1Q9JFNFUlZJQ0VfRlFETl9BRkZJTkUKICAgICAgLSBBRkZJTkVfU0VSVkVSX0VYVEVSTkFMX1VSTD0kU0VSVklDRV9GUUROX0FGRklORQogICAgICAtICdNQUlMRVJfSE9TVD0ke01BSUxFUl9IT1NUfScKICAgICAgLSAnTUFJTEVSX1BPUlQ9JHtNQUlMRVJfUE9SVH0nCiAgICAgIC0gJ01BSUxFUl9VU0VSPSR7TUFJTEVSX1VTRVJ9JwogICAgICAtICdNQUlMRVJfUEFTU1dPUkQ9JHtNQUlMRVJfUEFTU1dPUkR9JwogICAgICAtICdNQUlMRVJfU0VOREVSPSR7TUFJTEVSX1NFTkRFUn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gImJhc2ggLWMgJzo+IC9kZXYvdGNwLzEyNy4wLjAuMS8zMDEwJyB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIHJlZGlzOgogICAgaW1hZ2U6IHJlZGlzCiAgICB2b2x1bWVzOgogICAgICAtICdhZmZpbmUtcmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtICctLXJhdycKICAgICAgICAtIGluY3IKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgdm9sdW1lczoKICAgICAgLSAnYWZmaW5lLXBvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VIGFmZmluZScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWFmZmluZX0nCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YS9wZ2RhdGEK", + "tags": [ + "knowledge-management", + "notion", + "miro", + "workspace" + ], + "logo": "svgs/affine.svg", + "minversion": "0.0.0", + "port": "3010" + }, "anythingllm": { "documentation": "https://docs.anythingllm.com/installation-docker/overview?utm_source=coolify.io", "slogan": "AnythingLLM is the easiest to use, all-in-one AI application that can do RAG, AI Agents, and much more with no code or infrastructure headaches.", @@ -53,7 +67,7 @@ "appwrite": { "documentation": "https://appwrite.io?utm_source=coolify.io", "slogan": "A backend-as-a-service platform that simplifies the web & mobile app development.", - "compose": "eC1sb2dnaW5nOgogIGxvZ2dpbmc6CiAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgb3B0aW9uczoKICAgICAgbWF4LWZpbGU6ICc1JwogICAgICBtYXgtc2l6ZTogMTBtCnNlcnZpY2VzOgogIGFwcHdyaXRlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS8KICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX0xPQ0FMRT0ke19BUFBfTE9DQUxFOi1lbn0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVD0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVDotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTPSR7X0FQUF9DT05TT0xFX1dISVRFTElTVF9FTUFJTFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX0hPU1ROQU1FUz0ke19BUFBfQ09OU09MRV9IT1NUTkFNRVM6LWxvY2FsaG9zdCxhcHB3cml0ZS5pbywqLmFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfTkFNRT0ke19BUFBfU1lTVEVNX0VNQUlMX05BTUU6LUFwcHdyaXRlfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUzotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVQ9JHtfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVR9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQ9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9ST09UX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQlJPT1QKICAgICAgLSAnX0FQUF9TTVRQX0hPU1Q9JHtfQVBQX1NNVFBfSE9TVH0nCiAgICAgIC0gJ19BUFBfU01UUF9QT1JUPSR7X0FQUF9TTVRQX1BPUlR9JwogICAgICAtICdfQVBQX1NNVFBfU0VDVVJFPSR7X0FQUF9TTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ19BUFBfU01UUF9VU0VSTkFNRT0ke19BUFBfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfU01UUF9QQVNTV09SRD0ke19BUFBfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTUlUPSR7X0FQUF9TVE9SQUdFX0xJTUlUOi0zMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUPSR7X0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ6LTIwMDAwMDAwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVUz0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUOi1hcHB3cml0ZS1jbGFtYXZ9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ6LTMzMTB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfU0laRV9MSU1JVD0ke19BUFBfRlVOQ1RJT05TX1NJWkVfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfVElNRU9VVD0ke19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19DUFVTPSR7X0FQUF9GVU5DVElPTlNfQ1BVUzotMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX01FTU9SWT0ke19BUFBfRlVOQ1RJT05TX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTPSR7X0FQUF9GVU5DVElPTlNfUlVOVElNRVM6LW5vZGUtMjAuMCxwaHAtOC4yLHB5dGhvbi0zLjExLHJ1YnktMy4yfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9ERUxBWT0ke19BUFBfTUFJTlRFTkFOQ0VfREVMQVl9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZOi04NjQwMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTOi04NjQwMH0nCiAgICAgIC0gJ19BUFBfU01TX1BST1ZJREVSPSR7X0FQUF9TTVNfUFJPVklERVJ9JwogICAgICAtICdfQVBQX1NNU19GUk9NPSR7X0FQUF9TTVNfRlJPTX0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRT0ke19BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRTotMTB9JwogICAgICAtICdfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFk9JHtfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFk6LTI1MH0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfREVQVEg9JHtfQVBQX0dSQVBIUUxfTUFYX0RFUFRIOi0zfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZPSR7X0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0FQUF9JRD0ke19BUFBfVkNTX0dJVEhVQl9BUFBfSUR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVQ9JHtfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRD0ke19BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWT0ke19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZfScKICBhcHB3cml0ZS1yZWFsdGltZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogcmVhbHRpbWUKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVBQV1JJVEU9L3YxL3JlYWx0aW1lCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUFRJT05TX0FCVVNFPSR7X0FQUF9PUFRJT05TX0FCVVNFOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItYXVkaXRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItYXVkaXRzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWF1ZGl0cwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci13ZWJob29rczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLXdlYmhvb2tzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXdlYmhvb2tzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUzotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1kZWxldGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZGVsZXRlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kZWxldGVzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtdXBsb2Fkczovc3RvcmFnZS91cGxvYWRzOnJ3JwogICAgICAtICdhcHB3cml0ZS1jYWNoZTovc3RvcmFnZS9jYWNoZTpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogIGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1kYXRhYmFzZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZGF0YWJhc2VzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLWJ1aWxkczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLWJ1aWxkcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1idWlsZHMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUPSR7X0FQUF9GVU5DVElPTlNfQlVJTERfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfQ1BVUz0ke19BUFBfRlVOQ1RJT05TX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19NRU1PUlk9JHtfQVBQX0ZVTkNUSU9OU19NRU1PUlk6LTB9JwogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItY2VydGlmaWNhdGVzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gX0FQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVD0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9ET01BSU5fRlVOQ1RJT05TPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1M6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNScKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZnVuY3Rpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0ZVTkNUSU9OU19USU1FT1VUPSR7X0FQUF9GVU5DVElPTlNfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfQlVJTERfVElNRU9VVD0ke19BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX0NQVVM9JHtfQVBQX0ZVTkNUSU9OU19DUFVTOi0wfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfTUVNT1JZPSR7X0FQUF9GVU5DVElPTlNfTUVNT1JZOi0wfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FPSR7X0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FfScKICAgICAgLSAnX0FQUF9ET0NLRVJfSFVCX1BBU1NXT1JEPSR7X0FQUF9ET0NLRVJfSFVCX1BBU1NXT1JEfScKICAgICAgLSAnX0FQUF9ET0NLRVJfSFVCX0VNQUlMPSR7X0FQUF9ET0NLRVJfSFVCX0VNQUlMfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogIGFwcHdyaXRlLXdvcmtlci1tYWlsczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLW1haWxzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1haWxzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfU01UUF9IT1NUPSR7X0FQUF9TTVRQX0hPU1R9JwogICAgICAtICdfQVBQX1NNVFBfUE9SVD0ke19BUFBfU01UUF9QT1JUfScKICAgICAgLSAnX0FQUF9TTVRQX1NFQ1VSRT0ke19BUFBfU01UUF9TRUNVUkV9JwogICAgICAtICdfQVBQX1NNVFBfVVNFUk5BTUU9JHtfQVBQX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdfQVBQX1NNVFBfUEFTU1dPUkQ9JHtfQVBQX1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLW1lc3NhZ2luZzoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLW1lc3NhZ2luZwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1NNU19GUk9NPSR7X0FQUF9TTVNfRlJPTX0nCiAgICAgIC0gJ19BUFBfU01TX1BST1ZJREVSPSR7X0FQUF9TTVNfUFJPVklERVJ9JwogIGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWlncmF0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9ET01BSU5fVEFSR0VUPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1M6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVR9JwogIGFwcHdyaXRlLW1haW50ZW5hbmNlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiBtYWludGVuYW5jZQogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1haW50ZW5hbmNlCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQ9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUx9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT059JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZOi04NjQwMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTOi04NjQwMH0nCiAgYXBwd3JpdGUtd29ya2VyLXVzYWdlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItdXNhZ2UKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItdXNhZ2UKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTD0ke19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw6LTMwfScKICBhcHB3cml0ZS13b3JrZXItdXNhZ2UtZHVtcDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogd29ya2VyLXVzYWdlLWR1bXAKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItdXNhZ2UtZHVtcAogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw9JHtfQVBQX1VTQUdFX0FHR1JFR0FUSU9OX0lOVEVSVkFMOi0zMH0nCiAgYXBwd3JpdGUtc2NoZWR1bGVyLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS41JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtc2NoZWR1bGVyLWZ1bmN0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgYXBwd3JpdGUtc2NoZWR1bGVyLW1lc3NhZ2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjUnCiAgICBlbnRyeXBvaW50OiBzY2hlZHVsZS1tZXNzYWdlcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXNjaGVkdWxlci1tZXNzYWdlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgYXBwd3JpdGUtYXNzaXN0YW50OgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hc3Npc3RhbnQ6MC40LjAnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtYXNzaXN0YW50CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWQogIG9wZW5ydW50aW1lcy1leGVjdXRvcjoKICAgIGNvbnRhaW5lcl9uYW1lOiBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGhvc3RuYW1lOiBhcHB3cml0ZS1leGVjdXRvcgogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgc3RvcF9zaWduYWw6IFNJR0lOVAogICAgaW1hZ2U6ICdvcGVucnVudGltZXMvZXhlY3V0b3I6MC40LjknCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnL3RtcDovdG1wOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ09QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0ke19BUFBfRlVOQ1RJT05TX0lOQUNUSVZFX1RIUkVTSE9MRH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfRlVOQ1RJT05TX01BSU5URU5BTkNFX0lOVEVSVkFMfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX05FVFdPUks9JHtfQVBQX0ZVTkNUSU9OU19SVU5USU1FU19ORVRXT1JLfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfVVNFUk5BTUU9JHtfQVBQX0RPQ0tFUl9IVUJfVVNFUk5BTUV9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9SVU5USU1FUz0ke19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTfScKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZOi1sb2NhbH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICBhcHB3cml0ZS1tYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEwLjExJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1hcmlhZGIKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLW1hcmlhZGI6L3Zhci9saWIvbXlzcWw6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREJST09UCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgIGNvbW1hbmQ6ICdteXNxbGQgLS1pbm5vZGItZmx1c2gtbWV0aG9kPWZzeW5jJwogIGFwcHdyaXRlLXJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjIuNC1hbHBpbmUnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtcmVkaXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbW1hbmQ6ICJyZWRpcy1zZXJ2ZXIgLS1tYXhtZW1vcnkgICAgICAgICAgICA1MTJtYiAtLW1heG1lbW9yeS1wb2xpY3kgICAgIGFsbGtleXMtbHJ1IC0tbWF4bWVtb3J5LXNhbXBsZXMgICAgNVxuIgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtcmVkaXM6L2RhdGE6cncnCnZvbHVtZXM6CiAgYXBwd3JpdGUtbWFyaWFkYjogbnVsbAogIGFwcHdyaXRlLXJlZGlzOiBudWxsCiAgYXBwd3JpdGUtY2FjaGU6IG51bGwKICBhcHB3cml0ZS11cGxvYWRzOiBudWxsCiAgYXBwd3JpdGUtY2VydGlmaWNhdGVzOiBudWxsCiAgYXBwd3JpdGUtZnVuY3Rpb25zOiBudWxsCiAgYXBwd3JpdGUtYnVpbGRzOiBudWxsCiAgYXBwd3JpdGUtY29uZmlnOiBudWxsCg==", + "compose": "eC1sb2dnaW5nOgogIGxvZ2dpbmc6CiAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgb3B0aW9uczoKICAgICAgbWF4LWZpbGU6ICc1JwogICAgICBtYXgtc2l6ZTogMTBtCnNlcnZpY2VzOgogIGFwcHdyaXRlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLXVwbG9hZHM6L3N0b3JhZ2UvdXBsb2FkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2FjaGU6L3N0b3JhZ2UvY2FjaGU6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWZ1bmN0aW9uczovc3RvcmFnZS9mdW5jdGlvbnM6cncnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0FQUFdSSVRFPS8KICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX0xPQ0FMRT0ke19BUFBfTE9DQUxFOi1lbn0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVD0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfUk9PVDotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfQ09OU09MRV9XSElURUxJU1RfRU1BSUxTPSR7X0FQUF9DT05TT0xFX1dISVRFTElTVF9FTUFJTFN9JwogICAgICAtICdfQVBQX0NPTlNPTEVfV0hJVEVMSVNUX0lQUz0ke19BUFBfQ09OU09MRV9XSElURUxJU1RfSVBTfScKICAgICAgLSAnX0FQUF9DT05TT0xFX0hPU1ROQU1FUz0ke19BUFBfQ09OU09MRV9IT1NUTkFNRVM6LWxvY2FsaG9zdCxhcHB3cml0ZS5pbywqLmFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfTkFNRT0ke19BUFBfU1lTVEVNX0VNQUlMX05BTUU6LUFwcHdyaXRlfScKICAgICAgLSAnX0FQUF9TWVNURU1fRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M6LXRlYW1AYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUzotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVQ9JHtfQVBQX1NZU1RFTV9SRVNQT05TRV9GT1JNQVR9JwogICAgICAtICdfQVBQX09QVElPTlNfQUJVU0U9JHtfQVBQX09QVElPTlNfQUJVU0U6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQ9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9ST09UX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQlJPT1QKICAgICAgLSAnX0FQUF9TTVRQX0hPU1Q9JHtfQVBQX1NNVFBfSE9TVH0nCiAgICAgIC0gJ19BUFBfU01UUF9QT1JUPSR7X0FQUF9TTVRQX1BPUlR9JwogICAgICAtICdfQVBQX1NNVFBfU0VDVVJFPSR7X0FQUF9TTVRQX1NFQ1VSRX0nCiAgICAgIC0gJ19BUFBfU01UUF9VU0VSTkFNRT0ke19BUFBfU01UUF9VU0VSTkFNRX0nCiAgICAgIC0gJ19BUFBfU01UUF9QQVNTV09SRD0ke19BUFBfU01UUF9QQVNTV09SRH0nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTUlUPSR7X0FQUF9TVE9SQUdFX0xJTUlUOi0zMDAwMDAwMH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9QUkVWSUVXX0xJTUlUPSR7X0FQUF9TVE9SQUdFX1BSRVZJRVdfTElNSVQ6LTIwMDAwMDAwfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVUz0ke19BUFBfU1RPUkFHRV9BTlRJVklSVVM6LWRpc2FibGVkfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUPSR7X0FQUF9TVE9SQUdFX0FOVElWSVJVU19IT1NUOi1hcHB3cml0ZS1jbGFtYXZ9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ9JHtfQVBQX1NUT1JBR0VfQU5USVZJUlVTX1BPUlQ6LTMzMTB9JwogICAgICAtICdfQVBQX1NUT1JBR0VfREVWSUNFPSR7X0FQUF9TVE9SQUdFX0RFVklDRTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfU0laRV9MSU1JVD0ke19BUFBfRlVOQ1RJT05TX1NJWkVfTElNSVQ6LTMwMDAwMDAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfVElNRU9VVD0ke19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19DUFVTPSR7X0FQUF9GVU5DVElPTlNfQ1BVUzotMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX01FTU9SWT0ke19BUFBfRlVOQ1RJT05TX01FTU9SWTotMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTPSR7X0FQUF9GVU5DVElPTlNfUlVOVElNRVM6LW5vZGUtMjAuMCxwaHAtOC4yLHB5dGhvbi0zLjExLHJ1YnktMy4yfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUw6LTg2NDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9ERUxBWT0ke19BUFBfTUFJTlRFTkFOQ0VfREVMQVl9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT046LTEyMDk2MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZOi04NjQwMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTOi04NjQwMH0nCiAgICAgIC0gJ19BUFBfU01TX1BST1ZJREVSPSR7X0FQUF9TTVNfUFJPVklERVJ9JwogICAgICAtICdfQVBQX1NNU19GUk9NPSR7X0FQUF9TTVNfRlJPTX0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRT0ke19BUFBfR1JBUEhRTF9NQVhfQkFUQ0hfU0laRTotMTB9JwogICAgICAtICdfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFk9JHtfQVBQX0dSQVBIUUxfTUFYX0NPTVBMRVhJVFk6LTI1MH0nCiAgICAgIC0gJ19BUFBfR1JBUEhRTF9NQVhfREVQVEg9JHtfQVBQX0dSQVBIUUxfTUFYX0RFUFRIOi0zfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9OQU1FfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZPSR7X0FQUF9WQ1NfR0lUSFVCX1BSSVZBVEVfS0VZfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0FQUF9JRD0ke19BUFBfVkNTX0dJVEhVQl9BUFBfSUR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVQ9JHtfQVBQX1ZDU19HSVRIVUJfV0VCSE9PS19TRUNSRVR9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke19BUFBfVkNTX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnX0FQUF9WQ1NfR0lUSFVCX0NMSUVOVF9JRD0ke19BUFBfVkNTX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWT0ke19BUFBfQVNTSVNUQU5UX09QRU5BSV9BUElfS0VZfScKICBhcHB3cml0ZS1yZWFsdGltZToKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogcmVhbHRpbWUKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVBQV1JJVEU9L3YxL3JlYWx0aW1lCiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUFRJT05TX0FCVVNFPSR7X0FQUF9PUFRJT05TX0FCVVNFOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICBhcHB3cml0ZS13b3JrZXItYXVkaXRzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItYXVkaXRzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWF1ZGl0cwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci13ZWJob29rczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogd29ya2VyLXdlYmhvb2tzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLXdlYmhvb2tzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1NZU1RFTV9TRUNVUklUWV9FTUFJTF9BRERSRVNTPSR7X0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUzotY2VydHNAYXBwd3JpdGUuaW99JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1kZWxldGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItZGVsZXRlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1kZWxldGVzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtdXBsb2Fkczovc3RvcmFnZS91cGxvYWRzOnJ3JwogICAgICAtICdhcHB3cml0ZS1jYWNoZTovc3RvcmFnZS9jYWNoZTpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtY2VydGlmaWNhdGVzOi9zdG9yYWdlL2NlcnRpZmljYXRlczpydycKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfUzNfQUNDRVNTX0tFWTotbG9jYWx9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1MzX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OOi11cy1lYXN0LTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT046LXVzLXdlc3QtMDA0fScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0xJTk9ERV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9MSU5PREVfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfQlVDS0VUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9SRUdJT046LWV1LWNlbnRyYWwtMX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9CVUNLRVR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gX0FQUF9FWEVDVVRPUl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfQVBQV1JJVEUKICAgICAgLSAnX0FQUF9FWEVDVVRPUl9IT1NUPSR7X0FQUF9FWEVDVVRPUl9IT1NUOi1odHRwOi8vYXBwd3JpdGUtZXhlY3V0b3IvdjF9JwogIGFwcHdyaXRlLXdvcmtlci1kYXRhYmFzZXM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNicKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1kYXRhYmFzZXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZGF0YWJhc2VzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLWJ1aWxkczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogd29ya2VyLWJ1aWxkcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1idWlsZHMKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgICAgLSBhcHB3cml0ZS1tYXJpYWRiCiAgICB2b2x1bWVzOgogICAgICAtICdhcHB3cml0ZS1mdW5jdGlvbnM6L3N0b3JhZ2UvZnVuY3Rpb25zOnJ3JwogICAgICAtICdhcHB3cml0ZS1idWlsZHM6L3N0b3JhZ2UvYnVpbGRzOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtIF9BUFBfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfRVhFQ1VUT1JfSE9TVD0ke19BUFBfRVhFQ1VUT1JfSE9TVDotaHR0cDovL2FwcHdyaXRlLWV4ZWN1dG9yL3YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUU9JHtfQVBQX1ZDU19HSVRIVUJfQVBQX05BTUV9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVk9JHtfQVBQX1ZDU19HSVRIVUJfUFJJVkFURV9LRVl9JwogICAgICAtICdfQVBQX1ZDU19HSVRIVUJfQVBQX0lEPSR7X0FQUF9WQ1NfR0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX1RJTUVPVVQ9JHtfQVBQX0ZVTkNUSU9OU19USU1FT1VUOi05MDB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19CVUlMRF9USU1FT1VUPSR7X0FQUF9GVU5DVElPTlNfQlVJTERfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfQ1BVUz0ke19BUFBfRlVOQ1RJT05TX0NQVVM6LTB9JwogICAgICAtICdfQVBQX0ZVTkNUSU9OU19NRU1PUlk9JHtfQVBQX0ZVTkNUSU9OU19NRU1PUlk6LTB9JwogICAgICAtICdfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM9JHtfQVBQX09QVElPTlNfRk9SQ0VfSFRUUFM6LWRpc2FibGVkfScKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9ERVZJQ0U9JHtfQVBQX1NUT1JBR0VfREVWSUNFOi1sb2NhbH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1MzX0FDQ0VTU19LRVk6LWxvY2FsfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfUzNfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX1MzX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX1MzX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9TM19CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19SRUdJT049JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTjotdXMtZWFzdC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0RPX1NQQUNFU19CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OOi11cy13ZXN0LTAwNH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0FDQ0VTU19LRVl9JwogICAgICAtICdfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9MSU5PREVfU0VDUkVUfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTjotZXUtY2VudHJhbC0xfScKICAgICAgLSAnX0FQUF9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9XQVNBQklfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9TRUNSRVR9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OOi1ldS1jZW50cmFsLTF9JwogICAgICAtICdfQVBQX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICBhcHB3cml0ZS13b3JrZXItY2VydGlmaWNhdGVzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItY2VydGlmaWNhdGVzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLWNlcnRpZmljYXRlcwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLWNvbmZpZzovc3RvcmFnZS9jb25maWc6cncnCiAgICAgIC0gJ2FwcHdyaXRlLWNlcnRpZmljYXRlczovc3RvcmFnZS9jZXJ0aWZpY2F0ZXM6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gX0FQUF9ET01BSU49JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX1RBUkdFVD0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9ET01BSU5fRlVOQ1RJT05TPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1M6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogIGFwcHdyaXRlLXdvcmtlci1mdW5jdGlvbnM6CiAgICBpbWFnZTogJ2FwcHdyaXRlL2FwcHdyaXRlOjEuNicKICAgIGVudHJ5cG9pbnQ6IHdvcmtlci1mdW5jdGlvbnMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItZnVuY3Rpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIG9wZW5ydW50aW1lcy1leGVjdXRvcgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX0ZVTkNUSU9OU19USU1FT1VUPSR7X0FQUF9GVU5DVElPTlNfVElNRU9VVDotOTAwfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfQlVJTERfVElNRU9VVD0ke19BUFBfRlVOQ1RJT05TX0JVSUxEX1RJTUVPVVQ6LTkwMH0nCiAgICAgIC0gJ19BUFBfRlVOQ1RJT05TX0NQVVM9JHtfQVBQX0ZVTkNUSU9OU19DUFVTOi0wfScKICAgICAgLSAnX0FQUF9GVU5DVElPTlNfTUVNT1JZPSR7X0FQUF9GVU5DVElPTlNfTUVNT1JZOi0wfScKICAgICAgLSBfQVBQX0VYRUNVVE9SX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9BUFBXUklURQogICAgICAtICdfQVBQX0VYRUNVVE9SX0hPU1Q9JHtfQVBQX0VYRUNVVE9SX0hPU1Q6LWh0dHA6Ly9hcHB3cml0ZS1leGVjdXRvci92MX0nCiAgICAgIC0gJ19BUFBfVVNBR0VfU1RBVFM9JHtfQVBQX1VTQUdFX1NUQVRTOi1lbmFibGVkfScKICAgICAgLSAnX0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FPSR7X0FQUF9ET0NLRVJfSFVCX1VTRVJOQU1FfScKICAgICAgLSAnX0FQUF9ET0NLRVJfSFVCX1BBU1NXT1JEPSR7X0FQUF9ET0NLRVJfSFVCX1BBU1NXT1JEfScKICAgICAgLSAnX0FQUF9ET0NLRVJfSFVCX0VNQUlMPSR7X0FQUF9ET0NLRVJfSFVCX0VNQUlMfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogIGFwcHdyaXRlLXdvcmtlci1tYWlsczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogd29ya2VyLW1haWxzCiAgICBsb2dnaW5nOgogICAgICBkcml2ZXI6IGpzb24tZmlsZQogICAgICBvcHRpb25zOgogICAgICAgIG1heC1maWxlOiAnNScKICAgICAgICBtYXgtc2l6ZTogMTBtCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtd29ya2VyLW1haWxzCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX05BTUU9JHtfQVBQX1NZU1RFTV9FTUFJTF9OQU1FOi1BcHB3cml0ZX0nCiAgICAgIC0gJ19BUFBfU1lTVEVNX0VNQUlMX0FERFJFU1M9JHtfQVBQX1NZU1RFTV9FTUFJTF9BRERSRVNTOi10ZWFtQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfU01UUF9IT1NUPSR7X0FQUF9TTVRQX0hPU1R9JwogICAgICAtICdfQVBQX1NNVFBfUE9SVD0ke19BUFBfU01UUF9QT1JUfScKICAgICAgLSAnX0FQUF9TTVRQX1NFQ1VSRT0ke19BUFBfU01UUF9TRUNVUkV9JwogICAgICAtICdfQVBQX1NNVFBfVVNFUk5BTUU9JHtfQVBQX1NNVFBfVVNFUk5BTUV9JwogICAgICAtICdfQVBQX1NNVFBfUEFTU1dPUkQ9JHtfQVBQX1NNVFBfUEFTU1dPUkR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgYXBwd3JpdGUtd29ya2VyLW1lc3NhZ2luZzoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogd29ya2VyLW1lc3NhZ2luZwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1tZXNzYWdpbmcKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX1NNU19GUk9NPSR7X0FQUF9TTVNfRlJPTX0nCiAgICAgIC0gJ19BUFBfU01TX1BST1ZJREVSPSR7X0FQUF9TTVNfUFJPVklERVJ9JwogIGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItbWlncmF0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXdvcmtlci1taWdyYXRpb25zCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSBfQVBQX0RPTUFJTj0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gX0FQUF9ET01BSU5fVEFSR0VUPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSAnX0FQUF9TWVNURU1fU0VDVVJJVFlfRU1BSUxfQUREUkVTUz0ke19BUFBfU1lTVEVNX1NFQ1VSSVRZX0VNQUlMX0FERFJFU1M6LWNlcnRzQGFwcHdyaXRlLmlvfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnX0FQUF9MT0dHSU5HX0NPTkZJRz0ke19BUFBfTE9HR0lOR19DT05GSUd9JwogICAgICAtICdfQVBQX01JR1JBVElPTlNfRklSRUJBU0VfQ0xJRU5UX0lEPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9JRH0nCiAgICAgIC0gJ19BUFBfTUlHUkFUSU9OU19GSVJFQkFTRV9DTElFTlRfU0VDUkVUPSR7X0FQUF9NSUdSQVRJT05TX0ZJUkVCQVNFX0NMSUVOVF9TRUNSRVR9JwogIGFwcHdyaXRlLW1haW50ZW5hbmNlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiBtYWludGVuYW5jZQogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1haW50ZW5hbmNlCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtIF9BUFBfRE9NQUlOPSRTRVJWSUNFX0ZRRE5fQVBQV1JJVEUKICAgICAgLSBfQVBQX0RPTUFJTl9UQVJHRVQ9JFNFUlZJQ0VfRlFETl9BUFBXUklURQogICAgICAtIF9BUFBfRE9NQUlOX0ZVTkNUSU9OUz0kU0VSVklDRV9GUUROX0FQUFdSSVRFCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9SRURJU19IT1NUPSR7X0FQUF9SRURJU19IT1NUOi1hcHB3cml0ZS1yZWRpc30nCiAgICAgIC0gJ19BUFBfUkVESVNfUE9SVD0ke19BUFBfUkVESVNfUE9SVDotNjM3OX0nCiAgICAgIC0gJ19BUFBfUkVESVNfVVNFUj0ke19BUFBfUkVESVNfVVNFUn0nCiAgICAgIC0gJ19BUFBfUkVESVNfUEFTUz0ke19BUFBfUkVESVNfUEFTU30nCiAgICAgIC0gJ19BUFBfREJfSE9TVD0ke19BUFBfREJfSE9TVDotYXBwd3JpdGUtbWFyaWFkYn0nCiAgICAgIC0gJ19BUFBfREJfUE9SVD0ke19BUFBfREJfUE9SVDotMzMwNn0nCiAgICAgIC0gJ19BUFBfREJfU0NIRU1BPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBfQVBQX0RCX1VTRVI9JFNFUlZJQ0VfVVNFUl9NQVJJQURCCiAgICAgIC0gX0FQUF9EQl9QQVNTPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfTUFJTlRFTkFOQ0VfSU5URVJWQUx9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT049JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9FWEVDVVRJT059JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9DQUNIRT0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0NBQ0hFOi0yNTkyMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fQUJVU0U9JHtfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BQlVTRTotODY0MDB9JwogICAgICAtICdfQVBQX01BSU5URU5BTkNFX1JFVEVOVElPTl9BVURJVD0ke19BUFBfTUFJTlRFTkFOQ0VfUkVURU5USU9OX0FVRElUOi0xMjA5NjAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fVVNBR0VfSE9VUkxZOi04NjQwMDAwfScKICAgICAgLSAnX0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTPSR7X0FQUF9NQUlOVEVOQU5DRV9SRVRFTlRJT05fU0NIRURVTEVTOi04NjQwMH0nCiAgYXBwd3JpdGUtd29ya2VyLXVzYWdlOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiB3b3JrZXItdXNhZ2UKICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItdXNhZ2UKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ19BUFBfRU5WPSR7X0FQUF9FTlY6LXByb2R1Y3Rpb259JwogICAgICAtICdfQVBQX1dPUktFUl9QRVJfQ09SRT0ke19BUFBfV09SS0VSX1BFUl9DT1JFOi02fScKICAgICAgLSAnX0FQUF9PUEVOU1NMX0tFWV9WMT0ke19BUFBfT1BFTlNTTF9LRVlfVjF9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX1VTQUdFX1NUQVRTPSR7X0FQUF9VU0FHRV9TVEFUUzotZW5hYmxlZH0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19QUk9WSURFUj0ke19BUFBfTE9HR0lOR19QUk9WSURFUn0nCiAgICAgIC0gJ19BUFBfTE9HR0lOR19DT05GSUc9JHtfQVBQX0xPR0dJTkdfQ09ORklHfScKICAgICAgLSAnX0FQUF9VU0FHRV9BR0dSRUdBVElPTl9JTlRFUlZBTD0ke19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw6LTMwfScKICBhcHB3cml0ZS13b3JrZXItdXNhZ2UtZHVtcDoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogd29ya2VyLXVzYWdlLWR1bXAKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbnRhaW5lcl9uYW1lOiBhcHB3cml0ZS13b3JrZXItdXNhZ2UtZHVtcAogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcHB3cml0ZS1yZWRpcwogICAgICAtIGFwcHdyaXRlLW1hcmlhZGIKICAgIGVudmlyb25tZW50OgogICAgICAtICdfQVBQX0VOVj0ke19BUFBfRU5WOi1wcm9kdWN0aW9ufScKICAgICAgLSAnX0FQUF9XT1JLRVJfUEVSX0NPUkU9JHtfQVBQX1dPUktFUl9QRVJfQ09SRTotNn0nCiAgICAgIC0gJ19BUFBfT1BFTlNTTF9LRVlfVjE9JHtfQVBQX09QRU5TU0xfS0VZX1YxfScKICAgICAgLSAnX0FQUF9EQl9IT1NUPSR7X0FQUF9EQl9IT1NUOi1hcHB3cml0ZS1tYXJpYWRifScKICAgICAgLSAnX0FQUF9EQl9QT1JUPSR7X0FQUF9EQl9QT1JUOi0zMzA2fScKICAgICAgLSAnX0FQUF9EQl9TQ0hFTUE9JHtfQVBQX0RCX1NDSEVNQTotYXBwd3JpdGV9JwogICAgICAtIF9BUFBfREJfVVNFUj0kU0VSVklDRV9VU0VSX01BUklBREIKICAgICAgLSBfQVBQX0RCX1BBU1M9JFNFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQgogICAgICAtICdfQVBQX1JFRElTX0hPU1Q9JHtfQVBQX1JFRElTX0hPU1Q6LWFwcHdyaXRlLXJlZGlzfScKICAgICAgLSAnX0FQUF9SRURJU19QT1JUPSR7X0FQUF9SRURJU19QT1JUOi02Mzc5fScKICAgICAgLSAnX0FQUF9SRURJU19VU0VSPSR7X0FQUF9SRURJU19VU0VSfScKICAgICAgLSAnX0FQUF9SRURJU19QQVNTPSR7X0FQUF9SRURJU19QQVNTfScKICAgICAgLSAnX0FQUF9VU0FHRV9TVEFUUz0ke19BUFBfVVNBR0VfU1RBVFM6LWVuYWJsZWR9JwogICAgICAtICdfQVBQX0xPR0dJTkdfUFJPVklERVI9JHtfQVBQX0xPR0dJTkdfUFJPVklERVJ9JwogICAgICAtICdfQVBQX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ19BUFBfVVNBR0VfQUdHUkVHQVRJT05fSU5URVJWQUw9JHtfQVBQX1VTQUdFX0FHR1JFR0FUSU9OX0lOVEVSVkFMOi0zMH0nCiAgYXBwd3JpdGUtc2NoZWR1bGVyLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnYXBwd3JpdGUvYXBwd3JpdGU6MS42JwogICAgZW50cnlwb2ludDogc2NoZWR1bGUtZnVuY3Rpb25zCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtc2NoZWR1bGVyLWZ1bmN0aW9ucwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgYXBwd3JpdGUtc2NoZWR1bGVyLW1lc3NhZ2VzOgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hcHB3cml0ZToxLjYnCiAgICBlbnRyeXBvaW50OiBzY2hlZHVsZS1tZXNzYWdlcwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLXNjaGVkdWxlci1tZXNzYWdlcwogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBwd3JpdGUtbWFyaWFkYgogICAgICAtIGFwcHdyaXRlLXJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnX0FQUF9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ19BUFBfV09SS0VSX1BFUl9DT1JFPSR7X0FQUF9XT1JLRVJfUEVSX0NPUkU6LTZ9JwogICAgICAtICdfQVBQX09QRU5TU0xfS0VZX1YxPSR7X0FQUF9PUEVOU1NMX0tFWV9WMX0nCiAgICAgIC0gJ19BUFBfUkVESVNfSE9TVD0ke19BUFBfUkVESVNfSE9TVDotYXBwd3JpdGUtcmVkaXN9JwogICAgICAtICdfQVBQX1JFRElTX1BPUlQ9JHtfQVBQX1JFRElTX1BPUlQ6LTYzNzl9JwogICAgICAtICdfQVBQX1JFRElTX1VTRVI9JHtfQVBQX1JFRElTX1VTRVJ9JwogICAgICAtICdfQVBQX1JFRElTX1BBU1M9JHtfQVBQX1JFRElTX1BBU1N9JwogICAgICAtICdfQVBQX0RCX0hPU1Q9JHtfQVBQX0RCX0hPU1Q6LWFwcHdyaXRlLW1hcmlhZGJ9JwogICAgICAtICdfQVBQX0RCX1BPUlQ9JHtfQVBQX0RCX1BPUlQ6LTMzMDZ9JwogICAgICAtICdfQVBQX0RCX1NDSEVNQT0ke19BUFBfREJfU0NIRU1BOi1hcHB3cml0ZX0nCiAgICAgIC0gX0FQUF9EQl9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIF9BUFBfREJfUEFTUz0kU0VSVklDRV9QQVNTV09SRF9NQVJJQURCCiAgYXBwd3JpdGUtYXNzaXN0YW50OgogICAgaW1hZ2U6ICdhcHB3cml0ZS9hc3Npc3RhbnQ6MC40LjAnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtYXNzaXN0YW50CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBfQVBQX0FTU0lTVEFOVF9PUEVOQUlfQVBJX0tFWQogIG9wZW5ydW50aW1lcy1leGVjdXRvcjoKICAgIGNvbnRhaW5lcl9uYW1lOiBvcGVucnVudGltZXMtZXhlY3V0b3IKICAgIGhvc3RuYW1lOiBhcHB3cml0ZS1leGVjdXRvcgogICAgbG9nZ2luZzoKICAgICAgZHJpdmVyOiBqc29uLWZpbGUKICAgICAgb3B0aW9uczoKICAgICAgICBtYXgtZmlsZTogJzUnCiAgICAgICAgbWF4LXNpemU6IDEwbQogICAgc3RvcF9zaWduYWw6IFNJR0lOVAogICAgaW1hZ2U6ICdvcGVucnVudGltZXMvZXhlY3V0b3I6MC40LjknCiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnYXBwd3JpdGUtYnVpbGRzOi9zdG9yYWdlL2J1aWxkczpydycKICAgICAgLSAnYXBwd3JpdGUtZnVuY3Rpb25zOi9zdG9yYWdlL2Z1bmN0aW9uczpydycKICAgICAgLSAnL3RtcDovdG1wOnJ3JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ09QUl9FWEVDVVRPUl9JTkFDVElWRV9UUkVTSE9MRD0ke19BUFBfRlVOQ1RJT05TX0lOQUNUSVZFX1RIUkVTSE9MRH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9NQUlOVEVOQU5DRV9JTlRFUlZBTD0ke19BUFBfRlVOQ1RJT05TX01BSU5URU5BTkNFX0lOVEVSVkFMfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX05FVFdPUks9JHtfQVBQX0ZVTkNUSU9OU19SVU5USU1FU19ORVRXT1JLfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0RPQ0tFUl9IVUJfVVNFUk5BTUU9JHtfQVBQX0RPQ0tFUl9IVUJfVVNFUk5BTUV9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfRE9DS0VSX0hVQl9QQVNTV09SRD0ke19BUFBfRE9DS0VSX0hVQl9QQVNTV09SRH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9FTlY9JHtfQVBQX0VOVjotcHJvZHVjdGlvbn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9SVU5USU1FUz0ke19BUFBfRlVOQ1RJT05TX1JVTlRJTUVTfScKICAgICAgLSBPUFJfRVhFQ1VUT1JfU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0FQUFdSSVRFCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9MT0dHSU5HX1BST1ZJREVSPSR7X0FQUF9MT0dHSU5HX1BST1ZJREVSfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX0xPR0dJTkdfQ09ORklHPSR7X0FQUF9MT0dHSU5HX0NPTkZJR30nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RFVklDRT0ke19BUFBfU1RPUkFHRV9ERVZJQ0U6LWxvY2FsfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9TM19BQ0NFU1NfS0VZOi1sb2NhbH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1MzX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9TM19TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9TM19SRUdJT049JHtfQVBQX1NUT1JBR0VfUzNfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfUzNfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX1MzX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX0RPX1NQQUNFU19BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfRE9fU1BBQ0VTX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9ET19TUEFDRVNfQlVDS0VUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVk9JHtfQVBQX1NUT1JBR0VfQkFDS0JMQVpFX0FDQ0VTU19LRVl9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfU0VDUkVUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9TRUNSRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfUkVHSU9OPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9SRUdJT059JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9CQUNLQkxBWkVfQlVDS0VUPSR7X0FQUF9TVE9SQUdFX0JBQ0tCTEFaRV9CVUNLRVR9JwogICAgICAtICdPUFJfRVhFQ1VUT1JfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWT0ke19BUFBfU1RPUkFHRV9MSU5PREVfQUNDRVNTX0tFWX0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9TRUNSRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX1NFQ1JFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9SRUdJT049JHtfQVBQX1NUT1JBR0VfTElOT0RFX1JFR0lPTn0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX0xJTk9ERV9CVUNLRVQ9JHtfQVBQX1NUT1JBR0VfTElOT0RFX0JVQ0tFVH0nCiAgICAgIC0gJ09QUl9FWEVDVVRPUl9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZPSR7X0FQUF9TVE9SQUdFX1dBU0FCSV9BQ0NFU1NfS0VZfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1NFQ1JFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfU0VDUkVUfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX1JFR0lPTj0ke19BUFBfU1RPUkFHRV9XQVNBQklfUkVHSU9OfScKICAgICAgLSAnT1BSX0VYRUNVVE9SX1NUT1JBR0VfV0FTQUJJX0JVQ0tFVD0ke19BUFBfU1RPUkFHRV9XQVNBQklfQlVDS0VUfScKICBhcHB3cml0ZS1tYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjEwLjExJwogICAgY29udGFpbmVyX25hbWU6IGFwcHdyaXRlLW1hcmlhZGIKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIHZvbHVtZXM6CiAgICAgIC0gJ2FwcHdyaXRlLW1hcmlhZGI6L3Zhci9saWIvbXlzcWw6cncnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBNWVNRTF9ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREJST09UCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7X0FQUF9EQl9TQ0hFTUE6LWFwcHdyaXRlfScKICAgICAgLSBNWVNRTF9VU0VSPSRTRVJWSUNFX1VTRVJfTUFSSUFEQgogICAgICAtIE1ZU1FMX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01BUklBREIKICAgIGNvbW1hbmQ6ICdteXNxbGQgLS1pbm5vZGItZmx1c2gtbWV0aG9kPWZzeW5jJwogIGFwcHdyaXRlLXJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjIuNC1hbHBpbmUnCiAgICBjb250YWluZXJfbmFtZTogYXBwd3JpdGUtcmVkaXMKICAgIGxvZ2dpbmc6CiAgICAgIGRyaXZlcjoganNvbi1maWxlCiAgICAgIG9wdGlvbnM6CiAgICAgICAgbWF4LWZpbGU6ICc1JwogICAgICAgIG1heC1zaXplOiAxMG0KICAgIGNvbW1hbmQ6ICJyZWRpcy1zZXJ2ZXIgLS1tYXhtZW1vcnkgICAgICAgICAgICA1MTJtYiAtLW1heG1lbW9yeS1wb2xpY3kgICAgIGFsbGtleXMtbHJ1IC0tbWF4bWVtb3J5LXNhbXBsZXMgICAgNVxuIgogICAgdm9sdW1lczoKICAgICAgLSAnYXBwd3JpdGUtcmVkaXM6L2RhdGE6cncnCnZvbHVtZXM6CiAgYXBwd3JpdGUtbWFyaWFkYjogbnVsbAogIGFwcHdyaXRlLXJlZGlzOiBudWxsCiAgYXBwd3JpdGUtY2FjaGU6IG51bGwKICBhcHB3cml0ZS11cGxvYWRzOiBudWxsCiAgYXBwd3JpdGUtY2VydGlmaWNhdGVzOiBudWxsCiAgYXBwd3JpdGUtZnVuY3Rpb25zOiBudWxsCiAgYXBwd3JpdGUtYnVpbGRzOiBudWxsCiAgYXBwd3JpdGUtY29uZmlnOiBudWxsCg==", "tags": [ "backend-as-a-service", "platform" @@ -101,7 +115,7 @@ "authentik": { "documentation": "https://docs.goauthentik.io/docs/installation/docker-compose?utm_source=coolify.io", "slogan": "An open-source Identity Provider, focused on flexibility and versatility.", - "compose": "c2VydmljZXM6CiAgYXV0aGVudGlrLXNlcnZlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogc2VydmVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVVUSEVOVElLU0VSVkVSXzkwMDAKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL21lZGlhOi9tZWRpYScKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgYXV0aGVudGlrLXdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB1c2VyOiByb290CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY2VydHM6L2NlcnRzJwogICAgICAtICcuL2N1c3RvbS10ZW1wbGF0ZXM6L3RlbXBsYXRlcycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdkb2NrZXIuaW8vbGlicmFyeS9wb3N0Z3JlczoxNi1hbHBpbmUnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1kICQke1BPU1RHUkVTX0RCfSAtVSAkJHtQT1NUR1JFU19VU0VSfScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAnYXV0aGVudGlrLWRiOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gUE9TVEdSRVNfREI9YXV0aGVudGlrCiAgcmVkaXM6CiAgICBpbWFnZTogJ2RvY2tlci5pby9saWJyYXJ5L3JlZGlzOmFscGluZScKICAgIGNvbW1hbmQ6ICctLXNhdmUgNjAgMSAtLWxvZ2xldmVsIHdhcm5pbmcnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXM6L2RhdGEnCg==", + "compose": "c2VydmljZXM6CiAgYXV0aGVudGlrLXNlcnZlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogc2VydmVyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQVVUSEVOVElLU0VSVkVSXzkwMDAKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEOi10cnVlfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL21lZGlhOi9tZWRpYScKICAgICAgLSAnLi9jdXN0b20tdGVtcGxhdGVzOi90ZW1wbGF0ZXMnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgYXV0aGVudGlrLXdvcmtlcjoKICAgIGltYWdlOiAnZ2hjci5pby9nb2F1dGhlbnRpay9zZXJ2ZXI6JHtBVVRIRU5USUtfVEFHOi0yMDI0LjguMH0nCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgY29tbWFuZDogd29ya2VyCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQVVUSEVOVElLX1JFRElTX19IT1NUPSR7UkVESVNfSE9TVDotcmVkaXN9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fSE9TVD0ke1BPU1RHUkVTX0hPU1Q6LXBvc3RncmVzcWx9JwogICAgICAtICdBVVRIRU5USUtfUE9TVEdSRVNRTF9fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX05BTUU9JHtQT1NUR1JFU19EQjotYXV0aGVudGlrfScKICAgICAgLSAnQVVUSEVOVElLX1BPU1RHUkVTUUxfX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSAnQVVUSEVOVElLX1NFQ1JFVF9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0FVVEhFTlRJS1NFUlZFUn0nCiAgICAgIC0gJ0FVVEhFTlRJS19FUlJPUl9SRVBPUlRJTkdfX0VOQUJMRUQ9JHtBVVRIRU5USUtfRVJST1JfUkVQT1JUSU5HX19FTkFCTEVEfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19IT1NUPSR7QVVUSEVOVElLX0VNQUlMX19IT1NUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19QT1JUPSR7QVVUSEVOVElLX0VNQUlMX19QT1JUfScKICAgICAgLSAnQVVUSEVOVElLX0VNQUlMX19VU0VSTkFNRT0ke0FVVEhFTlRJS19FTUFJTF9fVVNFUk5BTUV9JwogICAgICAtICdBVVRIRU5USUtfRU1BSUxfX1BBU1NXT1JEPSR7QVVUSEVOVElLX0VNQUlMX19QQVNTV09SRH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMUz0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1RMU30nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTD0ke0FVVEhFTlRJS19FTUFJTF9fVVNFX1NTTH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVD0ke0FVVEhFTlRJS19FTUFJTF9fVElNRU9VVH0nCiAgICAgIC0gJ0FVVEhFTlRJS19FTUFJTF9fRlJPTT0ke0FVVEhFTlRJS19FTUFJTF9fRlJPTX0nCiAgICB1c2VyOiByb290CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnLi9tZWRpYTovbWVkaWEnCiAgICAgIC0gJy4vY2VydHM6L2NlcnRzJwogICAgICAtICcuL2N1c3RvbS10ZW1wbGF0ZXM6L3RlbXBsYXRlcycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1kICQke1BPU1RHUkVTX0RCfSAtVSAkJHtQT1NUR1JFU19VU0VSfScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAnYXV0aGVudGlrLWRiOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gUE9TVEdSRVNfREI9YXV0aGVudGlrCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOmFscGluZScKICAgIGNvbW1hbmQ6ICctLXNhdmUgNjAgMSAtLWxvZ2xldmVsIHdhcm5pbmcnCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgcGluZyB8IGdyZXAgUE9ORycKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXM6L2RhdGEnCg==", "tags": [ "identity", "login", @@ -135,7 +149,7 @@ "bitcoin-core": { "documentation": "https://hub.docker.com/r/ruimarinho/bitcoin-core/?utm_source=coolify.io", "slogan": "A self-hosted Bitcoin Core full node.", - "compose": "c2VydmljZXM6CiAgYml0Y29pbi1jb3JlOgogICAgaW1hZ2U6ICdydWltYXJpbmhvL2JpdGNvaW4tY29yZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQklUQ09JTl9SUENVU0VSPSR7QklUQ09JTl9SUENVU0VSOi1iaXRjb2ludXNlcn0nCiAgICAgIC0gJ0JJVENPSU5fUlBDUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BBU1NXT1JENjR9JwogICAgICAtICdCSVRDT0lOX05FVFdPUks9JHtCSVRDT0lOX05FVFdPUks6LW1haW5uZXR9JwogICAgICAtICdCSVRDT0lOX1BSSU5UVE9DT05TT0xFPSR7QklUQ09JTl9QUklOVFRPQ09OU09MRTotMX0nCiAgICAgIC0gJ0JJVENPSU5fVFhJTkRFWD0ke0JJVENPSU5fVFhJTkRFWDotMX0nCiAgICB2b2x1bWVzOgogICAgICAtICdiaXRjb2luX2RhdGE6L2hvbWUvYml0Y29pbi8uYml0Y29pbicK", + "compose": "c2VydmljZXM6CiAgYml0Y29pbi1jb3JlOgogICAgaW1hZ2U6ICdydWltYXJpbmhvL2JpdGNvaW4tY29yZTpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnQklUQ09JTl9SUENVU0VSPSR7QklUQ09JTl9SUENVU0VSOi1iaXRjb2ludXNlcn0nCiAgICAgIC0gJ0JJVENPSU5fUlBDUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BBU1NXT1JENjR9JwogICAgICAtICdCSVRDT0lOX1BSSU5UVE9DT05TT0xFPSR7QklUQ09JTl9QUklOVFRPQ09OU09MRTotMX0nCiAgICAgIC0gJ0JJVENPSU5fVFhJTkRFWD0ke0JJVENPSU5fVFhJTkRFWDotMX0nCiAgICAgIC0gJ0JJVENPSU5fU0VSVkVSPSR7QklUQ09JTl9TRVJWRVI6LTF9JwogICAgdm9sdW1lczoKICAgICAgLSAnYmxvY2tjaGFpbi1kYXRhOi9ob21lL2JpdGNvaW4vLmJpdGNvaW4nCiAgICBjb21tYW5kOgogICAgICAtICctZGF0YWRpcj0vaG9tZS9iaXRjb2luLy5iaXRjb2luJwogICAgICAtICctcnBjYmluZD0xMjcuMC4wLjEnCiAgICAgIC0gJy1ycGNhbGxvd2lwPTEyNy4wLjAuMScKICAgICAgLSAnLXJwY3VzZXI9JHtCSVRDT0lOX1JQQ1VTRVJ9JwogICAgICAtICctcnBjcGFzc3dvcmQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BBU1NXT1JENjR9JwogICAgICAtICctcHJpbnR0b2NvbnNvbGU9JHtCSVRDT0lOX1BSSU5UVE9DT05TT0xFfScKICAgICAgLSAnLXR4aW5kZXg9JHtCSVRDT0lOX1RYSU5ERVh9JwogICAgICAtICctc2VydmVyPSR7QklUQ09JTl9TRVJWRVJ9Jwo=", "tags": [ "cryptocurrency", "node", @@ -148,7 +162,7 @@ "bookstack": { "documentation": "https://www.bookstackapp.com/docs/?utm_source=coolify.io", "slogan": "BookStack is a simple, self-hosted, easy-to-use platform for organising and storing information", - "compose": "c2VydmljZXM6CiAgYm9va3N0YWNrOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2Jvb2tzdGFjazpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQk9PS1NUQUNLXzgwCiAgICAgIC0gJ0FQUF9VUkw9JHtTRVJWSUNFX0ZRRE5fQk9PS1NUQUNLfScKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gJ0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdEQl9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ0RCX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJvb2tzdGFja2FwcH0nCiAgICAgIC0gJ1FVRVVFX0NPTk5FQ1RJT049JHtRVUVVRV9DT05ORUNUSU9OfScKICAgICAgLSAnR0lUSFVCX0FQUF9JRD0ke0dJVEhVQl9BUFBfSUR9JwogICAgICAtICdHSVRIVUJfQVBQX1NFQ1JFVD0ke0dJVEhVQl9BUFBfU0VDUkVUfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Jvb2tzdGFjay1kYXRhOi9jb25maWcnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjgwLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgbWFyaWFkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG1hcmlhZGI6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvbWFyaWFkYjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJvb2tzdGFja30nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgdm9sdW1lczoKICAgICAgLSAnYm9va3N0YWNrLW1hcmlhZGItZGF0YTovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgYm9va3N0YWNrOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL2Jvb2tzdGFjazpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fQk9PS1NUQUNLXzgwCiAgICAgIC0gJ0FQUF9VUkw9JHtTRVJWSUNFX0ZRRE5fQk9PS1NUQUNLfScKICAgICAgLSAnQVBQX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfQVBQS0VZfScKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gJ0RCX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnREJfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotYm9va3N0YWNrYXBwfScKICAgICAgLSAnUVVFVUVfQ09OTkVDVElPTj0ke1FVRVVFX0NPTk5FQ1RJT059JwogICAgICAtICdHSVRIVUJfQVBQX0lEPSR7R0lUSFVCX0FQUF9JRH0nCiAgICAgIC0gJ0dJVEhVQl9BUFBfU0VDUkVUPSR7R0lUSFVCX0FQUF9TRUNSRVR9JwogICAgICAtICdNQUlMX0RSSVZFUj0ke01BSUxfRFJJVkVSOi1zbXRwfScKICAgICAgLSAnTUFJTF9IT1NUPSR7TUFJTF9IT1NUfScKICAgICAgLSAnTUFJTF9QT1JUPSR7TUFJTF9QT1JUOi01ODd9JwogICAgICAtICdNQUlMX0VOQ1JZUFRJT049JHtNQUlMX0VOQ1JZUFRJT046LXRsc30nCiAgICAgIC0gJ01BSUxfVVNFUk5BTUU9JHtNQUlMX1VTRVJOQU1FfScKICAgICAgLSAnTUFJTF9QQVNTV09SRD0ke01BSUxfUEFTU1dPUkR9JwogICAgICAtICdNQUlMX0ZST009JHtNQUlMX0ZST019JwogICAgICAtICdNQUlMX0ZST01fTkFNRT0ke01BSUxfRlJPTV9OQU1FOi1Cb29rU3RhY2t9JwogICAgdm9sdW1lczoKICAgICAgLSAnYm9va3N0YWNrLWRhdGE6L2NvbmZpZycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnY3VybCAtZiBodHRwOi8vMTI3LjAuMC4xOjgwLycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgbWFyaWFkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG1hcmlhZGI6CiAgICBpbWFnZTogJ2xzY3IuaW8vbGludXhzZXJ2ZXIvbWFyaWFkYjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSAnVFo9JHtUWjotRXVyb3BlL0Jlcmxpbn0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWJvb2tzdGFja30nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgdm9sdW1lczoKICAgICAgLSAnYm9va3N0YWNrLW1hcmlhZGItZGF0YTovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "free-and-open-source", "mfa", @@ -210,6 +224,21 @@ "minversion": "0.0.0", "port": "10000" }, + "calcom": { + "documentation": "https://cal.com/docs?utm_source=coolify.io", + "slogan": "Scheduling infrastructure for everyone.", + "compose": "c2VydmljZXM6CiAgY2FsY29tOgogICAgaW1hZ2U6IGNhbGNvbS5kb2NrZXIuc2NhcmYuc2gvY2FsY29tL2NhbC5jb20KICAgIHBsYXRmb3JtOiBsaW51eC9hbWQ2NAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NBTENPTV8zMDAwCiAgICAgIC0gTkVYVF9QVUJMSUNfTElDRU5TRV9DT05TRU5UPWFncmVlCiAgICAgIC0gTk9ERV9FTlY9cHJvZHVjdGlvbgogICAgICAtICdORVhUX1BVQkxJQ19XRUJBUFBfVVJMPSR7U0VSVklDRV9GUUROX0NBTENPTX0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0FQSV9WMl9VUkw9JHtTRVJWSUNFX0ZRRE5fQ0FMQ09NfS9hcGkvdjInCiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9DQUxDT019L2FwaS9hdXRoJwogICAgICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF9DQUxDT01TRUNSRVR9JwogICAgICAtICdDQUxFTkRTT19FTkNSWVBUSU9OX0tFWT0ke1NFUlZJQ0VfQkFTRTY0X0NBTENPTUtFWX0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1jYWxlbmRzb30nCiAgICAgIC0gREFUQUJBU0VfSE9TVD1wb3N0Z3Jlc3FsCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtEQVRBQkFTRV9IT1NUOi1wb3N0Z3Jlc3FsfS8ke1BPU1RHUkVTX0RCOi1jYWxlbmRzb30nCiAgICAgIC0gJ0RBVEFCQVNFX0RJUkVDVF9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7REFUQUJBU0VfSE9TVDotcG9zdGdyZXNxbH0vJHtQT1NUR1JFU19EQjotY2FsZW5kc299JwogICAgICAtIENBTENPTV9URUxFTUVUUllfRElTQUJMRUQ9MQogICAgICAtICdFTUFJTF9GUk9NPSR7RU1BSUxfRlJPTX0nCiAgICAgIC0gJ0VNQUlMX0ZST01fTkFNRT0ke0VNQUlMX0ZST01fTkFNRX0nCiAgICAgIC0gJ0VNQUlMX1NFUlZFUl9IT1NUPSR7RU1BSUxfU0VSVkVSX0hPU1R9JwogICAgICAtICdFTUFJTF9TRVJWRVJfUE9SVD0ke0VNQUlMX1NFUlZFUl9QT1JUfScKICAgICAgLSAnRU1BSUxfU0VSVkVSX1VTRVI9JHtFTUFJTF9TRVJWRVJfVVNFUn0nCiAgICAgIC0gJ0VNQUlMX1NFUlZFUl9QQVNTV09SRD0ke0VNQUlMX1NFUlZFUl9QQVNTV09SRH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0FQUF9OQU1FPSJDYWwuY29tIicKICAgICAgLSAnQUxMT1dFRF9IT1NUTkFNRVM9WyIke1NFUlZJQ0VfRlFETl9DQUxDT019Il0nCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzcWwKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LWNhbGVuZHNvfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NhbGNvbS1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "calcom", + "calendso", + "scheduling", + "open", + "source" + ], + "logo": "svgs/calcom.svg", + "minversion": "0.0.0", + "port": "3000" + }, "castopod": { "documentation": "https://docs.castopod.org/main/en/?utm_source=coolify.io", "slogan": "Castopod is a free & open-source hosting platform made for podcasters who want engage and interact with their audience.", @@ -321,6 +350,25 @@ "logo": "svgs/classicpress.svg", "minversion": "0.0.0" }, + "cloudbeaver": { + "documentation": "https://dbeaver.com/docs/cloudbeaver/?utm_source=coolify.io", + "slogan": "CloudBeaver is a lightweight web application designed for comprehensive data management.", + "compose": "c2VydmljZXM6CiAgY2xvdWRiZWF2ZXI6CiAgICBpbWFnZTogJ2RiZWF2ZXIvY2xvdWRiZWF2ZXI6MjQnCiAgICB2b2x1bWVzOgogICAgICAtICdjbG91ZGJlYXZlci1kYXRhOi9vcHQvY2xvdWRiZWF2ZXIvd29ya3NwYWNlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NMT1VEQkVBVkVSXzg5NzgKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4OTc4LycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "dbeaver", + "data management", + "data", + "database", + "mysql", + "postgres", + "sqlite", + "sql", + "mongodb" + ], + "logo": "svgs/cloudbeaver.svg", + "minversion": "0.0.0", + "port": "8978" + }, "cloudflared": { "documentation": "https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/?utm_source=coolify.io", "slogan": "Client for Cloudflare Tunnel, a daemon that exposes private services through the Cloudflare edge.", @@ -343,19 +391,37 @@ "minversion": "0.0.0", "port": "8443" }, - "dashboard": { - "documentation": "https://github.com/phntxx/dashboard?tab=readme-ov-file#dashboard?utm_source=coolify.io", - "slogan": "A dashboard, inspired by SUI.", - "compose": "c2VydmljZXM6CiAgZGFzaGJvYXJkOgogICAgaW1hZ2U6ICdwaG50eHgvZGFzaGJvYXJkOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9EQVNIQk9BUkRfODA4MAogICAgdm9sdW1lczoKICAgICAgLSAnZGFzaGJvYXJkLWRhdGE6L2FwcC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "coder": { + "documentation": "https://coder.com/docs?utm_source=coolify.io", + "slogan": "Coder is an open-source platform for creating and managing cloud development environments on your infrastructure, with the tools and IDEs your developers already love.", + "compose": "c2VydmljZXM6CiAgY29kZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vY29kZXIvY29kZXI6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NPREVSXzcwODAKICAgICAgLSAnQ09ERVJfUEdfQ09OTkVDVElPTl9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGNvZGVyLWRhdGFiYXNlLyR7UE9TVEdSRVNfREI6LWNvZGVyLWRifT9zc2xtb2RlPWRpc2FibGUnCiAgICAgIC0gJ0NPREVSX0hUVFBfQUREUkVTUz0wLjAuMC4wOjcwODAnCiAgICAgIC0gJ0NPREVSX0FDQ0VTU19VUkw9JHtTRVJWSUNFX0ZRRE5fQ09ERVJ9JwogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBkZXBlbmRzX29uOgogICAgICBjb2Rlci1kYXRhYmFzZToKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjcwODAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBjb2Rlci1kYXRhYmFzZToKICAgIGltYWdlOiAncG9zdGdyZXM6MTYuNC1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgUE9TVEdSRVNfVVNFUjogJyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICcke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LWNvZGVyLWRifScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2NvZGVyLXBvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICR7UE9TVEdSRVNfVVNFUjotdXNlcm5hbWV9IC1kICR7UE9TVEdSRVNfREI6LWNvZGVyfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDUK", "tags": [ - "dashboard", - "web", - "search", - "bookmarks" + "coder", + "development", + "environment", + "self-hosted", + "postgres" ], - "logo": "svgs/coolify.png", + "logo": "svgs/coder.svg", "minversion": "0.0.0", - "port": "8080" + "port": "7080" + }, + "cryptgeon": { + "documentation": "https://github.com/cupcakearmy/cryptgeon?utm_source=coolify.io", + "slogan": "Secure note / file sharing service inspired by PrivNote.", + "compose": "c2VydmljZXM6CiAgYXBwOgogICAgaW1hZ2U6ICdjdXBjYWtlYXJteS9jcnlwdGdlb246bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0NSWVBUR0VPTl84MDAwCiAgICAgIC0gJ1NJWkVfTElNSVQ9JHtTSVpFX0xJTUlUOi00IE1pQn0nCiAgICAgIC0gJ01BWF9WSUVXUz0ke01BWF9WSUVXUzotMTAwfScKICAgICAgLSAnTUFYX0VYUElSQVRJT049JHtNQVhfRVhQSVJBVElPTjotMzYwfScKICAgICAgLSAnQUxMT1dfQURWQU5DRUQ9JHtBTExPV19BRFZBTkNFRDotdHJ1ZX0nCiAgICAgIC0gJ0FMTE9XX0ZJTEVTPSR7QUxMT1dfRklMRVM6LXRydWV9JwogICAgZGVwZW5kc19vbjoKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLS1mYWlsJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAwMC9hcGkvbGl2ZS8nCiAgICAgIGludGVydmFsOiAxbQogICAgICB0aW1lb3V0OiAzcwogICAgICByZXRyaWVzOiAyCiAgICAgIHN0YXJ0X3BlcmlvZDogNXMKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny1hbHBpbmUnCiAgICBjb21tYW5kOiAncmVkaXMtc2VydmVyIC0tbWF4bWVtb3J5IDIwMG1iIC0tbWF4bWVtb3J5LXBvbGljeSBhbGxrZXlzLWxydScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyCg==", + "tags": [ + "cryptgeon", + "secure", + "note", + "sharing", + "privnote", + "file", + "sharing" + ], + "logo": "svgs/cryptgeon.png", + "minversion": "0.0.0", + "port": "8000" }, "directus-with-postgresql": { "documentation": "https://directus.io?utm_source=coolify.io", @@ -451,6 +517,20 @@ "logo": "svgs/dokuwiki.png", "minversion": "0.0.0" }, + "dozzle-with-auth": { + "documentation": "https://dozzle.dev/?utm_source=coolify.io", + "slogan": "Dozzle is a simple and lightweight web UI for Docker logs.", + "compose": "c2VydmljZXM6CiAgZG96emxlOgogICAgaW1hZ2U6ICdhbWlyMjAvZG96emxlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9ET1paTEVfODA4MAogICAgICAtIERPWlpMRV9BVVRIX1BST1ZJREVSPXNpbXBsZQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2RhdGEvdXNlcnMueW1sCiAgICAgICAgdGFyZ2V0OiAnL2RhdGEvdXNlcnMueW1sOnJvJwogICAgICAgIGNvbnRlbnQ6ICJ1c2VyczpcbiAgIyBcImFkbWluXCIgaXMgdGhlIHVzZXJuYW1lXG4gIGFkbWluOlxuICAgIGVtYWlsOiB0ZXN0QGVtYWlsLmNvbVxuICAgIG5hbWU6IEFkbWluXG4gICAgIyBBIHNoYS0yNTYgaGFzaCBvZiB0aGUgcGFzc3dvcmQgeW91IHdhbnQgdG8gdXNlLiBDYW4gYmUgY29tcHV0ZWQgd2l0aCBcImVjaG8gLW4gcGFzc3dvcmQgfCBzaGFzdW0gLWEgMjU2XCIuIERlZmF1bHQgcGFzc3dvcmQgaXMgXCJUZXN0XCIuXG4gICAgcGFzc3dvcmQ6ICQyYSQxMSR2aXVjQ3ZGTGxIV3ZCTk9PSTZ1eXB1VlUuRDA5VVdiLnpzd1J4RWcwTWtEUGkxcS9iS2JkR1xuIgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIC9kb3p6bGUKICAgICAgICAtIGhlYWx0aGNoZWNrCiAgICAgIGludGVydmFsOiAzcwogICAgICB0aW1lb3V0OiAzMHMKICAgICAgcmV0cmllczogNQo=", + "tags": [ + "dozzle", + "docker", + "logs", + "web-ui" + ], + "logo": "svgs/dozzle.svg", + "minversion": "0.0.0", + "port": "8080" + }, "dozzle": { "documentation": "https://dozzle.dev/guide/getting-started#running-with-docker?utm_source=coolify.io", "slogan": "Dozzle is a simple and lightweight web UI for Docker logs.", @@ -564,7 +644,7 @@ "firefly": { "documentation": "https://firefly-iii.org?utm_source=coolify.io", "slogan": "A personal finances manager that can help you save money.", - "compose": "c2VydmljZXM6CiAgZmlyZWZseToKICAgIGltYWdlOiAnZmlyZWZseWlpaS9jb3JlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSVJFRkxZXzgwODAKICAgICAgLSBBUFBfS0VZPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfQ09OTkVDVElPTj1teXNxbAogICAgICAtICdEQl9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCiAgICAgIC0gJ1RSVVNURURfUFJPWElFUz0qJwogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS11cGxvYWQ6L3Zhci93d3cvaHRtbC9zdG9yYWdlL3VwbG9hZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdtYXJpYWRiOmx0cycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRTotZmlyZWZseX0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbWFyaWFkYi1hZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gMTI3LjAuMC4xCiAgICAgICAgLSAnLXVyb290JwogICAgICAgIC0gJy1wJHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZpcmVmbHktbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICBjcm9uOgogICAgaW1hZ2U6IGFscGluZQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4jIFN1YnN0aXR1dGUgdGhlIGVudmlyb25tZW50IHZhcmlhYmxlIGludG8gdGhlIGNyb24gY29tbWFuZFxuQ1JPTl9DT01NQU5EPVwiMCAzICogKiAqIHdnZXQgLXFPLSBodHRwOi8vZmlyZWZseTo4MDgwL2FwaS92MS9jcm9uLyR7U1RBVElDX0NST05fVE9LRU59XCJcbiMgQWRkIHRoZSBjcm9uIGNvbW1hbmQgdG8gdGhlIGNyb250YWJcbmVjaG8gXCIkQ1JPTl9DT01NQU5EXCIgfCBjcm9udGFiIC1cbiMgU3RhcnQgdGhlIGNyb24gZGFlbW9uIGluIHRoZSBmb3JlZ3JvdW5kIHdpdGggbG9nZ2luZyB0byBzdGRvdXRcbmNyb25kIC1mIC1MIC9kZXYvc3Rkb3V0IgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU1RBVElDX0NST05fVE9LRU49JFNFUlZJQ0VfQkFTRTY0X0NST05UT0tFTgo=", + "compose": "c2VydmljZXM6CiAgZmlyZWZseToKICAgIGltYWdlOiAnZmlyZWZseWlpaS9jb3JlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GSVJFRkxZXzgwODAKICAgICAgLSBBUFBfS0VZPSRTRVJWSUNFX0JBU0U2NF9BUFBLRVkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gREJfUE9SVD0zMzA2CiAgICAgIC0gREJfQ09OTkVDVElPTj1teXNxbAogICAgICAtICdEQl9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCiAgICAgIC0gJ1RSVVNURURfUFJPWElFUz0qJwogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS11cGxvYWQ6L3Zhci93d3cvaHRtbC9zdG9yYWdlL3VwbG9hZCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MDgwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBteXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogIG15c3FsOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1maXJlZmx5fScKICAgICAgLSAnTVlTUUxfUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYXJpYWRiLWFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgICAtICctdXJvb3QnCiAgICAgICAgLSAnLXAke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUxST09UfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogICAgdm9sdW1lczoKICAgICAgLSAnZmlyZWZseS1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogIGNyb246CiAgICBpbWFnZTogYWxwaW5lCiAgICBlbnRyeXBvaW50OgogICAgICAtIC9lbnRyeXBvaW50LnNoCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9lbnRyeXBvaW50LnNoCiAgICAgICAgdGFyZ2V0OiAvZW50cnlwb2ludC5zaAogICAgICAgIGNvbnRlbnQ6ICIjIS9iaW4vc2hcbiMgU3Vic3RpdHV0ZSB0aGUgZW52aXJvbm1lbnQgdmFyaWFibGUgaW50byB0aGUgY3JvbiBjb21tYW5kXG5DUk9OX0NPTU1BTkQ9XCIwIDMgKiAqICogd2dldCAtcU8tIGh0dHA6Ly9maXJlZmx5OjgwODAvYXBpL3YxL2Nyb24vJHtTVEFUSUNfQ1JPTl9UT0tFTn1cIlxuIyBBZGQgdGhlIGNyb24gY29tbWFuZCB0byB0aGUgY3JvbnRhYlxuZWNobyBcIiRDUk9OX0NPTU1BTkRcIiB8IGNyb250YWIgLVxuIyBTdGFydCB0aGUgY3JvbiBkYWVtb24gaW4gdGhlIGZvcmVncm91bmQgd2l0aCBsb2dnaW5nIHRvIHN0ZG91dFxuY3JvbmQgLWYgLUwgL2Rldi9zdGRvdXQiCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTVEFUSUNfQ1JPTl9UT0tFTj0kU0VSVklDRV9CQVNFNjRfQ1JPTlRPS0VOCg==", "tags": [ "finance", "money", @@ -575,6 +655,117 @@ "minversion": "0.0.0", "port": "8080" }, + "flowise-with-databases": { + "documentation": "https://docs.flowiseai.com/?utm_source=coolify.io", + "slogan": "Flowise is an open source low-code tool for developers to build customized LLM orchestration flows & AI agents. Also deploys Redis, Postgres and other services.", + "compose": "c2VydmljZXM6CiAgZmxvd2lzZToKICAgIGltYWdlOiAnZmxvd2lzZWFpL2Zsb3dpc2U6bGF0ZXN0JwogICAgZGVwZW5kc19vbjoKICAgICAgcGctcmVjb3JkLW1hbmFnZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXMtY2FjaGU6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcWRyYW50OgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fRkxPV0lTRV8zMDAxCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LWZhbHNlfScKICAgICAgLSAnRElTQUJMRV9GTE9XSVNFX1RFTEVNRVRSWT0ke0RJU0FCTEVfRkxPV0lTRV9URUxFTUVUUlk6LXRydWV9JwogICAgICAtICdQT1JUPSR7UE9SVDotMzAwMX0nCiAgICAgIC0gREFUQUJBU0VfUEFUSD0vcm9vdC8uZmxvd2lzZQogICAgICAtIEFQSUtFWV9QQVRIPS9yb290Ly5mbG93aXNlCiAgICAgIC0gU0VDUkVUS0VZX1BBVEg9L3Jvb3QvLmZsb3dpc2UKICAgICAgLSBMT0dfUEFUSD0vcm9vdC8uZmxvd2lzZS9sb2dzCiAgICAgIC0gQkxPQl9TVE9SQUdFX1BBVEg9L3Jvb3QvLmZsb3dpc2Uvc3RvcmFnZQogICAgICAtICdGTE9XSVNFX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0ZMT1dJU0V9JwogICAgICAtICdGTE9XSVNFX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9GTE9XSVNFfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Zsb3dpc2UtZGF0YTovcm9vdC8uZmxvd2lzZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6MzAwMSB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgcGctcmVjb3JkLW1hbmFnZXI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wZy1yZWNvcmQtbWFuYWdlcn0nCiAgICB2b2x1bWVzOgogICAgICAtICdwZy1yZWNvcmQtbWFuYWdlci1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtaCBsb2NhbGhvc3QgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMKICByZWRpcy1jYWNoZToKICAgIGltYWdlOiAncmVkaXM6NycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Zsb3dpc2UtcmVkaXMtY2FjaGUtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncmVkaXMtY2xpIC1oIGxvY2FsaG9zdCAtcCA2Mzc5IHBpbmcnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAzCiAgcWRyYW50OgogICAgaW1hZ2U6ICdxZHJhbnQvcWRyYW50OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9RRFJBTlRfNjMzMwogICAgICAtICdRRFJBTlRfX1NFUlZJQ0VfX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX1FEUkFOVEFQSUtFWX0nCiAgICB2b2x1bWVzOgogICAgICAtICdmbG93aXNlLXFkcmFudC1kYXRhOi9xZHJhbnQvc3RvcmFnZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzYzMzMnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMK", + "tags": [ + "lowcode", + "nocode", + "ai", + "llm", + "openai", + "anthropic", + "machine-learning", + "rag", + "agents", + "chatbot", + "api", + "team", + "bot", + "flows" + ], + "logo": "svgs/flowise.png", + "minversion": "0.0.0", + "port": "3001" + }, + "flowise": { + "documentation": "https://docs.flowiseai.com/?utm_source=coolify.io", + "slogan": "Flowise is an open source low-code tool for developers to build customized LLM orchestration flows & AI agents.", + "compose": "c2VydmljZXM6CiAgZmxvd2lzZToKICAgIGltYWdlOiAnZmxvd2lzZWFpL2Zsb3dpc2U6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZMT1dJU0VfMzAwMQogICAgICAtICdERUJVRz0ke0RFQlVHOi1mYWxzZX0nCiAgICAgIC0gJ0RJU0FCTEVfRkxPV0lTRV9URUxFTUVUUlk9JHtESVNBQkxFX0ZMT1dJU0VfVEVMRU1FVFJZOi10cnVlfScKICAgICAgLSAnUE9SVD0ke1BPUlQ6LTMwMDF9JwogICAgICAtIERBVEFCQVNFX1BBVEg9L3Jvb3QvLmZsb3dpc2UKICAgICAgLSBBUElLRVlfUEFUSD0vcm9vdC8uZmxvd2lzZQogICAgICAtIFNFQ1JFVEtFWV9QQVRIPS9yb290Ly5mbG93aXNlCiAgICAgIC0gTE9HX1BBVEg9L3Jvb3QvLmZsb3dpc2UvbG9ncwogICAgICAtIEJMT0JfU1RPUkFHRV9QQVRIPS9yb290Ly5mbG93aXNlL3N0b3JhZ2UKICAgICAgLSAnRkxPV0lTRV9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9GTE9XSVNFfScKICAgICAgLSAnRkxPV0lTRV9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfRkxPV0lTRX0nCiAgICB2b2x1bWVzOgogICAgICAtICdmbG93aXNlLWRhdGE6L3Jvb3QvLmZsb3dpc2UnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjMwMDEgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwo=", + "tags": [ + "lowcode", + "nocode", + "ai", + "llm", + "openai", + "anthropic", + "machine-learning", + "rag", + "agents", + "chatbot", + "api", + "team", + "bot", + "flows" + ], + "logo": "svgs/flowise.png", + "minversion": "0.0.0", + "port": "3001" + }, + "forgejo-with-mariadb": { + "documentation": "https://forgejo.org/docs?utm_source=coolify.io", + "slogan": "Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.", + "compose": "c2VydmljZXM6CiAgZm9yZ2VqbzoKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2Zvcmdlam8vZm9yZ2Vqbzo4JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMAogICAgICAtICdGT1JHRUpPX19zZXJ2ZXJfX1JPT1RfVVJMPSR7U0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMH0nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOUz0ke0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOU30nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XX0xPQ0FMTkVUV09SS1M9JHtGT1JHRUpPX19taWdyYXRpb25zX19BTExPV19MT0NBTE5FVFdPUktTLWZhbHNlfScKICAgICAgLSBVU0VSX1VJRD0xMDAwCiAgICAgIC0gVVNFUl9HSUQ9MTAwMAogICAgICAtIEZPUkdFSk9fX2RhdGFiYXNlX19EQl9UWVBFPW15c3FsCiAgICAgIC0gRk9SR0VKT19fZGF0YWJhc2VfX0hPU1Q9bWFyaWFkYgogICAgICAtICdGT1JHRUpPX19kYXRhYmFzZV9fTkFNRT0ke01ZU1FMX0RBVEFCQVNFLWZvcmdlam99JwogICAgICAtIEZPUkdFSk9fX2RhdGFiYXNlX19VU0VSPSRTRVJWSUNFX1VTRVJfTVlTUUwKICAgICAgLSBGT1JHRUpPX19kYXRhYmFzZV9fUEFTU1dEPSRTRVJWSUNFX1BBU1NXT1JEX01ZU1FMCiAgICB2b2x1bWVzOgogICAgICAtICdmb3JnZWpvLWRhdGE6L2RhdGEnCiAgICAgIC0gJ2Zvcmdlam8tdGltZXpvbmU6L2V0Yy90aW1lem9uZTpybycKICAgICAgLSAnZm9yZ2Vqby1sb2NhbHRpbWU6L2V0Yy9sb2NhbHRpbWU6cm8nCiAgICBwb3J0czoKICAgICAgLSAnMjIyMjI6MjInCiAgICBkZXBlbmRzX29uOgogICAgICBtYXJpYWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1hcmlhZGI6CiAgICBpbWFnZTogJ21hcmlhZGI6MTEnCiAgICB2b2x1bWVzOgogICAgICAtICdmb3JnZWpvLW1hcmlhZGItZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNWVNRTF9EQVRBQkFTRX0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMUk9PVH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "version control", + "collaboration", + "code", + "hosting", + "lightweight", + "mariadb" + ], + "logo": "svgs/forgejo.svg", + "minversion": "0.0.0", + "port": "3000" + }, + "forgejo-with-mysql": { + "documentation": "https://forgejo.org/docs?utm_source=coolify.io", + "slogan": "Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.", + "compose": "c2VydmljZXM6CiAgZm9yZ2VqbzoKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2Zvcmdlam8vZm9yZ2Vqbzo4JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMAogICAgICAtICdGT1JHRUpPX19zZXJ2ZXJfX1JPT1RfVVJMPSR7U0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMH0nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOUz0ke0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOU30nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XX0xPQ0FMTkVUV09SS1M9JHtGT1JHRUpPX19taWdyYXRpb25zX19BTExPV19MT0NBTE5FVFdPUktTLWZhbHNlfScKICAgICAgLSBVU0VSX1VJRD0xMDAwCiAgICAgIC0gVVNFUl9HSUQ9MTAwMAogICAgICAtIEZPUkdFSk9fX2RhdGFiYXNlX19EQl9UWVBFPW15c3FsCiAgICAgIC0gRk9SR0VKT19fZGF0YWJhc2VfX0hPU1Q9bXlzcWwKICAgICAgLSAnRk9SR0VKT19fZGF0YWJhc2VfX05BTUU9JHtNWVNRTF9EQVRBQkFTRS1mb3JnZWpvfScKICAgICAgLSBGT1JHRUpPX19kYXRhYmFzZV9fVVNFUj0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gRk9SR0VKT19fZGF0YWJhc2VfX1BBU1NXRD0kU0VSVklDRV9QQVNTV09SRF9NWVNRTAogICAgdm9sdW1lczoKICAgICAgLSAnZm9yZ2Vqby1kYXRhOi9kYXRhJwogICAgICAtICdmb3JnZWpvLXRpbWV6b25lOi9ldGMvdGltZXpvbmU6cm8nCiAgICAgIC0gJ2Zvcmdlam8tbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgcG9ydHM6CiAgICAgIC0gJzIyMjIyOjIyJwogICAgZGVwZW5kc19vbjoKICAgICAgbXlzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOjgnCiAgICB2b2x1bWVzOgogICAgICAtICdmb3JnZWpvLW15c3FsLWRhdGE6L3Zhci9saWIvbXlzcWwnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0V9JwogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTFJPT1R9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "version control", + "collaboration", + "code", + "hosting", + "lightweight", + "mysql" + ], + "logo": "svgs/forgejo.svg", + "minversion": "0.0.0", + "port": "3000" + }, + "forgejo-with-postgresql": { + "documentation": "https://forgejo.org/docs?utm_source=coolify.io", + "slogan": "Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.", + "compose": "c2VydmljZXM6CiAgZm9yZ2VqbzoKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2Zvcmdlam8vZm9yZ2Vqbzo4JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMAogICAgICAtICdGT1JHRUpPX19zZXJ2ZXJfX1JPT1RfVVJMPSR7U0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMH0nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOUz0ke0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOU30nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XX0xPQ0FMTkVUV09SS1M9JHtGT1JHRUpPX19taWdyYXRpb25zX19BTExPV19MT0NBTE5FVFdPUktTLWZhbHNlfScKICAgICAgLSBVU0VSX1VJRD0xMDAwCiAgICAgIC0gVVNFUl9HSUQ9MTAwMAogICAgICAtIEZPUkdFSk9fX2RhdGFiYXNlX19EQl9UWVBFPXBvc3RncmVzCiAgICAgIC0gRk9SR0VKT19fZGF0YWJhc2VfX0hPU1Q9cG9zdGdyZXNxbAogICAgICAtICdGT1JHRUpPX19kYXRhYmFzZV9fTkFNRT0ke1BPU1RHUkVTUUxfREFUQUJBU0UtZm9yZ2Vqb30nCiAgICAgIC0gRk9SR0VKT19fZGF0YWJhc2VfX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMCiAgICAgIC0gRk9SR0VKT19fZGF0YWJhc2VfX1BBU1NXRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMCiAgICB2b2x1bWVzOgogICAgICAtICdmb3JnZWpvLWRhdGE6L2RhdGEnCiAgICAgIC0gJ2Zvcmdlam8tdGltZXpvbmU6L2V0Yy90aW1lem9uZTpybycKICAgICAgLSAnZm9yZ2Vqby1sb2NhbHRpbWU6L2V0Yy9sb2NhbHRpbWU6cm8nCiAgICBwb3J0czoKICAgICAgLSAnMjIyMjI6MjInCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2Zvcmdlam8tcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRX0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "version control", + "collaboration", + "code", + "hosting", + "lightweight", + "postgresql" + ], + "logo": "svgs/forgejo.svg", + "minversion": "0.0.0", + "port": "3000" + }, + "forgejo": { + "documentation": "https://forgejo.org/docs?utm_source=coolify.io", + "slogan": "Forgejo is a self-hosted lightweight software forge. Easy to install and low maintenance, it just does the job.", + "compose": "c2VydmljZXM6CiAgZm9yZ2VqbzoKICAgIGltYWdlOiAnY29kZWJlcmcub3JnL2Zvcmdlam8vZm9yZ2Vqbzo4JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMAogICAgICAtICdGT1JHRUpPX19zZXJ2ZXJfX1JPT1RfVVJMPSR7U0VSVklDRV9GUUROX0ZPUkdFSk9fMzAwMH0nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOUz0ke0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XRURfRE9NQUlOU30nCiAgICAgIC0gJ0ZPUkdFSk9fX21pZ3JhdGlvbnNfX0FMTE9XX0xPQ0FMTkVUV09SS1M9JHtGT1JHRUpPX19taWdyYXRpb25zX19BTExPV19MT0NBTE5FVFdPUktTLWZhbHNlfScKICAgICAgLSBVU0VSX1VJRD0xMDAwCiAgICAgIC0gVVNFUl9HSUQ9MTAwMAogICAgcG9ydHM6CiAgICAgIC0gJzIyMjIyOjIyJwogICAgdm9sdW1lczoKICAgICAgLSAnZm9yZ2Vqby1kYXRhOi9kYXRhJwogICAgICAtICdmb3JnZWpvLXRpbWV6b25lOi9ldGMvdGltZXpvbmU6cm8nCiAgICAgIC0gJ2Zvcmdlam8tbG9jYWx0aW1lOi9ldGMvbG9jYWx0aW1lOnJvJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUK", + "tags": [ + "version control", + "collaboration", + "code", + "hosting", + "lightweight" + ], + "logo": "svgs/forgejo.svg", + "minversion": "0.0.0", + "port": "3000" + }, "formbricks": { "documentation": "https://formbricks.com/docs/self-hosting/configuration?utm_source=coolify.io", "slogan": "Open Source Survey Platform", @@ -594,6 +785,69 @@ "minversion": "0.0.0", "port": "3000" }, + "foundryvtt": { + "documentation": "https://foundryvtt.com/kb/?utm_source=coolify.io", + "slogan": "Foundry Virtual Tabletop is a self-hosted & modern roleplaying platform", + "compose": "c2VydmljZXM6CiAgZm91bmRyeXZ0dDoKICAgIGltYWdlOiAnZmVsZGR5L2ZvdW5kcnl2dHQ6cmVsZWFzZScKICAgIGV4cG9zZToKICAgICAgLSAzMDAwMAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0ZPVU5EUllfMzAwMDAKICAgICAgLSAnRk9VTkRSWV9VU0VSTkFNRT0ke0ZPVU5EUllfVVNFUk5BTUV9JwogICAgICAtICdGT1VORFJZX1BBU1NXT1JEPSR7Rk9VTkRSWV9QQVNTV09SRH0nCiAgICAgIC0gJ0ZPVU5EUllfUkVMRUFTRV9VUkw9JHtGT1VORFJZX1JFTEVBU0VfVVJMfScKICAgICAgLSAnRk9VTkRSWV9MSUNFTlNFX0tFWT0ke0ZPVU5EUllfTElDRU5TRV9LRVl9JwogICAgICAtICdGT1VORFJZX0FETUlOX0tFWT0ke0ZPVU5EUllfQURNSU46LWF0cm9wb3N9JwogICAgICAtICdGT1VORFJZX0hPU1ROQU1FPSR7Rk9VTkRSWV9IT1NUTkFNRX0nCiAgICAgIC0gJ0ZPVU5EUllfUk9VVEVfUFJFRklYPSR7Rk9VTkRSWV9ST1VURV9QUkVGSVh9JwogICAgICAtICdGT1VORFJZX1BST1hZX1BPUlQ9JHtGT1VORFJZX1BST1hZX1BPUlQ6LTgwfScKICAgICAgLSAnRk9VTkRSWV9QUk9YWV9TU0w9JHtGT1VORFJZX1BST1hZX1NTTDotdHJ1ZX0nCiAgICAgIC0gJ0ZPVU5EUllfQVdTX0NPTkZJRz0ke0ZPVU5EUllfQVdTX0NPTkZJR30nCiAgICAgIC0gJ0ZPVU5EUllfTEFOR1VBR0U9JHtGT1VORFJZX0xBTkdVQUdFOi1lbi5jb3JlfScKICAgICAgLSAnRk9VTkRSWV9DU1NfVEhFTUU9JHtGT1VORFJZX0NTU19USEVNRTotZm91bmRyeX0nCiAgICAgIC0gJ0ZPVU5EUllfTUlOSUZZX1NUQVRJQ19GSUxFUz0ke0ZPVU5EUllfTUlOSUZZX1NUQVRJQ19GSUxFUzotdHJ1ZX0nCiAgICAgIC0gJ0ZPVU5EUllfV09STEQ9JHtGT1VORFJZX1dPUkxEfScKICAgICAgLSAnRk9VTkRSWV9URUxFTUVUUlk9JHtGT1VORFJZX1RFTEVNRVRSWTotZmFsc2V9JwogICAgICAtICdUSU1FWk9ORT0ke1RJTUVaT05FOi1VVEN9JwogICAgICAtIENPTlRBSU5FUl9DQUNIRT0vZGF0YS9jb250YWluZXJfY2FjaGUKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZvdW5kcnl2dHQtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwMCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICByZXRyaWVzOiAzCg==", + "tags": [ + "foundryvtt", + "foundry", + "vtt", + "ttrpg", + "roleplaying" + ], + "logo": "svgs/foundryvtt.png", + "minversion": "0.0.0", + "port": "30000" + }, + "freshrss-with-mariadb": { + "documentation": "https://freshrss.org/index.html?utm_source=coolify.io", + "slogan": "A free, self-hostable feed aggregator.", + "compose": "c2VydmljZXM6CiAgZnJlc2hyc3M6CiAgICBpbWFnZTogJ2ZyZXNocnNzL2ZyZXNocnNzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUkVTSFJTU184MAogICAgICAtICdDUk9OX01JTj0ke0NST05fTUlOOi0xLDMxfScKICAgICAgLSAnTUFSSUFEQl9EQj0ke01BUklBREJfREFUQUJBU0U6LWZyZXNocnNzfScKICAgICAgLSAnTUFSSUFEQl9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNQVJJQURCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZyZXNocnNzLWRhdGE6L3Zhci93d3cvRnJlc2hSU1MvZGF0YScKICAgICAgLSAnZnJlc2hyc3MtZXh0ZW5zaW9uczovdmFyL3d3dy9GcmVzaFJTUy9leHRlbnNpb25zJwogICAgZGVwZW5kc19vbjoKICAgICAgZnJlc2hyc3MtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwJyB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGZyZXNocnNzLWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTVlTUUxfUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9ST09UCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TUFSSUFEQl9EQVRBQkFTRTotZnJlc2hyc3N9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "rss", + "feed" + ], + "logo": "svgs/freshrss.png", + "minversion": "0.0.0", + "port": "80" + }, + "freshrss-with-mysql": { + "documentation": "https://freshrss.org/index.html?utm_source=coolify.io", + "slogan": "A free, self-hostable feed aggregator.", + "compose": "c2VydmljZXM6CiAgZnJlc2hyc3M6CiAgICBpbWFnZTogJ2ZyZXNocnNzL2ZyZXNocnNzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUkVTSFJTU184MAogICAgICAtICdDUk9OX01JTj0ke0NST05fTUlOOi0xLDMxfScKICAgICAgLSAnTVlTUUxfREI9JHtNWVNRTF9EQVRBQkFTRTotZnJlc2hyc3N9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZyZXNocnNzLWRhdGE6L3Zhci93d3cvRnJlc2hSU1MvZGF0YScKICAgICAgLSAnZnJlc2hyc3MtZXh0ZW5zaW9uczovdmFyL3d3dy9GcmVzaFJTUy9leHRlbnNpb25zJwogICAgZGVwZW5kc19vbjoKICAgICAgZnJlc2hyc3MtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwJyB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGZyZXNocnNzLWRiOgogICAgaW1hZ2U6ICdteXNxbDo4JwogICAgdm9sdW1lczoKICAgICAgLSAnbXlzcWwtZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGVudmlyb25tZW50OgogICAgICAtIE1ZU1FMX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUk9PVAogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1mcmVzaHJzc30nCiAgICAgIC0gTVlTUUxfVVNFUj0kU0VSVklDRV9VU0VSX01ZU1FMCiAgICAgIC0gTVlTUUxfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTVlTUUwKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "rss", + "feed" + ], + "logo": "svgs/freshrss.png", + "minversion": "0.0.0", + "port": "80" + }, + "freshrss-with-postgresql": { + "documentation": "https://freshrss.org/index.html?utm_source=coolify.io", + "slogan": "A free, self-hostable feed aggregator.", + "compose": "c2VydmljZXM6CiAgZnJlc2hyc3M6CiAgICBpbWFnZTogJ2ZyZXNocnNzL2ZyZXNocnNzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUkVTSFJTU184MAogICAgICAtICdDUk9OX01JTj0ke0NST05fTUlOOi0xLDMxfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1mcmVzaHJzc30nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMfScKICAgICAgLSBQT1NUR1JFU19IT1NUPXBvc3RncmVzcWwKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZyZXNocnNzLWRhdGE6L3Zhci93d3cvRnJlc2hSU1MvZGF0YScKICAgICAgLSAnZnJlc2hyc3MtZXh0ZW5zaW9uczovdmFyL3d3dy9GcmVzaFJTUy9leHRlbnNpb25zJwogICAgZGVwZW5kc19vbjoKICAgICAgZnJlc2hyc3MtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwJyB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMwogIGZyZXNocnNzLWRiOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZyZXNocnNzLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWZyZXNocnNzfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "rss", + "feed" + ], + "logo": "svgs/freshrss.png", + "minversion": "0.0.0", + "port": "80" + }, + "freshrss": { + "documentation": "https://freshrss.org/index.html?utm_source=coolify.io", + "slogan": "A free, self-hostable feed aggregator.", + "compose": "c2VydmljZXM6CiAgZnJlc2hyc3M6CiAgICBpbWFnZTogJ2ZyZXNocnNzL2ZyZXNocnNzOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9GUkVTSFJTU184MAogICAgICAtICdDUk9OX01JTj0ke0NST05fTUlOOi0xLDMxfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ZyZXNocnNzLWRhdGE6L3Zhci93d3cvRnJlc2hSU1MvZGF0YScKICAgICAgLSAnZnJlc2hyc3MtZXh0ZW5zaW9uczovdmFyL3d3dy9GcmVzaFJTUy9leHRlbnNpb25zJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJiYXNoIC1jICc6PiAvZGV2L3RjcC8xMjcuMC4wLjEvODAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAzCg==", + "tags": [ + "rss", + "feed" + ], + "logo": "svgs/freshrss.png", + "minversion": "0.0.0", + "port": "80" + }, "getoutline": { "documentation": "https://docs.getoutline.com/s/hosting/doc/hosting-outline-nipGaCRBDu?utm_source=coolify.io", "slogan": "Your team\u2019s knowledge base", @@ -740,7 +994,7 @@ "glitchtip": { "documentation": "https://glitchtip.com?utm_source=coolify.io", "slogan": "GlitchTip is a self-hosted, open-source error tracking system.", - "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BnLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6IHJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB3ZWI6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIHJlZGlzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwODAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gcmVkaXMKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9HTElUQ0hUSVAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgbWlncmF0ZToKICAgIGltYWdlOiBnbGl0Y2h0aXAvZ2xpdGNodGlwCiAgICByZXN0YXJ0OiAnbm8nCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gcmVkaXMKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=", + "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTUUx9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNRTH0nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNRTF9EQVRBQkFTRTotZ2xpdGNodGlwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2dsaXRjaHRpcC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiByZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gcGluZwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd2ViOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fR0xJVENIVElQXzgwODAKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFU1FMOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTUUxAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWdsaXRjaHRpcH0nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9CQVNFNjRfNjRfRU5DUllQVElPTgogICAgICAtICdFTUFJTF9VUkw9JHtFTUFJTF9VUkw6LWNvbnNvbGVtYWlsOi8vfScKICAgICAgLSAnR0xJVENIVElQX0RPTUFJTj0ke1NFUlZJQ0VfRlFETl9HTElUQ0hUSVB9JwogICAgICAtICdERUZBVUxUX0ZST01fRU1BSUw9JHtERUZBVUxUX0ZST01fRU1BSUw6LXRlc3RAZXhhbXBsZS5jb219JwogICAgICAtICdDRUxFUllfV09SS0VSX0FVVE9TQ0FMRT0ke0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFOi0xLDN9JwogICAgICAtICdDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ9JHtDRUxFUllfV09SS0VSX01BWF9UQVNLU19QRVJfQ0hJTEQ6LTEwMDAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3VwbG9hZHM6L2NvZGUvdXBsb2FkcycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSBvawogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgd29ya2VyOgogICAgaW1hZ2U6IGdsaXRjaHRpcC9nbGl0Y2h0aXAKICAgIGNvbW1hbmQ6IC4vYmluL3J1bi1jZWxlcnktd2l0aC1iZWF0LnNoCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0dMSVRDSFRJUF9ET01BSU49JHtTRVJWSUNFX0ZRRE5fR0xJVENIVElQfScKICAgICAgLSAnREVGQVVMVF9GUk9NX0VNQUlMPSR7REVGQVVMVF9GUk9NX0VNQUlMOi10ZXN0QGV4YW1wbGUuY29tfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9BVVRPU0NBTEU9JHtDRUxFUllfV09SS0VSX0FVVE9TQ0FMRTotMSwzfScKICAgICAgLSAnQ0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEPSR7Q0VMRVJZX1dPUktFUl9NQVhfVEFTS1NfUEVSX0NISUxEOi0xMDAwMH0nCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9jb2RlL3VwbG9hZHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gb2sKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pZ3JhdGU6CiAgICBpbWFnZTogZ2xpdGNodGlwL2dsaXRjaHRpcAogICAgcmVzdGFydDogJ25vJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGNvbW1hbmQ6ICcuL21hbmFnZS5weSBtaWdyYXRlJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVNRTDokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU1FMQHBvc3RncmVzOjU0MzIvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1nbGl0Y2h0aXB9JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfQkFTRTY0XzY0X0VOQ1JZUFRJT04KICAgICAgLSAnRU1BSUxfVVJMPSR7RU1BSUxfVVJMOi1jb25zb2xlbWFpbDovL30nCiAgICAgIC0gJ0RFRkFVTFRfRlJPTV9FTUFJTD0ke0RFRkFVTFRfRlJPTV9FTUFJTDotdGVzdEBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfQVVUT1NDQUxFPSR7Q0VMRVJZX1dPUktFUl9BVVRPU0NBTEU6LTEsM30nCiAgICAgIC0gJ0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRD0ke0NFTEVSWV9XT1JLRVJfTUFYX1RBU0tTX1BFUl9DSElMRDotMTAwMDB9Jwo=", "tags": [ "error", "tracking", @@ -807,6 +1061,24 @@ "logo": "svgs/coolify.png", "minversion": "0.0.0" }, + "heyform": { + "documentation": "https://docs.heyform.net/open-source/self-hosting?utm_source=coolify.io", + "slogan": "Allows anyone to create engaging conversational forms for surveys, questionnaires, quizzes, and polls. No coding skills required.", + "compose": "c2VydmljZXM6CiAgaGV5Zm9ybToKICAgIGltYWdlOiAnaGV5Zm9ybS9jb21tdW5pdHktZWRpdGlvbjpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdoZXlmb3JtLWFzc2V0czovYXBwL3N0YXRpYy91cGxvYWQnCiAgICBkZXBlbmRzX29uOgogICAgICBtb25nbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBrZXlkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0hFWUZPUk1fODAwMAogICAgICAtICdBUFBfSE9NRVBBR0VfVVJMPSR7U0VSVklDRV9GUUROX0hFWUZPUk19JwogICAgICAtICdTRVNTSU9OX0tFWT0ke1NFUlZJQ0VfQkFTRTY0XzY0X1NFU1NJT059JwogICAgICAtICdGT1JNX0VOQ1JZUFRJT05fS0VZPSR7U0VSVklDRV9CQVNFNjRfNjRfRk9STX0nCiAgICAgIC0gJ01PTkdPX1VSST1tb25nb2RiOi8vbW9uZ286MjcwMTcvaGV5Zm9ybScKICAgICAgLSBSRURJU19IT1NUPWtleWRiCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3dnZXQgLXFPLSBodHRwOi8vMTI3LjAuMC4xOjgwMDAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMwogIG1vbmdvOgogICAgaW1hZ2U6ICdwZXJjb25hL3BlcmNvbmEtc2VydmVyLW1vbmdvZGI6bGF0ZXN0JwogICAgdm9sdW1lczoKICAgICAgLSAnaGV5Zm9ybS1tb25nby1kYXRhOi9kYXRhL2RiJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJlY2hvICdvaycgPiAvZGV2L251bGwgMj4mMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICAgIHN0YXJ0X3BlcmlvZDogNXMKICBrZXlkYjoKICAgIGltYWdlOiAnZXFhbHBoYS9rZXlkYjpsYXRlc3QnCiAgICBjb21tYW5kOiAna2V5ZGItc2VydmVyIC0tYXBwZW5kb25seSB5ZXMnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnS0VZREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0tFWURCfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2hleWZvcm0ta2V5ZGItZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSBrZXlkYi1jbGkKICAgICAgICAtICctLXBhc3MnCiAgICAgICAgLSAnJHtTRVJWSUNFX1BBU1NXT1JEX0tFWURCfScKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICAgIHN0YXJ0X3BlcmlvZDogNXMK", + "tags": [ + "form", + "builder", + "forms", + "survey", + "quiz", + "open source", + "self-hosted", + "docker" + ], + "logo": "svgs/heyform.svg", + "minversion": "0.0.0", + "port": "8000" + }, "homarr": { "documentation": "https://homarr.dev?utm_source=coolify.io", "slogan": "Homarr is a self-hosted homepage for your services.", @@ -832,6 +1104,26 @@ "minversion": "0.0.0", "port": "3000" }, + "immich": { + "documentation": "https://immich.app/docs/overview/introduction?utm_source=coolify.io", + "slogan": "Self-hosted photo and video management solution.", + "compose": "c2VydmljZXM6CiAgaW1taWNoOgogICAgaW1hZ2U6ICdnaGNyLmlvL2ltbWljaC1hcHAvaW1taWNoLXNlcnZlcjpyZWxlYXNlJwogICAgdm9sdW1lczoKICAgICAgLSAnaW1taWNoLXVwbG9hZHM6L3Vzci9zcmMvYXBwL3VwbG9hZCcKICAgICAgLSAnL2V0Yy9sb2NhbHRpbWU6L2V0Yy9sb2NhbHRpbWU6cm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fSU1NSUNIXzMwMDEKICAgICAgLSBVUExPQURfTE9DQVRJT049Li9saWJyYXJ5CiAgICAgIC0gREJfREFUQV9MT0NBVElPTj0uL3Bvc3RncmVzCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gJ0RCX0RBVEFCQVNFX05BTUU9JHtEQl9EQVRBQkFTRV9OQU1FOi1pbW1pY2h9JwogICAgICAtICdUWj0ke1RaOi1FdGMvVVRDfScKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIGRhdGFiYXNlOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgZGlzYWJsZTogZmFsc2UKICBpbW1pY2gtbWFjaGluZS1sZWFybmluZzoKICAgIGNvbnRhaW5lcl9uYW1lOiBpbW1pY2hfbWFjaGluZV9sZWFybmluZwogICAgaW1hZ2U6ICdnaGNyLmlvL2ltbWljaC1hcHAvaW1taWNoLW1hY2hpbmUtbGVhcm5pbmc6cmVsZWFzZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ltbWljaC1tb2RlbC1jYWNoZTovY2FjaGUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBVUExPQURfTE9DQVRJT049Li9saWJyYXJ5CiAgICAgIC0gREJfREFUQV9MT0NBVElPTj0uL3Bvc3RncmVzCiAgICAgIC0gREJfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBEQl9VU0VSTkFNRT0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gJ0RCX0RBVEFCQVNFX05BTUU9JHtEQl9EQVRBQkFTRV9OQU1FOi1pbW1pY2h9JwogICAgICAtICdUWj0ke1RaOi1FdGMvVVRDfScKICAgIGhlYWx0aGNoZWNrOgogICAgICBkaXNhYmxlOiBmYWxzZQogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjQtYWxwaW5lJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCiAgZGF0YWJhc2U6CiAgICBpbWFnZTogJ2RvY2tlci5pby90ZW5zb3JjaG9yZC9wZ3ZlY3RvLXJzOnBnMTQtdjAuMi4wQHNoYTI1Njo5MDcyNDE4NmYwYTM1MTdjZjY5MTQyOTViNWFiNDEwZGI5Y2UyMzE5MGEyZDlkMGI5ZGQ2NDYzZTNmYTI5OGYwJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAnJHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgUE9TVEdSRVNfVVNFUjogJyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgUE9TVEdSRVNfREI6ICcke0RCX0RBVEFCQVNFX05BTUU6LWltbWljaH0nCiAgICAgIFBPU1RHUkVTX0lOSVREQl9BUkdTOiAnLS1kYXRhLWNoZWNrc3VtcycKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2ltbWljaC1wb3N0Z3Jlcy1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "photo", + "video", + "management", + "server", + "cloud", + "storage", + "sharing", + "metadata", + "face", + "recognition" + ], + "logo": "svgs/immich.svg", + "minversion": "0.0.0", + "port": "2283" + }, "infisical": { "documentation": "https://infisical.com/docs/documentation/getting-started/introduction?utm_source=coolify.io", "slogan": "Infisical is the open source secret management platform that developers use to centralize their application configuration and secrets like API keys and database credentials.", @@ -896,6 +1188,33 @@ "minversion": "0.0.0", "port": "8096" }, + "jenkins": { + "documentation": "https://www.jenkins.io/doc/?utm_source=coolify.io", + "slogan": "Jenkins is an open source automation server, Jenkins provides hundreds of plugins to support building, deploying and automating any project.", + "compose": "c2VydmljZXM6CiAgamVua2luczoKICAgIGltYWdlOiAnamVua2lucy9qZW5raW5zOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9KRU5LSU5TXzgwODAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2plbmtpbnMtaG9tZTovdmFyL2plbmtpbnNfaG9tZScKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9sb2dpbicKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDQwcwo=", + "tags": [ + "jenkins", + "automation", + "open-source" + ], + "logo": "svgs/jenkins.svg", + "minversion": "0.0.0", + "port": "8080" + }, + "jitsi": { + "documentation": "https://jitsi.github.io/handbook/docs/intro?utm_source=coolify.io", + "slogan": "World's easiest way to add meetings to your apps", + "compose": "c2VydmljZXM6CiAgaml0c2ktd2ViOgogICAgaW1hZ2U6ICdqaXRzaS93ZWI6JHtKSVRTSV9JTUFHRV9WRVJTSU9OOi11bnN0YWJsZX0nCiAgICBjb250YWluZXJfbmFtZTogaml0c2ktd2ViCiAgICByZXN0YXJ0OiB1bmxlc3Mtc3RvcHBlZAogICAgcG9ydHM6CiAgICAgIC0gJzgwMDE6ODAnCiAgICAgIC0gJzg0NDM6NDQzJwogICAgdm9sdW1lczoKICAgICAgLSAnfi8uaml0c2ktbWVldC1jZmcvd2ViOi9jb25maWc6WicKICAgICAgLSAnfi8uaml0c2ktbWVldC1jZmcvd2ViL2Nyb250YWJzOi92YXIvc3Bvb2wvY3Jvbi9jcm9udGFiczpaJwogICAgICAtICd+Ly5qaXRzaS1tZWV0LWNmZy90cmFuc2NyaXB0czovdXNyL3NoYXJlL2ppdHNpLW1lZXQvdHJhbnNjcmlwdHM6WicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9KSVRTSQogICAgICAtIFBVQkxJQ19VUkw9JFNFUlZJQ0VfRlFETl9KSVRTSQogICAgICAtIEpJVFNJX0lNQUdFX1ZFUlNJT049dW5zdGFibGUKICAgICAgLSBKSUJSSV9SRUNPUkRFUl9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9KSVRTSQogICAgICAtIEpJQlJJX1hNUFBfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfSklUU0kKICAgICAgLSBKSUNPRk9fQVVUSF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9KSVRTSQogICAgICAtIEpJR0FTSV9YTVBQX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX0pJVFNJCiAgICAgIC0gSlZCX0FVVEhfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfSklUU0kKICAgICAgLSBUWj1VVEMKICAgIG5ldHdvcmtzOgogICAgICBtZWV0LmppdHNpOgogICAgICAgIGFsaWFzZXM6CiAgICAgICAgICAtIG1lZXQuaml0c2kKICAgIGRlcGVuZHNfb246CiAgICAgIC0ganZiCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3QnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwcm9zb2R5OgogICAgaW1hZ2U6ICdqaXRzaS9wcm9zb2R5OiR7SklUU0lfSU1BR0VfVkVSU0lPTjotdW5zdGFibGV9JwogICAgZXhwb3NlOgogICAgICAtICc1MjIyJwogICAgICAtICc1MzQ3JwogICAgICAtICc1MjgwJwogICAgY29udGFpbmVyX25hbWU6IGppdHNpLXhtcHAKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICB2b2x1bWVzOgogICAgICAtICd+Ly5qaXRzaS1tZWV0LWNmZy9wcm9zb2R5L2NvbmZpZzovY29uZmlnOlonCiAgICAgIC0gJ34vLmppdHNpLW1lZXQtY2ZnL3Byb3NvZHkvcHJvc29keS1wbHVnaW5zLWN1c3RvbTovcHJvc29keS1wbHVnaW5zLWN1c3RvbTpaJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSklDT0ZPX0FVVEhfUEFTU1dPUkQKICAgICAgLSBKVkJfQVVUSF9QQVNTV09SRAogICAgICAtIFBVQkxJQ19VUkw9JFNFUlZJQ0VfRlFETl9KSVRTSQogICAgICAtIFRaCiAgICBuZXR3b3JrczoKICAgICAgbWVldC5qaXRzaToKICAgICAgICBhbGlhc2VzOgogICAgICAgICAgLSB4bXBwLm1lZXQuaml0c2kKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo1MjgwL2h0dHAtYmluZCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGppY29mbzoKICAgIGltYWdlOiAnaml0c2kvamljb2ZvOiR7SklUU0lfSU1BR0VfVkVSU0lPTjotdW5zdGFibGV9JwogICAgY29udGFpbmVyX25hbWU6IGppdHNpLWppY29mbwogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ34vLmppdHNpLW1lZXQtY2ZnL2ppY29mbzovY29uZmlnOlonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBYTVBQX1NFUlZFUj1wcm9zb2R5CiAgICAgIC0gSklDT0ZPX0FVVEhfUEFTU1dPUkQKICAgICAgLSBUWgogICAgICAtIEpJQ09GT19FTkFCTEVfSEVBTFRIX0NIRUNLUz0xCiAgICBkZXBlbmRzX29uOgogICAgICAtIHByb3NvZHkKICAgIG5ldHdvcmtzOgogICAgICBtZWV0LmppdHNpOiBudWxsCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODg4OC9hYm91dC9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBqdmI6CiAgICBpbWFnZTogJ2ppdHNpL2p2Yjoke0pJVFNJX0lNQUdFX1ZFUlNJT046LXVuc3RhYmxlfScKICAgIGNvbnRhaW5lcl9uYW1lOiBqaXRzaS1qdmIKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBleHBvc2U6CiAgICAgIC0gJzEwMDAwOjEwMDAwL3VkcCcKICAgICAgLSAnODA4MDo4MDgwJwogICAgICAtICcxMDAwMCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ34vLmppdHNpLW1lZXQtY2ZnL2p2YjovY29uZmlnOlonCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBKVkJfQURWRVJUSVNFX0lQUwogICAgICAtIEpWQl9BVVRIX1BBU1NXT1JECiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX0pJVFNJCiAgICAgIC0gVFoKICAgICAgLSBYTVBQX1NFUlZFUj1wcm9zb2R5CiAgICBkZXBlbmRzX29uOgogICAgICAtIHByb3NvZHkKICAgIG5ldHdvcmtzOgogICAgICBtZWV0LmppdHNpOiBudWxsCiAgICBsYWJlbHM6CiAgICAgIC0gdHJhZWZpay5lbmFibGU9dHJ1ZQogICAgICAtIHRyYWVmaWsudWRwLnJvdXRlcnMubXktdWRwLXJvdXRlci5lbnRyeXBvaW50cz12aWRlbwogICAgICAtIHRyYWVmaWsudWRwLnJvdXRlcnMubXktdWRwLXJvdXRlci5zZXJ2aWNlPW15LXVkcC1zZXJ2aWNlCiAgICAgIC0gdHJhZWZpay51ZHAuc2VydmljZXMubXktdWRwLXNlcnZpY2UubG9hZGJhbGFuY2VyLnNlcnZlci5wb3J0PTEwMDAwCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9hYm91dC9oZWFsdGgnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKbmV0d29ya3M6CiAgbWVldC5qaXRzaTogbnVsbAp2b2x1bWVzOgogIGppdHNpLXdlYjogbnVsbAogIGppdHNpLXhtcHA6IG51bGwKICBqaXRzaS1qaWNvZm86IG51bGwKICBqaXRzaS1qdmI6IG51bGwK", + "tags": [ + "video", + "conferencing", + "meetings", + "communication", + "open-source" + ], + "logo": "svgs/jitsi.svg", + "minversion": "0.0.0" + }, "joplin": { "documentation": "https://github.com/laurent22/joplin/blob/dev/packages/server/README.md?utm_source=coolify.io", "slogan": "Self-hosted sync server for Joplin", @@ -910,7 +1229,7 @@ "keycloak-with-postgres": { "documentation": "https://www.keycloak.org?utm_source=coolify.io", "slogan": "Keycloak is an open-source Identity and Access Management tool.", - "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjUuMC4yJwogICAgY29tbWFuZDoKICAgICAgLSBzdGFydAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0tFWUNMT0FLXzgwODAKICAgICAgLSAnVFo9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSAnS0VZQ0xPQUtfQURNSU49JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdLRVlDTE9BS19BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtIEtDX0RCPXBvc3RncmVzCiAgICAgIC0gJ0tDX0RCX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfScKICAgICAgLSAnS0NfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0RBVEFCQVNFfScKICAgICAgLSBLQ19EQl9VUkxfUE9SVD01NDMyCiAgICAgIC0gJ0tDX0RCX1VSTD1qZGJjOnBvc3RncmVzcWw6Ly9wb3N0Z3Jlcy8ke1BPU1RHUkVTUUxfREFUQUJBU0U6LWtleWNsb2FrfScKICAgICAgLSAnS0NfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fS0VZQ0xPQUt9JwogICAgICAtICdLQ19IVFRQX0VOQUJMRUQ9JHtLQ19IVFRQX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdLQ19IRUFMVEhfRU5BQkxFRD0ke0tDX0hFQUxUSF9FTkFCTEVEOi10cnVlfScKICAgICAgLSAnS0NfUFJPWFlfSEVBREVSUz0ke0tDX1BST1hZX0hFQURFUlM6LXhmb3J3YXJkZWR9JwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstZGF0YTovb3B0L2tleWNsb2FrL2RhdGEnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICJleGVjIDM8Pi9kZXYvdGNwLzEyNy4wLjAuMS85MDAwO2VjaG8gLWUgJ0dFVCAvaGVhbHRoL3JlYWR5IEhUVFAvMS4xXHJcbmhvc3Q6IGh0dHA6Ly9sb2NhbGhvc3RcclxuQ29ubmVjdGlvbjogY2xvc2VcclxuXHJcbicgPiYzO2lmIFsgJD8gLWVxIDAgXTsgdGhlbiBlY2hvICdIZWFsdGhjaGVjayBTdWNjZXNzZnVsJztleGl0IDA7ZWxzZSBlY2hvICdIZWFsdGhjaGVjayBGYWlsZWQnO2V4aXQgMTtmaTsiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0RBVEFCQVNFfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1rZXljbG9ha30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjYuMCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LRVlDTE9BS184MDgwCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0tFWUNMT0FLX0FETUlOPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnS0VZQ0xPQUtfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSBLQ19EQj1wb3N0Z3JlcwogICAgICAtICdLQ19EQl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9EQVRBQkFTRX0nCiAgICAgIC0gJ0tDX0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF82NF9EQVRBQkFTRX0nCiAgICAgIC0gS0NfREJfVVJMX1BPUlQ9NTQzMgogICAgICAtICdLQ19EQl9VUkw9amRiYzpwb3N0Z3Jlc3FsOi8vcG9zdGdyZXMvJHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1rZXljbG9ha30nCiAgICAgIC0gJ0tDX0hPU1ROQU1FPSR7U0VSVklDRV9GUUROX0tFWUNMT0FLfScKICAgICAgLSAnS0NfSFRUUF9FTkFCTEVEPSR7S0NfSFRUUF9FTkFCTEVEOi10cnVlfScKICAgICAgLSAnS0NfSEVBTFRIX0VOQUJMRUQ9JHtLQ19IRUFMVEhfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0tDX1BST1hZX0hFQURFUlM9JHtLQ19QUk9YWV9IRUFERVJTOi14Zm9yd2FyZGVkfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2tleWNsb2FrLWRhdGE6L29wdC9rZXljbG9hay9kYXRhJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiZXhlYyAzPD4vZGV2L3RjcC8xMjcuMC4wLjEvOTAwMDsgZWNobyAtZSAnR0VUIC9oZWFsdGgvcmVhZHkgSFRUUC8xLjFcclxuSG9zdDogbG9jYWxob3N0OjkwMDBcclxuQ29ubmVjdGlvbjogY2xvc2VcclxuXHJcbicgPiYzO2NhdCA8JjMgfCBncmVwIC1xICdcInN0YXR1c1wiOiBcIlVQXCInICYmIGV4aXQgMCB8fCBleGl0IDEiCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwb3N0Z3JlczoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX0RBVEFCQVNFfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEXzY0X0RBVEFCQVNFfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU1FMX0RBVEFCQVNFOi1rZXljbG9ha30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "keycloak", "identity", @@ -937,7 +1256,7 @@ "keycloak": { "documentation": "https://www.keycloak.org?utm_source=coolify.io", "slogan": "Keycloak is an open-source Identity and Access Management tool.", - "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjUuMC4yJwogICAgY29tbWFuZDoKICAgICAgLSBzdGFydAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX0tFWUNMT0FLXzgwODAKICAgICAgLSAnVFo9JHtUSU1FWk9ORTotVVRDfScKICAgICAgLSAnS0VZQ0xPQUtfQURNSU49JHtTRVJWSUNFX1VTRVJfQURNSU59JwogICAgICAtICdLRVlDTE9BS19BRE1JTl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgICAtICdLQ19IT1NUTkFNRT0ke1NFUlZJQ0VfRlFETl9LRVlDTE9BS30nCiAgICAgIC0gJ0tDX0hUVFBfRU5BQkxFRD0ke0tDX0hUVFBfRU5BQkxFRDotdHJ1ZX0nCiAgICAgIC0gJ0tDX0hFQUxUSF9FTkFCTEVEPSR7S0NfSEVBTFRIX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdLQ19QUk9YWV9IRUFERVJTPSR7S0NfUFJPWFlfSEVBREVSUzoteGZvcndhcmRlZH0nCiAgICB2b2x1bWVzOgogICAgICAtICdrZXljbG9hay1kYXRhOi9vcHQva2V5Y2xvYWsvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiZXhlYyAzPD4vZGV2L3RjcC8xMjcuMC4wLjEvOTAwMDtlY2hvIC1lICdHRVQgL2hlYWx0aC9yZWFkeSBIVFRQLzEuMVxyXG5ob3N0OiBodHRwOi8vbG9jYWxob3N0XHJcbkNvbm5lY3Rpb246IGNsb3NlXHJcblxyXG4nID4mMztpZiBbICQ/IC1lcSAwIF07IHRoZW4gZWNobyAnSGVhbHRoY2hlY2sgU3VjY2Vzc2Z1bCc7ZXhpdCAwO2Vsc2UgZWNobyAnSGVhbHRoY2hlY2sgRmFpbGVkJztleGl0IDE7Zmk7IgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAga2V5Y2xvYWs6CiAgICBpbWFnZTogJ3F1YXkuaW8va2V5Y2xvYWsva2V5Y2xvYWs6MjYuMCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9LRVlDTE9BS184MDgwCiAgICAgIC0gJ1RaPSR7VElNRVpPTkU6LVVUQ30nCiAgICAgIC0gJ0tFWUNMT0FLX0FETUlOPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnS0VZQ0xPQUtfQURNSU5fUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgLSAnS0NfSE9TVE5BTUU9JHtTRVJWSUNFX0ZRRE5fS0VZQ0xPQUt9JwogICAgICAtICdLQ19IVFRQX0VOQUJMRUQ9JHtLQ19IVFRQX0VOQUJMRUQ6LXRydWV9JwogICAgICAtICdLQ19IRUFMVEhfRU5BQkxFRD0ke0tDX0hFQUxUSF9FTkFCTEVEOi10cnVlfScKICAgICAgLSAnS0NfUFJPWFlfSEVBREVSUz0ke0tDX1BST1hZX0hFQURFUlM6LXhmb3J3YXJkZWR9JwogICAgdm9sdW1lczoKICAgICAgLSAna2V5Y2xvYWstZGF0YTovb3B0L2tleWNsb2FrL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gImV4ZWMgMzw+L2Rldi90Y3AvMTI3LjAuMC4xLzkwMDA7IGVjaG8gLWUgJ0dFVCAvaGVhbHRoL3JlYWR5IEhUVFAvMS4xXHJcbkhvc3Q6IGxvY2FsaG9zdDo5MDAwXHJcbkNvbm5lY3Rpb246IGNsb3NlXHJcblxyXG4nID4mMztjYXQgPCYzIHwgZ3JlcCAtcSAnXCJzdGF0dXNcIjogXCJVUFwiJyAmJiBleGl0IDAgfHwgZXhpdCAxIgogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "keycloak", "identity", @@ -961,6 +1280,18 @@ "minversion": "0.0.0", "port": "8080" }, + "kimai": { + "documentation": "https://www.kimai.org/?utm_source=coolify.io", + "slogan": "Open source time-tracking app.", + "compose": "c2VydmljZXM6CiAgbXlzcWw6CiAgICBpbWFnZTogJ215c3FsOjgnCiAgICB2b2x1bWVzOgogICAgICAtICdraW1haS1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LWtpbWFpfScKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NWVNRTH0nCiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgY29tbWFuZDogJy0tZGVmYXVsdC1zdG9yYWdlLWVuZ2luZSBpbm5vZGInCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbXlzcWxhZG1pbgogICAgICAgIC0gcGluZwogICAgICAgIC0gJy1oJwogICAgICAgIC0gMTI3LjAuMC4xCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBraW1haToKICAgIGltYWdlOiAna2ltYWkva2ltYWkyOmFwYWNoZS1sYXRlc3QnCiAgICBjb250YWluZXJfbmFtZToga2ltYWkKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICdraW1haS1kYXRhOi9vcHQva2ltYWkvdmFyL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fS0lNQUlfODAwMQogICAgICAtICdBUFBfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9BUFBTRUNSRVR9JwogICAgICAtICdNQUlMRVJfRlJPTT0ke01BSUxFUl9GUk9NOi1raW1haUBleGFtcGxlLmNvbX0nCiAgICAgIC0gJ01BSUxFUl9VUkw9JHtNQUlMRVJfVVJMOi1udWxsOi8vbnVsbH0nCiAgICAgIC0gJ0FETUlOTUFJTD0ke0FETUlOTUFJTDotYWRtaW5Aa2ltYWkubG9jYWx9JwogICAgICAtICdBRE1JTlBBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOUEFTU30nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1teXNxbDovLyR7U0VSVklDRV9VU0VSX01ZU1FMfToke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9QG15c3FsLyR7TVlTUUxfREFUQUJBU0V9P2NoYXJzZXQ9dXRmOG1iNCZzZXJ2ZXJWZXJzaW9uPTguMy4wJwogICAgICAtIFRSVVNURURfSE9TVFM9bG9jYWxob3N0CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODAwMScKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", + "tags": [ + "time-tracking", + "open-source" + ], + "logo": "svgs/kimai.svg", + "minversion": "0.0.0", + "port": "8001" + }, "kuzzle": { "documentation": "https://kuzzle.io?utm_source=coolify.io", "slogan": "Kuzzle is a generic backend offering the basic building blocks common to every application.", @@ -1043,6 +1374,18 @@ "minversion": "0.0.0", "port": "3000" }, + "libretranslate": { + "documentation": "https://libretranslate.com/docs/?utm_source=coolify.io", + "slogan": "Free and open-source machine translation API, entirely self-hosted.", + "compose": "c2VydmljZXM6CiAgbGlicmV0cmFuc2xhdGU6CiAgICBpbWFnZTogJ2xpYnJldHJhbnNsYXRlL2xpYnJldHJhbnNsYXRlOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9MSUJSRVRSQU5TTEFURV81MDAwCiAgICAgIC0gJ0xUX1NTTD0ke0xUX1NTTDotdHJ1ZX0nCiAgICAgIC0gJ0xUX1VQREFURV9NT0RFTFM9JHtMVF9VUERBVEVfTU9ERUxTOi10cnVlfScKICAgICAgLSAnTFRfTE9BRF9PTkxZPSR7TFRfTE9BRF9PTkxZOi1lbixlcyxmcixkZSxqYX0nCiAgICB2b2x1bWVzOgogICAgICAtICdsaWJyZXRyYW5zbGF0ZS1hcGkta2V5czovYXBwL2RiJwogICAgICAtICdsaWJyZXRyYW5zbGF0ZS1tb2RlbHM6L2hvbWUvbGlicmV0cmFuc2xhdGUvLmxvY2FsJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICcuL3ZlbnYvYmluL3B5dGhvbiBzY3JpcHRzL2hlYWx0aGNoZWNrLnB5Jwo=", + "tags": [ + "translate", + "api" + ], + "logo": "svgs/libretranslate.svg", + "minversion": "0.0.0", + "port": "5000" + }, "listmonk": { "documentation": "https://listmonk.app/?utm_source=coolify.io", "slogan": "Self-hosted newsletter and mailing list manager", @@ -1082,6 +1425,21 @@ "minversion": "0.0.0", "port": "4000" }, + "litequeen": { + "documentation": "https://litequeen.com/?utm_source=coolify.io", + "slogan": "Lite Queen is an open-source SQLite database management software that runs on your server.", + "compose": "c2VydmljZXM6CiAgbGl0ZXF1ZWVuOgogICAgaW1hZ2U6ICdraXZzZWdyb2IvbGl0ZS1xdWVlbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTElURVFVRUVOXzgwMDAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xpdGVxdWVlbi1kYXRhOi9ob21lL2xpdGVxdWVlbi9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi9kYXRhYmFzZXMKICAgICAgICB0YXJnZXQ6IC9zcnYKICAgICAgICBpc19kaXJlY3Rvcnk6IHRydWUKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAiYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzgwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMK", + "tags": [ + "sqlite", + "sqlite-database-management", + "self-hosted", + "vps", + "database" + ], + "logo": "svgs/litequeen.svg", + "minversion": "0.0.0", + "port": "8000" + }, "logto": { "documentation": "https://docs.logto.io/docs/tutorials/get-started/#logto-oss-self-hosted?utm_source=coolify.io", "slogan": "A comprehensive identity solution covering both the front and backend, complete with pre-built infrastructure and enterprise-grade solutions.", @@ -1112,6 +1470,19 @@ "minversion": "0.0.0", "port": "8025" }, + "martin": { + "documentation": "https://maplibre.org/martin/introduction.html/?utm_source=coolify.io", + "slogan": "Martin is a tile server able to generate and serve vector tiles on the fly from large PostGIS databases, PMTiles (local or remote), and MBTiles files, allowing multiple tile sources to be dynamically combined into one.", + "compose": "c2VydmljZXM6CiAgbWFydGluOgogICAgaW1hZ2U6ICdnaGNyLmlvL21hcGxpYnJlL21hcnRpbjp2MC4xMy4wJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01BUlRJTl8zMDAwCiAgICAgIC0gJ0hPU1Q9JHtTRVJWSUNFX0ZRRE5fTUFSVElOfScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlc3FsOjU0MzIvJHtQT1NUR1JFU19EQjotbWFydGluLWRifScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdpcy9wb3N0Z2lzOjE2LTMuNC1hbHBpbmUnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIHZvbHVtZXM6CiAgICAgIC0gJ21hcnRpbi1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbWFydGluLWRifScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "postgis", + "vector", + "tiles" + ], + "logo": "svgs/martin.png", + "minversion": "0.0.0", + "port": "3000" + }, "mattermost": { "documentation": "https://docs.mattermost.com?utm_source=coolify.io", "slogan": "Mattermost is an open source, self-hosted Slack-alternative.", @@ -1148,7 +1519,7 @@ "mediawiki": { "documentation": "https://www.mediawiki.org?utm_source=coolify.io", "slogan": "MediaWiki is a collaboration and documentation platform brought to you by a vibrant community.", - "compose": "c2VydmljZXM6CiAgbWVkaWF3aWtpOgogICAgaW1hZ2U6ICdtZWRpYXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01FRElBV0lLSV84MAogICAgdm9sdW1lczoKICAgICAgLSAnbWVkaWF3aWtpLWltYWdlczovdmFyL3d3dy9odG1sL2ltYWdlcycKICAgICAgLSAnbWVkaWF3aWtpLXNxbGl0ZTovdmFyL3d3dy9odG1sL2RhdGEnCiAgICAgIC0gJy4vTG9jYWxTZXR0aW5ncy5waHA6L3Zhci93d3cvaHRtbC9Mb2NhbFNldHRpbmdzLnBocCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MCcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "c2VydmljZXM6CiAgbWVkaWF3aWtpOgogICAgaW1hZ2U6ICdtZWRpYXdpa2k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX01FRElBV0lLSV84MAogICAgdm9sdW1lczoKICAgICAgLSAnbWVkaWF3aWtpLWltYWdlczovdmFyL3d3dy9odG1sL2ltYWdlcycKICAgICAgLSAnbWVkaWF3aWtpLXNxbGl0ZTovdmFyL3d3dy9odG1sL2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly9sb2NhbGhvc3Q6ODAnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", "tags": [ "wiki", "collaboration", @@ -1202,6 +1573,20 @@ "minversion": "0.0.0", "port": "8081" }, + "mindsdb": { + "documentation": "https://docs.mindsdb.com/what-is-mindsdb?utm_source=coolify.io", + "slogan": "MindsDB is the platform for building AI from enterprise data, enabling smarter organizations.", + "compose": "c2VydmljZXM6CiAgbWluZHNkYjoKICAgIGltYWdlOiAnbWluZHNkYi9taW5kc2RiOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9NSU5EU0RCXzQ3MzM0CiAgICAgIC0gU0VSVklDRV9GUUROX0FQSV80NzMzNT0vYXBpCiAgICAgIC0gTUlORFNEQl9ET0NLRVJfRU5WPXRydWUKICAgICAgLSBNSU5EU0RCX1NUT1JBR0VfRElSPS9taW5kc2RiL3ZhcgogICAgICAtICdGTEFTS19ERUJVRz0ke0ZMQVNLX0RFQlVHOi0xfScKICAgICAgLSAnT1BFTkFJX0FQSV9LRVk9JHtPUEVOQUlfQVBJX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX0hPU1Q9JHtMQU5HRlVTRV9IT1NUfScKICAgICAgLSAnTEFOR0ZVU0VfUFVCTElDX0tFWT0ke0xBTkdGVVNFX1BVQkxJQ19LRVl9JwogICAgICAtICdMQU5HRlVTRV9TRUNSRVRfS0VZPSR7TEFOR0ZVU0VfU0VDUkVUX0tFWX0nCiAgICAgIC0gJ0xBTkdGVVNFX1JFTEVBU0U9JHtMQU5HRlVTRV9SRUxFQVNFOi1sb2NhbH0nCiAgICAgIC0gJ0xBTkdGVVNFX0RFQlVHPSR7TEFOR0ZVU0VfREVCVUc6LUZhbHNlfScKICAgICAgLSAnTEFOR0ZVU0VfVElNRU9VVD0ke0xBTkdGVVNFX1RJTUVPVVQ6LTEwfScKICAgICAgLSAnTEFOR0ZVU0VfU0FNUExFX1JBVEU9JHtMQU5HRlVTRV9TQU1QTEVfUkFURTotMS4wfScKICAgICAgLSAnTUlORFNEQl9EQl9DT049cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzcWwvJHtQT1NUR1JFU19EQjotbWluZHNkYi1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5kc2RiLWRhdGE6L21pbmRzZGIvdmFyJwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjQ3MzM0L2FwaS91dGlsL3BpbmcnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdtaW5kc2RiLXBvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW1pbmRzZGItZGJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxNQo=", + "tags": [ + "mysql", + "postgresdb", + "machine-learning", + "ai" + ], + "logo": "svgs/mindsdb.svg", + "minversion": "0.0.0", + "port": "47334" + }, "minecraft": { "documentation": "https://github.com/itzg/docker-minecraft-server?utm_source=coolify.io", "slogan": "Minecraft Server that will automatically download selected version at startup.", @@ -1262,6 +1647,19 @@ "minversion": "0.0.0", "port": "8080" }, + "mosquitto": { + "documentation": "https://mosquitto.org/documentation/?utm_source=coolify.io", + "slogan": "Mosquitto is lightweight and suitable for use on all devices, from low-power single-board computers to full servers.", + "compose": "c2VydmljZXM6CiAgbW9zcXVpdHRvOgogICAgaW1hZ2U6IGVjbGlwc2UtbW9zcXVpdHRvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTU9TUVVJVFRPXzE4ODMKICAgICAgLSAnTVFUVF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9NT1NRVUlUVE99JwogICAgICAtICdNUVRUX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NT1NRVUlUVE99JwogICAgICAtICdSRVFVSVJFX0NFUlRJRklDQVRFPSR7UkVRVUlSRV9DRVJUSUZJQ0FURTotZmFsc2V9JwogICAgICAtICdBTExPV19BTk9OWU1PVVM9JHtBTExPV19BTk9OWU1PVVM6LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnbW9zcXVpdHRvLWNvbmZpZzovbW9zcXVpdHRvL2NvbmZpZycKICAgICAgLSAnbW9zcXVpdHRvLWNlcnRzOi9jZXJ0cycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICBlbnRyeXBvaW50OiAic2ggLWMgXCIgaWYgWyAnJFJFUVVJUkVfQ0VSVElGSUNBVEUnID0gJ3RydWUnIF07IHRoZW4gZWNobyAnbGlzdGVuZXIgODg4MycgPiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiBlY2hvICdjYWZpbGUgL2NlcnRzL2NhLmNydCcgPj4gL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmYgJiYgZWNobyAnY2VydGZpbGUgL2NlcnRzL3NlcnZlci5jcnQnID4+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mICYmIGVjaG8gJ2tleWZpbGUgIC9jZXJ0cy9zZXJ2ZXIua2V5JyA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZjsgZWxzZSBlY2hvICdsaXN0ZW5lciAxODgzJyA+IC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mOyBmaSAmJiBlY2hvICdyZXF1aXJlX2NlcnRpZmljYXRlICckUkVRVUlSRV9DRVJUSUZJQ0FURSA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiBlY2hvICdhbGxvd19hbm9ueW1vdXMgJyRBTExPV19BTk9OWU1PVVMgPj4gL21vc3F1aXR0by9jb25maWcvbW9zcXVpdHRvLmNvbmY7IGlmIFsgLW4gJyRTRVJWSUNFX1VTRVJfTU9TUVVJVFRPJ10gJiYgWyAtbiAnJFNFUlZJQ0VfUEFTU1dPUkRfTU9TUVVJVFRPJyBdOyB0aGVuIGVjaG8gJ3Bhc3N3b3JkX2ZpbGUgL21vc3F1aXR0by9jb25maWcvcGFzc3dvcmRzJyA+PiAvbW9zcXVpdHRvL2NvbmZpZy9tb3NxdWl0dG8uY29uZiAmJiB0b3VjaCAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHMgJiYgY2htb2QgMDcwMCAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHMgJiYgY2hvd24gcm9vdDpyb290IC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkcyAmJiBtb3NxdWl0dG9fcGFzc3dkIC1iIC1jIC9tb3NxdWl0dG8vY29uZmlnL3Bhc3N3b3JkcyAkU0VSVklDRV9VU0VSX01PU1FVSVRUTyAkU0VSVklDRV9QQVNTV09SRF9NT1NRVUlUVE8gJiYgY2hvd24gbW9zcXVpdHRvOm1vc3F1aXR0byAvbW9zcXVpdHRvL2NvbmZpZy9wYXNzd29yZHM7IGZpICYmIGV4ZWMgbW9zcXVpdHRvIC1jIC9tb3NxdWl0dG8vY29uZmlnL21vc3F1aXR0by5jb25mIFwiIgogICAgbGFiZWxzOgogICAgICAtIHRyYWVmaWsudGNwLnJvdXRlcnMubXF0dC5lbnRyeXBvaW50cz1tcXR0CiAgICAgIC0gdHJhZWZpay50Y3Aucm91dGVycy5tcXR0cy5lbnRyeXBvaW50cz1tcXR0cwo=", + "tags": [ + "mosquitto", + "mqtt", + "open-source" + ], + "logo": "svgs/mosquitto.svg", + "minversion": "0.0.0", + "port": "1883" + }, "n8n-with-postgresql": { "documentation": "https://n8n.io?utm_source=coolify.io", "slogan": "n8n is an extendable workflow automation tool.", @@ -1310,10 +1708,55 @@ "minversion": "0.0.0", "port": "3000" }, + "nextcloud-with-mariadb": { + "documentation": "https://docs.nextcloud.com?utm_source=coolify.io", + "slogan": "NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.", + "compose": "c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VEXzgwCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gJ1RaPSR7VFo6LUV1cm9wZS9QYXJpc30nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TUFSSUFEQl9EQVRBQkFTRTotbmV4dGNsb3VkfScKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtIE1ZU1FMX0hPU1Q9bmV4dGNsb3VkLWRiCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIG5leHRjbG91ZC1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbmV4dGNsb3VkLWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLW1hcmlhZGItZGF0YTovdmFyL2xpYi9teXNxbCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9ST09UfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtNQVJJQURCX0RBVEFCQVNFOi1uZXh0Y2xvdWR9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUFSSUFEQn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny40LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ25leHRjbG91ZC1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", + "tags": [ + "cloud", + "collaboration", + "communication", + "filestorage", + "data" + ], + "logo": "svgs/nextcloud.svg", + "minversion": "0.0.0", + "port": "80" + }, + "nextcloud-with-mysql": { + "documentation": "https://docs.nextcloud.com?utm_source=coolify.io", + "slogan": "NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.", + "compose": "c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VEXzgwCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gJ1RaPSR7VFo6LUV1cm9wZS9QYXJpc30nCiAgICAgIC0gJ01ZU1FMX0RBVEFCQVNFPSR7TVlTUUxfREFUQUJBU0U6LW5leHRjbG91ZH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTVlTUUx9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTVlTUUx9JwogICAgICAtIE1ZU1FMX0hPU1Q9bmV4dGNsb3VkLWRiCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIG5leHRjbG91ZC1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbmV4dGNsb3VkLWRiOgogICAgaW1hZ2U6ICdteXNxbDo4LjQuMicKICAgIHZvbHVtZXM6CiAgICAgIC0gJ25leHRjbG91ZC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi1uZXh0Y2xvdWR9JwogICAgICAtICdNWVNRTF9VU0VSPSR7U0VSVklDRV9VU0VSX01ZU1FMfScKICAgICAgLSAnTVlTUUxfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01ZU1FMfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBteXNxbGFkbWluCiAgICAgICAgLSBwaW5nCiAgICAgICAgLSAnLWgnCiAgICAgICAgLSAxMjcuMC4wLjEKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjQtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLXJlZGlzLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAK", + "tags": [ + "cloud", + "collaboration", + "communication", + "filestorage", + "data" + ], + "logo": "svgs/nextcloud.svg", + "minversion": "0.0.0", + "port": "80" + }, + "nextcloud-with-postgres": { + "documentation": "https://docs.nextcloud.com?utm_source=coolify.io", + "slogan": "NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.", + "compose": "c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VEXzgwCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gJ1RaPSR7VFo6LUV1cm9wZS9QYXJpc30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LW5leHRjbG91ZH0nCiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIFBPU1RHUkVTX0hPU1Q9bmV4dGNsb3VkLWRiCiAgICAgIC0gUkVESVNfSE9TVD1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGRlcGVuZHNfb246CiAgICAgIG5leHRjbG91ZC1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgbmV4dGNsb3VkLWRiOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICduZXh0Y2xvdWQtcG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotbmV4dGNsb3VkfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6Ny40LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ25leHRjbG91ZC1yZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", + "tags": [ + "cloud", + "collaboration", + "communication", + "filestorage", + "data" + ], + "logo": "svgs/nextcloud.svg", + "minversion": "0.0.0", + "port": "80" + }, "nextcloud": { "documentation": "https://docs.nextcloud.com?utm_source=coolify.io", "slogan": "NextCloud is a self-hosted, open-source platform that provides file storage, collaboration, and communication tools for seamless data management.", - "compose": "c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VECiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gVFo9RXVyb3BlL01hZHJpZAogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", + "compose": "c2VydmljZXM6CiAgbmV4dGNsb3VkOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL25leHRjbG91ZDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTkVYVENMT1VEXzgwCiAgICAgIC0gUFVJRD0xMDAwCiAgICAgIC0gUEdJRD0xMDAwCiAgICAgIC0gJ1RaPSR7VFo6LUV1cm9wZS9NYWRyaWR9JwogICAgdm9sdW1lczoKICAgICAgLSAnbmV4dGNsb3VkLWNvbmZpZzovY29uZmlnJwogICAgICAtICduZXh0Y2xvdWQtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo4MCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQo=", "tags": [ "cloud", "collaboration", @@ -1322,7 +1765,8 @@ "data" ], "logo": "svgs/nextcloud.svg", - "minversion": "0.0.0" + "minversion": "0.0.0", + "port": "80" }, "nitropage-with-postgresql": { "documentation": "https://nitropage.com?utm_source=coolify.io", @@ -1375,6 +1819,21 @@ "minversion": "0.0.0", "port": "8080" }, + "ntfy": { + "documentation": "https://docs.ntfy.sh/?utm_source=coolify.io", + "slogan": "ntfy is a simple HTTP-based pub-sub notification service. It allows you to send notifications to your phone or desktop via scripts from any computer, and/or using a REST API.", + "compose": "c2VydmljZXM6CiAgbnRmeToKICAgIGltYWdlOiBiaW53aWVkZXJoaWVyL250ZnkKICAgIGNvbW1hbmQ6CiAgICAgIC0gc2VydmUKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9OVEZZXzgwCiAgICAgIC0gJ05URllfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fTlRGWX0nCiAgICAgIC0gJ1RaPSR7VFo6LVVUQ30nCiAgICAgIC0gTlRGWV9DQUNIRV9GSUxFPS92YXIvY2FjaGUvbnRmeS9jYWNoZS5kYgogICAgICAtIE5URllfQVVUSF9GSUxFPS92YXIvbGliL250ZnkvYXV0aC5kYgogICAgICAtICdOVEZZX1VQU1RSRUFNX0JBU0VfVVJMPSR7VVBTVFJFQU1fQkFTRV9VUkw6LWh0dHBzOi8vbnRmeS5zaH0nCiAgICAgIC0gJ05URllfRU5BQkxFX1NJR05VUD0ke05URllfRU5BQkxFX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ05URllfRU5BQkxFX0xPR0lOPSR7TlRGWV9FTkFCTEVfTE9HSU46LXRydWV9JwogICAgICAtICdOVEZZX0NBQ0hFX0RVUkFUSU9OPSR7TlRGWV9DQUNIRV9EVVJBVElPTjotMjRofScKICAgICAgLSAnTlRGWV9BVFRBQ0hNRU5UX1RPVEFMX1NJWkVfTElNSVQ9JHtOVEZZX0FUVEFDSE1FTlRfVE9UQUxfU0laRV9MSU1JVDotMUd9JwogICAgICAtICdOVEZZX0FUVEFDSE1FTlRfRklMRV9TSVpFX0xJTUlUPSR7TlRGWV9BVFRBQ0hNRU5UX0ZJTEVfU0laRV9MSU1JVDotMTBNfScKICAgICAgLSAnTlRGWV9BVFRBQ0hNRU5UX0VYUElSWV9EVVJBVElPTj0ke05URllfQVRUQUNITUVOVF9FWFBJUllfRFVSQVRJT046LTI0aH0nCiAgICAgIC0gJ05URllfU01UUF9TRU5ERVJfQUREUj0ke05URllfU01UUF9TRU5ERVJfQUREUjotc210cC55b3VyLWRvbWFpbi5kZX0nCiAgICAgIC0gJ05URllfU01UUF9TRU5ERVJfVVNFUj0ke05URllfU01UUF9TRU5ERVJfVVNFUjotbm8tcmVwbHlAZGV9JwogICAgICAtICdOVEZZX1NNVFBfU0VOREVSX1BBU1M9JHtOVEZZX1NNVFBfU0VOREVSX1BBU1M6LXBhc3N3b3JkfScKICAgICAgLSAnTlRGWV9TTVRQX1NFTkRFUl9GUk9NPSR7TlRGWV9TTVRQX1NFTkRFUl9GUk9NOi1uby1yZXBseUBkZX0nCiAgICAgIC0gJ05URllfS0VFUEFMSVZFX0lOVEVSVkFMPSR7TlRGWV9LRUVQQUxJVkVfSU5URVJWQUw6LTVtfScKICAgICAgLSAnTlRGWV9NQU5BR0VSX0lOVEVSVkFMPSR7TlRGWV9NQU5BR0VSX0lOVEVSVkFMOi01bX0nCiAgICAgIC0gJ05URllfVklTSVRPUl9NRVNTQUdFX0RBSUxZX0xJTUlUPSR7TlRGWV9WSVNJVE9SX01FU1NBR0VfREFJTFlfTElNSVQ6LTEwMH0nCiAgICAgIC0gJ05URllfVklTSVRPUl9BVFRBQ0hNRU5UX0RBSUxZX0JBTkRXSURUSF9MSU1JVD0ke05URllfVklTSVRPUl9BVFRBQ0hNRU5UX0RBSUxZX0JBTkRXSURUSF9MSU1JVDotMUd9JwogICAgICAtICdOVEZZX1VQU1RSRUFNX0FDQ0VTU19UT0tFTj0ke1VQU1RSRUFNX0FDQ0VTU19UT0tFTn0nCiAgICAgIC0gJ05URllfQVVUSF9ERUZBVUxUX0FDQ0VTUz0ke05URllfQVVUSF9ERUZBVUxUX0FDQ0VTUzotcmVhZC13cml0ZX0nCiAgICAgIC0gJ05URllfV0VCX1BVU0hfUFVCTElDX0tFWT0ke05URllfV0VCX1BVU0hfUFVCTElDX0tFWX0nCiAgICAgIC0gJ05URllfV0VCX1BVU0hfUFJJVkFURV9LRVk9JHtOVEZZX1dFQl9QVVNIX1BSSVZBVEVfS0VZfScKICAgICAgLSAnTlRGWV9XRUJfUFVTSF9FTUFJTF9BRERSRVNTPSR7TlRGWV9XRUJfUFVTSF9FTUFJTF9BRERSRVNTfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ250ZnktY2FjaGU6L3Zhci9jYWNoZS9udGZ5JwogICAgICAtICdudGZ5LWRiOi92YXIvbGliL250ZnkvJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICd3Z2V0IC1xIC0tdHJpZXM9MSBodHRwOi8vbG9jYWxob3N0OjgwL3YxL2hlYWx0aCAtTyAtIHwgZ3JlcCAtRW8gJyciaGVhbHRoeSJccyo6XHMqdHJ1ZScnIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDYwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogICAgICBzdGFydF9wZXJpb2Q6IDQwcwo=", + "tags": [ + "ntfy", + "notification", + "push notification", + "pub-sub", + "notify" + ], + "logo": "svgs/ntfy.svg", + "minversion": "0.0.0", + "port": "80" + }, "odoo": { "documentation": "https://www.odoo.com/?utm_source=coolify.io", "slogan": "Odoo is a suite of open-source business apps that cover all your company needs.", @@ -1459,6 +1918,35 @@ "minversion": "0.0.0", "port": "80" }, + "osticket": { + "documentation": "https://docs.osticket.com/en/latest/?utm_source=coolify.io", + "slogan": "osTicket is a widely-used open source support ticket system.", + "compose": "c2VydmljZXM6CiAgb3N0aWNrZXQ6CiAgICBpbWFnZTogJ3RpcmVkb2ZpdC9vc3RpY2tldDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fT1NUSUNLRVRfODAKICAgICAgLSAnQVBQX1VSTD0ke1NFUlZJQ0VfRlFETl9PU1RJQ0tFVH0nCiAgICAgIC0gJ0NST05fSU5URVJWQUw9JHtDUk9OX0lOVEVSVkFMOi0xMH0nCiAgICAgIC0gREJfSE9TVD1tYXJpYWRiCiAgICAgIC0gJ0RCX05BTUU9JHtPU1RJQ0tFVF9EQVRBQkFTRTotb3N0aWNrZXQtZGJ9JwogICAgICAtICdEQl9VU0VSPSR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICAtICdEQl9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnSU5TVEFMTF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX09TVElDS0VUU0VDUkVUfScKICAgICAgLSAnQURNSU5fRklSU1ROQU1FPSR7T1NUSUNLRVRfRklSU1ROQU1FOi1BZG1pbn0nCiAgICAgIC0gJ0FETUlOX0xBU1ROQU1FPSR7T1NUSUNLRVRfTEFTVE5BTUU6LWlzdHJhdG9yfScKICAgICAgLSAnQURNSU5fRU1BSUw9JHtPU1RJQ0tFVF9BRE1JTl9FTUFJTDotYWRtaW5AZXhhbXBsZS5jb219JwogICAgICAtICdBRE1JTl9VU0VSPSR7U0VSVklDRV9VU0VSX09TVElDS0VUQURNSU59JwogICAgICAtICdBRE1JTl9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9PU1RJQ0tFVEFETUlOUEFTU30nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEvJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGRlcGVuZHNfb246CiAgICAgIG1hcmlhZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29zdGlja2V0LWRhdGE6L3d3dy9vc3RpY2tldCcKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIE1BUklBREJfUk9PVF9QQVNTV09SRDogJyR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCUk9PVH0nCiAgICAgIE1BUklBREJfREFUQUJBU0U6ICcke09TVElDS0VUX0RBVEFCQVNFOi1vc3RpY2tldC1kYn0nCiAgICAgIE1BUklBREJfVVNFUjogJyR7U0VSVklDRV9VU0VSX01BUklBREJ9JwogICAgICBNQVJJQURCX1BBU1NXT1JEOiAnJHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBzdGFydF9wZXJpb2Q6IDEwcwogICAgICBpbnRlcnZhbDogMTBzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ29zdGlja2V0LW1hcmlhZGItZGF0YTovdmFyL2xpYi9teXNxbCcK", + "tags": [ + "helpdesk", + "ticketing", + "support", + "open-source" + ], + "logo": "svgs/osticket.png", + "minversion": "0.0.0", + "port": "80" + }, + "owncloud": { + "documentation": "https://owncloud.com/docs?utm_source=coolify.io", + "slogan": "OwnCloud with Open Web UI integrates file management with a powerful, user-friendly interface.", + "compose": "c2VydmljZXM6CiAgb3duY2xvdWQ6CiAgICBpbWFnZTogJ293bmNsb3VkL3NlcnZlcjpsYXRlc3QnCiAgICBkZXBlbmRzX29uOgogICAgICBtYXJpYWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fT1dOQ0xPVURfODA4MAogICAgICAtICdPV05DTE9VRF9ET01BSU49JHtTRVJWSUNFX0ZRRE5fT1dOQ0xPVUR9JwogICAgICAtICdPV05DTE9VRF9UUlVTVEVEX0RPTUFJTlM9JHtTRVJWSUNFX1VSTF9PV05DTE9VRH0nCiAgICAgIC0gT1dOQ0xPVURfREJfVFlQRT1teXNxbAogICAgICAtIE9XTkNMT1VEX0RCX0hPU1Q9bWFyaWFkYgogICAgICAtICdPV05DTE9VRF9EQl9OQU1FPSR7REJfTkFNRTotb3duY2xvdWR9JwogICAgICAtICdPV05DTE9VRF9EQl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9NQVJJQURCfScKICAgICAgLSAnT1dOQ0xPVURfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01BUklBREJ9JwogICAgICAtICdPV05DTE9VRF9BRE1JTl9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9PV05DTE9VRH0nCiAgICAgIC0gJ09XTkNMT1VEX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9PV05DTE9VRH0nCiAgICAgIC0gJ09XTkNMT1VEX01ZU1FMX1VURjhNQjQ9JHtNWVNRTF9VVEY4TUI0Oi10cnVlfScKICAgICAgLSAnT1dOQ0xPVURfUkVESVNfRU5BQkxFRD0ke1JFRElTX0VOQUJMRUQ6LXRydWV9JwogICAgICAtIE9XTkNMT1VEX1JFRElTX0hPU1Q9cmVkaXMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSAvdXNyL2Jpbi9oZWFsdGhjaGVjawogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdvd25jbG91ZC1kYXRhOi9tbnQvZGF0YScKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtICdNWVNRTF9ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCUk9PVH0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUFSSUFEQn0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NQVJJQURCfScKICAgICAgLSAnTVlTUUxfREFUQUJBU0U9JHtEQl9OQU1FOi1vd25jbG91ZH0nCiAgICAgIC0gVFo9YXV0bwogICAgY29tbWFuZDoKICAgICAgLSAnLS1jaGFyYWN0ZXItc2V0LXNlcnZlcj11dGY4bWI0JwogICAgICAtICctLWNvbGxhdGlvbi1zZXJ2ZXI9dXRmOG1iNF9iaW4nCiAgICAgIC0gJy0tbWF4LWFsbG93ZWQtcGFja2V0PTEyOE0nCiAgICAgIC0gJy0taW5ub2RiLWxvZy1maWxlLXNpemU9NjRNJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGhlYWx0aGNoZWNrLnNoCiAgICAgICAgLSAnLS1jb25uZWN0JwogICAgICAgIC0gJy0taW5ub2RiX2luaXRpYWxpemVkJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICB2b2x1bWVzOgogICAgICAtICdvd25jbG91ZC1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo2JwogICAgY29tbWFuZDoKICAgICAgLSAnLS1kYXRhYmFzZXMnCiAgICAgIC0gJzEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiAxMHMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogNQo=", + "tags": [ + "owncloud", + "file-management", + "open-web-ui", + "integration", + "cloud" + ], + "logo": "svgs/owncloud.svg", + "minversion": "0.0.0", + "port": "8080" + }, "pairdrop": { "documentation": "https://pairdrop.net/?utm_source=coolify.io", "slogan": "Pairdrop is a self-hosted file sharing and collaboration platform, offering secure file sharing and collaboration capabilities for efficient teamwork.", @@ -1476,7 +1964,7 @@ "paperless": { "documentation": "https://docs.paperless-ngx.com/configuration/?utm_source=coolify.io", "slogan": "Paperless-ngx is a community-supported open-source document management system that transforms your physical documents into a searchable online archive so you can keep, well, less paper.", - "compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ2RvY2tlci5pby9saWJyYXJ5L3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSBQQVBFUkxFU1NfVVJMPSRTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfQURNSU5fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfUkVESVM9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdQQVBFUkxFU1NfU0VDUkVUX0tFWT0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9QQVBFUkxFU1N9Jwo=", + "compose": "c2VydmljZXM6CiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3BhcGVybGVzcy1yZWRpczovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICBwYXBlcmxlc3M6CiAgICBpbWFnZTogJ3BhcGVybGVzc25neC9wYXBlcmxlc3Mtbmd4OmxhdGVzdCcKICAgIGRlcGVuZHNfb246CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mcycKICAgICAgICAtICctUycKICAgICAgICAtICctLW1heC10aW1lJwogICAgICAgIC0gJzInCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICdwYXBlcmxlc3MtZGF0YTovdXNyL3NyYy9wYXBlcmxlc3MvZGF0YScKICAgICAgLSAncGFwZXJsZXNzLW1lZGlhOi91c3Ivc3JjL3BhcGVybGVzcy9tZWRpYScKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZXhwb3J0CiAgICAgICAgdGFyZ2V0OiAvdXNyL3NyYy9wYXBlcmxlc3MvZXhwb3J0CiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2NvbnN1bWUKICAgICAgICB0YXJnZXQ6IC91c3Ivc3JjL3BhcGVybGVzcy9jb25zdW1lCiAgICAgICAgaXNfZGlyZWN0b3J5OiB0cnVlCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSBQQVBFUkxFU1NfVVJMPSRTRVJWSUNFX0ZRRE5fUEFQRVJMRVNTXzgwMDAKICAgICAgLSAnUEFQRVJMRVNTX0FETUlOX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfQURNSU5fVVNFUj0ke1NFUlZJQ0VfVVNFUl9QQVBFUkxFU1N9JwogICAgICAtICdQQVBFUkxFU1NfUkVESVM9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdQQVBFUkxFU1NfU0VDUkVUX0tFWT0ke1NFUlZJQ0VfUkVBTEJBU0U2NF82NF9QQVBFUkxFU1N9Jwo=", "tags": null, "logo": "svgs/paperless.svg", "minversion": "0.0.0", @@ -1510,7 +1998,7 @@ "plane": { "documentation": "https://docs.plane.so/self-hosting/methods/docker-compose?utm_source=coolify.io", "slogan": "The open source project management tool", - "compose": "eC1hcHAtZW52OgogIGVudmlyb25tZW50OgogICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAtIEFXU19BQ0NFU1NfS0VZX0lEPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgLSAnQVdTX1MzX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgLSAnQlVDS0VUX05BTUU9JHtCVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgLSAnQURNSU5fQkFTRV9VUkw9JHtBRE1JTl9CQVNFX1VSTH0nCiAgICAtICdTUEFDRV9CQVNFX1VSTD0ke1NQQUNFX0JBU0VfVVJMfScKICAgIC0gJ0FQUF9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCnNlcnZpY2VzOgogIHByb3h5OgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BMQU5FCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtcHJveHk6c3RhYmxlJwogICAgZGVwZW5kc19vbjoKICAgICAgLSB3ZWIKICAgICAgLSBhcGkKICAgICAgLSBzcGFjZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgd2ViOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBRE1JTl9CQVNFX1VSTD0ke0FETUlOX0JBU0VfVVJMfScKICAgICAgLSAnU1BBQ0VfQkFTRV9VUkw9JHtTUEFDRV9CQVNFX1VSTH0nCiAgICAgIC0gJ0FQUF9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICBpbWFnZTogJ21ha2VwbGFuZS9wbGFuZS1mcm9udGVuZDpzdGFibGUnCiAgICBjb21tYW5kOiAnbm9kZSB3ZWIvc2VydmVyLmpzIHdlYicKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gd29ya2VyCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3dnZXQgLXFPLSBodHRwOi8vYGhvc3RuYW1lYDozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgc3BhY2U6CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgLSAnVVNFX01JTklPPSR7VVNFX01JTklPOi0xfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtIEFXU19BQ0NFU1NfS0VZX0lEPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQVdTX1MzX0VORFBPSU5UX1VSTD0ke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgLSAnQVdTX1MzX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQlVDS0VUX05BTUU9JHtCVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0FETUlOX0JBU0VfVVJMPSR7QURNSU5fQkFTRV9VUkx9JwogICAgICAtICdTUEFDRV9CQVNFX1VSTD0ke1NQQUNFX0JBU0VfVVJMfScKICAgICAgLSAnQVBQX0JBU0VfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgIGltYWdlOiAnbWFrZXBsYW5lL3BsYW5lLXNwYWNlOnN0YWJsZScKICAgIGNvbW1hbmQ6ICdub2RlIHNwYWNlL3NlcnZlci5qcyBzcGFjZScKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gd29ya2VyCiAgICAgIC0gd2ViCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIGFkbWluOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBRE1JTl9CQVNFX1VSTD0ke0FETUlOX0JBU0VfVVJMfScKICAgICAgLSAnU1BBQ0VfQkFTRV9VUkw9JHtTUEFDRV9CQVNFX1VSTH0nCiAgICAgIC0gJ0FQUF9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICBpbWFnZTogJ21ha2VwbGFuZS9wbGFuZS1hZG1pbjpzdGFibGUnCiAgICBjb21tYW5kOiAnbm9kZSBhZG1pbi9zZXJ2ZXIuanMgYWRtaW4nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBhcGk6CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgLSAnVVNFX01JTklPPSR7VVNFX01JTklPOi0xfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtIEFXU19BQ0NFU1NfS0VZX0lEPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQVdTX1MzX0VORFBPSU5UX1VSTD0ke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgLSAnQVdTX1MzX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQlVDS0VUX05BTUU9JHtCVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0FETUlOX0JBU0VfVVJMPSR7QURNSU5fQkFTRV9VUkx9JwogICAgICAtICdTUEFDRV9CQVNFX1VSTD0ke1NQQUNFX0JBU0VfVVJMfScKICAgICAgLSAnQVBQX0JBU0VfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgIGltYWdlOiAnbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6c3RhYmxlJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtYXBpLnNoCiAgICB2b2x1bWVzOgogICAgICAtICdsb2dzX2FwaTovY29kZS9wbGFuZS9sb2dzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHdvcmtlcjoKICAgIGVudmlyb25tZW50OgogICAgICAtICdXRUJfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgICAgLSAnREVCVUc9JHtERUJVRzotMH0nCiAgICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIC0gJ0dVTklDT1JOX1dPUktFUlM9JHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgLSBQR0hPU1Q9cGxhbmUtZGIKICAgICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBHREFUQT0vdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICAtIFJFRElTX0hPU1Q9cGxhbmUtcmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQURNSU5fQkFTRV9VUkw9JHtBRE1JTl9CQVNFX1VSTH0nCiAgICAgIC0gJ1NQQUNFX0JBU0VfVVJMPSR7U1BBQ0VfQkFTRV9VUkx9JwogICAgICAtICdBUFBfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtYmFja2VuZDpzdGFibGUnCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC13b3JrZXIuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0gJ2xvZ3Nfd29ya2VyOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHBsYW5lLWRiCiAgICAgIC0gcGxhbmUtcmVkaXMKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgYmVhdC13b3JrZXI6CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgLSAnVVNFX01JTklPPSR7VVNFX01JTklPOi0xfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtIEFXU19BQ0NFU1NfS0VZX0lEPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQVdTX1MzX0VORFBPSU5UX1VSTD0ke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgLSAnQVdTX1MzX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQlVDS0VUX05BTUU9JHtCVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0FETUlOX0JBU0VfVVJMPSR7QURNSU5fQkFTRV9VUkx9JwogICAgICAtICdTUEFDRV9CQVNFX1VSTD0ke1NQQUNFX0JBU0VfVVJMfScKICAgICAgLSAnQVBQX0JBU0VfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgIGltYWdlOiAnbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6c3RhYmxlJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtYmVhdC5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc19iZWF0LXdvcmtlcjovY29kZS9wbGFuZS9sb2dzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1pZ3JhdG9yOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBRE1JTl9CQVNFX1VSTD0ke0FETUlOX0JBU0VfVVJMfScKICAgICAgLSAnU1BBQ0VfQkFTRV9VUkw9JHtTUEFDRV9CQVNFX1VSTH0nCiAgICAgIC0gJ0FQUF9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICBpbWFnZTogJ21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOnN0YWJsZScKICAgIHJlc3RhcnQ6ICdubycKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LW1pZ3JhdG9yLnNoCiAgICB2b2x1bWVzOgogICAgICAtICdsb2dzX21pZ3JhdG9yOi9jb2RlL3BsYW5lL2xvZ3MnCiAgICBkZXBlbmRzX29uOgogICAgICAtIHBsYW5lLWRiCiAgICAgIC0gcGxhbmUtcmVkaXMKICBwbGFuZS1kYjoKICAgIGVudmlyb25tZW50OgogICAgICAtICdXRUJfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgICAgLSAnREVCVUc9JHtERUJVRzotMH0nCiAgICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIC0gJ0dVTklDT1JOX1dPUktFUlM9JHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgLSBQR0hPU1Q9cGxhbmUtZGIKICAgICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBHREFUQT0vdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICAtIFJFRElTX0hPU1Q9cGxhbmUtcmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQURNSU5fQkFTRV9VUkw9JHtBRE1JTl9CQVNFX1VSTH0nCiAgICAgIC0gJ1NQQUNFX0JBU0VfVVJMPSR7U1BBQ0VfQkFTRV9VUkx9JwogICAgICAtICdBUFBfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNS41LWFscGluZScKICAgIGNvbW1hbmQ6ICJwb3N0Z3JlcyAtYyAnbWF4X2Nvbm5lY3Rpb25zPTEwMDAnIgogICAgdm9sdW1lczoKICAgICAgLSAncGdkYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBwbGFuZS1yZWRpczoKICAgIGVudmlyb25tZW50OgogICAgICAtICdXRUJfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgICAgLSAnREVCVUc9JHtERUJVRzotMH0nCiAgICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIC0gJ0dVTklDT1JOX1dPUktFUlM9JHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgLSBQR0hPU1Q9cGxhbmUtZGIKICAgICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBHREFUQT0vdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICAtIFJFRElTX0hPU1Q9cGxhbmUtcmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQURNSU5fQkFTRV9VUkw9JHtBRE1JTl9CQVNFX1VSTH0nCiAgICAgIC0gJ1NQQUNFX0JBU0VfVVJMPSR7U1BBQ0VfQkFTRV9VUkx9JwogICAgICAtICdBUFBfQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjcuMi41LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLW1pbmlvOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBRE1JTl9CQVNFX1VSTD0ke0FETUlOX0JBU0VfVVJMfScKICAgICAgLSAnU1BBQ0VfQkFTRV9VUkw9JHtTUEFDRV9CQVNFX1VSTH0nCiAgICAgIC0gJ0FQUF9CQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICBpbWFnZTogJ21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2V4cG9ydCAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwOTAiJwogICAgdm9sdW1lczoKICAgICAgLSAndXBsb2FkczovZXhwb3J0JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG1jCiAgICAgICAgLSByZWFkeQogICAgICAgIC0gbG9jYWwKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "compose": "eC1hcHAtZW52OgogIGVudmlyb25tZW50OgogICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgIC0gUkFCQklUTVFfSE9TVD1wbGFuZS1tcQogICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgLSAnUkFCQklUTVFfREVGQVVMVF9VU0VSPSR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIC0gJ1JBQkJJVE1RX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgIC0gJ0FNUVBfVVJMPWFtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlR9L3BsYW5lJwogICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgLSAnVVNFX01JTklPPSR7VVNFX01JTklPOi0xfScKICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgLSBBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAtICdBUElfQkFTRV9VUkw9JHtBUElfQkFTRV9VUkw6LWh0dHA6Ly9hcGk6ODAwMH0nCnNlcnZpY2VzOgogIHByb3h5OgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BMQU5FCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtcHJveHk6c3RhYmxlJwogICAgZGVwZW5kc19vbjoKICAgICAgLSB3ZWIKICAgICAgLSBhcGkKICAgICAgLSBzcGFjZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgd2ViOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFJBQkJJVE1RX0hPU1Q9cGxhbmUtbXEKICAgICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1VTRVI9JHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdBTVFQX1VSTD1hbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUfS9wbGFuZScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQVBJX0JBU0VfVVJMPSR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtZnJvbnRlbmQ6c3RhYmxlJwogICAgY29tbWFuZDogJ25vZGUgd2ViL3NlcnZlci5qcyB3ZWInCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdvcmtlcgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICd3Z2V0IC1xTy0gaHR0cDovL2Bob3N0bmFtZWA6MzAwMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIHNwYWNlOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFJBQkJJVE1RX0hPU1Q9cGxhbmUtbXEKICAgICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1VTRVI9JHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdBTVFQX1VSTD1hbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUfS9wbGFuZScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQVBJX0JBU0VfVVJMPSR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtc3BhY2U6c3RhYmxlJwogICAgY29tbWFuZDogJ25vZGUgc3BhY2Uvc2VydmVyLmpzIHNwYWNlJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSB3b3JrZXIKICAgICAgLSB3ZWIKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnaGV5IHdoYXRzIHVwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgYWRtaW46CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gUkFCQklUTVFfSE9TVD1wbGFuZS1tcQogICAgICAtICdSQUJCSVRNUV9QT1JUPSR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVVNFUj0ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9WSE9TVD0ke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIC0gJ0FNUVBfVVJMPWFtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlR9L3BsYW5lJwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBUElfQkFTRV9VUkw9JHtBUElfQkFTRV9VUkw6LWh0dHA6Ly9hcGk6ODAwMH0nCiAgICBpbWFnZTogJ21ha2VwbGFuZS9wbGFuZS1hZG1pbjpzdGFibGUnCiAgICBjb21tYW5kOiAnbm9kZSBhZG1pbi9zZXJ2ZXIuanMgYWRtaW4nCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBsaXZlOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFJBQkJJVE1RX0hPU1Q9cGxhbmUtbXEKICAgICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1VTRVI9JHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdBTVFQX1VSTD1hbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUfS9wbGFuZScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQVBJX0JBU0VfVVJMPSR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtbGl2ZTpzdGFibGUnCiAgICBjb21tYW5kOiAnbm9kZSBsaXZlL2Rpc3Qvc2VydmVyLmpzIGxpdmUnCiAgICBkZXBlbmRzX29uOgogICAgICAtIGFwaQogICAgICAtIHdlYgogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBhcGk6CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gUkFCQklUTVFfSE9TVD1wbGFuZS1tcQogICAgICAtICdSQUJCSVRNUV9QT1JUPSR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVVNFUj0ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9WSE9TVD0ke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIC0gJ0FNUVBfVVJMPWFtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlR9L3BsYW5lJwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBUElfQkFTRV9VUkw9JHtBUElfQkFTRV9VUkw6LWh0dHA6Ly9hcGk6ODAwMH0nCiAgICBpbWFnZTogJ21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOnN0YWJsZScKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LWFwaS5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc19hcGk6L2NvZGUvcGxhbmUvbG9ncycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gcGxhbmUtZGIKICAgICAgLSBwbGFuZS1yZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICB3b3JrZXI6CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gUkFCQklUTVFfSE9TVD1wbGFuZS1tcQogICAgICAtICdSQUJCSVRNUV9QT1JUPSR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVVNFUj0ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9WSE9TVD0ke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIC0gJ0FNUVBfVVJMPWFtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlR9L3BsYW5lJwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBUElfQkFTRV9VUkw9JHtBUElfQkFTRV9VUkw6LWh0dHA6Ly9hcGk6ODAwMH0nCiAgICBpbWFnZTogJ21ha2VwbGFuZS9wbGFuZS1iYWNrZW5kOnN0YWJsZScKICAgIGNvbW1hbmQ6IC4vYmluL2RvY2tlci1lbnRyeXBvaW50LXdvcmtlci5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc193b3JrZXI6L2NvZGUvcGxhbmUvbG9ncycKICAgIGRlcGVuZHNfb246CiAgICAgIC0gYXBpCiAgICAgIC0gcGxhbmUtZGIKICAgICAgLSBwbGFuZS1yZWRpcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGVjaG8KICAgICAgICAtICdoZXkgd2hhdHMgdXAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBiZWF0LXdvcmtlcjoKICAgIGVudmlyb25tZW50OgogICAgICAtICdXRUJfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgICAgLSAnREVCVUc9JHtERUJVRzotMH0nCiAgICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIC0gJ0dVTklDT1JOX1dPUktFUlM9JHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgLSBQR0hPU1Q9cGxhbmUtZGIKICAgICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBHREFUQT0vdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICAtIFJFRElTX0hPU1Q9cGxhbmUtcmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgLSBSQUJCSVRNUV9IT1NUPXBsYW5lLW1xCiAgICAgIC0gJ1JBQkJJVE1RX1BPUlQ9JHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9VU0VSPSR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfUEFTUz0ke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9WSE9TVD0ke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnQU1RUF9VUkw9YW1xcDovLyR7U0VSVklDRV9VU0VSX1JBQkJJVE1RfToke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVF9QHBsYW5lLW1xOiR7UkFCQklUTVFfUE9SVH0vcGxhbmUnCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgLSAnVVNFX01JTklPPSR7VVNFX01JTklPOi0xfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtIEFXU19BQ0NFU1NfS0VZX0lEPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQVdTX1MzX0VORFBPSU5UX1VSTD0ke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgLSAnQVdTX1MzX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQlVDS0VUX05BTUU9JHtCVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0FQSV9CQVNFX1VSTD0ke0FQSV9CQVNFX1VSTDotaHR0cDovL2FwaTo4MDAwfScKICAgIGltYWdlOiAnbWFrZXBsYW5lL3BsYW5lLWJhY2tlbmQ6c3RhYmxlJwogICAgY29tbWFuZDogLi9iaW4vZG9ja2VyLWVudHJ5cG9pbnQtYmVhdC5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc19iZWF0LXdvcmtlcjovY29kZS9wbGFuZS9sb2dzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBhcGkKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gZWNobwogICAgICAgIC0gJ2hleSB3aGF0cyB1cCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAxNQogIG1pZ3JhdG9yOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFJBQkJJVE1RX0hPU1Q9cGxhbmUtbXEKICAgICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1VTRVI9JHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdBTVFQX1VSTD1hbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUfS9wbGFuZScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQVBJX0JBU0VfVVJMPSR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgaW1hZ2U6ICdtYWtlcGxhbmUvcGxhbmUtYmFja2VuZDpzdGFibGUnCiAgICByZXN0YXJ0OiAnbm8nCiAgICBjb21tYW5kOiAuL2Jpbi9kb2NrZXItZW50cnlwb2ludC1taWdyYXRvci5zaAogICAgdm9sdW1lczoKICAgICAgLSAnbG9nc19taWdyYXRvcjovY29kZS9wbGFuZS9sb2dzJwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwbGFuZS1kYgogICAgICAtIHBsYW5lLXJlZGlzCiAgcGxhbmUtZGI6CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCX1VSTD0ke1NFUlZJQ0VfRlFETl9QTEFORX0nCiAgICAgIC0gJ0RFQlVHPSR7REVCVUc6LTB9JwogICAgICAtICdDT1JTX0FMTE9XRURfT1JJR0lOUz0ke0NPUlNfQUxMT1dFRF9PUklHSU46LWh0dHA6Ly9sb2NhbGhvc3R9JwogICAgICAtICdHVU5JQ09STl9XT1JLRVJTPSR7R1VOSUNPUk5fV09SS0VSUzotMX0nCiAgICAgIC0gUEdIT1NUPXBsYW5lLWRiCiAgICAgIC0gUEdEQVRBQkFTRT1wbGFuZQogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfREI9cGxhbmUKICAgICAgLSBQT1NUR1JFU19QT1JUPTU0MzIKICAgICAgLSBQR0RBVEE9L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0BwbGFuZS1kYi9wbGFuZScKICAgICAgLSBSRURJU19IT1NUPXBsYW5lLXJlZGlzCiAgICAgIC0gUkVESVNfUE9SVD02Mzc5CiAgICAgIC0gJ1JFRElTX1VSTD0ke1JFRElTX1VSTDotcmVkaXM6Ly9wbGFuZS1yZWRpczo2Mzc5L30nCiAgICAgIC0gUkFCQklUTVFfSE9TVD1wbGFuZS1tcQogICAgICAtICdSQUJCSVRNUV9QT1JUPSR7UkFCQklUTVFfUE9SVDotNTY3Mn0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVVNFUj0ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9WSE9TVD0ke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIC0gJ0FNUVBfVVJMPWFtcXA6Ly8ke1NFUlZJQ0VfVVNFUl9SQUJCSVRNUX06JHtTRVJWSUNFX1BBU1NXT1JEX1JBQkJJVE1RfUBwbGFuZS1tcToke1JBQkJJVE1RX1BPUlR9L3BsYW5lJwogICAgICAtIFNFQ1JFVF9LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VDUkVUS0VZCiAgICAgIC0gJ1VTRV9NSU5JTz0ke1VTRV9NSU5JTzotMX0nCiAgICAgIC0gJ0FXU19SRUdJT049JHtBV1NfUkVHSU9OfScKICAgICAgLSBBV1NfQUNDRVNTX0tFWV9JRD0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gQVdTX1NFQ1JFVF9BQ0NFU1NfS0VZPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0FXU19TM19FTkRQT0lOVF9VUkw9JHtBV1NfUzNfRU5EUE9JTlRfVVJMOi1odHRwOi8vcGxhbmUtbWluaW86OTAwMH0nCiAgICAgIC0gJ0FXU19TM19CVUNLRVRfTkFNRT0ke0FXU19TM19CVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gTUlOSU9fUk9PVF9VU0VSPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBNSU5JT19ST09UX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX01JTklPCiAgICAgIC0gJ0JVQ0tFVF9OQU1FPSR7QlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtICdGSUxFX1NJWkVfTElNSVQ9JHtGSUxFX1NJWkVfTElNSVQ6LTUyNDI4ODB9JwogICAgICAtICdBUElfQkFTRV9VUkw9JHtBUElfQkFTRV9VUkw6LWh0dHA6Ly9hcGk6ODAwMH0nCiAgICBpbWFnZTogJ3Bvc3RncmVzOjE1LjctYWxwaW5lJwogICAgY29tbWFuZDogInBvc3RncmVzIC1jICdtYXhfY29ubmVjdGlvbnM9MTAwMCciCiAgICB2b2x1bWVzOgogICAgICAtICdwZ2RhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLXJlZGlzOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFJBQkJJVE1RX0hPU1Q9cGxhbmUtbXEKICAgICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1VTRVI9JHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdBTVFQX1VSTD1hbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUfS9wbGFuZScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQVBJX0JBU0VfVVJMPSR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgaW1hZ2U6ICd2YWxrZXkvdmFsa2V5OjcuMi41LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHBsYW5lLW1xOgogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1dFQl9VUkw9JHtTRVJWSUNFX0ZRRE5fUExBTkV9JwogICAgICAtICdERUJVRz0ke0RFQlVHOi0wfScKICAgICAgLSAnQ09SU19BTExPV0VEX09SSUdJTlM9JHtDT1JTX0FMTE9XRURfT1JJR0lOOi1odHRwOi8vbG9jYWxob3N0fScKICAgICAgLSAnR1VOSUNPUk5fV09SS0VSUz0ke0dVTklDT1JOX1dPUktFUlM6LTF9JwogICAgICAtIFBHSE9TVD1wbGFuZS1kYgogICAgICAtIFBHREFUQUJBU0U9cGxhbmUKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtIFBPU1RHUkVTX0RCPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfUE9SVD01NDMyCiAgICAgIC0gUEdEQVRBPS92YXIvbGliL3Bvc3RncmVzcWwvZGF0YQogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcGxhbmUtZGIvcGxhbmUnCiAgICAgIC0gUkVESVNfSE9TVD1wbGFuZS1yZWRpcwogICAgICAtIFJFRElTX1BPUlQ9NjM3OQogICAgICAtICdSRURJU19VUkw9JHtSRURJU19VUkw6LXJlZGlzOi8vcGxhbmUtcmVkaXM6NjM3OS99JwogICAgICAtIFJBQkJJVE1RX0hPU1Q9cGxhbmUtbXEKICAgICAgLSAnUkFCQklUTVFfUE9SVD0ke1JBQkJJVE1RX1BPUlQ6LTU2NzJ9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1VTRVI9JHtTRVJWSUNFX1VTRVJfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9QQVNTPSR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUTotcGxhbmV9JwogICAgICAtICdSQUJCSVRNUV9ERUZBVUxUX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfVkhPU1Q9JHtSQUJCSVRNUV9WSE9TVDotcGxhbmV9JwogICAgICAtICdBTVFQX1VSTD1hbXFwOi8vJHtTRVJWSUNFX1VTRVJfUkFCQklUTVF9OiR7U0VSVklDRV9QQVNTV09SRF9SQUJCSVRNUX1AcGxhbmUtbXE6JHtSQUJCSVRNUV9QT1JUfS9wbGFuZScKICAgICAgLSBTRUNSRVRfS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFQ1JFVEtFWQogICAgICAtICdVU0VfTUlOSU89JHtVU0VfTUlOSU86LTF9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gQVdTX0FDQ0VTU19LRVlfSUQ9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIEFXU19TRUNSRVRfQUNDRVNTX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdBV1NfUzNfRU5EUE9JTlRfVVJMPSR7QVdTX1MzX0VORFBPSU5UX1VSTDotaHR0cDovL3BsYW5lLW1pbmlvOjkwMDB9JwogICAgICAtICdBV1NfUzNfQlVDS0VUX05BTUU9JHtBV1NfUzNfQlVDS0VUX05BTUU6LXVwbG9hZHN9JwogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtICdCVUNLRVRfTkFNRT0ke0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSAnRklMRV9TSVpFX0xJTUlUPSR7RklMRV9TSVpFX0xJTUlUOi01MjQyODgwfScKICAgICAgLSAnQVBJX0JBU0VfVVJMPSR7QVBJX0JBU0VfVVJMOi1odHRwOi8vYXBpOjgwMDB9JwogICAgaW1hZ2U6ICdyYWJiaXRtcTozLjEzLjYtbWFuYWdlbWVudC1hbHBpbmUnCiAgICByZXN0YXJ0OiBhbHdheXMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JhYmJpdG1xX2RhdGE6L3Zhci9saWIvcmFiYml0bXEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3JhYmJpdG1xLWRpYWdub3N0aWNzIC1xIHBpbmcnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMzBzCiAgICAgIHJldHJpZXM6IDMKICBwbGFuZS1taW5pbzoKICAgIGVudmlyb25tZW50OgogICAgICAtICdXRUJfVVJMPSR7U0VSVklDRV9GUUROX1BMQU5FfScKICAgICAgLSAnREVCVUc9JHtERUJVRzotMH0nCiAgICAgIC0gJ0NPUlNfQUxMT1dFRF9PUklHSU5TPSR7Q09SU19BTExPV0VEX09SSUdJTjotaHR0cDovL2xvY2FsaG9zdH0nCiAgICAgIC0gJ0dVTklDT1JOX1dPUktFUlM9JHtHVU5JQ09STl9XT1JLRVJTOi0xfScKICAgICAgLSBQR0hPU1Q9cGxhbmUtZGIKICAgICAgLSBQR0RBVEFCQVNFPXBsYW5lCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19EQj1wbGFuZQogICAgICAtIFBPU1RHUkVTX1BPUlQ9NTQzMgogICAgICAtIFBHREFUQT0vdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBsYW5lLWRiL3BsYW5lJwogICAgICAtIFJFRElTX0hPU1Q9cGxhbmUtcmVkaXMKICAgICAgLSBSRURJU19QT1JUPTYzNzkKICAgICAgLSAnUkVESVNfVVJMPSR7UkVESVNfVVJMOi1yZWRpczovL3BsYW5lLXJlZGlzOjYzNzkvfScKICAgICAgLSBSQUJCSVRNUV9IT1NUPXBsYW5lLW1xCiAgICAgIC0gJ1JBQkJJVE1RX1BPUlQ9JHtSQUJCSVRNUV9QT1JUOi01NjcyfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9VU0VSPSR7U0VSVklDRV9VU0VSX1JBQkJJVE1ROi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX0RFRkFVTFRfUEFTUz0ke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVE6LXBsYW5lfScKICAgICAgLSAnUkFCQklUTVFfREVGQVVMVF9WSE9TVD0ke1JBQkJJVE1RX1ZIT1NUOi1wbGFuZX0nCiAgICAgIC0gJ1JBQkJJVE1RX1ZIT1NUPSR7UkFCQklUTVFfVkhPU1Q6LXBsYW5lfScKICAgICAgLSAnQU1RUF9VUkw9YW1xcDovLyR7U0VSVklDRV9VU0VSX1JBQkJJVE1RfToke1NFUlZJQ0VfUEFTU1dPUkRfUkFCQklUTVF9QHBsYW5lLW1xOiR7UkFCQklUTVFfUE9SVH0vcGxhbmUnCiAgICAgIC0gU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9TRUNSRVRLRVkKICAgICAgLSAnVVNFX01JTklPPSR7VVNFX01JTklPOi0xfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT059JwogICAgICAtIEFXU19BQ0NFU1NfS0VZX0lEPSRTRVJWSUNFX1VTRVJfTUlOSU8KICAgICAgLSBBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQVdTX1MzX0VORFBPSU5UX1VSTD0ke0FXU19TM19FTkRQT0lOVF9VUkw6LWh0dHA6Ly9wbGFuZS1taW5pbzo5MDAwfScKICAgICAgLSAnQVdTX1MzX0JVQ0tFVF9OQU1FPSR7QVdTX1MzX0JVQ0tFVF9OQU1FOi11cGxvYWRzfScKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgICAgLSAnQlVDS0VUX05BTUU9JHtCVUNLRVRfTkFNRTotdXBsb2Fkc30nCiAgICAgIC0gJ0ZJTEVfU0laRV9MSU1JVD0ke0ZJTEVfU0laRV9MSU1JVDotNTI0Mjg4MH0nCiAgICAgIC0gJ0FQSV9CQVNFX1VSTD0ke0FQSV9CQVNFX1VSTDotaHR0cDovL2FwaTo4MDAwfScKICAgIGltYWdlOiAnbWluaW8vbWluaW86bGF0ZXN0JwogICAgY29tbWFuZDogJ3NlcnZlciAvZXhwb3J0IC0tY29uc29sZS1hZGRyZXNzICI6OTA5MCInCiAgICB2b2x1bWVzOgogICAgICAtICd1cGxvYWRzOi9leHBvcnQnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbWMKICAgICAgICAtIHJlYWR5CiAgICAgICAgLSBsb2NhbAogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", "tags": [ "plane", "project-management", @@ -1530,7 +2018,7 @@ "plunk": { "documentation": "https://docs.useplunk.com/getting-started/introduction?utm_source=coolify.io", "slogan": "Plunk, The Open-Source Email Platform for AWS", - "compose": "dmVyc2lvbjogJzMnCnNlcnZpY2VzOgogIHBsdW5rOgogICAgaW1hZ2U6IGRyaWF1Zy9wbHVuawogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXNxbDoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICByZWRpczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2Vfc3RhcnRlZAogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1BMVU5LXzMwMDAKICAgICAgLSAnUkVESVNfVVJMPXJlZGlzOi8vcmVkaXM6NjM3OScKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzcWw6Ly8ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU306JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUBwb3N0Z3Jlc3FsL3BsdW5rP3NjaGVtYT1wdWJsaWMnCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVF9TRUNSRVR9JwogICAgICAtICdBV1NfUkVHSU9OPSR7QVdTX1JFR0lPTn0nCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7QVdTX0FDQ0VTU19LRVlfSUR9JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtBV1NfU0VDUkVUX0FDQ0VTU19LRVl9JwogICAgICAtICdBV1NfU0VTX0NPTkZJR1VSQVRJT05fU0VUPSR7QVdTX1NFU19DT05GSUdVUkFUSU9OX1NFVH0nCiAgICAgIC0gJ05FWFRfUFVCTElDX0FQSV9VUkk9JHtTRVJWSUNFX0ZRRE5fUExVTkt9L2FwaScKICAgICAgLSAnQVBQX1VSST0ke1NFUlZJQ0VfRlFETl9QTFVOS30nCiAgICAgIC0gJ0FQSV9VUkk9JHtTRVJWSUNFX0ZRRE5fUExVTkt9L2FwaScKICAgICAgLSBESVNBQkxFX1NJR05VUFM9RmFsc2UKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2FwcC9lbnRyeS5zaAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjMwMDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTUKICBwb3N0Z3Jlc3FsOgogICAgaW1hZ2U6ICdwb3N0Z3JlczoxNi1hbHBpbmUnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wbHVua30nCiAgICB2b2x1bWVzOgogICAgICAtICdwb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VIHBvc3RncmVzIC1kIHBvc3RncmVzJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcuNC1hbHBpbmUnCiAgICB2b2x1bWVzOgogICAgICAtICdyZWRpcy1kYXRhOi9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHJlZGlzLWNsaQogICAgICAgIC0gUElORwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDIwCg==", + "compose": "c2VydmljZXM6CiAgcGx1bms6CiAgICBpbWFnZTogJ2RyaWF1Zy9wbHVuazpsYXRlc3QnCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3Jlc3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHJlZGlzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9zdGFydGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fUExVTktfMzAwMAogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QHBvc3RncmVzcWwvcGx1bmstZGI/c2NoZW1hPXB1YmxpYycKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUU0VDUkVUfScKICAgICAgLSAnQVdTX1JFR0lPTj0ke0FXU19SRUdJT046P30nCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZX0lEPSR7QVdTX0FDQ0VTU19LRVlfSUQ6P30nCiAgICAgIC0gJ0FXU19TRUNSRVRfQUNDRVNTX0tFWT0ke0FXU19TRUNSRVRfQUNDRVNTX0tFWTo/fScKICAgICAgLSAnQVdTX1NFU19DT05GSUdVUkFUSU9OX1NFVD0ke0FXU19TRVNfQ09ORklHVVJBVElPTl9TRVQ6P30nCiAgICAgIC0gJ05FWFRfUFVCTElDX0FQSV9VUkk9JHtTRVJWSUNFX0ZRRE5fUExVTkt9L2FwaScKICAgICAgLSAnQVBQX1VSST0ke1NFUlZJQ0VfRlFETl9QTFVOS30nCiAgICAgIC0gJ0FQSV9VUkk9JHtTRVJWSUNFX0ZRRE5fUExVTkt9L2FwaScKICAgICAgLSAnRElTQUJMRV9TSUdOVVBTPSR7RElTQUJMRV9TSUdOVVBTOi1GYWxzZX0nCiAgICBlbnRyeXBvaW50OgogICAgICAtIC9hcHAvZW50cnkuc2gKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLXEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTozMDAwJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDE1CiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wbHVuay1kYn0nCiAgICB2b2x1bWVzOgogICAgICAtICdwbHVuay1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LjQtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncGx1bmstcmVkaXMtZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIFBJTkcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAyMAo=", "tags": [ "plunk", "email", @@ -1589,6 +2077,19 @@ "minversion": "0.0.0", "port": "4200" }, + "qbittorrent": { + "documentation": "https://docs.linuxserver.io/images/docker-qbittorrent/?utm_source=coolify.io", + "slogan": "The qBittorrent project aims to provide an open-source software alternative to \u03bcTorrent.", + "compose": "c2VydmljZXM6CiAgcWJpdDoKICAgIGltYWdlOiAnbHNjci5pby9saW51eHNlcnZlci9xYml0dG9ycmVudDpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnV0VCVUlfUE9SVD0ke1dFQlVJX1BPUlQ6LTgwODB9JwogICAgICAtIFBVSUQ9MTAwMAogICAgICAtIFBHSUQ9MTAwMAogICAgdm9sdW1lczoKICAgICAgLSAncWJpdHRvcnJlbnQtY29uZmlnOi9jb25maWcnCiAgICAgIC0gJ3FiaXR0b3JyZW50LWRvd25sb2FkczovZG93bmxvYWRzJwogICAgICAtICdxYml0dG9ycmVudC10b3JyZW50czovdG9ycmVudHMnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4MC8nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICB2dWV0b3JyZW50LWJhY2tlbmQ6CiAgICBpbWFnZTogJ2doY3IuaW8vdnVldG9ycmVudC92dWV0b3JyZW50LWJhY2tlbmQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1FCSVRPUlJFTlRfODA4MAogICAgICAtICdQT1JUPSR7V0VCVUlfUE9SVDotODA4MH0nCiAgICAgIC0gJ1FCSVRfQkFTRT0ke1NFUlZJQ0VfRlFETl9RQklUT1JSRU5UfScKICAgICAgLSAnUkVMRUFTRV9UWVBFPSR7UkVMRUFTRV9UWVBFOi1zdGFibGV9JwogICAgICAtICdVUERBVEVfVlRfQ1JPTj0ke1VQREFURV9WVF9DUk9OOi0iMCAqICogKiAqIn0nCiAgICB2b2x1bWVzOgogICAgICAtICd2dWV0b3JyZW50LWNvbmZpZzovY29uZmlnJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctcScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjgwODAvJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "torrent", + "streaming", + "webui" + ], + "logo": "svgs/qbittorrent.svg", + "minversion": "0.0.0", + "port": "8080" + }, "qdrant": { "documentation": "https://qdrant.tech/documentation/?utm_source=coolify.io", "slogan": "Qdrant is a vector similarity search engine that provides a production-ready service with a convenient API to store, search, and manage points (i.e. vectors) with an additional payload.", @@ -1627,7 +2128,7 @@ "reactive-resume": { "documentation": "https://rxresu.me/?utm_source=coolify.io", "slogan": "A one-of-a-kind resume builder that keeps your privacy in mind.", - "compose": "c2VydmljZXM6CiAgcmVhY3RpdmUtcmVzdW1lOgogICAgaW1hZ2U6ICdhbXJ1dGhwaWxsYWkvcmVhY3RpdmUtcmVzdW1lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SRUFDVElWRVJFU1VNRV8zMDAwCiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX1JFQUNUSVZFUkVTVU1FCiAgICAgIC0gJ1NUT1JBR0VfVVJMPSR7U0VSVklDRV9GUUROX01JTklPfS9kZWZhdWx0JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF9BQ0NFU1NUT0tFTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX1JFRlJFU0hUT0tFTgogICAgICAtIENIUk9NRV9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogICAgICAtICdDSFJPTUVfVVJMPXdzOi8vY2hyb21lOjMwMDAvY2hyb21lJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtIFNUT1JBR0VfRU5EUE9JTlQ9bWluaW8KICAgICAgLSBTVE9SQUdFX1BPUlQ9OTAwMAogICAgICAtIFNUT1JBR0VfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFNUT1JBR0VfQlVDS0VUPWRlZmF1bHQKICAgICAgLSBTVE9SQUdFX0FDQ0VTU19LRVk9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIFNUT1JBR0VfU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIFNUT1JBR0VfVVNFX1NTTD1mYWxzZQogICAgICAtICdESVNBQkxFX1NJR05VUFM9JHtTRVJWSUNFX0RJU0FCTEVfU0lHTlVQUzotZmFsc2V9JwogICAgICAtICdESVNBQkxFX0VNQUlMX0FVVEg9JHtTRVJWSUNFX0RJU0FCTEVfRU1BSUxfQVVUSDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIG1pbmlvCiAgICAgIC0gY2hyb21lCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBjb21tYW5kOiAnc2VydmVyIC9kYXRhIC0tY29uc29sZS1hZGRyZXNzICI6OTAwMSInCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fTUlOSU9fOTAwMAogICAgICAtIE1JTklPX1JPT1RfVVNFUj0kU0VSVklDRV9VU0VSX01JTklPCiAgICAgIC0gTUlOSU9fUk9PVF9QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgdm9sdW1lczoKICAgICAgLSAnbWluaW8tZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBtYwogICAgICAgIC0gcmVhZHkKICAgICAgICAtIGxvY2FsCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAKICBjaHJvbWU6CiAgICBpbWFnZTogJ2doY3IuaW8vYnJvd3Nlcmxlc3MvY2hyb21lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIEhFQUxUSD10cnVlCiAgICAgIC0gVElNRU9VVD0xMDAwMAogICAgICAtIENPTkNVUlJFTlQ9MTAKICAgICAgLSBUT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczphbHBpbmUnCiAgICBjb21tYW5kOiByZWRpcy1zZXJ2ZXIKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzX2RhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBwaW5nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "c2VydmljZXM6CiAgcmVhY3RpdmUtcmVzdW1lOgogICAgaW1hZ2U6ICdhbXJ1dGhwaWxsYWkvcmVhY3RpdmUtcmVzdW1lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9SRUFDVElWRVJFU1VNRV8zMDAwCiAgICAgIC0gUFVCTElDX1VSTD0kU0VSVklDRV9GUUROX1JFQUNUSVZFUkVTVU1FCiAgICAgIC0gJ1NUT1JBR0VfVVJMPSR7U0VSVklDRV9GUUROX01JTklPfS9kZWZhdWx0JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXNxbDovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXM6NTQzMi8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gQUNDRVNTX1RPS0VOX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF9BQ0NFU1NUT0tFTgogICAgICAtIFJFRlJFU0hfVE9LRU5fU0VDUkVUPSRTRVJWSUNFX1BBU1NXT1JEX1JFRlJFU0hUT0tFTgogICAgICAtIENIUk9NRV9UT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogICAgICAtICdDSFJPTUVfVVJMPXdzOi8vY2hyb21lOjMwMDAvY2hyb21lJwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtIFNUT1JBR0VfRU5EUE9JTlQ9bWluaW8KICAgICAgLSBTVE9SQUdFX1BPUlQ9OTAwMAogICAgICAtIFNUT1JBR0VfUkVHSU9OPXVzLWVhc3QtMQogICAgICAtIFNUT1JBR0VfQlVDS0VUPWRlZmF1bHQKICAgICAgLSBTVE9SQUdFX0FDQ0VTU19LRVk9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIFNUT1JBR0VfU0VDUkVUX0tFWT0kU0VSVklDRV9QQVNTV09SRF9NSU5JTwogICAgICAtIFNUT1JBR0VfVVNFX1NTTD1mYWxzZQogICAgICAtICdESVNBQkxFX1NJR05VUFM9JHtTRVJWSUNFX0RJU0FCTEVfU0lHTlVQUzotZmFsc2V9JwogICAgICAtICdESVNBQkxFX0VNQUlMX0FVVEg9JHtTRVJWSUNFX0RJU0FCTEVfRU1BSUxfQVVUSDotZmFsc2V9JwogICAgZGVwZW5kc19vbjoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtIG1pbmlvCiAgICAgIC0gY2hyb21lCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAogIG1pbmlvOgogICAgaW1hZ2U6ICdxdWF5LmlvL21pbmlvL21pbmlvOmxhdGVzdCcKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgL2RhdGEgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIicKICAgIGVudmlyb25tZW50OgogICAgICAtIE1JTklPX1NFUlZFUl9VUkw9JE1JTklPX1NFUlZFUl9VUkwKICAgICAgLSBNSU5JT19CUk9XU0VSX1JFRElSRUNUX1VSTD0kTUlOSU9fQlJPV1NFUl9SRURJUkVDVF9VUkwKICAgICAgLSBNSU5JT19ST09UX1VTRVI9JFNFUlZJQ0VfVVNFUl9NSU5JTwogICAgICAtIE1JTklPX1JPT1RfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfTUlOSU8KICAgIHZvbHVtZXM6CiAgICAgIC0gJ21pbmlvLWRhdGE6L2RhdGEnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbWMKICAgICAgICAtIHJlYWR5CiAgICAgICAgLSBsb2NhbAogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgY2hyb21lOgogICAgaW1hZ2U6ICdnaGNyLmlvL2Jyb3dzZXJsZXNzL2Nocm9tZTpsYXRlc3QnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIGVudmlyb25tZW50OgogICAgICAtIEhFQUxUSD10cnVlCiAgICAgIC0gVElNRU9VVD0xMDAwMAogICAgICAtIENPTkNVUlJFTlQ9MTAKICAgICAgLSBUT0tFTj0kU0VSVklDRV9QQVNTV09SRF9DSFJPTUVUT0tFTgogIHJlZGlzOgogICAgaW1hZ2U6ICdyZWRpczo3LWFscGluZScKICAgIGNvbW1hbmQ6IHJlZGlzLXNlcnZlcgogICAgdm9sdW1lczoKICAgICAgLSAncmVkaXNfZGF0YTovZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSByZWRpcy1jbGkKICAgICAgICAtIHBpbmcKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "reactive-resume", "resume-builder", @@ -1762,7 +2263,7 @@ "stirling-pdf": { "documentation": "https://github.com/Stirling-Tools/Stirling-PDF?utm_source=coolify.io", "slogan": "Stirling is a powerful web based PDF manipulation tool", - "compose": "c2VydmljZXM6CiAgc3RpcmxpbmctcGRmOgogICAgaW1hZ2U6ICdmcm9vb2RsZS9zLXBkZjpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdzdGlybGluZy10cmFpbmluZy1kYXRhOi91c3Ivc2hhcmUvdGVzc2VyYWN0LW9jci81L3Rlc3NkYXRhJwogICAgICAtICdzdGlybGluZy1jb25maWdzOi9jb25maWdzJwogICAgICAtICdzdGlybGluZy1jdXN0b20tZmlsZXM6L2N1c3RvbUZpbGVzLycKICAgICAgLSAnc3RpcmxpbmctbG9nczovbG9ncy8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1BERl84MDgwCiAgICAgIC0gRE9DS0VSX0VOQUJMRV9TRUNVUklUWT1mYWxzZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdjdXJsIC0tZmFpbCAtSSBodHRwOi8vMTI3LjAuMC4xOjgwODAgfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "compose": "c2VydmljZXM6CiAgc3RpcmxpbmctcGRmOgogICAgaW1hZ2U6ICdmcm9vb2RsZS9zLXBkZjpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdzdGlybGluZy10cmFpbmluZy1kYXRhOi91c3Ivc2hhcmUvdGVzc2VyYWN0LW9jci81L3Rlc3NkYXRhJwogICAgICAtICdzdGlybGluZy1jb25maWdzOi9jb25maWdzJwogICAgICAtICdzdGlybGluZy1jdXN0b20tZmlsZXM6L2N1c3RvbUZpbGVzLycKICAgICAgLSAnc3RpcmxpbmctbG9nczovbG9ncy8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fU1BERl84MDgwCiAgICAgIC0gRE9DS0VSX0VOQUJMRV9TRUNVUklUWT1mYWxzZQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdjdXJsIC0tZmFpbCAtLXNpbGVudCBodHRwOi8vMTI3LjAuMC4xOjgwODAvYXBpL3YxL2luZm8vc3RhdHVzIHwgZ3JlcCAtcSAiVVAiIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", "tags": [ "pdf", "manipulation", @@ -1790,7 +2291,7 @@ "supabase": { "documentation": "https://supabase.io?utm_source=coolify.io", "slogan": "The open source Firebase alternative.", - "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkcKICAgICAgLSAnSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBLT05HX0RBVEFCQVNFPW9mZgogICAgICAtIEtPTkdfREVDTEFSQVRJVkVfQ09ORklHPS9ob21lL2tvbmcva29uZy55bWwKICAgICAgLSAnS09OR19ETlNfT1JERVI9TEFTVCxBLENOQU1FJwogICAgICAtICdLT05HX1BMVUdJTlM9cmVxdWVzdC10cmFuc2Zvcm1lcixjb3JzLGtleS1hdXRoLGFjbCxiYXNpYy1hdXRoJwogICAgICAtIEtPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSX1NJWkU9MTYwawogICAgICAtICdLT05HX05HSU5YX1BST1hZX1BST1hZX0JVRkZFUlM9NjQgMTYwaycKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0RBU0hCT0FSRF9VU0VSTkFNRT0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ0RBU0hCT0FSRF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfQURNSU59JwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9hcGkva29uZy55bWwKICAgICAgICB0YXJnZXQ6IC9ob21lL2tvbmcvdGVtcC55bWwKICAgICAgICBjb250ZW50OiAiX2Zvcm1hdF92ZXJzaW9uOiAnMi4xJ1xuX3RyYW5zZm9ybTogdHJ1ZVxuXG4jIyNcbiMjIyBDb25zdW1lcnMgLyBVc2Vyc1xuIyMjXG5jb25zdW1lcnM6XG4gIC0gdXNlcm5hbWU6IERBU0hCT0FSRFxuICAtIHVzZXJuYW1lOiBhbm9uXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfQU5PTl9LRVlcbiAgLSB1c2VybmFtZTogc2VydmljZV9yb2xlXG4gICAga2V5YXV0aF9jcmVkZW50aWFsczpcbiAgICAgIC0ga2V5OiAkU1VQQUJBU0VfU0VSVklDRV9LRVlcblxuIyMjXG4jIyMgQWNjZXNzIENvbnRyb2wgTGlzdFxuIyMjXG5hY2xzOlxuICAtIGNvbnN1bWVyOiBhbm9uXG4gICAgZ3JvdXA6IGFub25cbiAgLSBjb25zdW1lcjogc2VydmljZV9yb2xlXG4gICAgZ3JvdXA6IGFkbWluXG5cbiMjI1xuIyMjIERhc2hib2FyZCBjcmVkZW50aWFsc1xuIyMjXG5iYXNpY2F1dGhfY3JlZGVudGlhbHM6XG4tIGNvbnN1bWVyOiBEQVNIQk9BUkRcbiAgdXNlcm5hbWU6ICREQVNIQk9BUkRfVVNFUk5BTUVcbiAgcGFzc3dvcmQ6ICREQVNIQk9BUkRfUEFTU1dPUkRcblxuXG4jIyNcbiMjIyBBUEkgUm91dGVzXG4jIyNcbnNlcnZpY2VzOlxuXG4gICMjIE9wZW4gQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvdmVyaWZ5XG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhdXRoLXYxLW9wZW5cbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL3ZlcmlmeVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tY2FsbGJhY2tcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1hdXRoOjk5OTkvY2FsbGJhY2tcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvY2FsbGJhY2tcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9hdXRob3JpemVcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1hdXRob3JpemVcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL2F1dGhvcml6ZVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBTZWN1cmUgQXV0aCByb3V0ZXNcbiAgLSBuYW1lOiBhdXRoLXYxXG4gICAgX2NvbW1lbnQ6ICdHb1RydWU6IC9hdXRoL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIFJFU1Qgcm91dGVzXG4gIC0gbmFtZTogcmVzdC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvcmVzdC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZXN0LXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3Jlc3QvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU2VjdXJlIEdyYXBoUUwgcm91dGVzXG4gIC0gbmFtZTogZ3JhcGhxbC12MVxuICAgIF9jb21tZW50OiAnUG9zdGdSRVNUOiAvZ3JhcGhxbC92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWwnXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtcmVzdDozMDAwL3JwYy9ncmFwaHFsXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBncmFwaHFsLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2dyYXBocWwvdjFcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IHJlcXVlc3QtdHJhbnNmb3JtZXJcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGFkZDpcbiAgICAgICAgICAgIGhlYWRlcnM6XG4gICAgICAgICAgICAgIC0gQ29udGVudC1Qcm9maWxlOmdyYXBocWxfcHVibGljXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUmVhbHRpbWUgcm91dGVzXG4gIC0gbmFtZTogcmVhbHRpbWUtdjEtd3NcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvc29ja2V0XG4gICAgcHJvdG9jb2w6IHdzXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3JlYWx0aW1lL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cbiAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgX2NvbW1lbnQ6ICdSZWFsdGltZTogL3JlYWx0aW1lL3YxLyogLT4gd3M6Ly9yZWFsdGltZTo0MDAwL3NvY2tldC8qJ1xuICAgIHVybDogaHR0cDovL3JlYWx0aW1lLWRldjo0MDAwL2FwaVxuICAgIHByb3RvY29sOiBodHRwXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiByZWFsdGltZS12MS1yZXN0XG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvYXBpXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFN0b3JhZ2Ugcm91dGVzOiB0aGUgc3RvcmFnZSBzZXJ2ZXIgbWFuYWdlcyBpdHMgb3duIGF1dGhcbiAgLSBuYW1lOiBzdG9yYWdlLXYxXG4gICAgX2NvbW1lbnQ6ICdTdG9yYWdlOiAvc3RvcmFnZS92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvKidcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1zdG9yYWdlOjUwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBzdG9yYWdlLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL3N0b3JhZ2UvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEVkZ2UgRnVuY3Rpb25zIHJvdXRlc1xuICAtIG5hbWU6IGZ1bmN0aW9ucy12MVxuICAgIF9jb21tZW50OiAnRWRnZSBGdW5jdGlvbnM6IC9mdW5jdGlvbnMvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWVkZ2UtZnVuY3Rpb25zOjkwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBmdW5jdGlvbnMtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZnVuY3Rpb25zL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcblxuICAjIyBBbmFseXRpY3Mgcm91dGVzXG4gIC0gbmFtZTogYW5hbHl0aWNzLXYxXG4gICAgX2NvbW1lbnQ6ICdBbmFseXRpY3M6IC9hbmFseXRpY3MvdjEvKiAtPiBodHRwOi8vbG9nZmxhcmU6NDAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYW5hbHl0aWNzLXYxLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2FuYWx5dGljcy92MS9cblxuICAjIyBTZWN1cmUgRGF0YWJhc2Ugcm91dGVzXG4gIC0gbmFtZTogbWV0YVxuICAgIF9jb21tZW50OiAncGctbWV0YTogL3BnLyogLT4gaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLW1ldGE6ODA4MC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IG1ldGEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcGcvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG5cbiAgIyMgUHJvdGVjdGVkIERhc2hib2FyZCAtIGNhdGNoIGFsbCByZW1haW5pbmcgcm91dGVzXG4gIC0gbmFtZTogZGFzaGJvYXJkXG4gICAgX2NvbW1lbnQ6ICdTdHVkaW86IC8qIC0+IGh0dHA6Ly9zdHVkaW86MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0dWRpbzozMDAwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogZGFzaGJvYXJkLWFsbFxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZTogYmFzaWMtYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogdHJ1ZVxuIgogIHN1cGFiYXNlLXN0dWRpbzoKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3R1ZGlvOjIwMjQwNzI5LWNlNDIxMzknCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gbm9kZQogICAgICAgIC0gJy1lJwogICAgICAgIC0gInJlcXVpcmUoJ2h0dHAnKS5nZXQoJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hcGkvcHJvZmlsZScsIChyKSA9PiB7aWYgKHIuc3RhdHVzQ29kZSAhPT0gMjAwKSBwcm9jZXNzLmV4aXQoMSk7IGVsc2UgcHJvY2Vzcy5leGl0KDApOyB9KS5vbignZXJyb3InLCAoKSA9PiBwcm9jZXNzLmV4aXQoMSkpIgogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBIT1NUTkFNRT0wLjAuMC4wCiAgICAgIC0gJ1NUVURJT19QR19NRVRBX1VSTD1odHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwJwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdERUZBVUxUX09SR0FOSVpBVElPTl9OQU1FPSR7U1RVRElPX0RFRkFVTFRfT1JHQU5JWkFUSU9OOi1EZWZhdWx0IE9yZ2FuaXphdGlvbn0nCiAgICAgIC0gJ0RFRkFVTFRfUFJPSkVDVF9OQU1FPSR7U1RVRElPX0RFRkFVTFRfUFJPSkVDVDotRGVmYXVsdCBQcm9qZWN0fScKICAgICAgLSAnU1VQQUJBU0VfVVJMPSR7U0VSVklDRV9GUUROX1NVUEFCQVNFS09OR30nCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VTRVJWSUNFX0tFWX0nCiAgICAgIC0gJ0FVVEhfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtICdMT0dGTEFSRV9VUkw9aHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwJwogICAgICAtIE5FWFRfUFVCTElDX0VOQUJMRV9MT0dTPXRydWUKICAgICAgLSBORVhUX0FOQUxZVElDU19CQUNLRU5EX1BST1ZJREVSPXBvc3RncmVzCiAgc3VwYWJhc2UtZGI6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzOjE1LjEuMS43OCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAncGdfaXNyZWFkeSAtVSBwb3N0Z3JlcyAtaCAxMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiAxMAogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtdmVjdG9yOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBjb21tYW5kOgogICAgICAtIHBvc3RncmVzCiAgICAgIC0gJy1jJwogICAgICAtIGNvbmZpZ19maWxlPS9ldGMvcG9zdGdyZXNxbC9wb3N0Z3Jlc3FsLmNvbmYKICAgICAgLSAnLWMnCiAgICAgIC0gbG9nX21pbl9tZXNzYWdlcz1mYXRhbAogICAgcmVzdGFydDogdW5sZXNzLXN0b3BwZWQKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0hPU1Q9L3Zhci9ydW4vcG9zdGdyZXNxbAogICAgICAtICdQR1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BHREFUQUJBU0U9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGFiYXNlLWRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3JlYWx0aW1lLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcmVhbHRpbWUuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfcmVhbHRpbWU7XG5hbHRlciBzY2hlbWEgX3JlYWx0aW1lIG93bmVyIHRvIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxuY3JlYXRlIHNjaGVtYSBpZiBub3QgZXhpc3RzIF9hbmFseXRpY3M7XG5hbHRlciBzY2hlbWEgX2FuYWx5dGljcyBvd25lciB0byA6cGd1c2VyO1xuIgogICAgICAtICdzdXBhYmFzZS1kYi1jb25maWc6L2V0Yy9wb3N0Z3Jlc3FsLWN1c3RvbScKICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2xvZ2ZsYXJlOjEuNC4wJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTE9HRkxBUkVfTk9ERV9IT1NUPTEyNy4wLjAuMQogICAgICAtIERCX1VTRVJOQU1FPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ0RCX0RBVEFCQVNFPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnREJfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gREJfU0NIRU1BPV9hbmFseXRpY3MKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlQ9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NJTkdMRV9URU5BTlRfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfU1VQQUJBU0VfTU9ERT10cnVlCiAgICAgIC0gTE9HRkxBUkVfTUlOX0NMVVNURVJfU0laRT0xCiAgICAgIC0gJ1BPU1RHUkVTX0JBQ0tFTkRfVVJMPXBvc3RncmVzcWw6Ly9zdXBhYmFzZV9hZG1pbjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSBQT1NUR1JFU19CQUNLRU5EX1NDSEVNQT1fYW5hbHl0aWNzCiAgICAgIC0gTE9HRkxBUkVfRkVBVFVSRV9GTEFHX09WRVJSSURFPW11bHRpYmFja2VuZD10cnVlCiAgc3VwYWJhc2UtdmVjdG9yOgogICAgaW1hZ2U6ICd0aW1iZXJpby92ZWN0b3I6MC4yOC4xLWFscGluZScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vc3VwYWJhc2UtdmVjdG9yOjkwMDEvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9sb2dzL3ZlY3Rvci55bWwKICAgICAgICB0YXJnZXQ6IC9ldGMvdmVjdG9yL3ZlY3Rvci55bWwKICAgICAgICByZWFkX29ubHk6IHRydWUKICAgICAgICBjb250ZW50OiAiYXBpOlxuICBlbmFibGVkOiB0cnVlXG4gIGFkZHJlc3M6IDAuMC4wLjA6OTAwMVxuXG5zb3VyY2VzOlxuICBkb2NrZXJfaG9zdDpcbiAgICB0eXBlOiBkb2NrZXJfbG9nc1xuICAgIGV4Y2x1ZGVfY29udGFpbmVyczpcbiAgICAgIC0gc3VwYWJhc2UtdmVjdG9yXG5cbnRyYW5zZm9ybXM6XG4gIHByb2plY3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gZG9ja2VyX2hvc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICAucHJvamVjdCA9IFwiZGVmYXVsdFwiXG4gICAgICAuZXZlbnRfbWVzc2FnZSA9IGRlbCgubWVzc2FnZSlcbiAgICAgIC5hcHBuYW1lID0gZGVsKC5jb250YWluZXJfbmFtZSlcbiAgICAgIGRlbCguY29udGFpbmVyX2NyZWF0ZWRfYXQpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9pZClcbiAgICAgIGRlbCguc291cmNlX3R5cGUpXG4gICAgICBkZWwoLnN0cmVhbSlcbiAgICAgIGRlbCgubGFiZWwpXG4gICAgICBkZWwoLmltYWdlKVxuICAgICAgZGVsKC5ob3N0KVxuICAgICAgZGVsKC5zdHJlYW0pXG4gIHJvdXRlcjpcbiAgICB0eXBlOiByb3V0ZVxuICAgIGlucHV0czpcbiAgICAgIC0gcHJvamVjdF9sb2dzXG4gICAgcm91dGU6XG4gICAgICBrb25nOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Uta29uZ1wiKSdcbiAgICAgIGF1dGg6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1hdXRoXCIpJ1xuICAgICAgcmVzdDogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLXJlc3RcIiknXG4gICAgICByZWFsdGltZTogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInJlYWx0aW1lLWRldlwiKSdcbiAgICAgIHN0b3JhZ2U6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1zdG9yYWdlXCIpJ1xuICAgICAgZnVuY3Rpb25zOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZnVuY3Rpb25zXCIpJ1xuICAgICAgZGI6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1kYlwiKSdcbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgcmVxLCBlcnIgPSBwYXJzZV9uZ2lueF9sb2coLmV2ZW50X21lc3NhZ2UsIFwiY29tYmluZWRcIilcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAudGltZXN0YW1wID0gcmVxLnRpbWVzdGFtcFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMucmVmZXJlciA9IHJlcS5yZWZlcmVyXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy51c2VyX2FnZW50ID0gcmVxLmFnZW50XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcmVxLmNsaWVudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0Lm1ldGhvZCA9IHJlcS5tZXRob2RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wYXRoID0gcmVxLnBhdGhcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHJlcS5wcm90b2NvbFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXNwb25zZS5zdGF0dXNfY29kZSA9IHJlcS5zdGF0dXNcbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBJZ25vcmVzIG5vbiBuZ2lueCBlcnJvcnMgc2luY2UgdGhleSBhcmUgcmVsYXRlZCB3aXRoIGtvbmcgYm9vdGluZyB1cFxuICBrb25nX2VycjpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmtvbmdcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSBcIkdFVFwiXG4gICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSAyMDBcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImVycm9yXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lc3RhbXBcbiAgICAgICAgICAuc2V2ZXJpdHkgPSBwYXJzZWQuc2V2ZXJpdHlcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5ob3N0ID0gcGFyc2VkLmhvc3RcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLmNmX2Nvbm5lY3RpbmdfaXAgPSBwYXJzZWQuY2xpZW50XG4gICAgICAgICAgdXJsLCBlcnIgPSBzcGxpdChwYXJzZWQucmVxdWVzdCwgXCIgXCIpXG4gICAgICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSB1cmxbMF1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHVybFsxXVxuICAgICAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5wcm90b2NvbCA9IHVybFsyXVxuICAgICAgICAgIH1cbiAgICAgIH1cbiAgICAgIGlmIGVyciAhPSBudWxsIHtcbiAgICAgICAgYWJvcnRcbiAgICAgIH1cbiAgIyBHb3RydWUgbG9ncyBhcmUgc3RydWN0dXJlZCBqc29uIHN0cmluZ3Mgd2hpY2ggZnJvbnRlbmQgcGFyc2VzIGRpcmVjdGx5LiBCdXQgd2Uga2VlcCBtZXRhZGF0YSBmb3IgY29uc2lzdGVuY3kuXG4gIGF1dGhfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmF1dGhcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhID0gbWVyZ2UhKC5tZXRhZGF0YSwgcGFyc2VkKVxuICAgICAgfVxuICAjIFBvc3RnUkVTVCBsb2dzIGFyZSBzdHJ1Y3R1cmVkIHNvIHdlIHNlcGFyYXRlIHRpbWVzdGFtcCBmcm9tIG1lc3NhZ2UgdXNpbmcgcmVnZXhcbiAgcmVzdF9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVzdFxuICAgIHNvdXJjZTogfC1cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPi4qKTogKD9QPG1zZz4uKikkJylcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAudGltZXN0YW1wID0gdG9fdGltZXN0YW1wIShwYXJzZWQudGltZSlcbiAgICAgICAgICAubWV0YWRhdGEuaG9zdCA9IC5wcm9qZWN0XG4gICAgICB9XG4gICMgUmVhbHRpbWUgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBwYXJzZSB0aGUgc2V2ZXJpdHkgbGV2ZWwgdXNpbmcgcmVnZXggKGlnbm9yZSB0aW1lIGJlY2F1c2UgaXQgaGFzIG5vIGRhdGUpXG4gIHJlYWx0aW1lX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5yZWFsdGltZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLmV4dGVybmFsX2lkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInXig/UDx0aW1lPlxcZCs6XFxkKzpcXGQrXFwuXFxkKykgXFxbKD9QPGxldmVsPlxcdyspXFxdICg/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICB9XG4gICMgU3RvcmFnZSBsb2dzIG1heSBjb250YWluIGpzb24gb2JqZWN0cyBzbyB3ZSBwYXJzZSB0aGVtIGZvciBjb21wbGV0ZW5lc3NcbiAgc3RvcmFnZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuc3RvcmFnZVxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5wcm9qZWN0ID0gZGVsKC5wcm9qZWN0KVxuICAgICAgLm1ldGFkYXRhLnRlbmFudElkID0gLm1ldGFkYXRhLnByb2plY3RcbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfanNvbiguZXZlbnRfbWVzc2FnZSlcbiAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAuZXZlbnRfbWVzc2FnZSA9IHBhcnNlZC5tc2dcbiAgICAgICAgICAubWV0YWRhdGEubGV2ZWwgPSBwYXJzZWQubGV2ZWxcbiAgICAgICAgICAubWV0YWRhdGEudGltZXN0YW1wID0gcGFyc2VkLnRpbWVcbiAgICAgICAgICAubWV0YWRhdGEuY29udGV4dFswXS5ob3N0ID0gcGFyc2VkLmhvc3RuYW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0ucGlkID0gcGFyc2VkLnBpZFxuICAgICAgfVxuICAjIFBvc3RncmVzIGxvZ3Mgc29tZSBtZXNzYWdlcyB0byBzdGRlcnIgd2hpY2ggd2UgbWFwIHRvIHdhcm5pbmcgc2V2ZXJpdHkgbGV2ZWxcbiAgZGJfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmRiXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLmhvc3QgPSBcImRiLWRlZmF1bHRcIlxuICAgICAgLm1ldGFkYXRhLnBhcnNlZC50aW1lc3RhbXAgPSAudGltZXN0YW1wXG5cbiAgICAgIHBhcnNlZCwgZXJyID0gcGFyc2VfcmVnZXgoLmV2ZW50X21lc3NhZ2UsIHInLiooP1A8bGV2ZWw+SU5GT3xOT1RJQ0V8V0FSTklOR3xFUlJPUnxMT0d8RkFUQUx8UEFOSUM/KTouKicsIG51bWVyaWNfZ3JvdXBzOiB0cnVlKVxuXG4gICAgICBpZiBlcnIgIT0gbnVsbCB8fCBwYXJzZWQgPT0gbnVsbCB7XG4gICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImluZm9cIlxuICAgICAgfVxuICAgICAgaWYgcGFyc2VkICE9IG51bGwge1xuICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAgICAgaWYgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9PSBcImluZm9cIiB7XG4gICAgICAgICAgLm1ldGFkYXRhLnBhcnNlZC5lcnJvcl9zZXZlcml0eSA9IFwibG9nXCJcbiAgICAgIH1cbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSB1cGNhc2UhKC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkpXG5cbnNpbmtzOlxuICBsb2dmbGFyZV9hdXRoOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gYXV0aF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWdvdHJ1ZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3JlYWx0aW1lOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVhbHRpbWVfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1yZWFsdGltZS5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3Jlc3Q6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByZXN0X2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9cG9zdGdSRVNULmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZGI6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBkYl9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgIyBXZSBtdXN0IHJvdXRlIHRoZSBzaW5rIHRocm91Z2gga29uZyBiZWNhdXNlIGluZ2VzdGluZyBsb2dzIGJlZm9yZSBsb2dmbGFyZSBpcyBmdWxseSBpbml0aWFsaXNlZCB3aWxsXG4gICAgIyBsZWFkIHRvIGJyb2tlbiBxdWVyaWVzIGZyb20gc3R1ZGlvLiBUaGlzIHdvcmtzIGJ5IHRoZSBhc3N1bXB0aW9uIHRoYXQgY29udGFpbmVycyBhcmUgc3RhcnRlZCBpbiB0aGVcbiAgICAjIGZvbGxvd2luZyBvcmRlcjogdmVjdG9yID4gZGIgPiBsb2dmbGFyZSA+IGtvbmdcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwL2FuYWx5dGljcy92MS9hcGkvbG9ncz9zb3VyY2VfbmFtZT1wb3N0Z3Jlcy5sb2dzJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9mdW5jdGlvbnM6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIuZnVuY3Rpb25zXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWRlbm8tcmVsYXktbG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfc3RvcmFnZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHN0b3JhZ2VfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1zdG9yYWdlLmxvZ3MucHJvZC4yJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9rb25nOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0ga29uZ19sb2dzXG4gICAgICAtIGtvbmdfZXJyXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPWNsb3VkZmxhcmUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuIgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jazpybycKICAgIGVudmlyb25tZW50OgogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICBjb21tYW5kOgogICAgICAtICctLWNvbmZpZycKICAgICAgLSBldGMvdmVjdG9yL3ZlY3Rvci55bWwKICBzdXBhYmFzZS1yZXN0OgogICAgaW1hZ2U6ICdwb3N0Z3Jlc3QvcG9zdGdyZXN0OnYxMi4yLjAnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHJlc3RhcnQ6IHVubGVzcy1zdG9wcGVkCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEdSU1RfREJfVVJJPXBvc3RncmVzOi8vYXV0aGVudGljYXRvcjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpY30nCiAgICAgIC0gUEdSU1RfREJfQU5PTl9ST0xFPWFub24KICAgICAgLSAnUEdSU1RfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBQR1JTVF9EQl9VU0VfTEVHQUNZX0dVQ1M9ZmFsc2UKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ1BHUlNUX0FQUF9TRVRUSU5HU19KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICBjb21tYW5kOiBwb3N0Z3Jlc3QKICAgIGV4Y2x1ZGVfZnJvbV9oYzogdHJ1ZQogIHN1cGFiYXNlLWF1dGg6CiAgICBpbWFnZTogJ3N1cGFiYXNlL2dvdHJ1ZTp2Mi4xNTEuMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6OTk5OS9oZWFsdGgnCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBHT1RSVUVfQVBJX0hPU1Q9MC4wLjAuMAogICAgICAtIEdPVFJVRV9BUElfUE9SVD05OTk5CiAgICAgIC0gJ0FQSV9FWFRFUk5BTF9VUkw9JHtBUElfRVhURVJOQUxfVVJMOi1odHRwOi8vc3VwYWJhc2Uta29uZzo4MDAwfScKICAgICAgLSBHT1RSVUVfREJfRFJJVkVSPXBvc3RncmVzCiAgICAgIC0gJ0dPVFJVRV9EQl9EQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9hdXRoX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdHT1RSVUVfU0lURV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnR09UUlVFX1VSSV9BTExPV19MSVNUPSR7QURESVRJT05BTF9SRURJUkVDVF9VUkxTfScKICAgICAgLSAnR09UUlVFX0RJU0FCTEVfU0lHTlVQPSR7RElTQUJMRV9TSUdOVVA6LWZhbHNlfScKICAgICAgLSBHT1RSVUVfSldUX0FETUlOX1JPTEVTPXNlcnZpY2Vfcm9sZQogICAgICAtIEdPVFJVRV9KV1RfQVVEPWF1dGhlbnRpY2F0ZWQKICAgICAgLSBHT1RSVUVfSldUX0RFRkFVTFRfR1JPVVBfTkFNRT1hdXRoZW50aWNhdGVkCiAgICAgIC0gJ0dPVFJVRV9KV1RfRVhQPSR7SldUX0VYUElSWTotMzYwMH0nCiAgICAgIC0gJ0dPVFJVRV9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfRU1BSUxfRU5BQkxFRD0ke0VOQUJMRV9FTUFJTF9TSUdOVVA6LXRydWV9JwogICAgICAtICdHT1RSVUVfRVhURVJOQUxfQU5PTllNT1VTX1VTRVJTX0VOQUJMRUQ9JHtFTkFCTEVfQU5PTllNT1VTX1VTRVJTOi1mYWxzZX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfQVVUT0NPTkZJUk09JHtFTkFCTEVfRU1BSUxfQVVUT0NPTkZJUk06LWZhbHNlfScKICAgICAgLSAnR09UUlVFX1NNVFBfQURNSU5fRU1BSUw9JHtTTVRQX0FETUlOX0VNQUlMfScKICAgICAgLSAnR09UUlVFX1NNVFBfSE9TVD0ke1NNVFBfSE9TVH0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BPUlQ9JHtTTVRQX1BPUlQ6LTU4N30nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1VTRVI9JHtTTVRQX1VTRVJ9JwogICAgICAtICdHT1RSVUVfU01UUF9QQVNTPSR7U01UUF9QQVNTfScKICAgICAgLSAnR09UUlVFX1NNVFBfU0VOREVSX05BTUU9JHtTTVRQX1NFTkRFUl9OQU1FfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19JTlZJVEU9JHtNQUlMRVJfVVJMUEFUSFNfSU5WSVRFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9VUkxQQVRIU19DT05GSVJNQVRJT046LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk9JHtNQUlMRVJfVVJMUEFUSFNfUkVDT1ZFUlk6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1VSTFBBVEhTX0VNQUlMX0NIQU5HRTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfSU5WSVRFPSR7TUFJTEVSX1RFTVBMQVRFU19JTlZJVEV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19DT05GSVJNQVRJT049JHtNQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZPSR7TUFJTEVSX1RFTVBMQVRFU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTks9JHtNQUlMRVJfVEVNUExBVEVTX01BR0lDX0xJTkt9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1NVQkpFQ1RTX0NPTkZJUk1BVElPTn0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUlk9JHtNQUlMRVJfU1VCSkVDVFNfUkVDT1ZFUll9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1NVQkpFQ1RTX01BR0lDX0xJTks9JHtNQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFPSR7TUFJTEVSX1NVQkpFQ1RTX0VNQUlMX0NIQU5HRX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfSU5WSVRFPSR7TUFJTEVSX1NVQkpFQ1RTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9QSE9ORV9FTkFCTEVEPSR7RU5BQkxFX1BIT05FX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9TTVNfQVVUT0NPTkZJUk09JHtFTkFCTEVfUEhPTkVfQVVUT0NPTkZJUk06LXRydWV9JwogIHJlYWx0aW1lLWRldjoKICAgIGltYWdlOiAnc3VwYWJhc2UvcmVhbHRpbWU6djIuMzAuMjMnCiAgICBjb250YWluZXJfbmFtZTogcmVhbHRpbWUtZGV2LnN1cGFiYXNlLXJlYWx0aW1lCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLXNTZkwnCiAgICAgICAgLSAnLS1oZWFkJwogICAgICAgIC0gJy1vJwogICAgICAgIC0gL2Rldi9udWxsCiAgICAgICAgLSAnLUgnCiAgICAgICAgLSAnQXV0aG9yaXphdGlvbjogQmVhcmVyICR7U0VSVklDRV9TVVBBQkFTRUFOT05fS0VZfScKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjQwMDAvYXBpL3RlbmFudHMvcmVhbHRpbWUtZGV2L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPUlQ9NDAwMAogICAgICAtICdEQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtIERCX1VTRVI9c3VwYWJhc2VfYWRtaW4KICAgICAgLSAnREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgICAgLSAnREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0RCX0FGVEVSX0NPTk5FQ1RfUVVFUlk9U0VUIHNlYXJjaF9wYXRoIFRPIF9yZWFsdGltZScKICAgICAgLSBEQl9FTkNfS0VZPXN1cGFiYXNlcmVhbHRpbWUKICAgICAgLSAnQVBJX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gRkxZX0FMTE9DX0lEPWZseTEyMwogICAgICAtIEZMWV9BUFBfTkFNRT1yZWFsdGltZQogICAgICAtICdTRUNSRVRfS0VZX0JBU0U9JHtTRUNSRVRfUEFTU1dPUkRfUkVBTFRJTUV9JwogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgICAtIEVOQUJMRV9UQUlMU0NBTEU9ZmFsc2UKICAgICAgLSAiRE5TX05PREVTPScnIgogICAgICAtIFJMSU1JVF9OT0ZJTEU9MTAwMDAKICAgICAgLSBBUFBfTkFNRT1yZWFsdGltZQogICAgICAtIFNFRURfU0VMRl9IT1NUPXRydWUKICAgIGNvbW1hbmQ6ICJzaCAtYyBcIi9hcHAvYmluL21pZ3JhdGUgJiYgL2FwcC9iaW4vcmVhbHRpbWUgZXZhbCAnUmVhbHRpbWUuUmVsZWFzZS5zZWVkcyhSZWFsdGltZS5SZXBvKScgJiYgL2FwcC9iaW4vc2VydmVyXCJcbiIKICBzdXBhYmFzZS1taW5pbzoKICAgIGltYWdlOiBtaW5pby9taW5pbwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01JTklPX1JPT1RfVVNFUj0ke1NFUlZJQ0VfVVNFUl9NSU5JT30nCiAgICAgIC0gJ01JTklPX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgIGNvbW1hbmQ6ICdzZXJ2ZXIgLS1jb25zb2xlLWFkZHJlc3MgIjo5MDAxIiAvZGF0YScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAnc2xlZXAgNSAmJiBleGl0IDAnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogNQogICAgdm9sdW1lczoKICAgICAgLSAnLi92b2x1bWVzL3N0b3JhZ2U6L2RhdGEnCiAgbWluaW8tY3JlYXRlYnVja2V0OgogICAgaW1hZ2U6IG1pbmlvL21jCiAgICByZXN0YXJ0OiAnbm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtbWluaW86CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudHJ5cG9pbnQ6CiAgICAgIC0gL2VudHJ5cG9pbnQuc2gKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL2VudHJ5cG9pbnQuc2gKICAgICAgICB0YXJnZXQ6IC9lbnRyeXBvaW50LnNoCiAgICAgICAgY29udGVudDogIiMhL2Jpbi9zaFxuL3Vzci9iaW4vbWMgYWxpYXMgc2V0IHN1cGFiYXNlLW1pbmlvIGh0dHA6Ly9zdXBhYmFzZS1taW5pbzo5MDAwICR7TUlOSU9fUk9PVF9VU0VSfSAke01JTklPX1JPT1RfUEFTU1dPUkR9O1xuL3Vzci9iaW4vbWMgbWIgLS1pZ25vcmUtZXhpc3Rpbmcgc3VwYWJhc2UtbWluaW8vc3R1YjtcbmV4aXQgMFxuIgogIHN1cGFiYXNlLXN0b3JhZ2U6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3N0b3JhZ2UtYXBpOnYxLjAuNicKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLXJlc3Q6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgICAgaW1ncHJveHk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUwMDAvc3RhdHVzJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVkVSX1BPUlQ9NTAwMAogICAgICAtIFNFUlZFUl9SRUdJT049bG9jYWwKICAgICAgLSBNVUxUSV9URU5BTlQ9ZmFsc2UKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9zdG9yYWdlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIERCX0lOU1RBTExfUk9MRVM9ZmFsc2UKICAgICAgLSBTVE9SQUdFX0JBQ0tFTkQ9czMKICAgICAgLSBTVE9SQUdFX1MzX0JVQ0tFVD1zdHViCiAgICAgIC0gJ1NUT1JBR0VfUzNfRU5EUE9JTlQ9aHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAnCiAgICAgIC0gU1RPUkFHRV9TM19GT1JDRV9QQVRIX1NUWUxFPXRydWUKICAgICAgLSBTVE9SQUdFX1MzX1JFR0lPTj11cy1lYXN0LTEKICAgICAgLSAnQVdTX0FDQ0VTU19LRVlfSUQ9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9GSUxFX1NJWkVfTElNSVRfU1RBTkRBUkQ9NTI0Mjg4MDAwCiAgICAgIC0gVVBMT0FEX1NJR05FRF9VUkxfRVhQSVJBVElPTl9USU1FPTEyMAogICAgICAtIFRVU19VUkxfUEFUSD0vdXBsb2FkL3Jlc3VtYWJsZQogICAgICAtIFRVU19NQVhfU0laRT0zNjAwMDAwCiAgICAgIC0gSU1BR0VfVFJBTlNGT1JNQVRJT05fRU5BQkxFRD10cnVlCiAgICAgIC0gJ0lNR1BST1hZX1VSTD1odHRwOi8vaW1ncHJveHk6ODA4MCcKICAgICAgLSBJTUdQUk9YWV9SRVFVRVNUX1RJTUVPVVQ9MTUKICAgICAgLSBEQVRBQkFTRV9TRUFSQ0hfUEFUSD1zdG9yYWdlCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIGltZ3Byb3h5OgogICAgaW1hZ2U6ICdkYXJ0aHNpbS9pbWdwcm94eTp2My44LjAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaW1ncHJveHkKICAgICAgICAtIGhlYWx0aAogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSU1HUFJPWFlfTE9DQUxfRklMRVNZU1RFTV9ST09UPS8KICAgICAgLSBJTUdQUk9YWV9VU0VfRVRBRz10cnVlCiAgICAgIC0gJ0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTj0ke0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTjotdHJ1ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIHN1cGFiYXNlLW1ldGE6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzLW1ldGE6djAuODMuMicKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEdfTUVUQV9QT1JUPTgwODAKICAgICAgLSAnUEdfTUVUQV9EQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUEdfTUVUQV9EQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BHX01FVEFfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUEdfTUVUQV9EQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ1BHX01FVEFfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICBzdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnc3VwYWJhc2UvZWRnZS1ydW50aW1lOnYxLjUzLjMnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnRWRnZSBGdW5jdGlvbnMgaXMgaGVhbHRoeScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX1JPTEVfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnU1VQQUJBU0VfREJfVVJMPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlczoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnVkVSSUZZX0pXVD0ke0ZVTkNUSU9OU19WRVJJRllfSldUOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvZnVuY3Rpb25zOi9ob21lL2Rlbm8vZnVuY3Rpb25zJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgdGFyZ2V0OiAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgY29udGVudDogImltcG9ydCB7IHNlcnZlIH0gZnJvbSAnaHR0cHM6Ly9kZW5vLmxhbmQvc3RkQDAuMTMxLjAvaHR0cC9zZXJ2ZXIudHMnXG5pbXBvcnQgKiBhcyBqb3NlIGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3gvam9zZUB2NC4xNC40L2luZGV4LnRzJ1xuXG5jb25zb2xlLmxvZygnbWFpbiBmdW5jdGlvbiBzdGFydGVkJylcblxuY29uc3QgSldUX1NFQ1JFVCA9IERlbm8uZW52LmdldCgnSldUX1NFQ1JFVCcpXG5jb25zdCBWRVJJRllfSldUID0gRGVuby5lbnYuZ2V0KCdWRVJJRllfSldUJykgPT09ICd0cnVlJ1xuXG5mdW5jdGlvbiBnZXRBdXRoVG9rZW4ocmVxOiBSZXF1ZXN0KSB7XG4gIGNvbnN0IGF1dGhIZWFkZXIgPSByZXEuaGVhZGVycy5nZXQoJ2F1dGhvcml6YXRpb24nKVxuICBpZiAoIWF1dGhIZWFkZXIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ01pc3NpbmcgYXV0aG9yaXphdGlvbiBoZWFkZXInKVxuICB9XG4gIGNvbnN0IFtiZWFyZXIsIHRva2VuXSA9IGF1dGhIZWFkZXIuc3BsaXQoJyAnKVxuICBpZiAoYmVhcmVyICE9PSAnQmVhcmVyJykge1xuICAgIHRocm93IG5ldyBFcnJvcihgQXV0aCBoZWFkZXIgaXMgbm90ICdCZWFyZXIge3Rva2VufSdgKVxuICB9XG4gIHJldHVybiB0b2tlblxufVxuXG5hc3luYyBmdW5jdGlvbiB2ZXJpZnlKV1Qoand0OiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgY29uc3QgZW5jb2RlciA9IG5ldyBUZXh0RW5jb2RlcigpXG4gIGNvbnN0IHNlY3JldEtleSA9IGVuY29kZXIuZW5jb2RlKEpXVF9TRUNSRVQpXG4gIHRyeSB7XG4gICAgYXdhaXQgam9zZS5qd3RWZXJpZnkoand0LCBzZWNyZXRLZXkpXG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGNvbnNvbGUuZXJyb3IoZXJyKVxuICAgIHJldHVybiBmYWxzZVxuICB9XG4gIHJldHVybiB0cnVlXG59XG5cbnNlcnZlKGFzeW5jIChyZXE6IFJlcXVlc3QpID0+IHtcbiAgaWYgKHJlcS5tZXRob2QgIT09ICdPUFRJT05TJyAmJiBWRVJJRllfSldUKSB7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IHRva2VuID0gZ2V0QXV0aFRva2VuKHJlcSlcbiAgICAgIGNvbnN0IGlzVmFsaWRKV1QgPSBhd2FpdCB2ZXJpZnlKV1QodG9rZW4pXG5cbiAgICAgIGlmICghaXNWYWxpZEpXVCkge1xuICAgICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KHsgbXNnOiAnSW52YWxpZCBKV1QnIH0pLCB7XG4gICAgICAgICAgc3RhdHVzOiA0MDEsXG4gICAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgY29uc29sZS5lcnJvcihlKVxuICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogZS50b1N0cmluZygpIH0pLCB7XG4gICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgIH0pXG4gICAgfVxuICB9XG5cbiAgY29uc3QgdXJsID0gbmV3IFVSTChyZXEudXJsKVxuICBjb25zdCB7IHBhdGhuYW1lIH0gPSB1cmxcbiAgY29uc3QgcGF0aF9wYXJ0cyA9IHBhdGhuYW1lLnNwbGl0KCcvJylcbiAgY29uc3Qgc2VydmljZV9uYW1lID0gcGF0aF9wYXJ0c1sxXVxuXG4gIGlmICghc2VydmljZV9uYW1lIHx8IHNlcnZpY2VfbmFtZSA9PT0gJycpIHtcbiAgICBjb25zdCBlcnJvciA9IHsgbXNnOiAnbWlzc2luZyBmdW5jdGlvbiBuYW1lIGluIHJlcXVlc3QnIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA0MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG5cbiAgY29uc3Qgc2VydmljZVBhdGggPSBgL2hvbWUvZGVuby9mdW5jdGlvbnMvJHtzZXJ2aWNlX25hbWV9YFxuICBjb25zb2xlLmVycm9yKGBzZXJ2aW5nIHRoZSByZXF1ZXN0IHdpdGggJHtzZXJ2aWNlUGF0aH1gKVxuXG4gIGNvbnN0IG1lbW9yeUxpbWl0TWIgPSAxNTBcbiAgY29uc3Qgd29ya2VyVGltZW91dE1zID0gMSAqIDYwICogMTAwMFxuICBjb25zdCBub01vZHVsZUNhY2hlID0gZmFsc2VcbiAgY29uc3QgaW1wb3J0TWFwUGF0aCA9IG51bGxcbiAgY29uc3QgZW52VmFyc09iaiA9IERlbm8uZW52LnRvT2JqZWN0KClcbiAgY29uc3QgZW52VmFycyA9IE9iamVjdC5rZXlzKGVudlZhcnNPYmopLm1hcCgoaykgPT4gW2ssIGVudlZhcnNPYmpba11dKVxuXG4gIHRyeSB7XG4gICAgY29uc3Qgd29ya2VyID0gYXdhaXQgRWRnZVJ1bnRpbWUudXNlcldvcmtlcnMuY3JlYXRlKHtcbiAgICAgIHNlcnZpY2VQYXRoLFxuICAgICAgbWVtb3J5TGltaXRNYixcbiAgICAgIHdvcmtlclRpbWVvdXRNcyxcbiAgICAgIG5vTW9kdWxlQ2FjaGUsXG4gICAgICBpbXBvcnRNYXBQYXRoLFxuICAgICAgZW52VmFycyxcbiAgICB9KVxuICAgIHJldHVybiBhd2FpdCB3b3JrZXIuZmV0Y2gocmVxKVxuICB9IGNhdGNoIChlKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogZS50b1N0cmluZygpIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA1MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG59KSIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICB0YXJnZXQ6IC9ob21lL2Rlbm8vZnVuY3Rpb25zL2hlbGxvL2luZGV4LnRzCiAgICAgICAgY29udGVudDogIi8vIEZvbGxvdyB0aGlzIHNldHVwIGd1aWRlIHRvIGludGVncmF0ZSB0aGUgRGVubyBsYW5ndWFnZSBzZXJ2ZXIgd2l0aCB5b3VyIGVkaXRvcjpcbi8vIGh0dHBzOi8vZGVuby5sYW5kL21hbnVhbC9nZXR0aW5nX3N0YXJ0ZWQvc2V0dXBfeW91cl9lbnZpcm9ubWVudFxuLy8gVGhpcyBlbmFibGVzIGF1dG9jb21wbGV0ZSwgZ28gdG8gZGVmaW5pdGlvbiwgZXRjLlxuXG5pbXBvcnQgeyBzZXJ2ZSB9IGZyb20gXCJodHRwczovL2Rlbm8ubGFuZC9zdGRAMC4xNzcuMS9odHRwL3NlcnZlci50c1wiXG5cbnNlcnZlKGFzeW5jICgpID0+IHtcbiAgcmV0dXJuIG5ldyBSZXNwb25zZShcbiAgICBgXCJIZWxsbyBmcm9tIEVkZ2UgRnVuY3Rpb25zIVwiYCxcbiAgICB7IGhlYWRlcnM6IHsgXCJDb250ZW50LVR5cGVcIjogXCJhcHBsaWNhdGlvbi9qc29uXCIgfSB9LFxuICApXG59KVxuXG4vLyBUbyBpbnZva2U6XG4vLyBjdXJsICdodHRwOi8vbG9jYWxob3N0OjxLT05HX0hUVFBfUE9SVD4vZnVuY3Rpb25zL3YxL2hlbGxvJyBcXFxuLy8gICAtLWhlYWRlciAnQXV0aG9yaXphdGlvbjogQmVhcmVyIDxhbm9uL3NlcnZpY2Vfcm9sZSBBUEkga2V5PidcbiIKICAgIGNvbW1hbmQ6CiAgICAgIC0gc3RhcnQKICAgICAgLSAnLS1tYWluLXNlcnZpY2UnCiAgICAgIC0gL2hvbWUvZGVuby9mdW5jdGlvbnMvbWFpbgo=", + "compose": "c2VydmljZXM6CiAgc3VwYWJhc2Uta29uZzoKICAgIGltYWdlOiAna29uZzoyLjguMScKICAgIGVudHJ5cG9pbnQ6ICdiYXNoIC1jICcnZXZhbCAiZWNobyBcIiQkKGNhdCB+L3RlbXAueW1sKVwiIiA+IH4va29uZy55bWwgJiYgL2RvY2tlci1lbnRyeXBvaW50LnNoIGtvbmcgZG9ja2VyLXN0YXJ0JycnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkdfODAwMAogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIEtPTkdfREFUQUJBU0U9b2ZmCiAgICAgIC0gS09OR19ERUNMQVJBVElWRV9DT05GSUc9L2hvbWUva29uZy9rb25nLnltbAogICAgICAtICdLT05HX0ROU19PUkRFUj1MQVNULEEsQ05BTUUnCiAgICAgIC0gJ0tPTkdfUExVR0lOUz1yZXF1ZXN0LXRyYW5zZm9ybWVyLGNvcnMsa2V5LWF1dGgsYWNsLGJhc2ljLWF1dGgnCiAgICAgIC0gS09OR19OR0lOWF9QUk9YWV9QUk9YWV9CVUZGRVJfU0laRT0xNjBrCiAgICAgIC0gJ0tPTkdfTkdJTlhfUFJPWFlfUFJPWFlfQlVGRkVSUz02NCAxNjBrJwogICAgICAtICdTVVBBQkFTRV9BTk9OX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX1NFUlZJQ0VfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnREFTSEJPQVJEX1VTRVJOQU1FPSR7U0VSVklDRV9VU0VSX0FETUlOfScKICAgICAgLSAnREFTSEJPQVJEX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9BRE1JTn0nCiAgICB2b2x1bWVzOgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2FwaS9rb25nLnltbAogICAgICAgIHRhcmdldDogL2hvbWUva29uZy90ZW1wLnltbAogICAgICAgIGNvbnRlbnQ6ICJfZm9ybWF0X3ZlcnNpb246ICcyLjEnXG5fdHJhbnNmb3JtOiB0cnVlXG5cbiMjI1xuIyMjIENvbnN1bWVycyAvIFVzZXJzXG4jIyNcbmNvbnN1bWVyczpcbiAgLSB1c2VybmFtZTogREFTSEJPQVJEXG4gIC0gdXNlcm5hbWU6IGFub25cbiAgICBrZXlhdXRoX2NyZWRlbnRpYWxzOlxuICAgICAgLSBrZXk6ICRTVVBBQkFTRV9BTk9OX0tFWVxuICAtIHVzZXJuYW1lOiBzZXJ2aWNlX3JvbGVcbiAgICBrZXlhdXRoX2NyZWRlbnRpYWxzOlxuICAgICAgLSBrZXk6ICRTVVBBQkFTRV9TRVJWSUNFX0tFWVxuXG4jIyNcbiMjIyBBY2Nlc3MgQ29udHJvbCBMaXN0XG4jIyNcbmFjbHM6XG4gIC0gY29uc3VtZXI6IGFub25cbiAgICBncm91cDogYW5vblxuICAtIGNvbnN1bWVyOiBzZXJ2aWNlX3JvbGVcbiAgICBncm91cDogYWRtaW5cblxuIyMjXG4jIyMgRGFzaGJvYXJkIGNyZWRlbnRpYWxzXG4jIyNcbmJhc2ljYXV0aF9jcmVkZW50aWFsczpcbi0gY29uc3VtZXI6IERBU0hCT0FSRFxuICB1c2VybmFtZTogJERBU0hCT0FSRF9VU0VSTkFNRVxuICBwYXNzd29yZDogJERBU0hCT0FSRF9QQVNTV09SRFxuXG5cbiMjI1xuIyMjIEFQSSBSb3V0ZXNcbiMjI1xuc2VydmljZXM6XG5cbiAgIyMgT3BlbiBBdXRoIHJvdXRlc1xuICAtIG5hbWU6IGF1dGgtdjEtb3BlblxuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS92ZXJpZnlcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGF1dGgtdjEtb3BlblxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvdmVyaWZ5XG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAtIG5hbWU6IGF1dGgtdjEtb3Blbi1jYWxsYmFja1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLWF1dGg6OTk5OS9jYWxsYmFja1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1vcGVuLWNhbGxiYWNrXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYXV0aC92MS9jYWxsYmFja1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgLSBuYW1lOiBhdXRoLXYxLW9wZW4tYXV0aG9yaXplXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5L2F1dGhvcml6ZVxuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1vcGVuLWF1dGhvcml6ZVxuICAgICAgICBzdHJpcF9wYXRoOiB0cnVlXG4gICAgICAgIHBhdGhzOlxuICAgICAgICAgIC0gL2F1dGgvdjEvYXV0aG9yaXplXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIFNlY3VyZSBBdXRoIHJvdXRlc1xuICAtIG5hbWU6IGF1dGgtdjFcbiAgICBfY29tbWVudDogJ0dvVHJ1ZTogL2F1dGgvdjEvKiAtPiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5LyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYXV0aDo5OTk5L1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogYXV0aC12MS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9hdXRoL3YxL1xuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IGZhbHNlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgUkVTVCByb3V0ZXNcbiAgLSBuYW1lOiByZXN0LXYxXG4gICAgX2NvbW1lbnQ6ICdQb3N0Z1JFU1Q6IC9yZXN0L3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlc3QtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVzdC92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4gICAgICAtIG5hbWU6IGFjbFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9ncm91cHNfaGVhZGVyOiB0cnVlXG4gICAgICAgICAgYWxsb3c6XG4gICAgICAgICAgICAtIGFkbWluXG4gICAgICAgICAgICAtIGFub25cblxuICAjIyBTZWN1cmUgR3JhcGhRTCByb3V0ZXNcbiAgLSBuYW1lOiBncmFwaHFsLXYxXG4gICAgX2NvbW1lbnQ6ICdQb3N0Z1JFU1Q6IC9ncmFwaHFsL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXJlc3Q6MzAwMC9ycGMvZ3JhcGhxbCdcbiAgICB1cmw6IGh0dHA6Ly9zdXBhYmFzZS1yZXN0OjMwMDAvcnBjL2dyYXBocWxcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGdyYXBocWwtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvZ3JhcGhxbC92MVxuICAgIHBsdWdpbnM6XG4gICAgICAtIG5hbWU6IGNvcnNcbiAgICAgIC0gbmFtZToga2V5LWF1dGhcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfY3JlZGVudGlhbHM6IHRydWVcbiAgICAgIC0gbmFtZTogcmVxdWVzdC10cmFuc2Zvcm1lclxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgYWRkOlxuICAgICAgICAgICAgaGVhZGVyczpcbiAgICAgICAgICAgICAgLSBDb250ZW50LVByb2ZpbGU6Z3JhcGhxbF9wdWJsaWNcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuXG4gICMjIFNlY3VyZSBSZWFsdGltZSByb3V0ZXNcbiAgLSBuYW1lOiByZWFsdGltZS12MS13c1xuICAgIF9jb21tZW50OiAnUmVhbHRpbWU6IC9yZWFsdGltZS92MS8qIC0+IHdzOi8vcmVhbHRpbWU6NDAwMC9zb2NrZXQvKidcbiAgICB1cmw6IGh0dHA6Ly9yZWFsdGltZS1kZXY6NDAwMC9zb2NrZXRcbiAgICBwcm90b2NvbDogd3NcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlYWx0aW1lLXYxLXdzXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvcmVhbHRpbWUvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cbiAgICAgICAgICAgIC0gYW5vblxuICAtIG5hbWU6IHJlYWx0aW1lLXYxLXJlc3RcbiAgICBfY29tbWVudDogJ1JlYWx0aW1lOiAvcmVhbHRpbWUvdjEvKiAtPiB3czovL3JlYWx0aW1lOjQwMDAvc29ja2V0LyonXG4gICAgdXJsOiBodHRwOi8vcmVhbHRpbWUtZGV2OjQwMDAvYXBpXG4gICAgcHJvdG9jb2w6IGh0dHBcbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHJlYWx0aW1lLXYxLXJlc3RcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9yZWFsdGltZS92MS9hcGlcbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG4gICAgICAtIG5hbWU6IGtleS1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiBmYWxzZVxuICAgICAgLSBuYW1lOiBhY2xcbiAgICAgICAgY29uZmlnOlxuICAgICAgICAgIGhpZGVfZ3JvdXBzX2hlYWRlcjogdHJ1ZVxuICAgICAgICAgIGFsbG93OlxuICAgICAgICAgICAgLSBhZG1pblxuICAgICAgICAgICAgLSBhbm9uXG5cbiAgIyMgU3RvcmFnZSByb3V0ZXM6IHRoZSBzdG9yYWdlIHNlcnZlciBtYW5hZ2VzIGl0cyBvd24gYXV0aFxuICAtIG5hbWU6IHN0b3JhZ2UtdjFcbiAgICBfY29tbWVudDogJ1N0b3JhZ2U6IC9zdG9yYWdlL3YxLyogLT4gaHR0cDovL3N1cGFiYXNlLXN0b3JhZ2U6NTAwMC8qJ1xuICAgIHVybDogaHR0cDovL3N1cGFiYXNlLXN0b3JhZ2U6NTAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IHN0b3JhZ2UtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvc3RvcmFnZS92MS9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBjb3JzXG5cbiAgIyMgRWRnZSBGdW5jdGlvbnMgcm91dGVzXG4gIC0gbmFtZTogZnVuY3Rpb25zLXYxXG4gICAgX2NvbW1lbnQ6ICdFZGdlIEZ1bmN0aW9uczogL2Z1bmN0aW9ucy92MS8qIC0+IGh0dHA6Ly9zdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczo5MDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtZWRnZS1mdW5jdGlvbnM6OTAwMC9cbiAgICByb3V0ZXM6XG4gICAgICAtIG5hbWU6IGZ1bmN0aW9ucy12MS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9mdW5jdGlvbnMvdjEvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuXG4gICMjIEFuYWx5dGljcyByb3V0ZXNcbiAgLSBuYW1lOiBhbmFseXRpY3MtdjFcbiAgICBfY29tbWVudDogJ0FuYWx5dGljczogL2FuYWx5dGljcy92MS8qIC0+IGh0dHA6Ly9sb2dmbGFyZTo0MDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBhbmFseXRpY3MtdjEtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvYW5hbHl0aWNzL3YxL1xuXG4gICMjIFNlY3VyZSBEYXRhYmFzZSByb3V0ZXNcbiAgLSBuYW1lOiBtZXRhXG4gICAgX2NvbW1lbnQ6ICdwZy1tZXRhOiAvcGcvKiAtPiBodHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2UtbWV0YTo4MDgwL1xuICAgIHJvdXRlczpcbiAgICAgIC0gbmFtZTogbWV0YS1hbGxcbiAgICAgICAgc3RyaXBfcGF0aDogdHJ1ZVxuICAgICAgICBwYXRoczpcbiAgICAgICAgICAtIC9wZy9cbiAgICBwbHVnaW5zOlxuICAgICAgLSBuYW1lOiBrZXktYXV0aFxuICAgICAgICBjb25maWc6XG4gICAgICAgICAgaGlkZV9jcmVkZW50aWFsczogZmFsc2VcbiAgICAgIC0gbmFtZTogYWNsXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2dyb3Vwc19oZWFkZXI6IHRydWVcbiAgICAgICAgICBhbGxvdzpcbiAgICAgICAgICAgIC0gYWRtaW5cblxuICAjIyBQcm90ZWN0ZWQgRGFzaGJvYXJkIC0gY2F0Y2ggYWxsIHJlbWFpbmluZyByb3V0ZXNcbiAgLSBuYW1lOiBkYXNoYm9hcmRcbiAgICBfY29tbWVudDogJ1N0dWRpbzogLyogLT4gaHR0cDovL3N0dWRpbzozMDAwLyonXG4gICAgdXJsOiBodHRwOi8vc3VwYWJhc2Utc3R1ZGlvOjMwMDAvXG4gICAgcm91dGVzOlxuICAgICAgLSBuYW1lOiBkYXNoYm9hcmQtYWxsXG4gICAgICAgIHN0cmlwX3BhdGg6IHRydWVcbiAgICAgICAgcGF0aHM6XG4gICAgICAgICAgLSAvXG4gICAgcGx1Z2luczpcbiAgICAgIC0gbmFtZTogY29yc1xuICAgICAgLSBuYW1lOiBiYXNpYy1hdXRoXG4gICAgICAgIGNvbmZpZzpcbiAgICAgICAgICBoaWRlX2NyZWRlbnRpYWxzOiB0cnVlXG4iCiAgc3VwYWJhc2Utc3R1ZGlvOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9zdHVkaW86MjAyNDA5MjMtMmUzZTkwYycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBub2RlCiAgICAgICAgLSAnLWUnCiAgICAgICAgLSAicmVxdWlyZSgnaHR0cCcpLmdldCgnaHR0cDovLzEyNy4wLjAuMTozMDAwL2FwaS9wcm9maWxlJywgKHIpID0+IHtpZiAoci5zdGF0dXNDb2RlICE9PSAyMDApIHByb2Nlc3MuZXhpdCgxKTsgZWxzZSBwcm9jZXNzLmV4aXQoMCk7IH0pLm9uKCdlcnJvcicsICgpID0+IHByb2Nlc3MuZXhpdCgxKSkiCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAzCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICAtIEhPU1ROQU1FPTAuMC4wLjAKICAgICAgLSAnU1RVRElPX1BHX01FVEFfVVJMPWh0dHA6Ly9zdXBhYmFzZS1tZXRhOjgwODAnCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RFRkFVTFRfT1JHQU5JWkFUSU9OX05BTUU9JHtTVFVESU9fREVGQVVMVF9PUkdBTklaQVRJT046LURlZmF1bHQgT3JnYW5pemF0aW9ufScKICAgICAgLSAnREVGQVVMVF9QUk9KRUNUX05BTUU9JHtTVFVESU9fREVGQVVMVF9QUk9KRUNUOi1EZWZhdWx0IFByb2plY3R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9aHR0cDovL3N1cGFiYXNlLWtvbmc6ODAwMCcKICAgICAgLSAnU1VQQUJBU0VfUFVCTElDX1VSTD0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtICdTVVBBQkFTRV9BTk9OX0tFWT0ke1NFUlZJQ0VfU1VQQUJBU0VBTk9OX0tFWX0nCiAgICAgIC0gJ1NVUEFCQVNFX1NFUlZJQ0VfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdMT0dGTEFSRV9BUElfS0VZPSR7U0VSVklDRV9QQVNTV09SRF9MT0dGTEFSRX0nCiAgICAgIC0gJ0xPR0ZMQVJFX1VSTD1odHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAnCiAgICAgIC0gJ1NVUEFCQVNFX1BVQkxJQ19BUEk9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSBORVhUX1BVQkxJQ19FTkFCTEVfTE9HUz10cnVlCiAgICAgIC0gTkVYVF9BTkFMWVRJQ1NfQkFDS0VORF9QUk9WSURFUj1wb3N0Z3JlcwogIHN1cGFiYXNlLWRiOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9wb3N0Z3JlczoxNS4xLjEuNzgnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDogJ3BnX2lzcmVhZHkgLVUgcG9zdGdyZXMgLWggMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLXZlY3RvcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgY29tbWFuZDoKICAgICAgLSBwb3N0Z3JlcwogICAgICAtICctYycKICAgICAgLSBjb25maWdfZmlsZT0vZXRjL3Bvc3RncmVzcWwvcG9zdGdyZXNxbC5jb25mCiAgICAgIC0gJy1jJwogICAgICAtIGxvZ19taW5fbWVzc2FnZXM9ZmF0YWwKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX0hPU1Q9L3Zhci9ydW4vcG9zdGdyZXNxbAogICAgICAtICdQR1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdQR1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BHREFUQUJBU0U9JHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3N1cGFiYXNlLWRiLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2RiL3JlYWx0aW1lLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktcmVhbHRpbWUuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfcmVhbHRpbWU7XG5hbHRlciBzY2hlbWEgX3JlYWx0aW1lIG93bmVyIHRvIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvX3N1cGFiYXNlLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTctX3N1cGFiYXNlLnNxbAogICAgICAgIGNvbnRlbnQ6ICJcXHNldCBwZ3VzZXIgYGVjaG8gXCIkUE9TVEdSRVNfVVNFUlwiYFxuXG5DUkVBVEUgREFUQUJBU0UgX3N1cGFiYXNlIFdJVEggT1dORVIgOnBndXNlcjtcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9wb29sZXIuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvbWlncmF0aW9ucy85OS1wb29sZXIuc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IHBndXNlciBgZWNobyBcInN1cGFiYXNlX2FkbWluXCJgXG5cXGMgX3N1cGFiYXNlXG5jcmVhdGUgc2NoZW1hIGlmIG5vdCBleGlzdHMgX3N1cGF2aXNvcjtcbmFsdGVyIHNjaGVtYSBfc3VwYXZpc29yIG93bmVyIHRvIDpwZ3VzZXI7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvd2ViaG9va3Muc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk4LXdlYmhvb2tzLnNxbAogICAgICAgIGNvbnRlbnQ6ICJCRUdJTjtcbi0tIENyZWF0ZSBwZ19uZXQgZXh0ZW5zaW9uXG5DUkVBVEUgRVhURU5TSU9OIElGIE5PVCBFWElTVFMgcGdfbmV0IFNDSEVNQSBleHRlbnNpb25zO1xuLS0gQ3JlYXRlIHN1cGFiYXNlX2Z1bmN0aW9ucyBzY2hlbWFcbkNSRUFURSBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIEFVVEhPUklaQVRJT04gc3VwYWJhc2VfYWRtaW47XG5HUkFOVCBVU0FHRSBPTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gVEFCTEVTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gRlVOQ1RJT05TIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5BTFRFUiBERUZBVUxUIFBSSVZJTEVHRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBHUkFOVCBBTEwgT04gU0VRVUVOQ0VTIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMubWlncmF0aW9ucyBkZWZpbml0aW9uXG5DUkVBVEUgVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLm1pZ3JhdGlvbnMgKFxuICB2ZXJzaW9uIHRleHQgUFJJTUFSWSBLRVksXG4gIGluc2VydGVkX2F0IHRpbWVzdGFtcHR6IE5PVCBOVUxMIERFRkFVTFQgTk9XKClcbik7XG4tLSBJbml0aWFsIHN1cGFiYXNlX2Z1bmN0aW9ucyBtaWdyYXRpb25cbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCdpbml0aWFsJyk7XG4tLSBzdXBhYmFzZV9mdW5jdGlvbnMuaG9va3MgZGVmaW5pdGlvblxuQ1JFQVRFIFRBQkxFIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyAoXG4gIGlkIGJpZ3NlcmlhbCBQUklNQVJZIEtFWSxcbiAgaG9va190YWJsZV9pZCBpbnRlZ2VyIE5PVCBOVUxMLFxuICBob29rX25hbWUgdGV4dCBOT1QgTlVMTCxcbiAgY3JlYXRlZF9hdCB0aW1lc3RhbXB0eiBOT1QgTlVMTCBERUZBVUxUIE5PVygpLFxuICByZXF1ZXN0X2lkIGJpZ2ludFxuKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfcmVxdWVzdF9pZF9pZHggT04gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIFVTSU5HIGJ0cmVlIChyZXF1ZXN0X2lkKTtcbkNSRUFURSBJTkRFWCBzdXBhYmFzZV9mdW5jdGlvbnNfaG9va3NfaF90YWJsZV9pZF9oX25hbWVfaWR4IE9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5ob29rcyBVU0lORyBidHJlZSAoaG9va190YWJsZV9pZCwgaG9va19uYW1lKTtcbkNPTU1FTlQgT04gVEFCTEUgc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzIElTICdTdXBhYmFzZSBGdW5jdGlvbnMgSG9va3M6IEF1ZGl0IHRyYWlsIGZvciB0cmlnZ2VyZWQgaG9va3MuJztcbkNSRUFURSBGVU5DVElPTiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KClcbiAgUkVUVVJOUyB0cmlnZ2VyXG4gIExBTkdVQUdFIHBscGdzcWxcbiAgQVMgJGZ1bmN0aW9uJFxuICBERUNMQVJFXG4gICAgcmVxdWVzdF9pZCBiaWdpbnQ7XG4gICAgcGF5bG9hZCBqc29uYjtcbiAgICB1cmwgdGV4dCA6PSBUR19BUkdWWzBdOjp0ZXh0O1xuICAgIG1ldGhvZCB0ZXh0IDo9IFRHX0FSR1ZbMV06OnRleHQ7XG4gICAgaGVhZGVycyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHBhcmFtcyBqc29uYiBERUZBVUxUICd7fSc6Ompzb25iO1xuICAgIHRpbWVvdXRfbXMgaW50ZWdlciBERUZBVUxUIDEwMDA7XG4gIEJFR0lOXG4gICAgSUYgdXJsIElTIE5VTEwgT1IgdXJsID0gJ251bGwnIFRIRU5cbiAgICAgIFJBSVNFIEVYQ0VQVElPTiAndXJsIGFyZ3VtZW50IGlzIG1pc3NpbmcnO1xuICAgIEVORCBJRjtcblxuICAgIElGIG1ldGhvZCBJUyBOVUxMIE9SIG1ldGhvZCA9ICdudWxsJyBUSEVOXG4gICAgICBSQUlTRSBFWENFUFRJT04gJ21ldGhvZCBhcmd1bWVudCBpcyBtaXNzaW5nJztcbiAgICBFTkQgSUY7XG5cbiAgICBJRiBUR19BUkdWWzJdIElTIE5VTEwgT1IgVEdfQVJHVlsyXSA9ICdudWxsJyBUSEVOXG4gICAgICBoZWFkZXJzID0gJ3tcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIn0nOjpqc29uYjtcbiAgICBFTFNFXG4gICAgICBoZWFkZXJzID0gVEdfQVJHVlsyXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVlszXSBJUyBOVUxMIE9SIFRHX0FSR1ZbM10gPSAnbnVsbCcgVEhFTlxuICAgICAgcGFyYW1zID0gJ3t9Jzo6anNvbmI7XG4gICAgRUxTRVxuICAgICAgcGFyYW1zID0gVEdfQVJHVlszXTo6anNvbmI7XG4gICAgRU5EIElGO1xuXG4gICAgSUYgVEdfQVJHVls0XSBJUyBOVUxMIE9SIFRHX0FSR1ZbNF0gPSAnbnVsbCcgVEhFTlxuICAgICAgdGltZW91dF9tcyA9IDEwMDA7XG4gICAgRUxTRVxuICAgICAgdGltZW91dF9tcyA9IFRHX0FSR1ZbNF06OmludGVnZXI7XG4gICAgRU5EIElGO1xuXG4gICAgQ0FTRVxuICAgICAgV0hFTiBtZXRob2QgPSAnR0VUJyBUSEVOXG4gICAgICAgIFNFTEVDVCBodHRwX2dldCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9nZXQoXG4gICAgICAgICAgdXJsLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIFdIRU4gbWV0aG9kID0gJ1BPU1QnIFRIRU5cbiAgICAgICAgcGF5bG9hZCA9IGpzb25iX2J1aWxkX29iamVjdChcbiAgICAgICAgICAnb2xkX3JlY29yZCcsIE9MRCxcbiAgICAgICAgICAncmVjb3JkJywgTkVXLFxuICAgICAgICAgICd0eXBlJywgVEdfT1AsXG4gICAgICAgICAgJ3RhYmxlJywgVEdfVEFCTEVfTkFNRSxcbiAgICAgICAgICAnc2NoZW1hJywgVEdfVEFCTEVfU0NIRU1BXG4gICAgICAgICk7XG5cbiAgICAgICAgU0VMRUNUIGh0dHBfcG9zdCBJTlRPIHJlcXVlc3RfaWQgRlJPTSBuZXQuaHR0cF9wb3N0KFxuICAgICAgICAgIHVybCxcbiAgICAgICAgICBwYXlsb2FkLFxuICAgICAgICAgIHBhcmFtcyxcbiAgICAgICAgICBoZWFkZXJzLFxuICAgICAgICAgIHRpbWVvdXRfbXNcbiAgICAgICAgKTtcbiAgICAgIEVMU0VcbiAgICAgICAgUkFJU0UgRVhDRVBUSU9OICdtZXRob2QgYXJndW1lbnQgJSBpcyBpbnZhbGlkJywgbWV0aG9kO1xuICAgIEVORCBDQVNFO1xuXG4gICAgSU5TRVJUIElOVE8gc3VwYWJhc2VfZnVuY3Rpb25zLmhvb2tzXG4gICAgICAoaG9va190YWJsZV9pZCwgaG9va19uYW1lLCByZXF1ZXN0X2lkKVxuICAgIFZBTFVFU1xuICAgICAgKFRHX1JFTElELCBUR19OQU1FLCByZXF1ZXN0X2lkKTtcblxuICAgIFJFVFVSTiBORVc7XG4gIEVORFxuJGZ1bmN0aW9uJDtcbi0tIFN1cGFiYXNlIHN1cGVyIGFkbWluXG5ET1xuJCRcbkJFR0lOXG4gIElGIE5PVCBFWElTVFMgKFxuICAgIFNFTEVDVCAxXG4gICAgRlJPTSBwZ19yb2xlc1xuICAgIFdIRVJFIHJvbG5hbWUgPSAnc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluJ1xuICApXG4gIFRIRU5cbiAgICBDUkVBVEUgVVNFUiBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gTk9JTkhFUklUIENSRUFURVJPTEUgTE9HSU4gTk9SRVBMSUNBVElPTjtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIFNDSEVNQSBzdXBhYmFzZV9mdW5jdGlvbnMgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuR1JBTlQgQUxMIFBSSVZJTEVHRVMgT04gQUxMIFRBQkxFUyBJTiBTQ0hFTUEgc3VwYWJhc2VfZnVuY3Rpb25zIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkdSQU5UIEFMTCBQUklWSUxFR0VTIE9OIEFMTCBTRVFVRU5DRVMgSU4gU0NIRU1BIHN1cGFiYXNlX2Z1bmN0aW9ucyBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5BTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBTRVQgc2VhcmNoX3BhdGggPSBcInN1cGFiYXNlX2Z1bmN0aW9uc1wiO1xuQUxURVIgdGFibGUgXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5taWdyYXRpb25zIE9XTkVSIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbjtcbkFMVEVSIHRhYmxlIFwic3VwYWJhc2VfZnVuY3Rpb25zXCIuaG9va3MgT1dORVIgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluO1xuQUxURVIgZnVuY3Rpb24gXCJzdXBhYmFzZV9mdW5jdGlvbnNcIi5odHRwX3JlcXVlc3QoKSBPV05FUiBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW47XG5HUkFOVCBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4gVE8gcG9zdGdyZXM7XG4tLSBSZW1vdmUgdW51c2VkIHN1cGFiYXNlX3BnX25ldF9hZG1pbiByb2xlXG5ET1xuJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX3JvbGVzXG4gICAgV0hFUkUgcm9sbmFtZSA9ICdzdXBhYmFzZV9wZ19uZXRfYWRtaW4nXG4gIClcbiAgVEhFTlxuICAgIFJFQVNTSUdOIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbiBUTyBzdXBhYmFzZV9hZG1pbjtcbiAgICBEUk9QIE9XTkVEIEJZIHN1cGFiYXNlX3BnX25ldF9hZG1pbjtcbiAgICBEUk9QIFJPTEUgc3VwYWJhc2VfcGdfbmV0X2FkbWluO1xuICBFTkQgSUY7XG5FTkRcbiQkO1xuLS0gcGdfbmV0IGdyYW50cyB3aGVuIGV4dGVuc2lvbiBpcyBhbHJlYWR5IGVuYWJsZWRcbkRPXG4kJFxuQkVHSU5cbiAgSUYgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXh0ZW5zaW9uXG4gICAgV0hFUkUgZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbi0tIEV2ZW50IHRyaWdnZXIgZm9yIHBnX25ldFxuQ1JFQVRFIE9SIFJFUExBQ0UgRlVOQ1RJT04gZXh0ZW5zaW9ucy5ncmFudF9wZ19uZXRfYWNjZXNzKClcblJFVFVSTlMgZXZlbnRfdHJpZ2dlclxuTEFOR1VBR0UgcGxwZ3NxbFxuQVMgJCRcbkJFR0lOXG4gIElGIEVYSVNUUyAoXG4gICAgU0VMRUNUIDFcbiAgICBGUk9NIHBnX2V2ZW50X3RyaWdnZXJfZGRsX2NvbW1hbmRzKCkgQVMgZXZcbiAgICBKT0lOIHBnX2V4dGVuc2lvbiBBUyBleHRcbiAgICBPTiBldi5vYmppZCA9IGV4dC5vaWRcbiAgICBXSEVSRSBleHQuZXh0bmFtZSA9ICdwZ19uZXQnXG4gIClcbiAgVEhFTlxuICAgIEdSQU5UIFVTQUdFIE9OIFNDSEVNQSBuZXQgVE8gc3VwYWJhc2VfZnVuY3Rpb25zX2FkbWluLCBwb3N0Z3JlcywgYW5vbiwgYXV0aGVudGljYXRlZCwgc2VydmljZV9yb2xlO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBTRUNVUklUWSBERUZJTkVSO1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VDVVJJVFkgREVGSU5FUjtcbiAgICBBTFRFUiBmdW5jdGlvbiBuZXQuaHR0cF9nZXQodXJsIHRleHQsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIEFMVEVSIGZ1bmN0aW9uIG5ldC5odHRwX3Bvc3QodXJsIHRleHQsIGJvZHkganNvbmIsIHBhcmFtcyBqc29uYiwgaGVhZGVycyBqc29uYiwgdGltZW91dF9taWxsaXNlY29uZHMgaW50ZWdlcikgU0VUIHNlYXJjaF9wYXRoID0gbmV0O1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfZ2V0KHVybCB0ZXh0LCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIEZST00gUFVCTElDO1xuICAgIFJFVk9LRSBBTEwgT04gRlVOQ1RJT04gbmV0Lmh0dHBfcG9zdCh1cmwgdGV4dCwgYm9keSBqc29uYiwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBGUk9NIFBVQkxJQztcbiAgICBHUkFOVCBFWEVDVVRFIE9OIEZVTkNUSU9OIG5ldC5odHRwX2dldCh1cmwgdGV4dCwgcGFyYW1zIGpzb25iLCBoZWFkZXJzIGpzb25iLCB0aW1lb3V0X21pbGxpc2Vjb25kcyBpbnRlZ2VyKSBUTyBzdXBhYmFzZV9mdW5jdGlvbnNfYWRtaW4sIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG4gICAgR1JBTlQgRVhFQ1VURSBPTiBGVU5DVElPTiBuZXQuaHR0cF9wb3N0KHVybCB0ZXh0LCBib2R5IGpzb25iLCBwYXJhbXMganNvbmIsIGhlYWRlcnMganNvbmIsIHRpbWVvdXRfbWlsbGlzZWNvbmRzIGludGVnZXIpIFRPIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiwgcG9zdGdyZXMsIGFub24sIGF1dGhlbnRpY2F0ZWQsIHNlcnZpY2Vfcm9sZTtcbiAgRU5EIElGO1xuRU5EO1xuJCQ7XG5DT01NRU5UIE9OIEZVTkNUSU9OIGV4dGVuc2lvbnMuZ3JhbnRfcGdfbmV0X2FjY2VzcyBJUyAnR3JhbnRzIGFjY2VzcyB0byBwZ19uZXQnO1xuRE9cbiQkXG5CRUdJTlxuICBJRiBOT1QgRVhJU1RTIChcbiAgICBTRUxFQ1QgMVxuICAgIEZST00gcGdfZXZlbnRfdHJpZ2dlclxuICAgIFdIRVJFIGV2dG5hbWUgPSAnaXNzdWVfcGdfbmV0X2FjY2VzcydcbiAgKSBUSEVOXG4gICAgQ1JFQVRFIEVWRU5UIFRSSUdHRVIgaXNzdWVfcGdfbmV0X2FjY2VzcyBPTiBkZGxfY29tbWFuZF9lbmQgV0hFTiBUQUcgSU4gKCdDUkVBVEUgRVhURU5TSU9OJylcbiAgICBFWEVDVVRFIFBST0NFRFVSRSBleHRlbnNpb25zLmdyYW50X3BnX25ldF9hY2Nlc3MoKTtcbiAgRU5EIElGO1xuRU5EXG4kJDtcbklOU0VSVCBJTlRPIHN1cGFiYXNlX2Z1bmN0aW9ucy5taWdyYXRpb25zICh2ZXJzaW9uKSBWQUxVRVMgKCcyMDIxMDgwOTE4MzQyM191cGRhdGVfZ3JhbnRzJyk7XG5BTFRFUiBmdW5jdGlvbiBzdXBhYmFzZV9mdW5jdGlvbnMuaHR0cF9yZXF1ZXN0KCkgU0VDVVJJVFkgREVGSU5FUjtcbkFMVEVSIGZ1bmN0aW9uIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBTRVQgc2VhcmNoX3BhdGggPSBzdXBhYmFzZV9mdW5jdGlvbnM7XG5SRVZPS0UgQUxMIE9OIEZVTkNUSU9OIHN1cGFiYXNlX2Z1bmN0aW9ucy5odHRwX3JlcXVlc3QoKSBGUk9NIFBVQkxJQztcbkdSQU5UIEVYRUNVVEUgT04gRlVOQ1RJT04gc3VwYWJhc2VfZnVuY3Rpb25zLmh0dHBfcmVxdWVzdCgpIFRPIHBvc3RncmVzLCBhbm9uLCBhdXRoZW50aWNhdGVkLCBzZXJ2aWNlX3JvbGU7XG5DT01NSVQ7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvcm9sZXMuc3FsCiAgICAgICAgdGFyZ2V0OiAvZG9ja2VyLWVudHJ5cG9pbnQtaW5pdGRiLmQvaW5pdC1zY3JpcHRzLzk5LXJvbGVzLnNxbAogICAgICAgIGNvbnRlbnQ6ICItLSBOT1RFOiBjaGFuZ2UgdG8geW91ciBvd24gcGFzc3dvcmRzIGZvciBwcm9kdWN0aW9uIGVudmlyb25tZW50c1xuIFxcc2V0IHBncGFzcyBgZWNobyBcIiRQT1NUR1JFU19QQVNTV09SRFwiYFxuXG4gQUxURVIgVVNFUiBhdXRoZW50aWNhdG9yIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgcGdib3VuY2VyIFdJVEggUEFTU1dPUkQgOidwZ3Bhc3MnO1xuIEFMVEVSIFVTRVIgc3VwYWJhc2VfYXV0aF9hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX2Z1bmN0aW9uc19hZG1pbiBXSVRIIFBBU1NXT1JEIDoncGdwYXNzJztcbiBBTFRFUiBVU0VSIHN1cGFiYXNlX3N0b3JhZ2VfYWRtaW4gV0lUSCBQQVNTV09SRCA6J3BncGFzcyc7XG4iCiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvZGIvand0LnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL2luaXQtc2NyaXB0cy85OS1qd3Quc3FsCiAgICAgICAgY29udGVudDogIlxcc2V0IGp3dF9zZWNyZXQgYGVjaG8gXCIkSldUX1NFQ1JFVFwiYFxuXFxzZXQgand0X2V4cCBgZWNobyBcIiRKV1RfRVhQXCJgXG5cXHNldCBkYl9uYW1lIGBlY2hvIFwiJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9XCJgXG5cbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3Rfc2VjcmV0XCIgVE8gOidqd3Rfc2VjcmV0JztcbkFMVEVSIERBVEFCQVNFIDpkYl9uYW1lIFNFVCBcImFwcC5zZXR0aW5ncy5qd3RfZXhwXCIgVE8gOidqd3RfZXhwJztcbiIKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vdm9sdW1lcy9kYi9sb2dzLnNxbAogICAgICAgIHRhcmdldDogL2RvY2tlci1lbnRyeXBvaW50LWluaXRkYi5kL21pZ3JhdGlvbnMvOTktbG9ncy5zcWwKICAgICAgICBjb250ZW50OiAiXFxzZXQgcGd1c2VyIGBlY2hvIFwic3VwYWJhc2VfYWRtaW5cImBcblxcYyBfc3VwYWJhc2VcbmNyZWF0ZSBzY2hlbWEgaWYgbm90IGV4aXN0cyBfYW5hbHl0aWNzO1xuYWx0ZXIgc2NoZW1hIF9hbmFseXRpY3Mgb3duZXIgdG8gOnBndXNlcjtcbiIKICAgICAgLSAnc3VwYWJhc2UtZGItY29uZmlnOi9ldGMvcG9zdGdyZXNxbC1jdXN0b20nCiAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9sb2dmbGFyZToxLjQuMCcKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo0MDAwL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDEwCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1kYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gTE9HRkxBUkVfTk9ERV9IT1NUPTEyNy4wLjAuMQogICAgICAtIERCX1VTRVJOQU1FPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gREJfREFUQUJBU0U9X3N1cGFiYXNlCiAgICAgIC0gJ0RCX0hPU1ROQU1FPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnREJfUE9SVD0ke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9JwogICAgICAtICdEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtIERCX1NDSEVNQT1fYW5hbHl0aWNzCiAgICAgIC0gJ0xPR0ZMQVJFX0FQSV9LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX0xPR0ZMQVJFfScKICAgICAgLSBMT0dGTEFSRV9TSU5HTEVfVEVOQU5UPXRydWUKICAgICAgLSBMT0dGTEFSRV9TSU5HTEVfVEVOQU5UX01PREU9dHJ1ZQogICAgICAtIExPR0ZMQVJFX1NVUEFCQVNFX01PREU9dHJ1ZQogICAgICAtIExPR0ZMQVJFX01JTl9DTFVTVEVSX1NJWkU9MQogICAgICAtICdQT1NUR1JFU19CQUNLRU5EX1VSTD1wb3N0Z3Jlc3FsOi8vc3VwYWJhc2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS9fc3VwYWJhc2UnCiAgICAgIC0gUE9TVEdSRVNfQkFDS0VORF9TQ0hFTUE9X2FuYWx5dGljcwogICAgICAtIExPR0ZMQVJFX0ZFQVRVUkVfRkxBR19PVkVSUklERT1tdWx0aWJhY2tlbmQ9dHJ1ZQogIHN1cGFiYXNlLXZlY3RvcjoKICAgIGltYWdlOiAndGltYmVyaW8vdmVjdG9yOjAuMjguMS1hbHBpbmUnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovL3N1cGFiYXNlLXZlY3Rvcjo5MDAxL2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvbG9ncy92ZWN0b3IueW1sCiAgICAgICAgdGFyZ2V0OiAvZXRjL3ZlY3Rvci92ZWN0b3IueW1sCiAgICAgICAgcmVhZF9vbmx5OiB0cnVlCiAgICAgICAgY29udGVudDogImFwaTpcbiAgZW5hYmxlZDogdHJ1ZVxuICBhZGRyZXNzOiAwLjAuMC4wOjkwMDFcblxuc291cmNlczpcbiAgZG9ja2VyX2hvc3Q6XG4gICAgdHlwZTogZG9ja2VyX2xvZ3NcbiAgICBleGNsdWRlX2NvbnRhaW5lcnM6XG4gICAgICAtIHN1cGFiYXNlLXZlY3RvclxuXG50cmFuc2Zvcm1zOlxuICBwcm9qZWN0X2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIGRvY2tlcl9ob3N0XG4gICAgc291cmNlOiB8LVxuICAgICAgLnByb2plY3QgPSBcImRlZmF1bHRcIlxuICAgICAgLmV2ZW50X21lc3NhZ2UgPSBkZWwoLm1lc3NhZ2UpXG4gICAgICAuYXBwbmFtZSA9IGRlbCguY29udGFpbmVyX25hbWUpXG4gICAgICBkZWwoLmNvbnRhaW5lcl9jcmVhdGVkX2F0KVxuICAgICAgZGVsKC5jb250YWluZXJfaWQpXG4gICAgICBkZWwoLnNvdXJjZV90eXBlKVxuICAgICAgZGVsKC5zdHJlYW0pXG4gICAgICBkZWwoLmxhYmVsKVxuICAgICAgZGVsKC5pbWFnZSlcbiAgICAgIGRlbCguaG9zdClcbiAgICAgIGRlbCguc3RyZWFtKVxuICByb3V0ZXI6XG4gICAgdHlwZTogcm91dGVcbiAgICBpbnB1dHM6XG4gICAgICAtIHByb2plY3RfbG9nc1xuICAgIHJvdXRlOlxuICAgICAga29uZzogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWtvbmdcIiknXG4gICAgICBhdXRoOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtYXV0aFwiKSdcbiAgICAgIHJlc3Q6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJzdXBhYmFzZS1yZXN0XCIpJ1xuICAgICAgcmVhbHRpbWU6ICdzdGFydHNfd2l0aChzdHJpbmchKC5hcHBuYW1lKSwgXCJyZWFsdGltZS1kZXZcIiknXG4gICAgICBzdG9yYWdlOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2Utc3RvcmFnZVwiKSdcbiAgICAgIGZ1bmN0aW9uczogJ3N0YXJ0c193aXRoKHN0cmluZyEoLmFwcG5hbWUpLCBcInN1cGFiYXNlLWZ1bmN0aW9uc1wiKSdcbiAgICAgIGRiOiAnc3RhcnRzX3dpdGgoc3RyaW5nISguYXBwbmFtZSksIFwic3VwYWJhc2UtZGJcIiknXG4gICMgSWdub3JlcyBub24gbmdpbnggZXJyb3JzIHNpbmNlIHRoZXkgYXJlIHJlbGF0ZWQgd2l0aCBrb25nIGJvb3RpbmcgdXBcbiAga29uZ19sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIua29uZ1xuICAgIHNvdXJjZTogfC1cbiAgICAgIHJlcSwgZXJyID0gcGFyc2VfbmdpbnhfbG9nKC5ldmVudF9tZXNzYWdlLCBcImNvbWJpbmVkXCIpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHJlcS50aW1lc3RhbXBcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5oZWFkZXJzLnJlZmVyZXIgPSByZXEucmVmZXJlclxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMudXNlcl9hZ2VudCA9IHJlcS5hZ2VudFxuICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LmhlYWRlcnMuY2ZfY29ubmVjdGluZ19pcCA9IHJlcS5jbGllbnRcbiAgICAgICAgICAubWV0YWRhdGEucmVxdWVzdC5tZXRob2QgPSByZXEubWV0aG9kXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucGF0aCA9IHJlcS5wYXRoXG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucHJvdG9jb2wgPSByZXEucHJvdG9jb2xcbiAgICAgICAgICAubWV0YWRhdGEucmVzcG9uc2Uuc3RhdHVzX2NvZGUgPSByZXEuc3RhdHVzXG4gICAgICB9XG4gICAgICBpZiBlcnIgIT0gbnVsbCB7XG4gICAgICAgIGFib3J0XG4gICAgICB9XG4gICMgSWdub3JlcyBub24gbmdpbnggZXJyb3JzIHNpbmNlIHRoZXkgYXJlIHJlbGF0ZWQgd2l0aCBrb25nIGJvb3RpbmcgdXBcbiAga29uZ19lcnI6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5rb25nXG4gICAgc291cmNlOiB8LVxuICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gXCJHRVRcIlxuICAgICAgLm1ldGFkYXRhLnJlc3BvbnNlLnN0YXR1c19jb2RlID0gMjAwXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX25naW54X2xvZyguZXZlbnRfbWVzc2FnZSwgXCJlcnJvclwiKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC50aW1lc3RhbXAgPSBwYXJzZWQudGltZXN0YW1wXG4gICAgICAgICAgLnNldmVyaXR5ID0gcGFyc2VkLnNldmVyaXR5XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaG9zdCA9IHBhcnNlZC5ob3N0XG4gICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QuaGVhZGVycy5jZl9jb25uZWN0aW5nX2lwID0gcGFyc2VkLmNsaWVudFxuICAgICAgICAgIHVybCwgZXJyID0gc3BsaXQocGFyc2VkLnJlcXVlc3QsIFwiIFwiKVxuICAgICAgICAgIGlmIGVyciA9PSBudWxsIHtcbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QubWV0aG9kID0gdXJsWzBdXG4gICAgICAgICAgICAgIC5tZXRhZGF0YS5yZXF1ZXN0LnBhdGggPSB1cmxbMV1cbiAgICAgICAgICAgICAgLm1ldGFkYXRhLnJlcXVlc3QucHJvdG9jb2wgPSB1cmxbMl1cbiAgICAgICAgICB9XG4gICAgICB9XG4gICAgICBpZiBlcnIgIT0gbnVsbCB7XG4gICAgICAgIGFib3J0XG4gICAgICB9XG4gICMgR290cnVlIGxvZ3MgYXJlIHN0cnVjdHVyZWQganNvbiBzdHJpbmdzIHdoaWNoIGZyb250ZW5kIHBhcnNlcyBkaXJlY3RseS4gQnV0IHdlIGtlZXAgbWV0YWRhdGEgZm9yIGNvbnNpc3RlbmN5LlxuICBhdXRoX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5hdXRoXG4gICAgc291cmNlOiB8LVxuICAgICAgcGFyc2VkLCBlcnIgPSBwYXJzZV9qc29uKC5ldmVudF9tZXNzYWdlKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5tZXRhZGF0YS50aW1lc3RhbXAgPSBwYXJzZWQudGltZVxuICAgICAgICAgIC5tZXRhZGF0YSA9IG1lcmdlISgubWV0YWRhdGEsIHBhcnNlZClcbiAgICAgIH1cbiAgIyBQb3N0Z1JFU1QgbG9ncyBhcmUgc3RydWN0dXJlZCBzbyB3ZSBzZXBhcmF0ZSB0aW1lc3RhbXAgZnJvbSBtZXNzYWdlIHVzaW5nIHJlZ2V4XG4gIHJlc3RfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnJlc3RcbiAgICBzb3VyY2U6IHwtXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX3JlZ2V4KC5ldmVudF9tZXNzYWdlLCByJ14oP1A8dGltZT4uKik6ICg/UDxtc2c+LiopJCcpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLnRpbWVzdGFtcCA9IHRvX3RpbWVzdGFtcCEocGFyc2VkLnRpbWUpXG4gICAgICAgICAgLm1ldGFkYXRhLmhvc3QgPSAucHJvamVjdFxuICAgICAgfVxuICAjIFJlYWx0aW1lIGxvZ3MgYXJlIHN0cnVjdHVyZWQgc28gd2UgcGFyc2UgdGhlIHNldmVyaXR5IGxldmVsIHVzaW5nIHJlZ2V4IChpZ25vcmUgdGltZSBiZWNhdXNlIGl0IGhhcyBubyBkYXRlKVxuICByZWFsdGltZV9sb2dzOlxuICAgIHR5cGU6IHJlbWFwXG4gICAgaW5wdXRzOlxuICAgICAgLSByb3V0ZXIucmVhbHRpbWVcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucHJvamVjdCA9IGRlbCgucHJvamVjdClcbiAgICAgIC5tZXRhZGF0YS5leHRlcm5hbF9pZCA9IC5tZXRhZGF0YS5wcm9qZWN0XG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX3JlZ2V4KC5ldmVudF9tZXNzYWdlLCByJ14oP1A8dGltZT5cXGQrOlxcZCs6XFxkK1xcLlxcZCspIFxcWyg/UDxsZXZlbD5cXHcrKVxcXSAoP1A8bXNnPi4qKSQnKVxuICAgICAgaWYgZXJyID09IG51bGwge1xuICAgICAgICAgIC5ldmVudF9tZXNzYWdlID0gcGFyc2VkLm1zZ1xuICAgICAgICAgIC5tZXRhZGF0YS5sZXZlbCA9IHBhcnNlZC5sZXZlbFxuICAgICAgfVxuICAjIFN0b3JhZ2UgbG9ncyBtYXkgY29udGFpbiBqc29uIG9iamVjdHMgc28gd2UgcGFyc2UgdGhlbSBmb3IgY29tcGxldGVuZXNzXG4gIHN0b3JhZ2VfbG9nczpcbiAgICB0eXBlOiByZW1hcFxuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLnN0b3JhZ2VcbiAgICBzb3VyY2U6IHwtXG4gICAgICAubWV0YWRhdGEucHJvamVjdCA9IGRlbCgucHJvamVjdClcbiAgICAgIC5tZXRhZGF0YS50ZW5hbnRJZCA9IC5tZXRhZGF0YS5wcm9qZWN0XG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX2pzb24oLmV2ZW50X21lc3NhZ2UpXG4gICAgICBpZiBlcnIgPT0gbnVsbCB7XG4gICAgICAgICAgLmV2ZW50X21lc3NhZ2UgPSBwYXJzZWQubXNnXG4gICAgICAgICAgLm1ldGFkYXRhLmxldmVsID0gcGFyc2VkLmxldmVsXG4gICAgICAgICAgLm1ldGFkYXRhLnRpbWVzdGFtcCA9IHBhcnNlZC50aW1lXG4gICAgICAgICAgLm1ldGFkYXRhLmNvbnRleHRbMF0uaG9zdCA9IHBhcnNlZC5ob3N0bmFtZVxuICAgICAgICAgIC5tZXRhZGF0YS5jb250ZXh0WzBdLnBpZCA9IHBhcnNlZC5waWRcbiAgICAgIH1cbiAgIyBQb3N0Z3JlcyBsb2dzIHNvbWUgbWVzc2FnZXMgdG8gc3RkZXJyIHdoaWNoIHdlIG1hcCB0byB3YXJuaW5nIHNldmVyaXR5IGxldmVsXG4gIGRiX2xvZ3M6XG4gICAgdHlwZTogcmVtYXBcbiAgICBpbnB1dHM6XG4gICAgICAtIHJvdXRlci5kYlxuICAgIHNvdXJjZTogfC1cbiAgICAgIC5tZXRhZGF0YS5ob3N0ID0gXCJkYi1kZWZhdWx0XCJcbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQudGltZXN0YW1wID0gLnRpbWVzdGFtcFxuXG4gICAgICBwYXJzZWQsIGVyciA9IHBhcnNlX3JlZ2V4KC5ldmVudF9tZXNzYWdlLCByJy4qKD9QPGxldmVsPklORk98Tk9USUNFfFdBUk5JTkd8RVJST1J8TE9HfEZBVEFMfFBBTklDPyk6LionLCBudW1lcmljX2dyb3VwczogdHJ1ZSlcblxuICAgICAgaWYgZXJyICE9IG51bGwgfHwgcGFyc2VkID09IG51bGwge1xuICAgICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gXCJpbmZvXCJcbiAgICAgIH1cbiAgICAgIGlmIHBhcnNlZCAhPSBudWxsIHtcbiAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBwYXJzZWQubGV2ZWxcbiAgICAgIH1cbiAgICAgIGlmIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPT0gXCJpbmZvXCIge1xuICAgICAgICAgIC5tZXRhZGF0YS5wYXJzZWQuZXJyb3Jfc2V2ZXJpdHkgPSBcImxvZ1wiXG4gICAgICB9XG4gICAgICAubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5ID0gdXBjYXNlISgubWV0YWRhdGEucGFyc2VkLmVycm9yX3NldmVyaXR5KVxuXG5zaW5rczpcbiAgbG9nZmxhcmVfYXV0aDpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGF1dGhfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1nb3RydWUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9yZWFsdGltZTpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIHJlYWx0aW1lX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9cmVhbHRpbWUubG9ncy5wcm9kJmFwaV9rZXk9JHtMT0dGTEFSRV9BUElfS0VZP0xPR0ZMQVJFX0FQSV9LRVkgaXMgcmVxdWlyZWR9J1xuICBsb2dmbGFyZV9yZXN0OlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcmVzdF9sb2dzXG4gICAgZW5jb2Rpbmc6XG4gICAgICBjb2RlYzogJ2pzb24nXG4gICAgbWV0aG9kOiAncG9zdCdcbiAgICByZXF1ZXN0OlxuICAgICAgcmV0cnlfbWF4X2R1cmF0aW9uX3NlY3M6IDEwXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWFuYWx5dGljczo0MDAwL2FwaS9sb2dzP3NvdXJjZV9uYW1lPXBvc3RnUkVTVC5sb2dzLnByb2QmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX2RiOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gZGJfbG9nc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgICMgV2UgbXVzdCByb3V0ZSB0aGUgc2luayB0aHJvdWdoIGtvbmcgYmVjYXVzZSBpbmdlc3RpbmcgbG9ncyBiZWZvcmUgbG9nZmxhcmUgaXMgZnVsbHkgaW5pdGlhbGlzZWQgd2lsbFxuICAgICMgbGVhZCB0byBicm9rZW4gcXVlcmllcyBmcm9tIHN0dWRpby4gVGhpcyB3b3JrcyBieSB0aGUgYXNzdW1wdGlvbiB0aGF0IGNvbnRhaW5lcnMgYXJlIHN0YXJ0ZWQgaW4gdGhlXG4gICAgIyBmb2xsb3dpbmcgb3JkZXI6IHZlY3RvciA+IGRiID4gbG9nZmxhcmUgPiBrb25nXG4gICAgdXJpOiAnaHR0cDovL3N1cGFiYXNlLWtvbmc6ODAwMC9hbmFseXRpY3MvdjEvYXBpL2xvZ3M/c291cmNlX25hbWU9cG9zdGdyZXMubG9ncyZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfZnVuY3Rpb25zOlxuICAgIHR5cGU6ICdodHRwJ1xuICAgIGlucHV0czpcbiAgICAgIC0gcm91dGVyLmZ1bmN0aW9uc1xuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1kZW5vLXJlbGF5LWxvZ3MmYXBpX2tleT0ke0xPR0ZMQVJFX0FQSV9LRVk/TE9HRkxBUkVfQVBJX0tFWSBpcyByZXF1aXJlZH0nXG4gIGxvZ2ZsYXJlX3N0b3JhZ2U6XG4gICAgdHlwZTogJ2h0dHAnXG4gICAgaW5wdXRzOlxuICAgICAgLSBzdG9yYWdlX2xvZ3NcbiAgICBlbmNvZGluZzpcbiAgICAgIGNvZGVjOiAnanNvbidcbiAgICBtZXRob2Q6ICdwb3N0J1xuICAgIHJlcXVlc3Q6XG4gICAgICByZXRyeV9tYXhfZHVyYXRpb25fc2VjczogMTBcbiAgICB1cmk6ICdodHRwOi8vc3VwYWJhc2UtYW5hbHl0aWNzOjQwMDAvYXBpL2xvZ3M/c291cmNlX25hbWU9c3RvcmFnZS5sb2dzLnByb2QuMiZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiAgbG9nZmxhcmVfa29uZzpcbiAgICB0eXBlOiAnaHR0cCdcbiAgICBpbnB1dHM6XG4gICAgICAtIGtvbmdfbG9nc1xuICAgICAgLSBrb25nX2VyclxuICAgIGVuY29kaW5nOlxuICAgICAgY29kZWM6ICdqc29uJ1xuICAgIG1ldGhvZDogJ3Bvc3QnXG4gICAgcmVxdWVzdDpcbiAgICAgIHJldHJ5X21heF9kdXJhdGlvbl9zZWNzOiAxMFxuICAgIHVyaTogJ2h0dHA6Ly9zdXBhYmFzZS1hbmFseXRpY3M6NDAwMC9hcGkvbG9ncz9zb3VyY2VfbmFtZT1jbG91ZGZsYXJlLmxvZ3MucHJvZCZhcGlfa2V5PSR7TE9HRkxBUkVfQVBJX0tFWT9MT0dGTEFSRV9BUElfS0VZIGlzIHJlcXVpcmVkfSdcbiIKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2s6cm8nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTE9HRkxBUkVfQVBJX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfTE9HRkxBUkV9JwogICAgY29tbWFuZDoKICAgICAgLSAnLS1jb25maWcnCiAgICAgIC0gZXRjL3ZlY3Rvci92ZWN0b3IueW1sCiAgc3VwYWJhc2UtcmVzdDoKICAgIGltYWdlOiAncG9zdGdyZXN0L3Bvc3RncmVzdDp2MTIuMi4wJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUEdSU1RfREJfVVJJPXBvc3RncmVzOi8vYXV0aGVudGljYXRvcjoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnUEdSU1RfREJfU0NIRU1BUz0ke1BHUlNUX0RCX1NDSEVNQVM6LXB1YmxpYyxzdG9yYWdlLGdyYXBocWxfcHVibGljfScKICAgICAgLSBQR1JTVF9EQl9BTk9OX1JPTEU9YW5vbgogICAgICAtICdQR1JTVF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFBHUlNUX0RCX1VTRV9MRUdBQ1lfR1VDUz1mYWxzZQogICAgICAtICdQR1JTVF9BUFBfU0VUVElOR1NfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnUEdSU1RfQVBQX1NFVFRJTkdTX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgIGNvbW1hbmQ6IHBvc3RncmVzdAogICAgZXhjbHVkZV9mcm9tX2hjOiB0cnVlCiAgc3VwYWJhc2UtYXV0aDoKICAgIGltYWdlOiAnc3VwYWJhc2UvZ290cnVlOnYyLjE1OC4xJwogICAgZGVwZW5kc19vbjoKICAgICAgc3VwYWJhc2UtZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgc3VwYWJhc2UtYW5hbHl0aWNzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy0tbm8tdmVyYm9zZScKICAgICAgICAtICctLXRyaWVzPTEnCiAgICAgICAgLSAnLS1zcGlkZXInCiAgICAgICAgLSAnaHR0cDovLzEyNy4wLjAuMTo5OTk5L2hlYWx0aCcKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtIEdPVFJVRV9BUElfSE9TVD0wLjAuMC4wCiAgICAgIC0gR09UUlVFX0FQSV9QT1JUPTk5OTkKICAgICAgLSAnQVBJX0VYVEVSTkFMX1VSTD0ke0FQSV9FWFRFUk5BTF9VUkw6LWh0dHA6Ly9zdXBhYmFzZS1rb25nOjgwMDB9JwogICAgICAtIEdPVFJVRV9EQl9EUklWRVI9cG9zdGdyZXMKICAgICAgLSAnR09UUlVFX0RCX0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3N1cGFiYXNlX2F1dGhfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS8ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ0dPVFJVRV9TSVRFX1VSTD0ke1NFUlZJQ0VfRlFETl9TVVBBQkFTRUtPTkd9JwogICAgICAtICdHT1RSVUVfVVJJX0FMTE9XX0xJU1Q9JHtBRERJVElPTkFMX1JFRElSRUNUX1VSTFN9JwogICAgICAtICdHT1RSVUVfRElTQUJMRV9TSUdOVVA9JHtESVNBQkxFX1NJR05VUDotZmFsc2V9JwogICAgICAtIEdPVFJVRV9KV1RfQURNSU5fUk9MRVM9c2VydmljZV9yb2xlCiAgICAgIC0gR09UUlVFX0pXVF9BVUQ9YXV0aGVudGljYXRlZAogICAgICAtIEdPVFJVRV9KV1RfREVGQVVMVF9HUk9VUF9OQU1FPWF1dGhlbnRpY2F0ZWQKICAgICAgLSAnR09UUlVFX0pXVF9FWFA9JHtKV1RfRVhQSVJZOi0zNjAwfScKICAgICAgLSAnR09UUlVFX0pXVF9TRUNSRVQ9JHtTRVJWSUNFX1BBU1NXT1JEX0pXVH0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9FTUFJTF9FTkFCTEVEPSR7RU5BQkxFX0VNQUlMX1NJR05VUDotdHJ1ZX0nCiAgICAgIC0gJ0dPVFJVRV9FWFRFUk5BTF9BTk9OWU1PVVNfVVNFUlNfRU5BQkxFRD0ke0VOQUJMRV9BTk9OWU1PVVNfVVNFUlM6LWZhbHNlfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9BVVRPQ09ORklSTT0ke0VOQUJMRV9FTUFJTF9BVVRPQ09ORklSTTotZmFsc2V9JwogICAgICAtICdHT1RSVUVfU01UUF9BRE1JTl9FTUFJTD0ke1NNVFBfQURNSU5fRU1BSUx9JwogICAgICAtICdHT1RSVUVfU01UUF9IT1NUPSR7U01UUF9IT1NUfScKICAgICAgLSAnR09UUlVFX1NNVFBfUE9SVD0ke1NNVFBfUE9SVDotNTg3fScKICAgICAgLSAnR09UUlVFX1NNVFBfVVNFUj0ke1NNVFBfVVNFUn0nCiAgICAgIC0gJ0dPVFJVRV9TTVRQX1BBU1M9JHtTTVRQX1BBU1N9JwogICAgICAtICdHT1RSVUVfU01UUF9TRU5ERVJfTkFNRT0ke1NNVFBfU0VOREVSX05BTUV9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1VSTFBBVEhTX0lOVklURT0ke01BSUxFUl9VUkxQQVRIU19JTlZJVEU6LS9hdXRoL3YxL3ZlcmlmeX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVVJMUEFUSFNfQ09ORklSTUFUSU9OPSR7TUFJTEVSX1VSTFBBVEhTX0NPTkZJUk1BVElPTjotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWT0ke01BSUxFUl9VUkxQQVRIU19SRUNPVkVSWTotL2F1dGgvdjEvdmVyaWZ5fScKICAgICAgLSAnR09UUlVFX01BSUxFUl9VUkxQQVRIU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfVVJMUEFUSFNfRU1BSUxfQ0hBTkdFOi0vYXV0aC92MS92ZXJpZnl9JwogICAgICAtICdHT1RSVUVfTUFJTEVSX1RFTVBMQVRFU19JTlZJVEU9JHtNQUlMRVJfVEVNUExBVEVTX0lOVklURX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0NPTkZJUk1BVElPTj0ke01BSUxFUl9URU1QTEFURVNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfUkVDT1ZFUlk9JHtNQUlMRVJfVEVNUExBVEVTX1JFQ09WRVJZfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOSz0ke01BSUxFUl9URU1QTEFURVNfTUFHSUNfTElOS30nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfVEVNUExBVEVTX0VNQUlMX0NIQU5HRT0ke01BSUxFUl9URU1QTEFURVNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19DT05GSVJNQVRJT049JHtNQUlMRVJfU1VCSkVDVFNfQ09ORklSTUFUSU9OfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWT0ke01BSUxFUl9TVUJKRUNUU19SRUNPVkVSWX0nCiAgICAgIC0gJ0dPVFJVRV9NQUlMRVJfU1VCSkVDVFNfTUFHSUNfTElOSz0ke01BSUxFUl9TVUJKRUNUU19NQUdJQ19MSU5LfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19FTUFJTF9DSEFOR0U9JHtNQUlMRVJfU1VCSkVDVFNfRU1BSUxfQ0hBTkdFfScKICAgICAgLSAnR09UUlVFX01BSUxFUl9TVUJKRUNUU19JTlZJVEU9JHtNQUlMRVJfU1VCSkVDVFNfSU5WSVRFfScKICAgICAgLSAnR09UUlVFX0VYVEVSTkFMX1BIT05FX0VOQUJMRUQ9JHtFTkFCTEVfUEhPTkVfU0lHTlVQOi10cnVlfScKICAgICAgLSAnR09UUlVFX1NNU19BVVRPQ09ORklSTT0ke0VOQUJMRV9QSE9ORV9BVVRPQ09ORklSTTotdHJ1ZX0nCiAgcmVhbHRpbWUtZGV2OgogICAgaW1hZ2U6ICdzdXBhYmFzZS9yZWFsdGltZTp2Mi4zMC4zNCcKICAgIGNvbnRhaW5lcl9uYW1lOiByZWFsdGltZS1kZXYuc3VwYWJhc2UtcmVhbHRpbWUKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctLWhlYWQnCiAgICAgICAgLSAnLW8nCiAgICAgICAgLSAvZGV2L251bGwKICAgICAgICAtICctSCcKICAgICAgICAtICdBdXRob3JpemF0aW9uOiBCZWFyZXIgJHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9hcGkvdGVuYW50cy9yZWFsdGltZS1kZXYvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9SVD00MDAwCiAgICAgIC0gJ0RCX0hPU1Q9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdEQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gREJfVVNFUj1zdXBhYmFzZV9hZG1pbgogICAgICAtICdEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdEQl9OQU1FPSR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnREJfQUZURVJfQ09OTkVDVF9RVUVSWT1TRVQgc2VhcmNoX3BhdGggVE8gX3JlYWx0aW1lJwogICAgICAtIERCX0VOQ19LRVk9c3VwYWJhc2VyZWFsdGltZQogICAgICAtICdBUElfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSBGTFlfQUxMT0NfSUQ9Zmx5MTIzCiAgICAgIC0gRkxZX0FQUF9OQU1FPXJlYWx0aW1lCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFQ1JFVF9QQVNTV09SRF9SRUFMVElNRX0nCiAgICAgIC0gJ0VSTF9BRkxBR1M9LXByb3RvX2Rpc3QgaW5ldF90Y3AnCiAgICAgIC0gRU5BQkxFX1RBSUxTQ0FMRT1mYWxzZQogICAgICAtICJETlNfTk9ERVM9JyciCiAgICAgIC0gUkxJTUlUX05PRklMRT0xMDAwMAogICAgICAtIEFQUF9OQU1FPXJlYWx0aW1lCiAgICAgIC0gU0VFRF9TRUxGX0hPU1Q9dHJ1ZQogICAgY29tbWFuZDogInNoIC1jIFwiL2FwcC9iaW4vbWlncmF0ZSAmJiAvYXBwL2Jpbi9yZWFsdGltZSBldmFsICdSZWFsdGltZS5SZWxlYXNlLnNlZWRzKFJlYWx0aW1lLlJlcG8pJyAmJiAvYXBwL2Jpbi9zZXJ2ZXJcIlxuIgogIHN1cGFiYXNlLW1pbmlvOgogICAgaW1hZ2U6IG1pbmlvL21pbmlvCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnTUlOSU9fUk9PVF9VU0VSPSR7U0VSVklDRV9VU0VSX01JTklPfScKICAgICAgLSAnTUlOSU9fUk9PVF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfTUlOSU99JwogICAgY29tbWFuZDogJ3NlcnZlciAtLWNvbnNvbGUtYWRkcmVzcyAiOjkwMDEiIC9kYXRhJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6ICdzbGVlcCA1ICYmIGV4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDJzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiA1CiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovZGF0YScKICBtaW5pby1jcmVhdGVidWNrZXQ6CiAgICBpbWFnZTogbWluaW8vbWMKICAgIHJlc3RhcnQ6ICdubycKICAgIGVudmlyb25tZW50OgogICAgICAtICdNSU5JT19ST09UX1VTRVI9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdNSU5JT19ST09UX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9NSU5JT30nCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1taW5pbzoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW50cnlwb2ludDoKICAgICAgLSAvZW50cnlwb2ludC5zaAogICAgdm9sdW1lczoKICAgICAgLQogICAgICAgIHR5cGU6IGJpbmQKICAgICAgICBzb3VyY2U6IC4vZW50cnlwb2ludC5zaAogICAgICAgIHRhcmdldDogL2VudHJ5cG9pbnQuc2gKICAgICAgICBjb250ZW50OiAiIyEvYmluL3NoXG4vdXNyL2Jpbi9tYyBhbGlhcyBzZXQgc3VwYWJhc2UtbWluaW8gaHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAgJHtNSU5JT19ST09UX1VTRVJ9ICR7TUlOSU9fUk9PVF9QQVNTV09SRH07XG4vdXNyL2Jpbi9tYyBtYiAtLWlnbm9yZS1leGlzdGluZyBzdXBhYmFzZS1taW5pby9zdHViO1xuZXhpdCAwXG4iCiAgc3VwYWJhc2Utc3RvcmFnZToKICAgIGltYWdlOiAnc3VwYWJhc2Uvc3RvcmFnZS1hcGk6djEuMTAuMScKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLXJlc3Q6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgICAgaW1ncHJveHk6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX3N0YXJ0ZWQKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSB3Z2V0CiAgICAgICAgLSAnLS1uby12ZXJib3NlJwogICAgICAgIC0gJy0tdHJpZXM9MScKICAgICAgICAtICctLXNwaWRlcicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xOjUwMDAvc3RhdHVzJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVkVSX1BPUlQ9NTAwMAogICAgICAtIFNFUlZFUl9SRUdJT049bG9jYWwKICAgICAgLSBNVUxUSV9URU5BTlQ9ZmFsc2UKICAgICAgLSAnQVVUSF9KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly9zdXBhYmFzZV9zdG9yYWdlX2FkbWluOiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AJHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9OiR7UE9TVEdSRVNfUE9SVDotNTQzMn0vJHtQT1NUR1JFU19EQjotcG9zdGdyZXN9JwogICAgICAtIERCX0lOU1RBTExfUk9MRVM9ZmFsc2UKICAgICAgLSBTVE9SQUdFX0JBQ0tFTkQ9czMKICAgICAgLSBTVE9SQUdFX1MzX0JVQ0tFVD1zdHViCiAgICAgIC0gJ1NUT1JBR0VfUzNfRU5EUE9JTlQ9aHR0cDovL3N1cGFiYXNlLW1pbmlvOjkwMDAnCiAgICAgIC0gU1RPUkFHRV9TM19GT1JDRV9QQVRIX1NUWUxFPXRydWUKICAgICAgLSBTVE9SQUdFX1MzX1JFR0lPTj11cy1lYXN0LTEKICAgICAgLSAnQVdTX0FDQ0VTU19LRVlfSUQ9JHtTRVJWSUNFX1VTRVJfTUlOSU99JwogICAgICAtICdBV1NfU0VDUkVUX0FDQ0VTU19LRVk9JHtTRVJWSUNFX1BBU1NXT1JEX01JTklPfScKICAgICAgLSBVUExPQURfRklMRV9TSVpFX0xJTUlUPTUyNDI4ODAwMAogICAgICAtIFVQTE9BRF9GSUxFX1NJWkVfTElNSVRfU1RBTkRBUkQ9NTI0Mjg4MDAwCiAgICAgIC0gVVBMT0FEX1NJR05FRF9VUkxfRVhQSVJBVElPTl9USU1FPTEyMAogICAgICAtIFRVU19VUkxfUEFUSD0vdXBsb2FkL3Jlc3VtYWJsZQogICAgICAtIFRVU19NQVhfU0laRT0zNjAwMDAwCiAgICAgIC0gSU1BR0VfVFJBTlNGT1JNQVRJT05fRU5BQkxFRD10cnVlCiAgICAgIC0gJ0lNR1BST1hZX1VSTD1odHRwOi8vaW1ncHJveHk6ODA4MCcKICAgICAgLSBJTUdQUk9YWV9SRVFVRVNUX1RJTUVPVVQ9MTUKICAgICAgLSBEQVRBQkFTRV9TRUFSQ0hfUEFUSD1zdG9yYWdlCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIGltZ3Byb3h5OgogICAgaW1hZ2U6ICdkYXJ0aHNpbS9pbWdwcm94eTp2My44LjAnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaW1ncHJveHkKICAgICAgICAtIGhlYWx0aAogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gSU1HUFJPWFlfTE9DQUxfRklMRVNZU1RFTV9ST09UPS8KICAgICAgLSBJTUdQUk9YWV9VU0VfRVRBRz10cnVlCiAgICAgIC0gJ0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTj0ke0lNR1BST1hZX0VOQUJMRV9XRUJQX0RFVEVDVElPTjotdHJ1ZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvc3RvcmFnZTovdmFyL2xpYi9zdG9yYWdlJwogIHN1cGFiYXNlLW1ldGE6CiAgICBpbWFnZTogJ3N1cGFiYXNlL3Bvc3RncmVzLW1ldGE6djAuODMuMicKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUEdfTUVUQV9QT1JUPTgwODAKICAgICAgLSAnUEdfTUVUQV9EQl9IT1NUPSR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifScKICAgICAgLSAnUEdfTUVUQV9EQl9QT1JUPSR7UE9TVEdSRVNfUE9SVDotNTQzMn0nCiAgICAgIC0gJ1BHX01FVEFfREJfTkFNRT0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gUEdfTUVUQV9EQl9VU0VSPXN1cGFiYXNlX2FkbWluCiAgICAgIC0gJ1BHX01FVEFfREJfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICBzdXBhYmFzZS1lZGdlLWZ1bmN0aW9uczoKICAgIGltYWdlOiAnc3VwYWJhc2UvZWRnZS1ydW50aW1lOnYxLjU4LjMnCiAgICBkZXBlbmRzX29uOgogICAgICBzdXBhYmFzZS1hbmFseXRpY3M6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBlY2hvCiAgICAgICAgLSAnRWRnZSBGdW5jdGlvbnMgaXMgaGVhbHRoeScKICAgICAgdGltZW91dDogNXMKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIGVudmlyb25tZW50OgogICAgICAtICdKV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtICdTVVBBQkFTRV9VUkw9JHtTRVJWSUNFX0ZRRE5fU1VQQUJBU0VLT05HfScKICAgICAgLSAnU1VQQUJBU0VfQU5PTl9LRVk9JHtTRVJWSUNFX1NVUEFCQVNFQU5PTl9LRVl9JwogICAgICAtICdTVVBBQkFTRV9TRVJWSUNFX1JPTEVfS0VZPSR7U0VSVklDRV9TVVBBQkFTRVNFUlZJQ0VfS0VZfScKICAgICAgLSAnU1VQQUJBU0VfREJfVVJMPXBvc3RncmVzcWw6Ly9wb3N0Z3Jlczoke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QCR7UE9TVEdSRVNfSE9TVE5BTUU6LXN1cGFiYXNlLWRifToke1BPU1RHUkVTX1BPUlQ6LTU0MzJ9LyR7UE9TVEdSRVNfREI6LXBvc3RncmVzfScKICAgICAgLSAnVkVSSUZZX0pXVD0ke0ZVTkNUSU9OU19WRVJJRllfSldUOi1mYWxzZX0nCiAgICB2b2x1bWVzOgogICAgICAtICcuL3ZvbHVtZXMvZnVuY3Rpb25zOi9ob21lL2Rlbm8vZnVuY3Rpb25zJwogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgdGFyZ2V0OiAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluL2luZGV4LnRzCiAgICAgICAgY29udGVudDogImltcG9ydCB7IHNlcnZlIH0gZnJvbSAnaHR0cHM6Ly9kZW5vLmxhbmQvc3RkQDAuMTMxLjAvaHR0cC9zZXJ2ZXIudHMnXG5pbXBvcnQgKiBhcyBqb3NlIGZyb20gJ2h0dHBzOi8vZGVuby5sYW5kL3gvam9zZUB2NC4xNC40L2luZGV4LnRzJ1xuXG5jb25zb2xlLmxvZygnbWFpbiBmdW5jdGlvbiBzdGFydGVkJylcblxuY29uc3QgSldUX1NFQ1JFVCA9IERlbm8uZW52LmdldCgnSldUX1NFQ1JFVCcpXG5jb25zdCBWRVJJRllfSldUID0gRGVuby5lbnYuZ2V0KCdWRVJJRllfSldUJykgPT09ICd0cnVlJ1xuXG5mdW5jdGlvbiBnZXRBdXRoVG9rZW4ocmVxOiBSZXF1ZXN0KSB7XG4gIGNvbnN0IGF1dGhIZWFkZXIgPSByZXEuaGVhZGVycy5nZXQoJ2F1dGhvcml6YXRpb24nKVxuICBpZiAoIWF1dGhIZWFkZXIpIHtcbiAgICB0aHJvdyBuZXcgRXJyb3IoJ01pc3NpbmcgYXV0aG9yaXphdGlvbiBoZWFkZXInKVxuICB9XG4gIGNvbnN0IFtiZWFyZXIsIHRva2VuXSA9IGF1dGhIZWFkZXIuc3BsaXQoJyAnKVxuICBpZiAoYmVhcmVyICE9PSAnQmVhcmVyJykge1xuICAgIHRocm93IG5ldyBFcnJvcihgQXV0aCBoZWFkZXIgaXMgbm90ICdCZWFyZXIge3Rva2VufSdgKVxuICB9XG4gIHJldHVybiB0b2tlblxufVxuXG5hc3luYyBmdW5jdGlvbiB2ZXJpZnlKV1Qoand0OiBzdHJpbmcpOiBQcm9taXNlPGJvb2xlYW4+IHtcbiAgY29uc3QgZW5jb2RlciA9IG5ldyBUZXh0RW5jb2RlcigpXG4gIGNvbnN0IHNlY3JldEtleSA9IGVuY29kZXIuZW5jb2RlKEpXVF9TRUNSRVQpXG4gIHRyeSB7XG4gICAgYXdhaXQgam9zZS5qd3RWZXJpZnkoand0LCBzZWNyZXRLZXkpXG4gIH0gY2F0Y2ggKGVycikge1xuICAgIGNvbnNvbGUuZXJyb3IoZXJyKVxuICAgIHJldHVybiBmYWxzZVxuICB9XG4gIHJldHVybiB0cnVlXG59XG5cbnNlcnZlKGFzeW5jIChyZXE6IFJlcXVlc3QpID0+IHtcbiAgaWYgKHJlcS5tZXRob2QgIT09ICdPUFRJT05TJyAmJiBWRVJJRllfSldUKSB7XG4gICAgdHJ5IHtcbiAgICAgIGNvbnN0IHRva2VuID0gZ2V0QXV0aFRva2VuKHJlcSlcbiAgICAgIGNvbnN0IGlzVmFsaWRKV1QgPSBhd2FpdCB2ZXJpZnlKV1QodG9rZW4pXG5cbiAgICAgIGlmICghaXNWYWxpZEpXVCkge1xuICAgICAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KHsgbXNnOiAnSW52YWxpZCBKV1QnIH0pLCB7XG4gICAgICAgICAgc3RhdHVzOiA0MDEsXG4gICAgICAgICAgaGVhZGVyczogeyAnQ29udGVudC1UeXBlJzogJ2FwcGxpY2F0aW9uL2pzb24nIH0sXG4gICAgICAgIH0pXG4gICAgICB9XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgY29uc29sZS5lcnJvcihlKVxuICAgICAgcmV0dXJuIG5ldyBSZXNwb25zZShKU09OLnN0cmluZ2lmeSh7IG1zZzogZS50b1N0cmluZygpIH0pLCB7XG4gICAgICAgIHN0YXR1czogNDAxLFxuICAgICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICAgIH0pXG4gICAgfVxuICB9XG5cbiAgY29uc3QgdXJsID0gbmV3IFVSTChyZXEudXJsKVxuICBjb25zdCB7IHBhdGhuYW1lIH0gPSB1cmxcbiAgY29uc3QgcGF0aF9wYXJ0cyA9IHBhdGhuYW1lLnNwbGl0KCcvJylcbiAgY29uc3Qgc2VydmljZV9uYW1lID0gcGF0aF9wYXJ0c1sxXVxuXG4gIGlmICghc2VydmljZV9uYW1lIHx8IHNlcnZpY2VfbmFtZSA9PT0gJycpIHtcbiAgICBjb25zdCBlcnJvciA9IHsgbXNnOiAnbWlzc2luZyBmdW5jdGlvbiBuYW1lIGluIHJlcXVlc3QnIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA0MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG5cbiAgY29uc3Qgc2VydmljZVBhdGggPSBgL2hvbWUvZGVuby9mdW5jdGlvbnMvJHtzZXJ2aWNlX25hbWV9YFxuICBjb25zb2xlLmVycm9yKGBzZXJ2aW5nIHRoZSByZXF1ZXN0IHdpdGggJHtzZXJ2aWNlUGF0aH1gKVxuXG4gIGNvbnN0IG1lbW9yeUxpbWl0TWIgPSAxNTBcbiAgY29uc3Qgd29ya2VyVGltZW91dE1zID0gMSAqIDYwICogMTAwMFxuICBjb25zdCBub01vZHVsZUNhY2hlID0gZmFsc2VcbiAgY29uc3QgaW1wb3J0TWFwUGF0aCA9IG51bGxcbiAgY29uc3QgZW52VmFyc09iaiA9IERlbm8uZW52LnRvT2JqZWN0KClcbiAgY29uc3QgZW52VmFycyA9IE9iamVjdC5rZXlzKGVudlZhcnNPYmopLm1hcCgoaykgPT4gW2ssIGVudlZhcnNPYmpba11dKVxuXG4gIHRyeSB7XG4gICAgY29uc3Qgd29ya2VyID0gYXdhaXQgRWRnZVJ1bnRpbWUudXNlcldvcmtlcnMuY3JlYXRlKHtcbiAgICAgIHNlcnZpY2VQYXRoLFxuICAgICAgbWVtb3J5TGltaXRNYixcbiAgICAgIHdvcmtlclRpbWVvdXRNcyxcbiAgICAgIG5vTW9kdWxlQ2FjaGUsXG4gICAgICBpbXBvcnRNYXBQYXRoLFxuICAgICAgZW52VmFycyxcbiAgICB9KVxuICAgIHJldHVybiBhd2FpdCB3b3JrZXIuZmV0Y2gocmVxKVxuICB9IGNhdGNoIChlKSB7XG4gICAgY29uc3QgZXJyb3IgPSB7IG1zZzogZS50b1N0cmluZygpIH1cbiAgICByZXR1cm4gbmV3IFJlc3BvbnNlKEpTT04uc3RyaW5naWZ5KGVycm9yKSwge1xuICAgICAgc3RhdHVzOiA1MDAsXG4gICAgICBoZWFkZXJzOiB7ICdDb250ZW50LVR5cGUnOiAnYXBwbGljYXRpb24vanNvbicgfSxcbiAgICB9KVxuICB9XG59KVxuIgogICAgICAtCiAgICAgICAgdHlwZTogYmluZAogICAgICAgIHNvdXJjZTogLi92b2x1bWVzL2Z1bmN0aW9ucy9oZWxsby9pbmRleC50cwogICAgICAgIHRhcmdldDogL2hvbWUvZGVuby9mdW5jdGlvbnMvaGVsbG8vaW5kZXgudHMKICAgICAgICBjb250ZW50OiAiLy8gRm9sbG93IHRoaXMgc2V0dXAgZ3VpZGUgdG8gaW50ZWdyYXRlIHRoZSBEZW5vIGxhbmd1YWdlIHNlcnZlciB3aXRoIHlvdXIgZWRpdG9yOlxuLy8gaHR0cHM6Ly9kZW5vLmxhbmQvbWFudWFsL2dldHRpbmdfc3RhcnRlZC9zZXR1cF95b3VyX2Vudmlyb25tZW50XG4vLyBUaGlzIGVuYWJsZXMgYXV0b2NvbXBsZXRlLCBnbyB0byBkZWZpbml0aW9uLCBldGMuXG5cbmltcG9ydCB7IHNlcnZlIH0gZnJvbSBcImh0dHBzOi8vZGVuby5sYW5kL3N0ZEAwLjE3Ny4xL2h0dHAvc2VydmVyLnRzXCJcblxuc2VydmUoYXN5bmMgKCkgPT4ge1xuICByZXR1cm4gbmV3IFJlc3BvbnNlKFxuICAgIGBcIkhlbGxvIGZyb20gRWRnZSBGdW5jdGlvbnMhXCJgLFxuICAgIHsgaGVhZGVyczogeyBcIkNvbnRlbnQtVHlwZVwiOiBcImFwcGxpY2F0aW9uL2pzb25cIiB9IH0sXG4gIClcbn0pXG5cbi8vIFRvIGludm9rZTpcbi8vIGN1cmwgJ2h0dHA6Ly9sb2NhbGhvc3Q6PEtPTkdfSFRUUF9QT1JUPi9mdW5jdGlvbnMvdjEvaGVsbG8nIFxcXG4vLyAgIC0taGVhZGVyICdBdXRob3JpemF0aW9uOiBCZWFyZXIgPGFub24vc2VydmljZV9yb2xlIEFQSSBrZXk+J1xuIgogICAgY29tbWFuZDoKICAgICAgLSBzdGFydAogICAgICAtICctLW1haW4tc2VydmljZScKICAgICAgLSAvaG9tZS9kZW5vL2Z1bmN0aW9ucy9tYWluCiAgc3VwYWJhc2Utc3VwYXZpc29yOgogICAgaW1hZ2U6ICdzdXBhYmFzZS9zdXBhdmlzb3I6MS4xLjU2JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctbycKICAgICAgICAtIC9kZXYvbnVsbAogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6NDAwMC9hcGkvaGVhbHRoJwogICAgICB0aW1lb3V0OiA1cwogICAgICBpbnRlcnZhbDogNXMKICAgICAgcmV0cmllczogMTAKICAgIGRlcGVuZHNfb246CiAgICAgIHN1cGFiYXNlLWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICAgIHN1cGFiYXNlLWFuYWx5dGljczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gUE9PTEVSX1RFTkFOVF9JRD1kZXZfdGVuYW50CiAgICAgIC0gUE9PTEVSX1BPT0xfTU9ERT10cmFuc2FjdGlvbgogICAgICAtICdQT09MRVJfREVGQVVMVF9QT09MX1NJWkU9JHtQT09MRVJfREVGQVVMVF9QT09MX1NJWkU6LTIwfScKICAgICAgLSAnUE9PTEVSX01BWF9DTElFTlRfQ09OTj0ke1BPT0xFUl9NQVhfQ0xJRU5UX0NPTk46LTEwMH0nCiAgICAgIC0gUE9SVD00MDAwCiAgICAgIC0gJ1BPU1RHUkVTX1BPUlQ9JHtQT1NUR1JFU19QT1JUOi01NDMyfScKICAgICAgLSAnUE9TVEdSRVNfSE9TVE5BTUU9JHtQT1NUR1JFU19IT1NUTkFNRTotc3VwYWJhc2UtZGJ9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi1wb3N0Z3Jlc30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1lY3RvOi8vc3VwYWJhc2VfYWRtaW46JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfUAke1BPU1RHUkVTX0hPU1ROQU1FOi1zdXBhYmFzZS1kYn06JHtQT1NUR1JFU19QT1JUOi01NDMyfS9fc3VwYWJhc2UnCiAgICAgIC0gQ0xVU1RFUl9QT1NUR1JFUz10cnVlCiAgICAgIC0gJ1NFQ1JFVF9LRVlfQkFTRT0ke1NFUlZJQ0VfUEFTU1dPUkRfU1VQQVZJU09SU0VDUkVUfScKICAgICAgLSAnVkFVTFRfRU5DX0tFWT0ke1NFUlZJQ0VfUEFTU1dPUkRfVkFVTFRFTkN9JwogICAgICAtICdBUElfSldUX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfSldUfScKICAgICAgLSAnTUVUUklDU19KV1RfU0VDUkVUPSR7U0VSVklDRV9QQVNTV09SRF9KV1R9JwogICAgICAtIFJFR0lPTj1sb2NhbAogICAgICAtICdFUkxfQUZMQUdTPS1wcm90b19kaXN0IGluZXRfdGNwJwogICAgY29tbWFuZDoKICAgICAgLSAvYmluL3NoCiAgICAgIC0gJy1jJwogICAgICAtICcvYXBwL2Jpbi9taWdyYXRlICYmIC9hcHAvYmluL3N1cGF2aXNvciBldmFsICIkJChjYXQgL2V0Yy9wb29sZXIvcG9vbGVyLmV4cykiICYmIC9hcHAvYmluL3NlcnZlcicKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3ZvbHVtZXMvcG9vbGVyL3Bvb2xlci5leHMKICAgICAgICB0YXJnZXQ6IC9ldGMvcG9vbGVyL3Bvb2xlci5leHMKICAgICAgICBjb250ZW50OiAiezpvaywgX30gPSBBcHBsaWNhdGlvbi5lbnN1cmVfYWxsX3N0YXJ0ZWQoOnN1cGF2aXNvcilcbns6b2ssIHZlcnNpb259ID1cbiAgICBjYXNlIFN1cGF2aXNvci5SZXBvLnF1ZXJ5IShcInNlbGVjdCB2ZXJzaW9uKClcIikgZG9cbiAgICAle3Jvd3M6IFtbdmVyXV19IC0+IFN1cGF2aXNvci5IZWxwZXJzLnBhcnNlX3BnX3ZlcnNpb24odmVyKVxuICAgIF8gLT4gbmlsXG4gICAgZW5kXG5wYXJhbXMgPSAle1xuICAgIFwiZXh0ZXJuYWxfaWRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPT0xFUl9URU5BTlRfSURcIiksXG4gICAgXCJkYl9ob3N0XCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19IT1NUTkFNRVwiKSxcbiAgICBcImRiX3BvcnRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPU1RHUkVTX1BPUlRcIikgfD4gU3RyaW5nLnRvX2ludGVnZXIoKSxcbiAgICBcImRiX2RhdGFiYXNlXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT1NUR1JFU19EQlwiKSxcbiAgICBcInJlcXVpcmVfdXNlclwiID0+IGZhbHNlLFxuICAgIFwiYXV0aF9xdWVyeVwiID0+IFwiU0VMRUNUICogRlJPTSBwZ2JvdW5jZXIuZ2V0X2F1dGgoJDEpXCIsXG4gICAgXCJkZWZhdWx0X21heF9jbGllbnRzXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfTUFYX0NMSUVOVF9DT05OXCIpLFxuICAgIFwiZGVmYXVsdF9wb29sX3NpemVcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPT0xFUl9ERUZBVUxUX1BPT0xfU0laRVwiKSxcbiAgICBcImRlZmF1bHRfcGFyYW1ldGVyX3N0YXR1c1wiID0+ICV7XCJzZXJ2ZXJfdmVyc2lvblwiID0+IHZlcnNpb259LFxuICAgIFwidXNlcnNcIiA9PiBbJXtcbiAgICBcImRiX3VzZXJcIiA9PiBcInBnYm91bmNlclwiLFxuICAgIFwiZGJfcGFzc3dvcmRcIiA9PiBTeXN0ZW0uZ2V0X2VudihcIlBPU1RHUkVTX1BBU1NXT1JEXCIpLFxuICAgIFwibW9kZV90eXBlXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfUE9PTF9NT0RFXCIpLFxuICAgIFwicG9vbF9zaXplXCIgPT4gU3lzdGVtLmdldF9lbnYoXCJQT09MRVJfREVGQVVMVF9QT09MX1NJWkVcIiksXG4gICAgXCJpc19tYW5hZ2VyXCIgPT4gdHJ1ZVxuICAgIH1dXG59XG5cbnRlbmFudCA9IFN1cGF2aXNvci5UZW5hbnRzLmdldF90ZW5hbnRfYnlfZXh0ZXJuYWxfaWQocGFyYW1zW1wiZXh0ZXJuYWxfaWRcIl0pXG5cbmlmIHRlbmFudCBkb1xuICB7Om9rLCBffSA9IFN1cGF2aXNvci5UZW5hbnRzLnVwZGF0ZV90ZW5hbnQodGVuYW50LCBwYXJhbXMpXG5lbHNlXG4gIHs6b2ssIF99ID0gU3VwYXZpc29yLlRlbmFudHMuY3JlYXRlX3RlbmFudChwYXJhbXMpXG5lbmRcbiIK", "tags": [ "firebase", "alternative", @@ -1869,10 +2370,38 @@ "minversion": "0.0.0", "port": "8080" }, + "traccar": { + "documentation": "https://www.traccar.org/documentation/?utm_source=coolify.io", + "slogan": "Traccar is a free and open source modern GPS tracking system.", + "compose": "c2VydmljZXM6CiAgdHJhY2NhcjoKICAgIGltYWdlOiAndHJhY2Nhci90cmFjY2FyOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUkFDQ0FSXzgwODIKICAgICAgLSBTRVJWSUNFX0ZRRE5fVFJBQ0NBUkFQSV81MTU5CiAgICAgIC0gJ0NPTkZJR19VU0VfRU5WSVJPTk1FTlRfVkFSSUFCTEVTPSR7Q09ORklHX1VTRV9FTlZJUk9OTUVOVF9WQVJJQUJMRVM6LXRydWV9JwogICAgICAtICdEQVRBQkFTRV9VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnREFUQUJBU0VfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTfScKICAgIHZvbHVtZXM6CiAgICAgIC0KICAgICAgICB0eXBlOiBiaW5kCiAgICAgICAgc291cmNlOiAuL3Nydi90cmFjY2FyL2NvbmYvdHJhY2Nhci54bWwKICAgICAgICB0YXJnZXQ6IC9vcHQvdHJhY2Nhci9jb25mL3RyYWNjYXIueG1sCiAgICAgICAgY29udGVudDogIjw/eG1sIHZlcnNpb249JzEuMCcgZW5jb2Rpbmc9J1VURi04Jz8+XG48IURPQ1RZUEUgcHJvcGVydGllcyBTWVNURU0gJ2h0dHA6Ly9qYXZhLnN1bi5jb20vZHRkL3Byb3BlcnRpZXMuZHRkJz5cbjxwcm9wZXJ0aWVzPlxuICAgIDxlbnRyeSBrZXk9J2NvbmZpZy5kZWZhdWx0Jz4uL2NvbmYvZGVmYXVsdC54bWw8L2VudHJ5PlxuICAgIDxlbnRyeSBrZXk9J2RhdGFiYXNlLmRyaXZlcic+b3JnLnBvc3RncmVzcWwuRHJpdmVyPC9lbnRyeT5cbiAgICA8ZW50cnkga2V5PSdkYXRhYmFzZS51cmwnPmpkYmM6cG9zdGdyZXNxbDovL3Bvc3RncmVzOjU0MzIvdHJhY2NhcjwvZW50cnk+XG48L3Byb3BlcnRpZXM+XG4iCiAgICBkZXBlbmRzX29uOgogICAgICBwb3N0Z3JlczoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIHdnZXQKICAgICAgICAtICctLW5vLXZlcmJvc2UnCiAgICAgICAgLSAnLS10cmllcz0xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6ODA4Mi9waW5nJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgICAgIHN0YXJ0X3BlcmlvZDogMTVzCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1VTRVI9JFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTUUxfREFUQUJBU0U6LXRyYWNjYXJ9JwogICAgdm9sdW1lczoKICAgICAgLSAndHJhY2Nhci1wb3N0Z3Jlc3FsLWRhdGE6L3Zhci9saWIvcG9zdGdyZXNxbC9kYXRhLycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "traccar", + "gps", + "tracking", + "open", + "source" + ], + "logo": "svgs/traccar.png", + "minversion": "0.0.0", + "port": "8082" + }, + "transmission": { + "documentation": "https://docs.linuxserver.io/images/docker-transmission/?utm_source=coolify.io", + "slogan": "Fast, easy, and free BitTorrent client.", + "compose": "c2VydmljZXM6CiAgdHJhbnNtaXNzaW9uOgogICAgaW1hZ2U6ICdsc2NyLmlvL2xpbnV4c2VydmVyL3RyYW5zbWlzc2lvbjpsYXRlc3QnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVFJBTlNNSVNTSU9OXzkwOTEKICAgICAgLSBQVUlEPTEwMDAKICAgICAgLSBQR0lEPTEwMDAKICAgICAgLSAnVVNFUj0ke1NFUlZJQ0VfVVNFUl9BRE1JTn0nCiAgICAgIC0gJ1BBU1M9JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3RyYW5zbWlzc2lvbi1jb25maWc6L2NvbmZpZycKICAgICAgLSAndHJhbnNtaXNzaW9uLWRvd25sb2FkczovZG93bmxvYWRzJwogICAgICAtICd0cmFuc21pc3Npb24td2F0Y2g6L3dhdGNoJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctc1NmTCcKICAgICAgICAtICctdScKICAgICAgICAtICcke1NFUlZJQ0VfVVNFUl9BRE1JTn06JHtTRVJWSUNFX1BBU1NXT1JEX0FETUlOfScKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjkwOTEvJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCg==", + "tags": [ + "bittorrent", + "torrent", + "peer-to-peer" + ], + "logo": "svgs/transmission.svg", + "minversion": "0.0.0", + "port": "9091" + }, "trigger-with-external-database": { "documentation": "https://trigger.dev?utm_source=coolify.io", "slogan": "The open source Background Jobs framework for TypeScript", - "compose": "c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBMT0dJTl9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gQVBQX09SSUdJTj0kU0VSVklDRV9GUUROX1RSSUdHRVIKICAgICAgLSBNQUdJQ19MSU5LX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICAtIEVOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD0ke0RBVEFCQVNFX1VSTH0nCiAgICAgIC0gJ0RJUkVDVF9VUkw9JHtEQVRBQkFTRV9VUkx9JwogICAgICAtIFJVTlRJTUVfUExBVEZPUk09ZG9ja2VyLWNvbXBvc2UKICAgICAgLSBOT0RFX0VOVj1wcm9kdWN0aW9uCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9JRD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgICAgIC0gJ0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVQ9JHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgLSAnUkVTRU5EX0FQSV9LRVk9JHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIC0gJ0ZST01fRU1BSUw9JHtGUk9NX0VNQUlMfScKICAgICAgLSAnUkVQTFlfVE9fRU1BSUw9JHtSRVBMWV9UT19FTUFJTH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIE5PTkUK", + "compose": "c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2Om1haW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIC0gTE9HSU5fT1JJR0lOPSRTRVJWSUNFX0ZRRE5fVFJJR0dFUgogICAgICAtIEFQUF9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gTUFHSUNfTElOS19TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgLSBFTkNSWVBUSU9OX0tFWT0kU0VSVklDRV9QQVNTV09SRF82NF9FTkNSWVBUSU9OCiAgICAgIC0gU0VTU0lPTl9TRUNSRVQ9JFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogICAgICAtICdEQVRBQkFTRV9VUkw9JHtEQVRBQkFTRV9VUkw6P30nCiAgICAgIC0gJ0RJUkVDVF9VUkw9JHtEQVRBQkFTRV9VUkw6P30nCiAgICAgIC0gUlVOVElNRV9QTEFURk9STT1kb2NrZXItY29tcG9zZQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX0lEPSR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdSRVNFTkRfQVBJX0tFWT0ke1JFU0VORF9BUElfS0VZfScKICAgICAgLSAnRlJPTV9FTUFJTD0ke0ZST01fRU1BSUx9JwogICAgICAtICdSRVBMWV9UT19FTUFJTD0ke1JFUExZX1RPX0VNQUlMfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAidGltZW91dCAxMHMgYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzMwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1Cg==", "tags": [ "trigger.dev", "background jobs", @@ -1889,7 +2418,7 @@ "trigger": { "documentation": "https://trigger.dev?utm_source=coolify.io", "slogan": "The open source Background Jobs framework for TypeScript", - "compose": "c2VydmljZXM6CiAgdHJpZ2dlcjoKICAgIGltYWdlOiAnZ2hjci5pby90cmlnZ2VyZG90ZGV2L3RyaWdnZXIuZGV2OmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgLSBMT0dJTl9PUklHSU49JFNFUlZJQ0VfRlFETl9UUklHR0VSCiAgICAgIC0gQVBQX09SSUdJTj0kU0VSVklDRV9GUUROX1RSSUdHRVIKICAgICAgLSBNQUdJQ19MSU5LX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICAtIEVOQ1JZUFRJT05fS0VZPSRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgLSBTRVNTSU9OX1NFQ1JFVD0kU0VSVklDRV9QQVNTV09SRF82NF9TRVNTSU9OCiAgICAgIC0gUE9TVEdSRVNfVVNFUj0kU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIC0gUE9TVEdSRVNfUEFTU1dPUkQ9JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVMKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICAgIC0gUE9TVEdSRVNfSE9TVD1wb3N0Z3JlcwogICAgICAtICdEQVRBQkFTRV9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gJ0RJUkVDVF9VUkw9cG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIC0gUlVOVElNRV9QTEFURk9STT1kb2NrZXItY29tcG9zZQogICAgICAtIE5PREVfRU5WPXByb2R1Y3Rpb24KICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX0lEPSR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgLSAnQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVD0ke0FVVEhfR0lUSFVCX0NMSUVOVF9TRUNSRVR9JwogICAgICAtICdSRVNFTkRfQVBJX0tFWT0ke1JFU0VORF9BUElfS0VZfScKICAgICAgLSAnRlJPTV9FTUFJTD0ke0ZST01fRU1BSUx9JwogICAgICAtICdSRVBMWV9UT19FTUFJTD0ke1JFUExZX1RPX0VNQUlMfScKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gTk9ORQogIHBvc3RncmVzcWw6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Bvc3RncmVzcWwtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBQT1NUR1JFU19VU0VSPSRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgLSBQT1NUR1JFU19QQVNTV09SRD0kU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICAtICdQT1NUR1JFU19EQj0ke1BPU1RHUkVTX0RCOi10cmlnZ2VyfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSAkJHtQT1NUR1JFU19VU0VSfSAtZCAkJHtQT1NUR1JFU19EQn0nCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "compose": "eC1jb21tb24tZW52OgogIFBPUlQ6IDMwMzAKICBSRU1JWF9BUFBfUE9SVDogMzAwMAogIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgUlVOVElNRV9QTEFURk9STTogZG9ja2VyLWNvbXBvc2UKICBWM19FTkFCTEVEOiB0cnVlCiAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogIElOVEVSTkFMX09URUxfVFJBQ0VfTE9HR0lOR19FTkFCTEVEOiAwCiAgUE9TVEdSRVNfVVNFUjogJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUwogIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgTUFHSUNfTElOS19TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X01BR0lDCiAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICBFTkNSWVBUSU9OX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTgogIFBST1ZJREVSX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfUFJPVklERVIKICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgREFUQUJBU0VfSE9TVDogcG9zdGdyZXNxbAogIERBVEFCQVNFX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCJwogIERJUkVDVF9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICBSRURJU19IT1NUOiByZWRpcwogIFJFRElTX1BPUlQ6IDYzNzkKICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICBDT09SRElOQVRPUl9IT1NUOiAxMjcuMC4wLjEKICBDT09SRElOQVRPUl9QT1JUOiA5MDIwCiAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogIEFETUlOX0VNQUlMUzogJycKICBERUZBVUxUX09SR19FWEVDVVRJT05fQ09OQ1VSUkVOQ1lfTElNSVQ6IDMwMAogIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgREVQTE9ZX1JFR0lTVFJZX0hPU1Q6IGRvY2tlci5pbwogIERFUExPWV9SRUdJU1RSWV9OQU1FU1BBQ0U6IHRyaWdnZXIKICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgUkVHSVNUUllfTkFNRVNQQUNFOiAnJHtERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFfScKICBBVVRIX0dJVEhVQl9DTElFTlRfSUQ6ICcke0FVVEhfR0lUSFVCX0NMSUVOVF9JRH0nCiAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgUkVTRU5EX0FQSV9LRVk6ICcke1JFU0VORF9BUElfS0VZfScKICBGUk9NX0VNQUlMOiAnJHtGUk9NX0VNQUlMfScKICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogIExPR0lOX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICBBUFBfT1JJR0lOOiAkU0VSVklDRV9GUUROX1RSSUdHRVJfMzAwMAogIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICBPVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICdodHRwOi8vdHJpZ2dlcjozMDQwL290ZWwnCiAgRUxFQ1RSSUNfT1JJR0lOOiAnaHR0cDovL2VsZWN0cmljOjMwMDAnCnNlcnZpY2VzOgogIHRyaWdnZXI6CiAgICBpbWFnZTogJ2doY3IuaW8vdHJpZ2dlcmRvdGRldi90cmlnZ2VyLmRldjp2MycKICAgIGVudmlyb25tZW50OgogICAgICBTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwOiAnJwogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6IHBvc3RncmVzcWwKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIERJUkVDVF9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgZWxlY3RyaWM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OiAidGltZW91dCAxMHMgYmFzaCAtYyAnOj4gL2Rldi90Y3AvMTI3LjAuMC4xLzMwMDAnIHx8IGV4aXQgMSIKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgZWxlY3RyaWM6CiAgICBpbWFnZTogZWxlY3RyaWNzcWwvZWxlY3RyaWMKICAgIGVudmlyb25tZW50OgogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6IHBvc3RncmVzcWwKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIERJUkVDVF9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzcWw6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSBwd2QKICByZWRpczoKICAgIGltYWdlOiAncmVkaXM6NycKICAgIGVudmlyb25tZW50OgogICAgICAtIEFMTE9XX0VNUFRZX1BBU1NXT1JEPXllcwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdyZWRpcy1jbGkgLWggbG9jYWxob3N0IC1wIDYzNzkgcGluZycKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDVzCiAgICAgIHJldHJpZXM6IDMKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3JlZGlzLWRhdGE6L2RhdGEnCiAgcG9zdGdyZXNxbDoKICAgIGltYWdlOiAncG9zdGdyZXM6MTYtYWxwaW5lJwogICAgdm9sdW1lczoKICAgICAgLSAncG9zdGdyZXNxbC1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6IHBvc3RncmVzcWwKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIERJUkVDVF9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgIGNvbW1hbmQ6CiAgICAgIC0gJy1jJwogICAgICAtIHdhbF9sZXZlbD1sb2dpY2FsCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgZG9ja2VyLXByb3ZpZGVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL3RyaWdnZXJkb3RkZXYvcHJvdmlkZXIvZG9ja2VyOnYzJwogICAgcGxhdGZvcm06IGxpbnV4L2FtZDY0CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgIHVzZXI6IHJvb3QKICAgIGRlcGVuZHNfb246CiAgICAgIHRyaWdnZXI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGVudmlyb25tZW50OgogICAgICBQT1JUOiAzMDMwCiAgICAgIFJFTUlYX0FQUF9QT1JUOiAzMDAwCiAgICAgIE5PREVfRU5WOiBwcm9kdWN0aW9uCiAgICAgIFJVTlRJTUVfUExBVEZPUk06IGRvY2tlci1jb21wb3NlCiAgICAgIFYzX0VOQUJMRUQ6IHRydWUKICAgICAgSU5URVJOQUxfT1RFTF9UUkFDRV9ESVNBQkxFRDogMQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0xPR0dJTkdfRU5BQkxFRDogMAogICAgICBQT1NUR1JFU19VU0VSOiAkU0VSVklDRV9VU0VSX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX1BBU1NXT1JEOiAkU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFUwogICAgICBQT1NUR1JFU19EQjogJyR7UE9TVEdSRVNfREI6LXRyaWdnZXJ9JwogICAgICBNQUdJQ19MSU5LX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfTUFHSUMKICAgICAgU0VTU0lPTl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1NFU1NJT04KICAgICAgRU5DUllQVElPTl9LRVk6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0VOQ1JZUFRJT04KICAgICAgUFJPVklERVJfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9QUk9WSURFUgogICAgICBDT09SRElOQVRPUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X0NPT1JESU5BVE9SCiAgICAgIERBVEFCQVNFX0hPU1Q6IHBvc3RncmVzcWwKICAgICAgREFUQUJBU0VfVVJMOiAncG9zdGdyZXM6Ly8kU0VSVklDRV9VU0VSX1BPU1RHUkVTOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQHBvc3RncmVzcWw6NTQzMi8kUE9TVEdSRVNfREInCiAgICAgIERJUkVDVF9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgUkVESVNfSE9TVDogcmVkaXMKICAgICAgUkVESVNfUE9SVDogNjM3OQogICAgICBSRURJU19UTFNfRElTQUJMRUQ6IHRydWUKICAgICAgQ09PUkRJTkFUT1JfSE9TVDogMTI3LjAuMC4xCiAgICAgIENPT1JESU5BVE9SX1BPUlQ6IDkwMjAKICAgICAgV0hJVEVMSVNURURfRU1BSUxTOiAnJwogICAgICBBRE1JTl9FTUFJTFM6ICcnCiAgICAgIERFRkFVTFRfT1JHX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMzAwCiAgICAgIERFRkFVTFRfRU5WX0VYRUNVVElPTl9DT05DVVJSRU5DWV9MSU1JVDogMTAwCiAgICAgIERFUExPWV9SRUdJU1RSWV9IT1NUOiBkb2NrZXIuaW8KICAgICAgREVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRTogdHJpZ2dlcgogICAgICBSRUdJU1RSWV9IT1NUOiAnJHtERVBMT1lfUkVHSVNUUllfSE9TVH0nCiAgICAgIFJFR0lTVFJZX05BTUVTUEFDRTogJyR7REVQTE9ZX1JFR0lTVFJZX05BTUVTUEFDRX0nCiAgICAgIEFVVEhfR0lUSFVCX0NMSUVOVF9JRDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX0lEfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVDogJyR7QVVUSF9HSVRIVUJfQ0xJRU5UX1NFQ1JFVH0nCiAgICAgIFJFU0VORF9BUElfS0VZOiAnJHtSRVNFTkRfQVBJX0tFWX0nCiAgICAgIEZST01fRU1BSUw6ICcke0ZST01fRU1BSUx9JwogICAgICBSRVBMWV9UT19FTUFJTDogJyR7UkVQTFlfVE9fRU1BSUx9JwogICAgICBMT0dJTl9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIEFQUF9PUklHSU46ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwCiAgICAgIERFVl9PVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICRTRVJWSUNFX0ZRRE5fVFJJR0dFUl8zMDAwL290ZWwKICAgICAgT1RFTF9FWFBPUlRFUl9PVExQX0VORFBPSU5UOiAnaHR0cDovL3RyaWdnZXI6MzA0MC9vdGVsJwogICAgICBFTEVDVFJJQ19PUklHSU46ICdodHRwOi8vZWxlY3RyaWM6MzAwMCcKICAgICAgUExBVEZPUk1fSE9TVDogdHJpZ2dlcgogICAgICBQTEFURk9STV9XU19QT1JUOiAzMDMwCiAgICAgIFNFQ1VSRV9DT05ORUNUSU9OOiAnZmFsc2UnCiAgICAgIFBMQVRGT1JNX1NFQ1JFVDogJFBST1ZJREVSX1NFQ1JFVAogIGNvb3JkaW5hdG9yOgogICAgaW1hZ2U6ICdnaGNyLmlvL3RyaWdnZXJkb3RkZXYvY29vcmRpbmF0b3I6djMnCiAgICBwbGF0Zm9ybTogbGludXgvYW1kNjQKICAgIHZvbHVtZXM6CiAgICAgIC0gJy92YXIvcnVuL2RvY2tlci5zb2NrOi92YXIvcnVuL2RvY2tlci5zb2NrJwogICAgdXNlcjogcm9vdAogICAgZGVwZW5kc19vbjoKICAgICAgdHJpZ2dlcjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgZW52aXJvbm1lbnQ6CiAgICAgIFBPUlQ6IDMwMzAKICAgICAgUkVNSVhfQVBQX1BPUlQ6IDMwMDAKICAgICAgTk9ERV9FTlY6IHByb2R1Y3Rpb24KICAgICAgUlVOVElNRV9QTEFURk9STTogZG9ja2VyLWNvbXBvc2UKICAgICAgVjNfRU5BQkxFRDogdHJ1ZQogICAgICBJTlRFUk5BTF9PVEVMX1RSQUNFX0RJU0FCTEVEOiAxCiAgICAgIElOVEVSTkFMX09URUxfVFJBQ0VfTE9HR0lOR19FTkFCTEVEOiAwCiAgICAgIFBPU1RHUkVTX1VTRVI6ICRTRVJWSUNFX1VTRVJfUE9TVEdSRVMKICAgICAgUE9TVEdSRVNfUEFTU1dPUkQ6ICRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIFBPU1RHUkVTX0RCOiAnJHtQT1NUR1JFU19EQjotdHJpZ2dlcn0nCiAgICAgIE1BR0lDX0xJTktfU0VDUkVUOiAkU0VSVklDRV9QQVNTV09SRF82NF9NQUdJQwogICAgICBTRVNTSU9OX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfU0VTU0lPTgogICAgICBFTkNSWVBUSU9OX0tFWTogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfRU5DUllQVElPTgogICAgICBQUk9WSURFUl9TRUNSRVQ6ICRTRVJWSUNFX1BBU1NXT1JEXzY0X1BST1ZJREVSCiAgICAgIENPT1JESU5BVE9SX1NFQ1JFVDogJFNFUlZJQ0VfUEFTU1dPUkRfNjRfQ09PUkRJTkFUT1IKICAgICAgREFUQUJBU0VfSE9TVDogcG9zdGdyZXNxbAogICAgICBEQVRBQkFTRV9VUkw6ICdwb3N0Z3JlczovLyRTRVJWSUNFX1VTRVJfUE9TVEdSRVM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAcG9zdGdyZXNxbDo1NDMyLyRQT1NUR1JFU19EQicKICAgICAgRElSRUNUX1VSTDogJ3Bvc3RncmVzOi8vJFNFUlZJQ0VfVVNFUl9QT1NUR1JFUzokU0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU0Bwb3N0Z3Jlc3FsOjU0MzIvJFBPU1RHUkVTX0RCJwogICAgICBSRURJU19IT1NUOiByZWRpcwogICAgICBSRURJU19QT1JUOiA2Mzc5CiAgICAgIFJFRElTX1RMU19ESVNBQkxFRDogdHJ1ZQogICAgICBDT09SRElOQVRPUl9IT1NUOiAxMjcuMC4wLjEKICAgICAgQ09PUkRJTkFUT1JfUE9SVDogOTAyMAogICAgICBXSElURUxJU1RFRF9FTUFJTFM6ICcnCiAgICAgIEFETUlOX0VNQUlMUzogJycKICAgICAgREVGQVVMVF9PUkdfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAzMDAKICAgICAgREVGQVVMVF9FTlZfRVhFQ1VUSU9OX0NPTkNVUlJFTkNZX0xJTUlUOiAxMDAKICAgICAgREVQTE9ZX1JFR0lTVFJZX0hPU1Q6IGRvY2tlci5pbwogICAgICBERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFOiB0cmlnZ2VyCiAgICAgIFJFR0lTVFJZX0hPU1Q6ICcke0RFUExPWV9SRUdJU1RSWV9IT1NUfScKICAgICAgUkVHSVNUUllfTkFNRVNQQUNFOiAnJHtERVBMT1lfUkVHSVNUUllfTkFNRVNQQUNFfScKICAgICAgQVVUSF9HSVRIVUJfQ0xJRU5UX0lEOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfSUR9JwogICAgICBBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUOiAnJHtBVVRIX0dJVEhVQl9DTElFTlRfU0VDUkVUfScKICAgICAgUkVTRU5EX0FQSV9LRVk6ICcke1JFU0VORF9BUElfS0VZfScKICAgICAgRlJPTV9FTUFJTDogJyR7RlJPTV9FTUFJTH0nCiAgICAgIFJFUExZX1RPX0VNQUlMOiAnJHtSRVBMWV9UT19FTUFJTH0nCiAgICAgIExPR0lOX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgQVBQX09SSUdJTjogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAKICAgICAgREVWX09URUxfRVhQT1JURVJfT1RMUF9FTkRQT0lOVDogJFNFUlZJQ0VfRlFETl9UUklHR0VSXzMwMDAvb3RlbAogICAgICBPVEVMX0VYUE9SVEVSX09UTFBfRU5EUE9JTlQ6ICdodHRwOi8vdHJpZ2dlcjozMDQwL290ZWwnCiAgICAgIEVMRUNUUklDX09SSUdJTjogJ2h0dHA6Ly9lbGVjdHJpYzozMDAwJwogICAgICBQTEFURk9STV9IT1NUOiB0cmlnZ2VyCiAgICAgIFBMQVRGT1JNX1dTX1BPUlQ6IDMwMzAKICAgICAgU0VDVVJFX0NPTk5FQ1RJT046ICdmYWxzZScKICAgICAgUExBVEZPUk1fU0VDUkVUOiAkQ09PUkRJTkFUT1JfU0VDUkVUCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gcHdkCg==", "tags": [ "trigger.dev", "background jobs", @@ -1959,6 +2488,22 @@ "minversion": "0.0.0", "port": "4242" }, + "unsend": { + "documentation": "https://docs.unsend.dev/get-started/self-hosting?utm_source=coolify.io", + "slogan": "Unsend is an open-source alternative to Resend, Sendgrid, Mailgun and Postmark etc.", + "compose": "c2VydmljZXM6CiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ1BPU1RHUkVTX1VTRVI9JHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19EQj0ke1NFUlZJQ0VfREJfUE9TVEdSRVM6LXVuc2VuZH0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ3BnX2lzcmVhZHkgLVUgJCR7UE9TVEdSRVNfVVNFUn0gLWQgJCR7UE9TVEdSRVNfREJ9JwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgICB2b2x1bWVzOgogICAgICAtICd1bnNlbmQtcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgcmVkaXM6CiAgICBpbWFnZTogJ3JlZGlzOjcnCiAgICB2b2x1bWVzOgogICAgICAtICd1bnNlbmQtcmVkaXMtZGF0YTovZGF0YScKICAgIGNvbW1hbmQ6CiAgICAgIC0gcmVkaXMtc2VydmVyCiAgICAgIC0gJy0tbWF4bWVtb3J5LXBvbGljeScKICAgICAgLSBub2V2aWN0aW9uCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gcmVkaXMtY2xpCiAgICAgICAgLSBQSU5HCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMjAKICB1bnNlbmQ6CiAgICBpbWFnZTogJ3Vuc2VuZC91bnNlbmQ6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1VOU0VORF8zMDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3Jlc3FsOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXM6NTQzMi8ke1NFUlZJQ0VfREJfUE9TVEdSRVM6LXVuc2VuZH0nCiAgICAgIC0gJ05FWFRBVVRIX1VSTD0ke1NFUlZJQ0VfRlFETl9VTlNFTkR9JwogICAgICAtICdORVhUQVVUSF9TRUNSRVQ9JHtTRVJWSUNFX0JBU0U2NF82NF9ORVhUQVVUSFNFQ1JFVH0nCiAgICAgIC0gJ0FXU19BQ0NFU1NfS0VZPSR7QVdTX0FDQ0VTU19LRVk6P30nCiAgICAgIC0gJ0FXU19TRUNSRVRfS0VZPSR7QVdTX1NFQ1JFVF9LRVk6P30nCiAgICAgIC0gJ0FXU19ERUZBVUxUX1JFR0lPTj0ke0FXU19ERUZBVUxUX1JFR0lPTjo/fScKICAgICAgLSAnR0lUSFVCX0lEPSR7R0lUSFVCX0lEfScKICAgICAgLSAnR0lUSFVCX1NFQ1JFVD0ke0dJVEhVQl9TRUNSRVR9JwogICAgICAtICdSRURJU19VUkw9cmVkaXM6Ly9yZWRpczo2Mzc5JwogICAgICAtICdORVhUX1BVQkxJQ19JU19DTE9VRD0ke05FWFRfUFVCTElDX0lTX0NMT1VEOi1mYWxzZX0nCiAgICAgIC0gJ0FQSV9SQVRFX0xJTUlUPSR7QVBJX1JBVEVfTElNSVQ6LTF9JwogICAgZGVwZW5kc19vbjoKICAgICAgcG9zdGdyZXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgICAgcmVkaXM6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnd2dldCAtcU8tIGh0dHA6Ly8xMjcuMC4wLjE6MzAwMCB8fCBleGl0IDEnCiAgICAgIGludGVydmFsOiA1cwogICAgICByZXRyaWVzOiAxMAogICAgICB0aW1lb3V0OiAycwo=", + "tags": [ + "resend", + "mailer", + "marketing emails", + "transaction emails", + "self-hosting", + "postmark" + ], + "logo": "svgs/unsend.svg", + "minversion": "0.0.0", + "port": "3000" + }, "unstructured": { "documentation": "https://github.com/Unstructured-IO/unstructured-api?tab=readme-ov-file#--general-pre-processing-pipeline-for-documents?utm_source=coolify.io", "slogan": "Unstructured provides a platform and tools to ingest and process unstructured documents for Retrieval Augmented Generation (RAG) and model fine-tuning.", @@ -2033,6 +2578,66 @@ "minversion": "0.0.0", "port": "3456" }, + "vvveb-with-mariadb": { + "documentation": "https://docs.vvveb.com?utm_source=coolify.io", + "slogan": "Powerful and easy to use cms to build websites, blogs or ecommerce stores.", + "compose": "c2VydmljZXM6CiAgdnZ2ZWI6CiAgICBpbWFnZTogJ3Z2dmViL3Z2dmViY21zOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Z2dmViLWRhdGE6L3Zhci93d3cvaHRtbCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9WVlZFQl84MAogICAgICAtIERCX0VOR0lORT1teXNxbGkKICAgICAgLSBEQl9IT1NUPW1hcmlhZGIKICAgICAgLSAnREJfVVNFUj0ke1NFUlZJQ0VfVVNFUl9WVlZFQn0nCiAgICAgIC0gJ0RCX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9WVlZFQn0nCiAgICAgIC0gJ0RCX05BTUU9JHtNQVJJQURCX0RBVEFCQVNFOi12dnZlYn0nCiAgICBkZXBlbmRzX29uOgogICAgICBtYXJpYWRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBtYXJpYWRiOgogICAgaW1hZ2U6ICdtYXJpYWRiOjExJwogICAgdm9sdW1lczoKICAgICAgLSAndnZ2ZWItbWFyaWFkYi1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01BUklBREJfREFUQUJBU0U6LXZ2dmVifScKICAgICAgLSAnTVlTUUxfVVNFUj0ke1NFUlZJQ0VfVVNFUl9WVlZFQn0nCiAgICAgIC0gJ01ZU1FMX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9WVlZFQn0nCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gaGVhbHRoY2hlY2suc2gKICAgICAgICAtICctLWNvbm5lY3QnCiAgICAgICAgLSAnLS1pbm5vZGJfaW5pdGlhbGl6ZWQnCiAgICAgIGludGVydmFsOiA1cwogICAgICB0aW1lb3V0OiAyMHMKICAgICAgcmV0cmllczogMTAK", + "tags": [ + "cms", + "blog", + "content", + "management", + "ecommerce", + "page-builder", + "nocode", + "mysql", + "sqlite", + "pgsql" + ], + "logo": "svgs/vvveb.svg", + "minversion": "0.0.0", + "port": "80" + }, + "vvveb-with-mysql": { + "documentation": "https://docs.vvveb.com?utm_source=coolify.io", + "slogan": "Powerful and easy to use cms to build websites, blogs or ecommerce stores.", + "compose": "c2VydmljZXM6CiAgdnZ2ZWI6CiAgICBpbWFnZTogJ3Z2dmViL3Z2dmViY21zOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Z2dmViLWRhdGE6L3Zhci93d3cvaHRtbCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9WVlZFQl84MAogICAgICAtIERCX0VOR0lORT1teXNxbGkKICAgICAgLSBEQl9IT1NUPW15c3FsCiAgICAgIC0gJ0RCX1VTRVI9JHtTRVJWSUNFX1VTRVJfVlZWRUJ9JwogICAgICAtICdEQl9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfVlZWRUJ9JwogICAgICAtICdEQl9OQU1FPSR7TVlTUUxfREFUQUJBU0U6LXZ2dmVifScKICAgIGRlcGVuZHNfb246CiAgICAgIG15c3FsOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gY3VybAogICAgICAgIC0gJy1mJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjEnCiAgICAgIGludGVydmFsOiAycwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMTAKICBteXNxbDoKICAgIGltYWdlOiAnbXlzcWw6OC40LjInCiAgICB2b2x1bWVzOgogICAgICAtICd2dnZlYi1teXNxbC1kYXRhOi92YXIvbGliL215c3FsJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ01ZU1FMX1JPT1RfUEFTU1dPUkQ9JHtTRVJWSUNFX1BBU1NXT1JEX1JPT1R9JwogICAgICAtICdNWVNRTF9EQVRBQkFTRT0ke01ZU1FMX0RBVEFCQVNFOi12dnZlYn0nCiAgICAgIC0gJ01ZU1FMX1VTRVI9JHtTRVJWSUNFX1VTRVJfVlZWRUJ9JwogICAgICAtICdNWVNRTF9QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfVlZWRUJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIG15c3FsYWRtaW4KICAgICAgICAtIHBpbmcKICAgICAgICAtICctaCcKICAgICAgICAtIDEyNy4wLjAuMQogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "cms", + "blog", + "content", + "management", + "ecommerce", + "page-builder", + "nocode", + "mysql", + "sqlite", + "pgsql" + ], + "logo": "svgs/vvveb.svg", + "minversion": "0.0.0", + "port": "80" + }, + "vvveb": { + "documentation": "https://docs.vvveb.com?utm_source=coolify.io", + "slogan": "Powerful and easy to use cms to build websites, blogs or ecommerce stores.", + "compose": "c2VydmljZXM6CiAgdnZ2ZWI6CiAgICBpbWFnZTogJ3Z2dmViL3Z2dmViY21zOmxhdGVzdCcKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3Z2dmViLWRhdGE6L3Zhci93d3cvaHRtbCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9WVlZFQl84MAogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vMTI3LjAuMC4xJwogICAgICBpbnRlcnZhbDogMnMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDEwCg==", + "tags": [ + "cms", + "blog", + "content", + "management", + "ecommerce", + "page-builder", + "nocode", + "mysql", + "sqlite", + "pgsql" + ], + "logo": "svgs/vvveb.svg", + "minversion": "0.0.0", + "port": "80" + }, "weaviate": { "documentation": "https://weaviate.io/developers/weaviate?utm_source=coolify.io", "slogan": "Weaviate is an open-source vector database that stores both objects and vectors, allowing for combining vector search with structured filtering.", @@ -2082,7 +2687,7 @@ "windmill": { "documentation": "https://www.windmill.dev/docs/?utm_source=coolify.io", "slogan": "Windmill is a developer platform to build production-grade multi-steps automations and internal apps.", - "compose": "c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgc2htX3NpemU6IDFnCiAgICB2b2x1bWVzOgogICAgICAtICdkYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtIFBPU1RHUkVTX1BBU1NXT1JEPSRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXdpbmRtaWxsfScKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAncGdfaXNyZWFkeSAtVSBwb3N0Z3JlcycKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgd2luZG1pbGwtc2VydmVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dpbmRtaWxsLWxhYnMvd2luZG1pbGw6bWFpbicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XSU5ETUlMTF84MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiL3dpbmRtaWxsJwogICAgICAtICdNT0RFPSR7TU9ERTotc2VydmVyfScKICAgICAgLSBCQVNFX1VSTD0kU0VSVklDRV9GUUROX1dJTkRNSUxMCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvYXBpL3ZlcnNpb24gfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTE6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiL3dpbmRtaWxsJwogICAgICAtICdNT0RFPSR7TU9ERTotd29ya2VyfScKICAgICAgLSAnV09SS0VSX0dST1VQPSR7V09SS0VSX0dST1VQOi1kZWZhdWx0fScKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnd29ya2VyLWRlcGVuZGVuY3ktY2FjaGU6L3RtcC93aW5kbWlsbC9jYWNoZScKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvYXBpL3ZlcnNpb24gfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTI6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiL3dpbmRtaWxsJwogICAgICAtICdNT0RFPSR7TU9ERTotd29ya2VyfScKICAgICAgLSAnV09SS0VSX0dST1VQPSR7V09SS0VSX0dST1VQOi1kZWZhdWx0fScKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnd29ya2VyLWRlcGVuZGVuY3ktY2FjaGU6L3RtcC93aW5kbWlsbC9jYWNoZScKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvYXBpL3ZlcnNpb24gfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTM6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovL3Bvc3RncmVzOiRTRVJWSUNFX1BBU1NXT1JEX1BPU1RHUkVTQGRiL3dpbmRtaWxsJwogICAgICAtICdNT0RFPSR7TU9ERTotd29ya2VyfScKICAgICAgLSAnV09SS0VSX0dST1VQPSR7V09SS0VSX0dST1VQOi1kZWZhdWx0fScKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICcvdmFyL3J1bi9kb2NrZXIuc29jazovdmFyL3J1bi9kb2NrZXIuc29jaycKICAgICAgLSAnd29ya2VyLWRlcGVuZGVuY3ktY2FjaGU6L3RtcC93aW5kbWlsbC9jYWNoZScKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvYXBpL3ZlcnNpb24gfHwgZXhpdCAxJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLW5hdGl2ZToKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsOm1haW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vcG9zdGdyZXM6JFNFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVNAZGIvd2luZG1pbGwnCiAgICAgIC0gJ01PREU9JHtNT0RFOi13b3JrZXJ9JwogICAgICAtICdXT1JLRVJfR1JPVVA9JHtXT1JLRVJfR1JPVVA6LW5hdGl2ZX0nCiAgICAgIC0gJ05VTV9XT1JLRVJTPSR7TlVNX1dPUktFUlM6LTh9JwogICAgICAtICdTTEVFUF9RVUVVRT0ke1NMRUVQX1FVRVVFOi0yMDB9JwogICAgZGVwZW5kc19vbjoKICAgICAgZGI6CiAgICAgICAgY29uZGl0aW9uOiBzZXJ2aWNlX2hlYWx0aHkKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ECiAgICAgICAgLSBjdXJsCiAgICAgICAgLSAnLWYnCiAgICAgICAgLSAnaHR0cDovL2xvY2FsaG9zdDo4MDAwL2FwaS92ZXJzaW9uIHx8IGV4aXQgMScKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIGxzcDoKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsLWxzcDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdsc3AtY2FjaGU6L3Jvb3QvLmNhY2hlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdleGl0IDAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMK", + "compose": "c2VydmljZXM6CiAgZGI6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2JwogICAgc2htX3NpemU6IDFnCiAgICB2b2x1bWVzOgogICAgICAtICdkYi1kYXRhOi92YXIvbGliL3Bvc3RncmVzcWwvZGF0YScKICAgIGVudmlyb25tZW50OgogICAgICAtICdQT1NUR1JFU19QQVNTV09SRD0ke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9JwogICAgICAtICdQT1NUR1JFU19VU0VSPSR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfScKICAgICAgLSAnUE9TVEdSRVNfREI9JHtQT1NUR1JFU19EQjotd2luZG1pbGwtZGJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDEwcwogICAgICB0aW1lb3V0OiA1cwogICAgICByZXRyaWVzOiA1CiAgd2luZG1pbGwtc2VydmVyOgogICAgaW1hZ2U6ICdnaGNyLmlvL3dpbmRtaWxsLWxhYnMvd2luZG1pbGw6bWFpbicKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9XSU5ETUlMTF84MDAwCiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXNlcnZlcgogICAgICAtICdCQVNFX1VSTD0ke1NFUlZJQ0VfRlFETl9XSU5ETUlMTH0nCiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnd29ya2VyLWxvZ3M6L3RtcC93aW5kbWlsbC9sb2dzJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQKICAgICAgICAtIGN1cmwKICAgICAgICAtICctZicKICAgICAgICAtICdodHRwOi8vbG9jYWxob3N0OjgwMDAvaGVhbHRoJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTE6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXdvcmtlcgogICAgICAtIFdPUktFUl9HUk9VUD1kZWZhdWx0CiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTI6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXdvcmtlcgogICAgICAtIFdPUktFUl9HUk9VUD1kZWZhdWx0CiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLTM6CiAgICBpbWFnZTogJ2doY3IuaW8vd2luZG1pbGwtbGFicy93aW5kbWlsbDptYWluJwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gJ0RBVEFCQVNFX1VSTD1wb3N0Z3JlczovLyR7U0VSVklDRV9VU0VSX1BPU1RHUkVTfToke1NFUlZJQ0VfUEFTU1dPUkRfUE9TVEdSRVN9QGRiLyR7UE9TVEdSRVNfREI6LXdpbmRtaWxsLWRifScKICAgICAgLSBNT0RFPXdvcmtlcgogICAgICAtIFdPUktFUl9HUk9VUD1kZWZhdWx0CiAgICBkZXBlbmRzX29uOgogICAgICBkYjoKICAgICAgICBjb25kaXRpb246IHNlcnZpY2VfaGVhbHRoeQogICAgdm9sdW1lczoKICAgICAgLSAnL3Zhci9ydW4vZG9ja2VyLnNvY2s6L3Zhci9ydW4vZG9ja2VyLnNvY2snCiAgICAgIC0gJ3dvcmtlci1kZXBlbmRlbmN5LWNhY2hlOi90bXAvd2luZG1pbGwvY2FjaGUnCiAgICAgIC0gJ3dvcmtlci1sb2dzOi90bXAvd2luZG1pbGwvbG9ncycKICAgIGhlYWx0aGNoZWNrOgogICAgICB0ZXN0OgogICAgICAgIC0gQ01ELVNIRUxMCiAgICAgICAgLSAnZXhpdCAwJwogICAgICBpbnRlcnZhbDogMzBzCiAgICAgIHRpbWVvdXQ6IDEwcwogICAgICByZXRyaWVzOiAzCiAgd2luZG1pbGwtd29ya2VyLW5hdGl2ZToKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsOm1haW4nCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AZGIvJHtQT1NUR1JFU19EQjotd2luZG1pbGwtZGJ9JwogICAgICAtIE1PREU9d29ya2VyCiAgICAgIC0gV09SS0VSX0dST1VQPW5hdGl2ZQogICAgICAtIE5VTV9XT1JLRVJTPTgKICAgICAgLSBTTEVFUF9RVUVVRT0yMDAKICAgIGRlcGVuZHNfb246CiAgICAgIGRiOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICB2b2x1bWVzOgogICAgICAtICd3b3JrZXItbG9nczovdG1wL3dpbmRtaWxsL2xvZ3MnCiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRC1TSEVMTAogICAgICAgIC0gJ2V4aXQgMCcKICAgICAgaW50ZXJ2YWw6IDMwcwogICAgICB0aW1lb3V0OiAxMHMKICAgICAgcmV0cmllczogMwogIGxzcDoKICAgIGltYWdlOiAnZ2hjci5pby93aW5kbWlsbC1sYWJzL3dpbmRtaWxsLWxzcDpsYXRlc3QnCiAgICB2b2x1bWVzOgogICAgICAtICdsc3AtY2FjaGU6L3Jvb3QvLmNhY2hlJwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdleGl0IDAnCiAgICAgIGludGVydmFsOiAzMHMKICAgICAgdGltZW91dDogMTBzCiAgICAgIHJldHJpZXM6IDMKICAgICAgc3RhcnRfcGVyaW9kOiAyMHMK", "tags": [ "windmill", "workflow", @@ -2094,6 +2699,20 @@ "minversion": "0.0.0", "port": "8000" }, + "wireguard-easy": { + "documentation": "https://github.com/wg-easy/wg-easy?utm_source=coolify.io", + "slogan": "The easiest way to run WireGuard VPN + Web-based Admin UI.", + "compose": "c2VydmljZXM6CiAgd2ctZWFzeToKICAgIGltYWdlOiAnZ2hjci5pby93Zy1lYXN5L3dnLWVhc3k6bGF0ZXN0JwogICAgZW52aXJvbm1lbnQ6CiAgICAgIC0gU0VSVklDRV9GUUROX1dJUkVHVUFSREVBU1lfODAwMAogICAgICAtICdXR19IT1NUPSR7U0VSVklDRV9GUUROX1dJUkVHVUFSREVBU1l9JwogICAgICAtICdMQU5HPSR7TEFORzotZW59JwogICAgICAtIFBPUlQ9ODAwMAogICAgICAtIFdHX1BPUlQ9NTE4MjAKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3dnLWVhc3k6L2V0Yy93aXJlZ3VhcmQnCiAgICBwb3J0czoKICAgICAgLSAnNTE4MjA6NTE4MjAvdWRwJwogICAgY2FwX2FkZDoKICAgICAgLSBORVRfQURNSU4KICAgICAgLSBTWVNfTU9EVUxFCiAgICBzeXNjdGxzOgogICAgICAtIG5ldC5pcHY0LmNvbmYuYWxsLnNyY192YWxpZF9tYXJrPTEKICAgICAgLSBuZXQuaXB2NC5pcF9mb3J3YXJkPTEK", + "tags": [ + "wireguard", + "vpn", + "web", + "admin" + ], + "logo": "svgs/wireguard.svg", + "minversion": "0.0.0", + "port": "8000" + }, "wordpress-with-mariadb": { "documentation": "https://wordpress.org?utm_source=coolify.io", "slogan": "Wordpress is open source software you can use to create a beautiful website, blog, or app.", @@ -2134,5 +2753,19 @@ ], "logo": "svgs/wordpress.svg", "minversion": "0.0.0" + }, + "zipline": { + "documentation": "https://github.com/diced/zipline?utm_source=coolify.io", + "slogan": "A ShareX/file upload server that is easy to use, packed with features, and with an easy setup!", + "compose": "c2VydmljZXM6CiAgemlwbGluZToKICAgIGltYWdlOiAnZ2hjci5pby9kaWNlZC96aXBsaW5lOmxhdGVzdCcKICAgIGVudmlyb25tZW50OgogICAgICAtIFNFUlZJQ0VfRlFETl9aSVBMSU5FXzMwMDAKICAgICAgLSAnQ09SRV9SRVRVUk5fSFRUUFM9JHtDT1JFX1JFVFVSTl9IVFRQUzotZmFsc2V9JwogICAgICAtICdDT1JFX1NFQ1JFVD0ke1NFUlZJQ0VfUEFTU1dPUkRfNjRfWklQTElORX0nCiAgICAgIC0gJ0NPUkVfREFUQUJBU0VfVVJMPXBvc3RncmVzOi8vJHtTRVJWSUNFX1VTRVJfUE9TVEdSRVN9OiR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU31AcG9zdGdyZXMvJHtQT1NUR1JFU19EQjotemlwbGluZS1kYn0nCiAgICAgIC0gJ0NPUkVfTE9HR0VSPSR7Q09SRV9MT0dHRVI6LXRydWV9JwogICAgdm9sdW1lczoKICAgICAgLSAnemlwbGluZS11cGxvYWRzOi96aXBsaW5lL3VwbG9hZHMnCiAgICAgIC0gJ3ppcGxpbmUtcHVibGljOi96aXBsaW5lL3B1YmxpYycKICAgIGRlcGVuZHNfb246CiAgICAgIHBvc3RncmVzOgogICAgICAgIGNvbmRpdGlvbjogc2VydmljZV9oZWFsdGh5CiAgICBoZWFsdGhjaGVjazoKICAgICAgdGVzdDoKICAgICAgICAtIENNRAogICAgICAgIC0gd2dldAogICAgICAgIC0gJy1xJwogICAgICAgIC0gJy0tc3BpZGVyJwogICAgICAgIC0gJ2h0dHA6Ly8xMjcuMC4wLjE6MzAwMC9hdXRoL2xvZ2luJwogICAgICBpbnRlcnZhbDogNXMKICAgICAgdGltZW91dDogMjBzCiAgICAgIHJldHJpZXM6IDEwCiAgcG9zdGdyZXM6CiAgICBpbWFnZTogJ3Bvc3RncmVzOjE2LWFscGluZScKICAgIHZvbHVtZXM6CiAgICAgIC0gJ3ppcGxpbmUtcG9zdGdyZXMtZGF0YTovdmFyL2xpYi9wb3N0Z3Jlc3FsL2RhdGEnCiAgICBlbnZpcm9ubWVudDoKICAgICAgLSAnUE9TVEdSRVNfVVNFUj0ke1NFUlZJQ0VfVVNFUl9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX1BBU1NXT1JEPSR7U0VSVklDRV9QQVNTV09SRF9QT1NUR1JFU30nCiAgICAgIC0gJ1BPU1RHUkVTX0RCPSR7UE9TVEdSRVNfREI6LXppcGxpbmUtZGJ9JwogICAgaGVhbHRoY2hlY2s6CiAgICAgIHRlc3Q6CiAgICAgICAgLSBDTUQtU0hFTEwKICAgICAgICAtICdwZ19pc3JlYWR5IC1VICQke1BPU1RHUkVTX1VTRVJ9IC1kICQke1BPU1RHUkVTX0RCfScKICAgICAgaW50ZXJ2YWw6IDVzCiAgICAgIHRpbWVvdXQ6IDIwcwogICAgICByZXRyaWVzOiAxMAo=", + "tags": [ + "zipline", + "file-sharing", + "upload", + "sharing" + ], + "logo": "svgs/zipline.png", + "minversion": "0.0.0", + "port": "3000" } } diff --git a/tests/Browser/ExampleTest.php b/tests/Browser/ExampleTest.php deleted file mode 100644 index 15dc8f5f12..0000000000 --- a/tests/Browser/ExampleTest.php +++ /dev/null @@ -1,20 +0,0 @@ -browse(function (Browser $browser) { - $browser->visit('/') - ->assertSee('Laravel'); - }); - } -} diff --git a/tests/Browser/LoginTest.php b/tests/Browser/LoginTest.php new file mode 100644 index 0000000000..5e8d5c53eb --- /dev/null +++ b/tests/Browser/LoginTest.php @@ -0,0 +1,27 @@ +browse(callback: function (Browser $browser) { + $browser->loginWithRootUser() + ->assertPathIs('/') + ->assertSee('Dashboard'); + }); + } +} diff --git a/tests/Browser/Pages/HomePage.php b/tests/Browser/Pages/HomePage.php deleted file mode 100644 index 45d9283f37..0000000000 --- a/tests/Browser/Pages/HomePage.php +++ /dev/null @@ -1,36 +0,0 @@ - - */ - public function elements(): array - { - return [ - '@element' => '#selector', - ]; - } -} diff --git a/tests/Browser/Pages/Page.php b/tests/Browser/Pages/Page.php deleted file mode 100644 index eb9a2ded25..0000000000 --- a/tests/Browser/Pages/Page.php +++ /dev/null @@ -1,20 +0,0 @@ - - */ - public static function siteElements(): array - { - return [ - '@element' => '#selector', - ]; - } -} diff --git a/tests/Browser/Project/ProjectAddNewTest.php b/tests/Browser/Project/ProjectAddNewTest.php new file mode 100644 index 0000000000..0dae7603ea --- /dev/null +++ b/tests/Browser/Project/ProjectAddNewTest.php @@ -0,0 +1,34 @@ +browse(function (Browser $browser) { + $browser->loginWithRootUser() + ->visit('/projects') + ->pressAndWaitFor('+ Add', 1) + ->assertSee('New Project') + ->screenshot('project-add-new-1') + ->type('name', 'Test Project') + ->screenshot('project-add-new-2') + ->press('Continue') + ->assertSee('Test Project.') + ->screenshot('project-add-new-3'); + }); + } +} diff --git a/tests/Browser/Project/ProjectSearchTest.php b/tests/Browser/Project/ProjectSearchTest.php new file mode 100644 index 0000000000..aedf17183e --- /dev/null +++ b/tests/Browser/Project/ProjectSearchTest.php @@ -0,0 +1,29 @@ +browse(function (Browser $browser) { + $browser->loginWithRootUser() + ->visit('/projects') + ->type('[x-model="search"]', 'joi43j4oi32j4o2') + ->assertSee('No project found with the search term "joi43j4oi32j4o2".') + ->screenshot('project-search-not-found'); + }); + } +} diff --git a/tests/Browser/Project/ProjectTest.php b/tests/Browser/Project/ProjectTest.php new file mode 100644 index 0000000000..e4707da8ae --- /dev/null +++ b/tests/Browser/Project/ProjectTest.php @@ -0,0 +1,27 @@ +browse(function (Browser $browser) { + $browser->loginWithRootUser() + ->visit('/projects') + ->assertSee('Projects'); + }); + } +} diff --git a/tests/DuskTestCase.php b/tests/DuskTestCase.php index 8628871a18..98e90fa79c 100644 --- a/tests/DuskTestCase.php +++ b/tests/DuskTestCase.php @@ -39,7 +39,7 @@ protected function driver(): RemoteWebDriver })->all()); return RemoteWebDriver::create( - $_ENV['DUSK_DRIVER_URL'] ?? 'http://localhost:9515', + 'http://localhost:4444', DesiredCapabilities::chrome()->setCapability( ChromeOptions::CAPABILITY, $options @@ -50,23 +50,8 @@ protected function driver(): RemoteWebDriver /** * Determine if the browser window should start maximized. */ - protected function shouldStartMaximized(): bool - { - return isset($_SERVER['DUSK_START_MAXIMIZED']) || - isset($_ENV['DUSK_START_MAXIMIZED']); - } - - /** - * Determine whether the Dusk command has disabled headless mode. - */ - protected function hasHeadlessDisabled(): bool - { - return isset($_SERVER['DUSK_HEADLESS_DISABLED']) || - isset($_ENV['DUSK_HEADLESS_DISABLED']); - } - protected function baseUrl() { - return rtrim(config('app.url'), '/'); + return 'http://localhost:8000'; } } diff --git a/tests/Feature/DockerComposeParseTest.php b/tests/Feature/DockerComposeParseTest.php index daaa8b2f1d..8810280dcc 100644 --- a/tests/Feature/DockerComposeParseTest.php +++ b/tests/Feature/DockerComposeParseTest.php @@ -172,9 +172,6 @@ test('ServiceComposeParseNew', function () { $output = newParser($this->service); $this->service->saveComposeConfigs(); - // ray('New parser'); - // ray($output->toArray()); - ray($this->service->environment_variables->pluck('value', 'key')->toArray()); expect($output)->toBeInstanceOf(Collection::class); }); diff --git a/tests/Feature/DockerCustomCommandsTest.php b/tests/Feature/DockerCustomCommandsTest.php index a0baeb2159..a073720696 100644 --- a/tests/Feature/DockerCustomCommandsTest.php +++ b/tests/Feature/DockerCustomCommandsTest.php @@ -2,7 +2,7 @@ test('ConvertCapAdd', function () { $input = '--cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add SYS_ADMIN'; - $output = convert_docker_run_to_compose($input); + $output = convertDockerRunToCompose($input); expect($output)->toBe([ 'cap_add' => ['NET_ADMIN', 'NET_RAW', 'SYS_ADMIN'], ]); @@ -10,7 +10,7 @@ test('ConvertIp', function () { $input = '--cap-add=NET_ADMIN --cap-add=NET_RAW --cap-add SYS_ADMIN --ip 127.0.0.1 --ip 127.0.0.2'; - $output = convert_docker_run_to_compose($input); + $output = convertDockerRunToCompose($input); expect($output)->toBe([ 'cap_add' => ['NET_ADMIN', 'NET_RAW', 'SYS_ADMIN'], 'ip' => ['127.0.0.1', '127.0.0.2'], @@ -19,7 +19,7 @@ test('ConvertPrivilegedAndInit', function () { $input = '---privileged --init'; - $output = convert_docker_run_to_compose($input); + $output = convertDockerRunToCompose($input); expect($output)->toBe([ 'privileged' => true, 'init' => true, @@ -28,7 +28,7 @@ test('ConvertUlimit', function () { $input = '--ulimit nofile=262144:262144'; - $output = convert_docker_run_to_compose($input); + $output = convertDockerRunToCompose($input); expect($output)->toBe([ 'ulimits' => [ 'nofile' => [ @@ -38,3 +38,61 @@ ], ]); }); +test('ConvertGpusWithGpuId', function () { + $input = '--gpus "device=GPU-0000000000000000"'; + $output = convertDockerRunToCompose($input); + expect($output)->toBe([ + 'deploy' => [ + 'resources' => [ + 'reservations' => [ + 'devices' => [ + [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + 'device_ids' => ['GPU-0000000000000000'], + ], + ], + ], + ], + ], + ]); +}); + +test('ConvertGpus', function () { + $input = '--gpus all'; + $output = convertDockerRunToCompose($input); + expect($output)->toBe([ + 'deploy' => [ + 'resources' => [ + 'reservations' => [ + 'devices' => [ + [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + ], + ], + ], + ], + ], + ]); +}); + +test('ConvertGpusWithQuotes', function () { + $input = '--gpus "device=0,1"'; + $output = convertDockerRunToCompose($input); + expect($output)->toBe([ + 'deploy' => [ + 'resources' => [ + 'reservations' => [ + 'devices' => [ + [ + 'driver' => 'nvidia', + 'capabilities' => ['gpu'], + 'device_ids' => ['0', '1'], + ], + ], + ], + ], + ], + ]); +}); diff --git a/versions.json b/versions.json index a3a9c040b5..dbb2955909 100644 --- a/versions.json +++ b/versions.json @@ -4,13 +4,16 @@ "version": "4.0.0-beta.360" }, "nightly": { - "version": "4.0.0-beta.361" + "version": "4.0.0-beta.362" }, "helper": { - "version": "1.0.2" + "version": "1.0.3" }, "realtime": { - "version": "1.0.3" + "version": "1.0.4" + }, + "sentinel": { + "version": "0.0.15" } } }