diff --git a/.dockerignore b/.dockerignore index 66ff0bf1f..1b0bcf1c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -8,7 +8,6 @@ package .env.* !.env.example dist -client apps/api/db/*.db local-serve apps/api/db/migration.db-journal diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml index 53853d157..65f2ed40d 100644 --- a/.github/ISSUE_TEMPLATE/--bug-report.yaml +++ b/.github/ISSUE_TEMPLATE/--bug-report.yaml @@ -9,13 +9,21 @@ body: - type: markdown attributes: value: | - Thanks for taking the time to fill out this bug report! Please fill the form in English + Thanks for taking the time to fill out this bug report! Please fill the form in English. - type: checkboxes attributes: label: Is there an existing issue for this? options: - label: I have searched the existing issues required: true +- type: input + id: repository + attributes: + label: Example public repository + description: "An example public git repository to reproduce the issue easily (if applicable)." + placeholder: "ex: https://github.com/coollabsio/coolify" + validations: + required: false - type: textarea attributes: label: Description diff --git a/.github/workflows/pocketbase-release.yml b/.github/workflows/pocketbase-release.yml index 27b639a42..6c8070904 100644 --- a/.github/workflows/pocketbase-release.yml +++ b/.github/workflows/pocketbase-release.yml @@ -3,10 +3,8 @@ name: pocketbase-release on: push: paths: - - "others/pocketbase" + - "others/pocketbase/*" - ".github/workflows/pocketbase-release.yml" - branches: - - next jobs: arm64: @@ -29,7 +27,7 @@ jobs: context: others/pocketbase/ platforms: linux/arm64 push: true - tags: coollabsio/pocketbase:0.8.0-arm64 + tags: coollabsio/pocketbase:0.10.2-arm64 amd64: runs-on: ubuntu-latest steps: @@ -50,7 +48,7 @@ jobs: context: others/pocketbase/ platforms: linux/amd64 push: true - tags: coollabsio/pocketbase:0.8.0-amd64 + tags: coollabsio/pocketbase:0.10.2-amd64 aarch64: runs-on: [self-hosted, arm64] steps: @@ -71,7 +69,7 @@ jobs: context: others/pocketbase/ platforms: linux/aarch64 push: true - tags: coollabsio/pocketbase:0.8.0-aarch64 + tags: coollabsio/pocketbase:0.10.2-aarch64 merge-manifest: runs-on: ubuntu-latest needs: [amd64, arm64, aarch64] @@ -89,5 +87,5 @@ jobs: password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Create & publish manifest run: | - docker manifest create coollabsio/pocketbase:0.8.0 --amend coollabsio/pocketbase:0.8.0-amd64 --amend coollabsio/pocketbase:0.8.0-arm64 --amend coollabsio/pocketbase:0.8.0-aarch64 - docker manifest push coollabsio/pocketbase:0.8.0 + docker manifest create coollabsio/pocketbase:0.10.2 --amend coollabsio/pocketbase:0.10.2-amd64 --amend coollabsio/pocketbase:0.10.2-arm64 --amend coollabsio/pocketbase:0.10.2-aarch64 + docker manifest push coollabsio/pocketbase:0.10.2 diff --git a/.github/workflows/production-release.yml b/.github/workflows/production-release.yml index 44b5f7003..7b98d5cf9 100644 --- a/.github/workflows/production-release.yml +++ b/.github/workflows/production-release.yml @@ -104,7 +104,9 @@ jobs: - name: Create & publish manifest run: | docker manifest create coollabsio/coolify:${{steps.package-version.outputs.current-version}} --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 + docker manifest create coollabsio/coolify:latest --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-amd64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-arm64 --amend coollabsio/coolify:${{steps.package-version.outputs.current-version}}-aarch64 docker manifest push coollabsio/coolify:${{steps.package-version.outputs.current-version}} + docker manifest push coollabsio/coolify:latest - uses: sarisia/actions-status-discord@v1 if: always() with: diff --git a/.gitignore b/.gitignore index c31810490..6ace702bf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,25 @@ .DS_Store node_modules .pnpm-store -build +/apps/ui/build +/build .svelte-kit package .env .env.* !.env.example dist -client apps/api/db/*.db apps/api/db/migration.db-journal apps/api/core* +apps/server/build apps/backup/backups/* !apps/backup/backups/.gitkeep -logs +/logs others/certificates backups/* -!backups/.gitkeep \ No newline at end of file +!backups/.gitkeep + +# Trpc +apps/server/db/*.db +apps/server/db/*.db-journal \ No newline at end of file diff --git a/apps/api/db/dev.db.bak b/apps/api/db/dev.db.bak new file mode 100644 index 000000000..2a6385b11 Binary files /dev/null and b/apps/api/db/dev.db.bak differ diff --git a/apps/api/devTags.json b/apps/api/devTags.json index 0d84f6caa..bc5273fc0 100644 --- a/apps/api/devTags.json +++ b/apps/api/devTags.json @@ -1 +1 @@ -[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.8","v1.8.6","v1.8.4","v1.8.2","v1.8.10","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24","v1.5.22"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.2","1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_8e5cff30d95963eaee2587488d351e0d658c8195","SHA_8cabe2817ce7ccaf2f0a9fdbb1b5d3411de87f81","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_255c30ed012fc4c39ffc97efc1d3b00425b17c72","SHA_17f92b16ef790003338f0926fc8d791a9a61333c"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.24.2","5.24.1","5.24.0","5.23.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0","v1.0.8"]},{"name":"grafana","image":"grafana/grafana","tags":["9.3.1","9.3.0","9.2.7","9.2.6","9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.15.2","v2.15.1","v2.15.0","v2.14.1","v2.14.0","v2.13.2","v2.13.1","v2.13.0","v2.12.1","v2.12.0","v2.11.3"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.7","v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.30.0","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0","v0.10.0","0.14.1"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-11-29T23-40-49Z.fips","RELEASE.2022-11-26T22-43-32Z.fips","RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.hotfix.ce525fdaf","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips","RELEASE.2022-08-08T18-34-09Z.fips","RELEASE.2022-08-05T23-27-09Z.fips","RELEASE.2022-08-02T23-59-16Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.99.2","0.99.0","0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22"]},{"name":"plausibleanalytics-arm","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"pocketbase","image":"coollabsio/pocketbase","tags":["0.8.0-arm64","0.8.0-amd64","0.8.0-aarch64","0.8.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.12.02-ffb72dfd","2022.12.02-890d63b9","2022.12.02-4970db05","2022.12.02-317fe0a2","2022.11.30-f19837cf","2022.11.30-44d4a171","2022.11.30-0361f836","2022.11.29-a8359dd4","2022.11.29-82af2f44","2022.11.29-768659f2","2022.11.29-5b19f892","2022.11.29-3579a38a","2022.11.29-1b2f1c17","2022.11.25-5ca6868c","2022.11.25-28ae469f","2022.11.25-1314c1c5","2022.11.19-b5371b7a","2022.11.18-fe8b0472","2022.11.18-1cdadf4b","2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff","2022.11.07-d3949269","2022.11.07-8f19bdaf","2022.11.06-ae54c7d5","2022.11.06-2dc5c0e1","2022.11.05-e9f42e1c","2022.11.05-d764d94a","2022.11.05-d3a7399e"]},{"name":"trilium","image":"zadam/trilium","tags":["0.57.2","0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6","0.44.4"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-12-01-0295bd44d4d9da0e0836b9152319fba173a0825e","edge-2022-11-28-f28431a1e78f88bf49ccf539fbc00afe0925542d","edge-2022-11-26-558811de16025b83de43d2747f1fe209a5b829f1","edge-2022-11-23-4a1fe25c7b70e49156e02183a8deec3b357b9030","edge-2022-11-22-9a178e7f5c2e387329592a1dd7700671f64f6682","edge-2022-11-21-eb741ebad70211ecb1babdfd23e4f43c5a59fc7b","edge-2022-11-21-4580d37f616650cf5b0851fee051651f785e8852","edge-2022-11-21-0f74d6c4d3777dbf28affd09b45c69c85ed01d84","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file +[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.9","v1.8.7","v1.8.5","v1.8.3","v1.8.12","v1.8.10","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.2","1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_8e5cff30d95963eaee2587488d351e0d658c8195","SHA_8cabe2817ce7ccaf2f0a9fdbb1b5d3411de87f81","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_255c30ed012fc4c39ffc97efc1d3b00425b17c72","SHA_17f92b16ef790003338f0926fc8d791a9a61333c"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.25.3","5.25.2","5.25.1","5.25.0","5.24.2","5.24.1","5.24.0","5.23.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.25.3","5.25.2","5.25.1","5.25.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.25.3","5.25.2","5.25.1","5.25.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v3.0.0","v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0"]},{"name":"grafana","image":"grafana/grafana","tags":["9.3.1","9.3.0","9.2.7","9.2.6","9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.16.0","v2.15.2","v2.14.1","v2.13.2","v2.12.1","v2.11.3","v2.10.2","v2.1.1","v2.1.0","v2.0.9","v2.0.8"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.7","v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.30.1","v0.30.0","v0.29.3","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-12-12T19-27-27Z.fips","RELEASE.2022-12-07T00-56-37Z.fips","RELEASE.2022-12-02T19-19-22Z.fips","RELEASE.2022-11-29T23-40-49Z.fips","RELEASE.2022-11-26T22-43-32Z.fips","RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.hotfix.ce525fdaf","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.99.1","0.98.4","0.98.2","0.98.0","0.96.4","0.96.2","0.96.0","0.92.3","0.91.9","0.91.7","0.91.10","0.91.0","0.90.7","0.90.4","0.90.2","0.90.10","0.90.0","0.9.8","0.9.6","0.9.42","0.9.40","0.9.38","0.9.36","0.9.34","0.9.32","0.9.30","0.9.28","0.9.26","0.9.24","0.9.21"]},{"name":"plausibleanalytics-arm","image":"plausible/analytics","tags":["v1.5.1","v1.5.0-rc.2","v1.5.0-rc.1","v1.5.0","v1.5","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.1","v1.5.0-rc.2","v1.5.0-rc.1","v1.5.0","v1.5","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master"]},{"name":"pocketbase","image":"coollabsio/pocketbase","tags":["0.8.0-arm64","0.8.0-amd64","0.8.0-aarch64","0.8.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.12.12-966e9c3c","2022.12.09-abb33bd7","2022.12.09-a6d870d5","2022.12.09-6479b67c","2022.12.09-3df1a983","2022.12.09-0b1f09fa","2022.12.05-67eea86b","2022.12.02-ffb72dfd","2022.12.02-890d63b9","2022.12.02-4970db05","2022.12.02-317fe0a2","2022.11.30-f19837cf","2022.11.30-44d4a171","2022.11.30-0361f836","2022.11.29-a8359dd4","2022.11.29-82af2f44","2022.11.29-768659f2","2022.11.29-5b19f892","2022.11.29-3579a38a","2022.11.29-1b2f1c17","2022.11.25-5ca6868c","2022.11.25-28ae469f","2022.11.25-1314c1c5","2022.11.19-b5371b7a","2022.11.18-fe8b0472","2022.11.18-1cdadf4b","2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff"]},{"name":"trilium","image":"zadam/trilium","tags":["0.57.4","0.57.2","0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.9.0","4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-12-12-c0db193a3baacd107c5f2c28c6e0af89c3d5afa3","edge-2022-12-09-647d40c67cf405870ba71a01584a42cfaec5915f","edge-2022-12-09-5f92b1c3243ef445a9b5929eda8bd0307584f36c","edge-2022-12-08-d1801c18970ee45fcd6c5a691d79613110537152","edge-2022-12-08-c66eb8190d1e55fe6e53d3fa87f101c189c635d4","edge-2022-12-07-6b4c53cba19003d5cf15dcbaa8edb8304fb1ae6c","edge-2022-12-06-f7644c880ee17e917d5fb72497aa5b75ecca7dd2","edge-2022-12-05-d3bf38faf4599fe6c7c86c20f33f4dd7a98de645","edge-2022-12-01-0295bd44d4d9da0e0836b9152319fba173a0825e","edge-2022-11-28-f28431a1e78f88bf49ccf539fbc00afe0925542d","edge-2022-11-26-558811de16025b83de43d2747f1fe209a5b829f1","edge-2022-11-23-4a1fe25c7b70e49156e02183a8deec3b357b9030","edge-2022-11-22-9a178e7f5c2e387329592a1dd7700671f64f6682","edge-2022-11-21-eb741ebad70211ecb1babdfd23e4f43c5a59fc7b","edge-2022-11-21-4580d37f616650cf5b0851fee051651f785e8852","edge-2022-11-21-0f74d6c4d3777dbf28affd09b45c69c85ed01d84","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file diff --git a/apps/api/devTemplates.yaml b/apps/api/devTemplates.yaml index 975b7b397..1e84a6655 100644 --- a/apps/api/devTemplates.yaml +++ b/apps/api/devTemplates.yaml @@ -1,5 +1,5 @@ - templateVersion: 1.0.0 - defaultVersion: '0.8.0' + defaultVersion: "0.10.2" documentation: https://pocketbase.io/docs/ type: pocketbase name: Pocketbase @@ -12,7 +12,7 @@ ports: - "8080" - templateVersion: 1.0.0 - defaultVersion: 1.5.0-rc.0 + defaultVersion: v1.5.1 documentation: https://plausible.io/doc/ type: plausibleanalytics-arm name: Plausible Analytics (ARM) @@ -313,7 +313,7 @@ defaultValue: keycloak description: "" - templateVersion: 1.0.0 - defaultVersion: v3.6 + defaultVersion: v3.7 documentation: https://github.com/freyacodes/Lavalink description: Standalone audio sending node based on Lavaplayer. type: lavalink @@ -375,7 +375,7 @@ defaultValue: $$generate_password required: true - templateVersion: 1.0.0 - defaultVersion: v1.8.6 + defaultVersion: v1.8.9 documentation: https://docs.appsmith.com/getting-started/setup/instance-configuration/ type: appsmith name: Appsmith @@ -408,7 +408,7 @@ defaultValue: "true" description: "" - templateVersion: 1.0.0 - defaultVersion: 0.56.2 + defaultVersion: 0.57.4 documentation: https://hub.docker.com/r/zadam/trilium description: "A hierarchical note taking application with focus on building large personal knowledge bases." labels: @@ -428,7 +428,7 @@ - "8080" variables: [] - templateVersion: 1.0.0 - defaultVersion: 1.9.2 + defaultVersion: 1.18.5 documentation: https://hub.docker.com/r/louislam/uptime-kuma description: A free & fancy self-hosted monitoring tool. labels: @@ -478,7 +478,7 @@ - "80" variables: [] - templateVersion: 1.0.0 - defaultVersion: 9.2.3 + defaultVersion: 9.3.1 documentation: https://hub.docker.com/r/grafana/grafana type: grafana name: Grafana @@ -499,7 +499,7 @@ - "3000" variables: [] - templateVersion: 1.0.0 - defaultVersion: 1.0.3 + defaultVersion: 1.1.2 documentation: https://appwrite.io/docs type: appwrite name: Appwrite @@ -646,6 +646,7 @@ - "$$id-functions:/storage/functions" - "$$id-builds:/storage/builds" - "/var/run/docker.sock:/var/run/docker.sock" + - "/tmp:/tmp:rw" entrypoint: executor "$$id-influxdb": image: appwrite/influxdb:1.5.0 @@ -1668,7 +1669,7 @@ defaultValue: weblate description: "" - templateVersion: 1.0.0 - defaultVersion: 2022.10.14-1a5b0965 + defaultVersion: 2022.12.12-966e9c3c documentation: https://docs.searxng.org/ type: searxng name: SearXNG @@ -1741,7 +1742,7 @@ defaultValue: $$generate_password description: "" - templateVersion: 1.0.0 - defaultVersion: v2.0.6 + defaultVersion: v3.0.0 documentation: https://glitchtip.com/documentation type: glitchtip name: GlitchTip @@ -1963,7 +1964,7 @@ defaultValue: glitchtip description: "" - templateVersion: 1.0.0 - defaultVersion: v2.13.0 + defaultVersion: v2.16.0 documentation: https://hasura.io/docs/latest/index/ type: hasura name: Hasura @@ -2030,7 +2031,7 @@ defaultValue: hasura description: "" - templateVersion: 1.0.0 - defaultVersion: postgresql-v1.38.0 + defaultVersion: postgresql-v1.39.5 documentation: https://umami.is/docs/getting-started type: umami-postgresql name: Umami @@ -2074,10 +2075,10 @@ "is_admin" BOOLEAN NOT NULL DEFAULT false, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - + PRIMARY KEY ("user_id") ); - + -- CreateTable CREATE TABLE "event" ( "event_id" SERIAL NOT NULL, @@ -2087,10 +2088,10 @@ "url" VARCHAR(500) NOT NULL, "event_type" VARCHAR(50) NOT NULL, "event_value" VARCHAR(50) NOT NULL, - + PRIMARY KEY ("event_id") ); - + -- CreateTable CREATE TABLE "pageview" ( "view_id" SERIAL NOT NULL, @@ -2099,10 +2100,10 @@ "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "url" VARCHAR(500) NOT NULL, "referrer" VARCHAR(500), - + PRIMARY KEY ("view_id") ); - + -- CreateTable CREATE TABLE "session" ( "session_id" SERIAL NOT NULL, @@ -2116,10 +2117,10 @@ "screen" VARCHAR(11), "language" VARCHAR(35), "country" CHAR(2), - + PRIMARY KEY ("session_id") ); - + -- CreateTable CREATE TABLE "website" ( "website_id" SERIAL NOT NULL, @@ -2129,73 +2130,73 @@ "domain" VARCHAR(500), "share_id" VARCHAR(64), "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - + PRIMARY KEY ("website_id") ); - + -- CreateIndex CREATE UNIQUE INDEX "account.username_unique" ON "account"("username"); - + -- CreateIndex CREATE INDEX "event_created_at_idx" ON "event"("created_at"); - + -- CreateIndex CREATE INDEX "event_session_id_idx" ON "event"("session_id"); - + -- CreateIndex CREATE INDEX "event_website_id_idx" ON "event"("website_id"); - + -- CreateIndex CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at"); - + -- CreateIndex CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id"); - + -- CreateIndex CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at"); - + -- CreateIndex CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id"); - + -- CreateIndex CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at"); - + -- CreateIndex CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid"); - + -- CreateIndex CREATE INDEX "session_created_at_idx" ON "session"("created_at"); - + -- CreateIndex CREATE INDEX "session_website_id_idx" ON "session"("website_id"); - + -- CreateIndex CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid"); - + -- CreateIndex CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id"); - + -- CreateIndex CREATE INDEX "website_user_id_idx" ON "website"("user_id"); - + -- AddForeignKey ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; - + insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true); variables: - id: $$secret_database_url @@ -2237,7 +2238,7 @@ showOnConfiguration: true - templateVersion: 1.0.0 ignore: true - defaultVersion: postgresql-v1.38.0 + defaultVersion: postgresql-v1.39.5 documentation: https://umami.is/docs/getting-started type: umami name: Umami @@ -2281,10 +2282,10 @@ "is_admin" BOOLEAN NOT NULL DEFAULT false, "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "updated_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - + PRIMARY KEY ("user_id") ); - + -- CreateTable CREATE TABLE "event" ( "event_id" SERIAL NOT NULL, @@ -2294,10 +2295,10 @@ "url" VARCHAR(500) NOT NULL, "event_type" VARCHAR(50) NOT NULL, "event_value" VARCHAR(50) NOT NULL, - + PRIMARY KEY ("event_id") ); - + -- CreateTable CREATE TABLE "pageview" ( "view_id" SERIAL NOT NULL, @@ -2306,10 +2307,10 @@ "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, "url" VARCHAR(500) NOT NULL, "referrer" VARCHAR(500), - + PRIMARY KEY ("view_id") ); - + -- CreateTable CREATE TABLE "session" ( "session_id" SERIAL NOT NULL, @@ -2323,10 +2324,10 @@ "screen" VARCHAR(11), "language" VARCHAR(35), "country" CHAR(2), - + PRIMARY KEY ("session_id") ); - + -- CreateTable CREATE TABLE "website" ( "website_id" SERIAL NOT NULL, @@ -2336,73 +2337,73 @@ "domain" VARCHAR(500), "share_id" VARCHAR(64), "created_at" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP, - + PRIMARY KEY ("website_id") ); - + -- CreateIndex CREATE UNIQUE INDEX "account.username_unique" ON "account"("username"); - + -- CreateIndex CREATE INDEX "event_created_at_idx" ON "event"("created_at"); - + -- CreateIndex CREATE INDEX "event_session_id_idx" ON "event"("session_id"); - + -- CreateIndex CREATE INDEX "event_website_id_idx" ON "event"("website_id"); - + -- CreateIndex CREATE INDEX "pageview_created_at_idx" ON "pageview"("created_at"); - + -- CreateIndex CREATE INDEX "pageview_session_id_idx" ON "pageview"("session_id"); - + -- CreateIndex CREATE INDEX "pageview_website_id_created_at_idx" ON "pageview"("website_id", "created_at"); - + -- CreateIndex CREATE INDEX "pageview_website_id_idx" ON "pageview"("website_id"); - + -- CreateIndex CREATE INDEX "pageview_website_id_session_id_created_at_idx" ON "pageview"("website_id", "session_id", "created_at"); - + -- CreateIndex CREATE UNIQUE INDEX "session.session_uuid_unique" ON "session"("session_uuid"); - + -- CreateIndex CREATE INDEX "session_created_at_idx" ON "session"("created_at"); - + -- CreateIndex CREATE INDEX "session_website_id_idx" ON "session"("website_id"); - + -- CreateIndex CREATE UNIQUE INDEX "website.website_uuid_unique" ON "website"("website_uuid"); - + -- CreateIndex CREATE UNIQUE INDEX "website.share_id_unique" ON "website"("share_id"); - + -- CreateIndex CREATE INDEX "website_user_id_idx" ON "website"("user_id"); - + -- AddForeignKey ALTER TABLE "event" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "event" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "pageview" ADD FOREIGN KEY ("session_id") REFERENCES "session"("session_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "pageview" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "session" ADD FOREIGN KEY ("website_id") REFERENCES "website"("website_id") ON DELETE CASCADE ON UPDATE CASCADE; - + -- AddForeignKey ALTER TABLE "website" ADD FOREIGN KEY ("user_id") REFERENCES "account"("user_id") ON DELETE CASCADE ON UPDATE CASCADE; - + insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true); variables: - id: $$secret_database_url @@ -2443,7 +2444,7 @@ description: "" showOnConfiguration: true - templateVersion: 1.0.0 - defaultVersion: v0.29.1 + defaultVersion: v0.30.1 documentation: https://docs.meilisearch.com/learn/getting_started/quick_start.html type: meilisearch name: MeiliSearch @@ -2591,7 +2592,7 @@ defaultValue: $$generate_password description: "" - templateVersion: 1.0.0 - defaultVersion: "5.22" + defaultVersion: "5.25.3" documentation: https://docs.ghost.org type: ghost-only name: Ghost @@ -2655,7 +2656,7 @@ placeholder: "ghost_db" required: true - templateVersion: 1.0.0 - defaultVersion: "5.22" + defaultVersion: "5.25.3" documentation: https://docs.ghost.org type: ghost-mysql name: Ghost @@ -2896,7 +2897,7 @@ define('WP_DEBUG_DISPLAY', false); @ini_set('display_errors', 0); - templateVersion: 1.0.0 - defaultVersion: 4.7.1 + defaultVersion: 4.9.0 documentation: https://coder.com/docs/coder-oss/latest type: vscodeserver name: VSCode Server @@ -2927,7 +2928,7 @@ description: "" showOnConfiguration: true - templateVersion: 1.0.0 - defaultVersion: RELEASE.2022-10-15T19-57-03Z + defaultVersion: RELEASE.2022-12-12T19-27-27Z documentation: https://min.io/docs/minio type: minio name: MinIO @@ -3105,7 +3106,7 @@ defaultValue: $$generate_username description: "" - templateVersion: 1.0.0 - defaultVersion: 0.198.1 + defaultVersion: 0.207.0 documentation: https://docs.n8n.io type: n8n name: n8n.io @@ -3282,7 +3283,7 @@ defaultValue: plausible.js description: This is the default script name. - templateVersion: 1.0.0 - defaultVersion: 0.98.1 + defaultVersion: 0.99.1 documentation: https://docs.nocodb.com type: nocodb name: NocoDB diff --git a/apps/api/package.json b/apps/api/package.json index ad987b719..59d0c1855 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,85 +1,85 @@ { - "name": "api", - "description": "Coolify's Fastify API", - "license": "Apache-2.0", - "scripts": { - "db:generate": "prisma generate", - "db:push": "prisma db push && prisma generate", - "db:seed": "prisma db seed", - "db:studio": "prisma studio", - "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", - "dev": "nodemon", - "build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs", - "format": "prettier --write 'src/**/*.{js,ts,json,md}'", - "lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .", - "start": "NODE_ENV=production pnpm prisma migrate deploy && pnpm prisma generate && pnpm prisma db seed && node index.js" - }, - "dependencies": { - "@breejs/ts-worker": "2.0.0", - "@fastify/autoload": "5.5.0", - "@fastify/cookie": "8.3.0", - "@fastify/cors": "8.2.0", - "@fastify/env": "4.1.0", - "@fastify/jwt": "6.3.3", - "@fastify/multipart": "7.3.0", - "@fastify/static": "6.5.1", - "@iarna/toml": "2.2.5", - "@ladjs/graceful": "3.0.2", - "@prisma/client": "4.6.1", - "@sentry/node": "7.21.1", - "@sentry/tracing": "7.21.1", - "axe": "11.0.0", - "bcryptjs": "2.4.3", - "bree": "9.1.2", - "cabin": "11.0.1", - "compare-versions": "5.0.1", - "csv-parse": "5.3.2", - "csvtojson": "2.0.10", - "cuid": "2.1.8", - "dayjs": "1.11.6", - "dockerode": "3.3.4", - "dotenv-extended": "2.9.0", - "execa": "6.1.0", - "fastify": "4.10.2", - "fastify-plugin": "4.3.0", - "fastify-socket.io": "4.0.0", - "generate-password": "1.7.0", - "got": "12.5.3", - "is-ip": "5.0.0", - "is-port-reachable": "4.0.0", - "js-yaml": "4.1.0", - "jsonwebtoken": "8.5.1", - "minimist": "^1.2.7", - "node-forge": "1.3.1", - "node-os-utils": "1.3.7", - "p-all": "4.0.0", - "p-throttle": "5.0.0", - "prisma": "4.6.1", - "public-ip": "6.0.1", - "pump": "3.0.0", - "shell-quote": "^1.7.4", - "socket.io": "4.5.3", - "ssh-config": "4.1.6", - "strip-ansi": "7.0.1", - "unique-names-generator": "4.7.1" - }, - "devDependencies": { - "@types/node": "18.11.9", - "@types/node-os-utils": "1.3.0", - "@typescript-eslint/eslint-plugin": "5.44.0", - "@typescript-eslint/parser": "5.44.0", - "esbuild": "0.15.15", - "eslint": "8.28.0", - "eslint-config-prettier": "8.5.0", - "eslint-plugin-prettier": "4.2.1", - "nodemon": "2.0.20", - "prettier": "2.7.1", - "rimraf": "3.0.2", - "tsconfig-paths": "4.1.0", - "types-fastify-socket.io": "0.0.1", - "typescript": "4.9.3" - }, - "prisma": { - "seed": "node prisma/seed.js" - } -} \ No newline at end of file + "name": "api", + "description": "Coolify's Fastify API", + "license": "Apache-2.0", + "scripts": { + "db:generate": "prisma generate", + "db:push": "prisma db push && prisma generate", + "db:seed": "prisma db seed", + "db:studio": "prisma studio", + "db:migrate": "COOLIFY_DATABASE_URL=file:../db/migration.db prisma migrate dev --skip-seed --name", + "dev": "nodemon", + "build": "rimraf build && esbuild `find src \\( -name '*.ts' \\)| grep -v client/` --platform=node --outdir=build --format=cjs", + "format": "prettier --write 'src/**/*.{js,ts,json,md}'", + "lint": "prettier --check 'src/**/*.{js,ts,json,md}' && eslint --ignore-path .eslintignore .", + "start": "NODE_ENV=production pnpm prisma migrate deploy && pnpm prisma generate && pnpm prisma db seed && node index.js" + }, + "dependencies": { + "@breejs/ts-worker": "2.0.0", + "@fastify/autoload": "5.5.0", + "@fastify/cookie": "8.3.0", + "@fastify/cors": "8.2.0", + "@fastify/env": "4.1.0", + "@fastify/jwt": "6.3.3", + "@fastify/multipart": "7.3.0", + "@fastify/static": "6.5.1", + "@iarna/toml": "2.2.5", + "@ladjs/graceful": "3.0.2", + "@prisma/client": "4.6.1", + "@sentry/node": "7.21.1", + "@sentry/tracing": "7.21.1", + "axe": "11.0.0", + "bcryptjs": "2.4.3", + "bree": "9.1.2", + "cabin": "11.0.1", + "compare-versions": "5.0.1", + "csv-parse": "5.3.2", + "csvtojson": "2.0.10", + "cuid": "2.1.8", + "dayjs": "1.11.6", + "dockerode": "3.3.4", + "dotenv-extended": "2.9.0", + "execa": "6.1.0", + "fastify": "4.10.2", + "fastify-plugin": "4.3.0", + "fastify-socket.io": "4.0.0", + "generate-password": "1.7.0", + "got": "12.5.3", + "is-ip": "5.0.0", + "is-port-reachable": "4.0.0", + "js-yaml": "4.1.0", + "jsonwebtoken": "8.5.1", + "minimist": "^1.2.7", + "node-forge": "1.3.1", + "node-os-utils": "1.3.7", + "p-all": "4.0.0", + "p-throttle": "5.0.0", + "prisma": "4.6.1", + "public-ip": "6.0.1", + "pump": "3.0.0", + "shell-quote": "^1.7.4", + "socket.io": "4.5.3", + "ssh-config": "4.1.6", + "strip-ansi": "7.0.1", + "unique-names-generator": "4.7.1" + }, + "devDependencies": { + "@types/node": "18.11.9", + "@types/node-os-utils": "1.3.0", + "@typescript-eslint/eslint-plugin": "5.44.0", + "@typescript-eslint/parser": "5.44.0", + "esbuild": "0.15.15", + "eslint": "8.28.0", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-prettier": "4.2.1", + "nodemon": "2.0.20", + "prettier": "2.7.1", + "rimraf": "3.0.2", + "tsconfig-paths": "4.1.0", + "types-fastify-socket.io": "0.0.1", + "typescript": "4.9.3" + }, + "prisma": { + "seed": "node prisma/seed.js" + } +} diff --git a/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql b/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql index 2fdff0eca..e0d64cdc8 100644 --- a/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql +++ b/apps/api/prisma/migrations/20221129121702_preview_separator/migration.sql @@ -25,7 +25,7 @@ CREATE TABLE "new_Setting" ( "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "updatedAt" DATETIME NOT NULL ); -INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt" FROM "Setting"; +INSERT INTO "new_Setting" ("DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", "numberOfDockerImagesKeptLocally", "proxyDefaultRedirect", "sentryDSN", "updatedAt") SELECT "DNSServers", "applicationStoragePathMigrationFinished", "arch", "concurrentBuilds", "createdAt", "doNotTrack", "dualCerts", "fqdn", "id", "ipv4", "ipv6", "isAPIDebuggingEnabled", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "isTraefikUsed", "maxPort", "minPort", 3, "proxyDefaultRedirect", "sentryDSN", "updatedAt" FROM "Setting"; DROP TABLE "Setting"; ALTER TABLE "new_Setting" RENAME TO "Setting"; CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); diff --git a/apps/api/prisma/migrations/20221221140911_git_source_custom_user/migration.sql b/apps/api/prisma/migrations/20221221140911_git_source_custom_user/migration.sql new file mode 100644 index 000000000..d9d5a7204 --- /dev/null +++ b/apps/api/prisma/migrations/20221221140911_git_source_custom_user/migration.sql @@ -0,0 +1,27 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_GitSource" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "forPublic" BOOLEAN NOT NULL DEFAULT false, + "type" TEXT, + "apiUrl" TEXT, + "htmlUrl" TEXT, + "customPort" INTEGER NOT NULL DEFAULT 22, + "customUser" TEXT NOT NULL DEFAULT 'git', + "organization" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + "githubAppId" TEXT, + "gitlabAppId" TEXT, + "isSystemWide" BOOLEAN NOT NULL DEFAULT false, + CONSTRAINT "GitSource_gitlabAppId_fkey" FOREIGN KEY ("gitlabAppId") REFERENCES "GitlabApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE, + CONSTRAINT "GitSource_githubAppId_fkey" FOREIGN KEY ("githubAppId") REFERENCES "GithubApp" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); +INSERT INTO "new_GitSource" ("apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt") SELECT "apiUrl", "createdAt", "customPort", "forPublic", "githubAppId", "gitlabAppId", "htmlUrl", "id", "isSystemWide", "name", "organization", "type", "updatedAt" FROM "GitSource"; +DROP TABLE "GitSource"; +ALTER TABLE "new_GitSource" RENAME TO "GitSource"; +CREATE UNIQUE INDEX "GitSource_githubAppId_key" ON "GitSource"("githubAppId"); +CREATE UNIQUE INDEX "GitSource_gitlabAppId_key" ON "GitSource"("gitlabAppId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma index 54eefc504..1e9b75aa5 100644 --- a/apps/api/prisma/schema.prisma +++ b/apps/api/prisma/schema.prisma @@ -325,6 +325,7 @@ model GitSource { apiUrl String? htmlUrl String? customPort Int @default(22) + customUser String @default("git") organization String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index b1d830adf..1505faa17 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -6,14 +6,27 @@ import cookie from '@fastify/cookie'; import multipart from '@fastify/multipart'; import path, { join } from 'path'; import autoLoad from '@fastify/autoload'; -import socketIO from 'fastify-socket.io' -import socketIOServer from './realtime' +import socketIO from 'fastify-socket.io'; +import socketIOServer from './realtime'; -import { cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeCommand, generateDatabaseConfiguration, isDev, listSettings, prisma, sentryDSN, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; +import { + cleanupDockerStorage, + createRemoteEngineConfiguration, + decrypt, + executeCommand, + generateDatabaseConfiguration, + isDev, + listSettings, + prisma, + sentryDSN, + startTraefikProxy, + startTraefikTCPProxy, + version +} from './lib/common'; import { scheduler } from './lib/scheduler'; import { compareVersions } from 'compare-versions'; -import Graceful from '@ladjs/graceful' -import yaml from 'js-yaml' +import Graceful from '@ladjs/graceful'; +import yaml from 'js-yaml'; import fs from 'fs/promises'; import { verifyRemoteDockerEngineFn } from './routes/api/v1/destinations/handlers'; import { checkContainer } from './lib/docker'; @@ -23,13 +36,13 @@ import * as Sentry from '@sentry/node'; declare module 'fastify' { interface FastifyInstance { config: { - COOLIFY_APP_ID: string, - COOLIFY_SECRET_KEY: string, - COOLIFY_DATABASE_URL: string, - COOLIFY_IS_ON: string, - COOLIFY_WHITE_LABELED: string, - COOLIFY_WHITE_LABELED_ICON: string | null, - COOLIFY_AUTO_UPDATE: string, + COOLIFY_APP_ID: string; + COOLIFY_SECRET_KEY: string; + COOLIFY_DATABASE_URL: string; + COOLIFY_IS_ON: string; + COOLIFY_WHITE_LABELED: string; + COOLIFY_WHITE_LABELED_ICON: string | null; + COOLIFY_AUTO_UPDATE: string; }; } } @@ -38,7 +51,7 @@ const port = isDev ? 3001 : 3000; const host = '0.0.0.0'; (async () => { - const settings = await prisma.setting.findFirst() + const settings = await prisma.setting.findFirst(); const fastify = Fastify({ logger: settings?.isAPIDebuggingEnabled || false, trustProxy: true @@ -49,10 +62,10 @@ const host = '0.0.0.0'; required: ['COOLIFY_SECRET_KEY', 'COOLIFY_DATABASE_URL', 'COOLIFY_IS_ON'], properties: { COOLIFY_APP_ID: { - type: 'string', + type: 'string' }, COOLIFY_SECRET_KEY: { - type: 'string', + type: 'string' }, COOLIFY_DATABASE_URL: { type: 'string', @@ -73,8 +86,7 @@ const host = '0.0.0.0'; COOLIFY_AUTO_UPDATE: { type: 'string', default: 'false' - }, - + } } }; const options = { @@ -103,13 +115,13 @@ const host = '0.0.0.0'; fastify.register(autoLoad, { dir: join(__dirname, 'routes') }); - fastify.register(cookie) + fastify.register(cookie); fastify.register(cors); fastify.register(socketIO, { cors: { - origin: isDev ? "*" : '' + origin: isDev ? '*' : '' } - }) + }); // To detect allowed origins // fastify.addHook('onRequest', async (request, reply) => { // console.log(request.headers.host) @@ -131,10 +143,9 @@ const host = '0.0.0.0'; // } // }) - try { - await fastify.listen({ port, host }) - await socketIOServer(fastify) + await fastify.listen({ port, host }); + await socketIOServer(fastify); console.log(`Coolify's API is listening on ${host}:${port}`); migrateServicesToNewTemplate(); @@ -145,108 +156,103 @@ const host = '0.0.0.0'; graceful.listen(); setInterval(async () => { - if (!scheduler.workers.has('deployApplication')) { + if (!scheduler.workers.has('deployApplication')) { scheduler.run('deployApplication'); } - }, 2000) + }, 2000); // autoUpdater setInterval(async () => { - await autoUpdater() - }, 60000 * 15) + await autoUpdater(); + }, 60000 * 15); // cleanupStorage setInterval(async () => { - await cleanupStorage() - }, 60000 * 10) + await cleanupStorage(); + }, 60000 * 15); // checkProxies, checkFluentBit & refresh templates setInterval(async () => { await checkProxies(); await checkFluentBit(); - }, 60000) + }, 60000); // Refresh and check templates setInterval(async () => { - await refreshTemplates() - }, 60000) + await refreshTemplates(); + }, 60000); setInterval(async () => { - await refreshTags() - }, 60000) + await refreshTags(); + }, 60000); - setInterval(async () => { - await migrateServicesToNewTemplate() - }, isDev ? 10000 : 60000) + setInterval( + async () => { + await migrateServicesToNewTemplate(); + }, + isDev ? 10000 : 60000 + ); setInterval(async () => { await copySSLCertificates(); - }, 10000) - - await Promise.all([ - getTagsTemplates(), - getArch(), - getIPAddress(), - configureRemoteDockers(), - ]) + }, 10000); + await Promise.all([getTagsTemplates(), getArch(), getIPAddress(), configureRemoteDockers()]); } catch (error) { console.error(error); process.exit(1); } })(); - async function getIPAddress() { - const { publicIpv4, publicIpv6 } = await import('public-ip') + const { publicIpv4, publicIpv6 } = await import('public-ip'); try { const settings = await listSettings(); if (!settings.ipv4) { - const ipv4 = await publicIpv4({ timeout: 2000 }) + const ipv4 = await publicIpv4({ timeout: 2000 }); console.log(`Getting public IPv4 address...`); - await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } }) + await prisma.setting.update({ where: { id: settings.id }, data: { ipv4 } }); } if (!settings.ipv6) { - const ipv6 = await publicIpv6({ timeout: 2000 }) + const ipv6 = await publicIpv6({ timeout: 2000 }); console.log(`Getting public IPv6 address...`); - await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } }) + await prisma.setting.update({ where: { id: settings.id }, data: { ipv6 } }); } - - } catch (error) { } + } catch (error) {} } async function getTagsTemplates() { - const { default: got } = await import('got') + const { default: got } = await import('got'); try { if (isDev) { - const templates = await fs.readFile('./devTemplates.yaml', 'utf8') - const tags = await fs.readFile('./devTags.json', 'utf8') - await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates))) - await fs.writeFile('./tags.json', tags) - console.log('[004] Tags and templates loaded in dev mode...') + const templates = await fs.readFile('./devTemplates.yaml', 'utf8'); + const tags = await fs.readFile('./devTags.json', 'utf8'); + await fs.writeFile('./templates.json', JSON.stringify(yaml.load(templates))); + await fs.writeFile('./tags.json', tags); + console.log('[004] Tags and templates loaded in dev mode...'); } else { - const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text() - const response = await got.get('https://get.coollabs.io/coolify/service-templates.yaml').text() - await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))) - await fs.writeFile('/app/tags.json', tags) - console.log('[004] Tags and templates loaded...') + const tags = await got.get('https://get.coollabs.io/coolify/service-tags.json').text(); + const response = await got + .get('https://get.coollabs.io/coolify/service-templates.yaml') + .text(); + await fs.writeFile('/app/templates.json', JSON.stringify(yaml.load(response))); + await fs.writeFile('/app/tags.json', tags); + console.log('[004] Tags and templates loaded...'); } - } catch (error) { - console.log("Couldn't get latest templates.") - console.log(error) + console.log("Couldn't get latest templates."); + console.log(error); } } async function initServer() { const appId = process.env['COOLIFY_APP_ID']; - const settings = await prisma.setting.findUnique({ where: { id: '0' } }) + const settings = await prisma.setting.findUnique({ where: { id: '0' } }); try { if (settings.doNotTrack === true) { - console.log('[000] Telemetry disabled...') - + console.log('[000] Telemetry disabled...'); } else { if (settings.sentryDSN !== sentryDSN) { - await prisma.setting.update({ where: { id: '0' }, data: { sentryDSN } }) + await prisma.setting.update({ where: { id: '0' }, data: { sentryDSN } }); } // Initialize Sentry // Sentry.init({ @@ -257,35 +263,38 @@ async function initServer() { // console.log('[000] Sentry initialized...') } } catch (error) { - console.error(error) + console.error(error); } try { console.log(`[001] Initializing server...`); await executeCommand({ command: `docker network create --attachable coolify` }); - } catch (error) { } + } catch (error) {} try { console.log(`[002] Cleanup stucked builds...`); const isOlder = compareVersions('3.8.1', version); if (isOlder === 1) { - await prisma.build.updateMany({ where: { status: { in: ['running', 'queued'] } }, data: { status: 'failed' } }); + await prisma.build.updateMany({ + where: { status: { in: ['running', 'queued'] } }, + data: { status: 'failed' } + }); } - } catch (error) { } + } catch (error) {} try { console.log('[003] Cleaning up old build sources under /tmp/build-sources/...'); - await fs.rm('/tmp/build-sources', { recursive: true, force: true }) + await fs.rm('/tmp/build-sources', { recursive: true, force: true }); } catch (error) { - console.log(error) + console.log(error); } } async function getArch() { try { - const settings = await prisma.setting.findFirst({}) + const settings = await prisma.setting.findFirst({}); if (settings && !settings.arch) { console.log(`Getting architecture...`); - await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } }) + await prisma.setting.update({ where: { id: settings.id }, data: { arch: process.arch } }); } - } catch (error) { } + } catch (error) {} } async function configureRemoteDockers() { @@ -296,37 +305,44 @@ async function configureRemoteDockers() { if (remoteDocker.length > 0) { console.log(`Verifying Remote Docker Engines...`); for (const docker of remoteDocker) { - console.log('Verifying:', docker.remoteIpAddress) + console.log('Verifying:', docker.remoteIpAddress); await verifyRemoteDockerEngineFn(docker.id); } } } catch (error) { - console.log(error) + console.log(error); } } async function autoUpdater() { try { - const { default: got } = await import('got') + const { default: got } = await import('got'); const currentVersion = version; - const { coolify } = await got.get('https://get.coollabs.io/versions.json', { - searchParams: { - appId: process.env['COOLIFY_APP_ID'] || undefined, - version: currentVersion - } - }).json() + const { coolify } = await got + .get('https://get.coollabs.io/versions.json', { + searchParams: { + appId: process.env['COOLIFY_APP_ID'] || undefined, + version: currentVersion + } + }) + .json(); const latestVersion = coolify.main.version; const isUpdateAvailable = compareVersions(latestVersion, currentVersion); if (isUpdateAvailable === 1) { - const activeCount = 0 + const activeCount = 0; if (activeCount === 0) { if (!isDev) { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); if (isAutoUpdateEnabled) { - await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }) - await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }) - await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }) - await executeCommand({ shell: true, command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` }) + await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }); + await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }); + await executeCommand({ + command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + }); + await executeCommand({ + shell: true, + command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` + }); } } else { console.log('Updating (not really in dev mode).'); @@ -334,7 +350,7 @@ async function autoUpdater() { } } } catch (error) { - console.log(error) + console.log(error); } } @@ -345,14 +361,18 @@ async function checkFluentBit() { const { id } = await prisma.destinationDocker.findFirst({ where: { engine, network: 'coolify' } }); - const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true }); + const { found } = await checkContainer({ + dockerId: id, + container: 'coolify-fluentbit', + remove: true + }); if (!found) { await executeCommand({ shell: true, command: `env | grep '^COOLIFY' > .env` }); await executeCommand({ command: `docker compose up -d fluent-bit` }); } } } catch (error) { - console.log(error) + console.log(error); } } async function checkProxies() { @@ -368,7 +388,7 @@ async function checkProxies() { where: { engine, network: 'coolify', isCoolifyProxyUsed: true } }); if (localDocker) { - portReachable = await isReachable(80, { host: ipv4 || ipv6 }) + portReachable = await isReachable(80, { host: ipv4 || ipv6 }); if (!portReachable) { await startTraefikProxy(localDocker.id); } @@ -380,14 +400,14 @@ async function checkProxies() { if (remoteDocker.length > 0) { for (const docker of remoteDocker) { if (docker.isCoolifyProxyUsed) { - portReachable = await isReachable(80, { host: docker.remoteIpAddress }) + portReachable = await isReachable(80, { host: docker.remoteIpAddress }); if (!portReachable) { await startTraefikProxy(docker.id); } } try { - await createRemoteEngineConfiguration(docker.id) - } catch (error) { } + await createRemoteEngineConfiguration(docker.id); + } catch (error) {} } } // TCP Proxies @@ -426,80 +446,105 @@ async function checkProxies() { // await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); // } // } - } catch (error) { - - } + } catch (error) {} } async function copySSLCertificates() { try { const pAll = await import('p-all'); - const actions = [] - const certificates = await prisma.certificate.findMany({ include: { team: true } }) - const teamIds = certificates.map(c => c.teamId) - const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } }) + const actions = []; + const certificates = await prisma.certificate.findMany({ include: { team: true } }); + const teamIds = certificates.map((c) => c.teamId); + const destinations = await prisma.destinationDocker.findMany({ + where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } + }); for (const certificate of certificates) { - const { id, key, cert } = certificate - const decryptedKey = decrypt(key) - await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) - await fs.writeFile(`/tmp/${id}-cert.pem`, cert) + const { id, key, cert } = certificate; + const decryptedKey = decrypt(key); + await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey); + await fs.writeFile(`/tmp/${id}-cert.pem`, cert); for (const destination of destinations) { if (destination.remoteEngine) { if (destination.remoteVerified) { - const { id: dockerId, remoteIpAddress } = destination - actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress)) + const { id: dockerId, remoteIpAddress } = destination; + actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress)); } } else { - actions.push(async () => copyLocalCertificates(id)) + actions.push(async () => copyLocalCertificates(id)); } } } - await pAll.default(actions, { concurrency: 1 }) + await pAll.default(actions, { concurrency: 1 }); } catch (error) { - console.log(error) + console.log(error); } finally { - await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }) + await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }); } } async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) { try { - await executeCommand({ command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/` }) - await executeCommand({ sshCommand: true, shell: true, dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) - await executeCommand({ sshCommand: true, dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) - await executeCommand({ sshCommand: true, dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) + await executeCommand({ + command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/` + }); + await executeCommand({ + sshCommand: true, + shell: true, + dockerId, + command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` + }); + await executeCommand({ + sshCommand: true, + dockerId, + command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` + }); + await executeCommand({ + sshCommand: true, + dockerId, + command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` + }); } catch (error) { - console.log({ error }) + console.log({ error }); } } async function copyLocalCertificates(id: string) { try { - await executeCommand({ command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`, shell: true }) - await executeCommand({ command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) - await executeCommand({ command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) + await executeCommand({ + command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`, + shell: true + }); + await executeCommand({ + command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` + }); + await executeCommand({ + command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` + }); } catch (error) { - console.log({ error }) + console.log({ error }); } } async function cleanupStorage() { const destinationDockers = await prisma.destinationDocker.findMany(); - let enginesDone = new Set() + let enginesDone = new Set(); for (const destination of destinationDockers) { - if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return - if (destination.engine) enginesDone.add(destination.engine) - if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress) - + if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return; + if (destination.engine) enginesDone.add(destination.engine); + if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress); + let force = false; let lowDiskSpace = false; try { - let stdout = null + let stdout = null; if (!isDev) { - const output = await executeCommand({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`, shell: true }) + const output = await executeCommand({ + dockerId: destination.id, + command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`, + shell: true + }); stdout = output.stdout; } else { const output = await executeCommand({ - command: - `df -kPT /` + command: `df -kPT /` }); stdout = output.stdout; } @@ -531,7 +576,7 @@ async function cleanupStorage() { lowDiskSpace = true; } } - } catch (error) { } - await cleanupDockerStorage(destination.id, lowDiskSpace, false) + } catch (error) {} + await cleanupDockerStorage(destination.id, lowDiskSpace, force); } -} \ No newline at end of file +} diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index 90afea210..cc3740af4 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -3,8 +3,26 @@ import crypto from 'crypto'; import fs from 'fs/promises'; import yaml from 'js-yaml'; -import { copyBaseConfigurationFiles, makeLabelForSimpleDockerfile, makeLabelForStandaloneApplication, saveBuildLog, saveDockerRegistryCredentials, setDefaultConfiguration } from '../lib/buildPacks/common'; -import { createDirectories, decrypt, defaultComposeConfiguration, getDomain, prisma, decryptApplication, isDev, pushToRegistry, executeCommand } from '../lib/common'; +import { + copyBaseConfigurationFiles, + makeLabelForSimpleDockerfile, + makeLabelForStandaloneApplication, + saveBuildLog, + saveDockerRegistryCredentials, + setDefaultConfiguration +} from '../lib/buildPacks/common'; +import { + createDirectories, + decrypt, + defaultComposeConfiguration, + getDomain, + prisma, + decryptApplication, + isDev, + pushToRegistry, + executeCommand, + generateSecrets +} from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -14,33 +32,55 @@ import * as buildpacks from '../lib/buildPacks'; if (message === 'error') throw new Error('oops'); if (message === 'cancel') { parentPort.postMessage('cancelled'); - await prisma.$disconnect() + await prisma.$disconnect(); process.exit(0); } }); - const pThrottle = await import('p-throttle') + const pThrottle = await import('p-throttle'); const throttle = pThrottle.default({ limit: 1, interval: 2000 }); - const th = throttle(async () => { try { - const queuedBuilds = await prisma.build.findMany({ where: { status: { in: ['queued', 'running'] } }, orderBy: { createdAt: 'asc' } }); - const { concurrentBuilds } = await prisma.setting.findFirst({}) + const queuedBuilds = await prisma.build.findMany({ + where: { status: { in: ['queued', 'running'] } }, + orderBy: { createdAt: 'asc' } + }); + const { concurrentBuilds } = await prisma.setting.findFirst({}); if (queuedBuilds.length > 0) { parentPort.postMessage({ deploying: true }); const concurrency = concurrentBuilds; const pAll = await import('p-all'); - const actions = [] + const actions = []; for (const queueBuild of queuedBuilds) { actions.push(async () => { - let application = await prisma.application.findUnique({ where: { id: queueBuild.applicationId }, include: { dockerRegistry: true, destinationDocker: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, persistentStorage: true, secrets: true, settings: true, teams: true } }) + let application = await prisma.application.findUnique({ + where: { id: queueBuild.applicationId }, + include: { + dockerRegistry: true, + destinationDocker: true, + gitSource: { include: { githubApp: true, gitlabApp: true } }, + persistentStorage: true, + secrets: true, + settings: true, + teams: true + } + }); - let { id: buildId, type, gitSourceId, sourceBranch = null, pullmergeRequestId = null, previewApplicationId = null, forceRebuild, sourceRepository = null } = queueBuild - application = decryptApplication(application) + let { + id: buildId, + type, + gitSourceId, + sourceBranch = null, + pullmergeRequestId = null, + previewApplicationId = null, + forceRebuild, + sourceRepository = null + } = queueBuild; + application = decryptApplication(application); if (!gitSourceId && application.simpleDockerfile) { const { @@ -53,77 +93,71 @@ import * as buildpacks from '../lib/buildPacks'; exposePort, simpleDockerfile, dockerRegistry - } = application + } = application; const { workdir } = await createDirectories({ repository: applicationId, buildId }); try { if (queueBuild.status === 'running') { - await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); + await saveBuildLog({ + line: 'Building halted, restarting...', + buildId, + applicationId: application.id + }); } const volumes = persistentStorage?.map((storage) => { if (storage.oldPath) { - return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`; + return `${applicationId}${storage.path + .replace(/\//gi, '-') + .replace('-app', '')}:${storage.path}`; } return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; }) || []; if (destinationDockerId) { - await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); + await prisma.build.update({ + where: { id: buildId }, + data: { status: 'running' } + }); try { const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}` - }) + }); if (containers) { const containerArray = containers.split('\n'); if (containerArray.length > 0) { for (const container of containerArray) { - await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) - await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + await executeCommand({ + dockerId: destinationDockerId, + command: `docker stop -t 0 ${container}` + }); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker rm --force ${container}` + }); } } } } catch (error) { // } - const envs = [ - `PORT=${port}` - ]; + let envs = []; if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } - } - }); + envs = [ + ...envs, + ...generateSecrets(secrets, pullmergeRequestId, false, port) + ]; } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile); if (dockerRegistry) { - const { url, username, password } = dockerRegistry - await saveDockerRegistryCredentials({ url, username, password, workdir }) + const { url, username, password } = dockerRegistry; + await saveDockerRegistryCredentials({ url, username, password, workdir }); } const labels = makeLabelForSimpleDockerfile({ applicationId, type, - port: exposePort ? `${exposePort}:${port}` : port, + port: exposePort ? `${exposePort}:${port}` : port }); try { const composeVolumes = volumes.map((volume) => { @@ -138,17 +172,17 @@ import * as buildpacks from '../lib/buildPacks'; services: { [applicationId]: { build: { - context: workdir, + context: workdir }, image: `${applicationId}:${buildId}`, container_name: applicationId, volumes, labels, - env_file: envFound ? [`${workdir}/.env`] : [], + environment: envs, depends_on: [], expose: [port], ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(destinationDocker.network), + ...defaultComposeConfiguration(destinationDocker.network) } }, networks: { @@ -159,11 +193,15 @@ import * as buildpacks from '../lib/buildPacks'; volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeCommand({ debug: true, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await executeCommand({ + debug: true, + dockerId: destinationDocker.id, + command: `docker compose --project-directory ${workdir} up -d` + }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); - const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }); if (foundBuild) { await prisma.build.update({ where: { id: buildId }, @@ -174,10 +212,9 @@ import * as buildpacks from '../lib/buildPacks'; } throw new Error(error); } - } } catch (error) { - const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }); if (foundBuild) { await prisma.build.update({ where: { id: buildId }, @@ -190,7 +227,11 @@ import * as buildpacks from '../lib/buildPacks'; await saveBuildLog({ line: error, buildId, applicationId: application.id }); } if (error instanceof Error) { - await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); + await saveBuildLog({ + line: error.message, + buildId, + applicationId: application.id + }); } await fs.rm(workdir, { recursive: true, force: true }); return; @@ -199,9 +240,13 @@ import * as buildpacks from '../lib/buildPacks'; if (application.dockerRegistryImageName) { const customTag = application.dockerRegistryImageName.split(':')[1] || buildId; const imageName = application.dockerRegistryImageName.split(':')[0]; - await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id }); - await pushToRegistry(application, workdir, buildId, imageName, customTag) - await saveBuildLog({ line: "Success", buildId, applicationId: application.id }); + await saveBuildLog({ + line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, + buildId, + applicationId: application.id + }); + await pushToRegistry(application, workdir, buildId, imageName, customTag); + await saveBuildLog({ line: 'Success', buildId, applicationId: application.id }); } } catch (error) { if (error.stdout) { @@ -212,12 +257,15 @@ import * as buildpacks from '../lib/buildPacks'; } } finally { await fs.rm(workdir, { recursive: true, force: true }); - await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); + await prisma.build.update({ + where: { id: buildId }, + data: { status: 'success' } + }); } return; } - const originalApplicationId = application.id + const originalApplicationId = application.id; const { id: applicationId, name, @@ -241,7 +289,7 @@ import * as buildpacks from '../lib/buildPacks'; deploymentType, gitCommitHash, dockerRegistry - } = application + } = application; let { branch, @@ -257,7 +305,7 @@ import * as buildpacks from '../lib/buildPacks'; dockerComposeFileLocation, dockerComposeConfiguration, denoMainFile - } = application + } = application; let imageId = applicationId; let domain = getDomain(fqdn); @@ -272,9 +320,11 @@ import * as buildpacks from '../lib/buildPacks'; let imageFoundRemotely = false; if (pullmergeRequestId) { - const previewApplications = await prisma.previewApplication.findMany({ where: { applicationId: originalApplicationId, pullmergeRequestId } }) + const previewApplications = await prisma.previewApplication.findMany({ + where: { applicationId: originalApplicationId, pullmergeRequestId } + }); if (previewApplications.length > 0) { - previewApplicationId = previewApplications[0].id + previewApplicationId = previewApplications[0].id; } // Previews, we need to get the source branch and set subdomain branch = sourceBranch; @@ -285,7 +335,11 @@ import * as buildpacks from '../lib/buildPacks'; const { workdir, repodir } = await createDirectories({ repository, buildId }); try { if (queueBuild.status === 'running') { - await saveBuildLog({ line: 'Building halted, restarting...', buildId, applicationId: application.id }); + await saveBuildLog({ + line: 'Building halted, restarting...', + buildId, + applicationId: application.id + }); } const currentHash = crypto @@ -323,15 +377,16 @@ import * as buildpacks from '../lib/buildPacks'; const volumes = persistentStorage?.map((storage) => { if (storage.oldPath) { - return `${applicationId}${storage.path.replace(/\//gi, '-').replace('-app', '')}:${storage.path}`; + return `${applicationId}${storage.path + .replace(/\//gi, '-') + .replace('-app', '')}:${storage.path}`; } return `${applicationId}${storage.path.replace(/\//gi, '-')}:${storage.path}`; }) || []; - try { - dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration) - } catch (error) { } + dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration); + } catch (error) {} let deployNeeded = true; let destinationType; @@ -339,7 +394,10 @@ import * as buildpacks from '../lib/buildPacks'; destinationType = 'docker'; } if (destinationType === 'docker') { - await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); + await prisma.build.update({ + where: { id: buildId }, + data: { status: 'running' } + }); const configuration = await setDefaultConfiguration(application); @@ -361,6 +419,7 @@ import * as buildpacks from '../lib/buildPacks'; githubAppId: gitSource.githubApp?.id, gitlabAppId: gitSource.gitlabApp?.id, customPort: gitSource.customPort, + customUser: gitSource.customUser, gitCommitHash, configuration, repository, @@ -381,10 +440,10 @@ import * as buildpacks from '../lib/buildPacks'; tag = `${commit.slice(0, 7)}-${pullmergeRequestId}`; } if (application.dockerRegistryImageName) { - imageName = application.dockerRegistryImageName.split(':')[0] - customTag = application.dockerRegistryImageName.split(':')[1] || tag + imageName = application.dockerRegistryImageName.split(':')[0]; + customTag = application.dockerRegistryImageName.split(':')[1] || tag; } else { - customTag = tag + customTag = tag; imageName = applicationId; } @@ -394,13 +453,17 @@ import * as buildpacks from '../lib/buildPacks'; try { await prisma.build.update({ where: { id: buildId }, data: { commit } }); - } catch (err) { } + } catch (err) {} if (!pullmergeRequestId) { if (configHash !== currentHash) { deployNeeded = true; if (configHash) { - await saveBuildLog({ line: 'Configuration changed', buildId, applicationId }); + await saveBuildLog({ + line: 'Configuration changed', + buildId, + applicationId + }); } } else { deployNeeded = false; @@ -413,30 +476,43 @@ import * as buildpacks from '../lib/buildPacks'; await executeCommand({ dockerId: destinationDocker.id, command: `docker image inspect ${applicationId}:${tag}` - }) + }); imageFoundLocally = true; } catch (error) { // } if (dockerRegistry) { - const { url, username, password } = dockerRegistry - location = await saveDockerRegistryCredentials({ url, username, password, workdir }) + const { url, username, password } = dockerRegistry; + location = await saveDockerRegistryCredentials({ + url, + username, + password, + workdir + }); } try { await executeCommand({ dockerId: destinationDocker.id, - command: `docker ${location ? `--config ${location}` : ''} pull ${imageName}:${customTag}` - }) + command: `docker ${ + location ? `--config ${location}` : '' + } pull ${imageName}:${customTag}` + }); imageFoundRemotely = true; } catch (error) { // } - let imageFound = `${applicationId}:${tag}` + let imageFound = `${applicationId}:${tag}`; if (imageFoundRemotely) { - imageFound = `${imageName}:${customTag}` + imageFound = `${imageName}:${customTag}`; } - await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId, baseImage); + await copyBaseConfigurationFiles( + buildPack, + workdir, + buildId, + applicationId, + baseImage + ); const labels = makeLabelForStandaloneApplication({ applicationId, fqdn, @@ -455,7 +531,7 @@ import * as buildpacks from '../lib/buildPacks'; baseDirectory, publishDirectory }); - if (forceRebuild) deployNeeded = true + if (forceRebuild) deployNeeded = true; if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) { if (buildpacks[buildPack]) await buildpacks[buildPack]({ @@ -496,17 +572,30 @@ import * as buildpacks from '../lib/buildPacks'; baseImage, baseBuildImage, deploymentType, + forceRebuild }); else { - await saveBuildLog({ line: `Build pack ${buildPack} not found`, buildId, applicationId }); + await saveBuildLog({ + line: `Build pack ${buildPack} not found`, + buildId, + applicationId + }); throw new Error(`Build pack ${buildPack} not found.`); } } else { if (imageFoundRemotely || deployNeeded) { - await saveBuildLog({ line: `Container image ${imageFound} found in Docker Registry - reuising it`, buildId, applicationId }); + await saveBuildLog({ + line: `Container image ${imageFound} found in Docker Registry - reuising it`, + buildId, + applicationId + }); } else { if (imageFoundLocally || deployNeeded) { - await saveBuildLog({ line: `Container image ${imageFound} found locally - reuising it`, buildId, applicationId }); + await saveBuildLog({ + line: `Container image ${imageFound} found locally - reuising it`, + buildId, + applicationId + }); } } } @@ -516,13 +605,19 @@ import * as buildpacks from '../lib/buildPacks'; const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}` - }) + }); if (containers) { const containerArray = containers.split('\n'); if (containerArray.length > 0) { for (const container of containerArray) { - await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) - await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + await executeCommand({ + dockerId: destinationDockerId, + command: `docker stop -t 0 ${container}` + }); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker rm --force ${container}` + }); } } } @@ -530,17 +625,25 @@ import * as buildpacks from '../lib/buildPacks'; // } try { - console.log({ debug }) - await executeCommand({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await executeCommand({ + debug, + buildId, + applicationId, + dockerId: destinationDocker.id, + command: `docker compose --project-directory ${workdir} up -d` + }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); - await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); + await prisma.build.update({ + where: { id: buildId }, + data: { status: 'success' } + }); await prisma.application.update({ where: { id: applicationId }, data: { configHash: currentHash } }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); - const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }); if (foundBuild) { await prisma.build.update({ where: { id: buildId }, @@ -551,55 +654,42 @@ import * as buildpacks from '../lib/buildPacks'; } throw new Error(error); } - } else { try { const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}` - }) + command: `docker ps -a --filter 'label=com.docker.compose.service=${ + pullmergeRequestId ? imageId : applicationId + }' --format {{.ID}}` + }); if (containers) { const containerArray = containers.split('\n'); if (containerArray.length > 0) { for (const container of containerArray) { - await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) - await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + await executeCommand({ + dockerId: destinationDockerId, + command: `docker stop -t 0 ${container}` + }); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker rm --force ${container}` + }); } } } } catch (error) { // } - const envs = [ - `PORT=${port}` - ]; + let envs = []; if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } - } - }); + envs = [ + ...envs, + ...generateSecrets(secrets, pullmergeRequestId, false, port) + ]; } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); if (dockerRegistry) { - const { url, username, password } = dockerRegistry - await saveDockerRegistryCredentials({ url, username, password, workdir }) - } - - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // + const { url, username, password } = dockerRegistry; + await saveDockerRegistryCredentials({ url, username, password, workdir }); } try { const composeVolumes = volumes.map((volume) => { @@ -616,12 +706,12 @@ import * as buildpacks from '../lib/buildPacks'; image: imageFound, container_name: imageId, volumes, - env_file: envFound ? [`${workdir}/.env`] : [], + environment: envs, labels, depends_on: [], expose: [port], ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(destinationDocker.network), + ...defaultComposeConfiguration(destinationDocker.network) } }, networks: { @@ -632,11 +722,15 @@ import * as buildpacks from '../lib/buildPacks'; volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeCommand({ debug, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await executeCommand({ + debug, + dockerId: destinationDocker.id, + command: `docker compose --project-directory ${workdir} up -d` + }); await saveBuildLog({ line: 'Deployed 🎉', buildId, applicationId }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); - const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }); if (foundBuild) { await prisma.build.update({ where: { id: buildId }, @@ -648,14 +742,15 @@ import * as buildpacks from '../lib/buildPacks'; throw new Error(error); } - if (!pullmergeRequestId) await prisma.application.update({ - where: { id: applicationId }, - data: { configHash: currentHash } - }); + if (!pullmergeRequestId) + await prisma.application.update({ + where: { id: applicationId }, + data: { configHash: currentHash } + }); } } } catch (error) { - const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }) + const foundBuild = await prisma.build.findUnique({ where: { id: buildId } }); if (foundBuild) { await prisma.build.update({ where: { id: buildId }, @@ -668,16 +763,24 @@ import * as buildpacks from '../lib/buildPacks'; await saveBuildLog({ line: error, buildId, applicationId: application.id }); } if (error instanceof Error) { - await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); + await saveBuildLog({ + line: error.message, + buildId, + applicationId: application.id + }); } await fs.rm(workdir, { recursive: true, force: true }); return; } try { if (application.dockerRegistryImageName && (!imageFoundRemotely || forceRebuild)) { - await saveBuildLog({ line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, buildId, applicationId: application.id }); - await pushToRegistry(application, workdir, tag, imageName, customTag) - await saveBuildLog({ line: "Success", buildId, applicationId: application.id }); + await saveBuildLog({ + line: `Pushing ${imageName}:${customTag} to Docker Registry... It could take a while...`, + buildId, + applicationId: application.id + }); + await pushToRegistry(application, workdir, tag, imageName, customTag); + await saveBuildLog({ line: 'Success', buildId, applicationId: application.id }); } } catch (error) { if (error.stdout) { @@ -686,21 +789,20 @@ import * as buildpacks from '../lib/buildPacks'; if (error.stderr) { await saveBuildLog({ line: error.stderr, buildId, applicationId }); } - } finally { await fs.rm(workdir, { recursive: true, force: true }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); } }); } - await pAll.default(actions, { concurrency }) + await pAll.default(actions, { concurrency }); } } catch (error) { - console.log(error) + console.log(error); } - }) + }); while (true) { - await th() + await th(); } } else process.exit(0); })(); diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index ba50b32b4..1a0d18d73 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -1,6 +1,18 @@ -import { base64Encode, decrypt, encrypt, executeCommand, generateTimestamp, getDomain, isARM, isDev, prisma, version } from "../common"; +import { + base64Encode, + decrypt, + encrypt, + executeCommand, + generateSecrets, + generateTimestamp, + getDomain, + isARM, + isDev, + prisma, + version +} from '../common'; import { promises as fs } from 'fs'; -import { day } from "../dayjs"; +import { day } from '../dayjs'; const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy']; const nodeBased = [ @@ -17,7 +29,10 @@ const nodeBased = [ 'nextjs' ]; -export function setDefaultBaseImage(buildPack: string | null, deploymentType: string | null = null) { +export function setDefaultBaseImage( + buildPack: string | null, + deploymentType: string | null = null +) { const nodeVersions = [ { value: 'node:lts', @@ -316,8 +331,8 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st { value: 'heroku/builder-classic:22', label: 'heroku/builder-classic:22' - }, - ] + } + ]; let payload: any = { baseImage: null, baseBuildImage: null, @@ -327,7 +342,9 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st if (nodeBased.includes(buildPack)) { if (deploymentType === 'static') { payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; - payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions; + payload.baseImages = isARM(process.arch) + ? staticVersions.filter((version) => !version.value.includes('webdevops')) + : staticVersions; payload.baseBuildImage = 'node:lts'; payload.baseBuildImages = nodeVersions; } else { @@ -339,7 +356,9 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st } if (staticApps.includes(buildPack)) { payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; - payload.baseImages = isARM(process.arch) ? staticVersions.filter((version) => !version.value.includes('webdevops')) : staticVersions; + payload.baseImages = isARM(process.arch) + ? staticVersions.filter((version) => !version.value.includes('webdevops')) + : staticVersions; payload.baseBuildImage = 'node:lts'; payload.baseBuildImages = nodeVersions; } @@ -357,12 +376,20 @@ export function setDefaultBaseImage(buildPack: string | null, deploymentType: st payload.baseImage = 'denoland/deno:latest'; } if (buildPack === 'php') { - payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine'; - payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions + payload.baseImage = isARM(process.arch) + ? 'php:8.1-fpm-alpine' + : 'webdevops/php-apache:8.2-alpine'; + payload.baseImages = isARM(process.arch) + ? phpVersions.filter((version) => !version.value.includes('webdevops')) + : phpVersions; } if (buildPack === 'laravel') { - payload.baseImage = isARM(process.arch) ? 'php:8.1-fpm-alpine' : 'webdevops/php-apache:8.2-alpine'; - payload.baseImages = isARM(process.arch) ? phpVersions.filter((version) => !version.value.includes('webdevops')) : phpVersions + payload.baseImage = isARM(process.arch) + ? 'php:8.1-fpm-alpine' + : 'webdevops/php-apache:8.2-alpine'; + payload.baseImages = isARM(process.arch) + ? phpVersions.filter((version) => !version.value.includes('webdevops')) + : phpVersions; payload.baseBuildImage = 'node:18'; payload.baseBuildImages = nodeVersions; } @@ -405,7 +432,8 @@ export const setDefaultConfiguration = async (data: any) => { if (!publishDirectory) publishDirectory = template?.publishDirectory || null; if (baseDirectory) { if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; - if (baseDirectory.endsWith('/') && baseDirectory !== '/') baseDirectory = baseDirectory.slice(0, -1); + if (baseDirectory.endsWith('/') && baseDirectory !== '/') + baseDirectory = baseDirectory.slice(0, -1); } if (dockerFileLocation) { if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; @@ -414,8 +442,10 @@ export const setDefaultConfiguration = async (data: any) => { dockerFileLocation = '/Dockerfile'; } if (dockerComposeFileLocation) { - if (!dockerComposeFileLocation.startsWith('/')) dockerComposeFileLocation = `/${dockerComposeFileLocation}`; - if (dockerComposeFileLocation.endsWith('/')) dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1); + if (!dockerComposeFileLocation.startsWith('/')) + dockerComposeFileLocation = `/${dockerComposeFileLocation}`; + if (dockerComposeFileLocation.endsWith('/')) + dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1); } else { dockerComposeFileLocation = '/Dockerfile'; } @@ -479,7 +509,6 @@ export const scanningTemplates = { } }; - export const saveBuildLog = async ({ line, buildId, @@ -491,7 +520,7 @@ export const saveBuildLog = async ({ }): Promise => { if (buildId === 'undefined' || buildId === 'null' || !buildId) return; if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return; - const { default: got } = await import('got') + const { default: got } = await import('got'); if (typeof line === 'object' && line) { if (line.shortMessage) { line = line.shortMessage + '\n' + line.stderr; @@ -504,7 +533,11 @@ export const saveBuildLog = async ({ line = line.replace(regex, '@'); } const addTimestamp = `[${generateTimestamp()}] ${line}`; - const fluentBitUrl = isDev ? process.env.COOLIFY_CONTAINER_DEV === 'true' ? 'http://coolify-fluentbit:24224' : 'http://localhost:24224' : 'http://coolify-fluentbit:24224'; + const fluentBitUrl = isDev + ? process.env.COOLIFY_CONTAINER_DEV === 'true' + ? 'http://coolify-fluentbit:24224' + : 'http://localhost:24224' + : 'http://coolify-fluentbit:24224'; if (isDev && !process.env.COOLIFY_CONTAINER_DEV) { console.debug(`[${applicationId}] ${addTimestamp}`); @@ -514,15 +547,17 @@ export const saveBuildLog = async ({ json: { line: encrypt(line) } - }) + }); } catch (error) { return await prisma.buildLog.create({ data: { - line: addTimestamp, buildId, time: Number(day().valueOf()), applicationId + line: addTimestamp, + buildId, + time: Number(day().valueOf()), + applicationId } }); } - }; export async function copyBaseConfigurationFiles( @@ -610,7 +645,7 @@ export function checkPnpm(installCommand = null, buildCommand = null, startComma export async function saveDockerRegistryCredentials({ url, username, password, workdir }) { if (!username || !password) { - return null + return null; } let decryptedPassword = decrypt(password); @@ -619,17 +654,17 @@ export async function saveDockerRegistryCredentials({ url, username, password, w try { await fs.mkdir(`${workdir}/.docker`); } catch (error) { - console.log(error); + // console.log(error); } const payload = JSON.stringify({ - "auths": { + auths: { [url]: { - "auth": Buffer.from(`${username}:${decryptedPassword}`).toString('base64') + auth: Buffer.from(`${username}:${decryptedPassword}`).toString('base64') } } - }) - await fs.writeFile(`${location}/config.json`, payload) - return location + }); + await fs.writeFile(`${location}/config.json`, payload); + return location; } export async function buildImage({ applicationId, @@ -640,29 +675,41 @@ export async function buildImage({ isCache = false, debug = false, dockerFileLocation = '/Dockerfile', - commit + commit, + forceRebuild = false }) { if (isCache) { await saveBuildLog({ line: `Building cache image...`, buildId, applicationId }); } else { await saveBuildLog({ line: `Building production image...`, buildId, applicationId }); } - const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}` - const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}` + const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`; + const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`; + let location = null; - let location = null - - const { dockerRegistry } = await prisma.application.findUnique({ where: { id: applicationId }, select: { dockerRegistry: true } }) + const { dockerRegistry } = await prisma.application.findUnique({ + where: { id: applicationId }, + select: { dockerRegistry: true } + }); if (dockerRegistry) { - const { url, username, password } = dockerRegistry - location = await saveDockerRegistryCredentials({ url, username, password, workdir }) + const { url, username, password } = dockerRegistry; + location = await saveDockerRegistryCredentials({ url, username, password, workdir }); } - await executeCommand({ stream: true, debug, buildId, applicationId, dockerId, command: `docker ${location ? `--config ${location}` : ''} build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` }) + await executeCommand({ + stream: true, + debug, + buildId, + applicationId, + dockerId, + command: `docker ${location ? `--config ${location}` : ''} build ${ + forceRebuild ? '--no-cache' : '' + } --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` + }); - const { status } = await prisma.build.findUnique({ where: { id: buildId } }) + const { status } = await prisma.build.findUnique({ where: { id: buildId } }); if (status === 'canceled') { - throw new Error('Canceled.') + throw new Error('Canceled.'); } } export function makeLabelForSimpleDockerfile({ applicationId, port, type }) { @@ -741,21 +788,8 @@ export async function buildCacheImageWithNode(data, imageForBuild) { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { @@ -765,7 +799,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) { if (installCommand) { Dockerfile.push(`RUN ${installCommand}`); } - // Dockerfile.push(`ARG CACHEBUST=1`); Dockerfile.push(`RUN ${buildCommand}`); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await buildImage({ ...data, isCache: true }); @@ -773,27 +806,13 @@ export async function buildCacheImageWithNode(data, imageForBuild) { export async function buildCacheImageForLaravel(data, imageForBuild) { const { workdir, buildId, secrets, pullmergeRequestId } = data; - const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } Dockerfile.push(`COPY *.json *.mix.js /app/`); @@ -804,11 +823,7 @@ export async function buildCacheImageForLaravel(data, imageForBuild) { } export async function buildCacheImageWithCargo(data, imageForBuild) { - const { - applicationId, - workdir, - buildId, - } = data; + const { applicationId, workdir, buildId } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index f180630dc..c8cbc9f16 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -1,5 +1,5 @@ import { promises as fs } from 'fs'; -import { defaultComposeConfiguration, executeCommand } from '../common'; +import { defaultComposeConfiguration, executeCommand, generateSecrets } from '../common'; import { saveBuildLog } from './common'; import yaml from 'js-yaml'; @@ -25,30 +25,11 @@ export default async function (data) { if (!dockerComposeYaml.services) { throw 'No Services found in docker-compose file.'; } - const envs = []; + let envs = []; if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } - } - }); - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)]; } + const composeVolumes = []; if (volumes.length > 0) { for (const volume of volumes) { @@ -62,7 +43,8 @@ export default async function (data) { let networks = {}; for (let [key, value] of Object.entries(dockerComposeYaml.services)) { value['container_name'] = `${applicationId}-${key}`; - value['env_file'] = envFound ? [`${workdir}/.env`] : []; + let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'] + value['environment'] = [...environment, ...envs]; value['labels'] = labels; // TODO: If we support separated volume for each service, we need to add it here if (value['volumes']?.length > 0) { @@ -106,6 +88,7 @@ export default async function (data) { dockerComposeYaml['volumes'] = { ...composeVolumes }; } dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }); + await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); await executeCommand({ debug, diff --git a/apps/api/src/lib/buildPacks/deno.ts b/apps/api/src/lib/buildPacks/deno.ts index 074464445..2649e3d0a 100644 --- a/apps/api/src/lib/buildPacks/deno.ts +++ b/apps/api/src/lib/buildPacks/deno.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; const createDockerfile = async (data, image): Promise => { @@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (depsFound) { diff --git a/apps/api/src/lib/buildPacks/docker.ts b/apps/api/src/lib/buildPacks/docker.ts index 902dd449b..e02103f88 100644 --- a/apps/api/src/lib/buildPacks/docker.ts +++ b/apps/api/src/lib/buildPacks/docker.ts @@ -1,47 +1,27 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; export default async function (data) { - let { - applicationId, - debug, - tag, - workdir, - buildId, - baseDirectory, - secrets, - pullmergeRequestId, - dockerFileLocation - } = data + let { workdir, buildId, baseDirectory, secrets, pullmergeRequestId, dockerFileLocation } = data; const file = `${workdir}${baseDirectory}${dockerFileLocation}`; data.workdir = `${workdir}${baseDirectory}`; - const DockerfileRaw = await fs.readFile(`${file}`, 'utf8') - const Dockerfile: Array = DockerfileRaw - .toString() - .trim() - .split('\n'); + const DockerfileRaw = await fs.readFile(`${file}`, 'utf8'); + const Dockerfile: Array = DockerfileRaw.toString().trim().split('\n'); Dockerfile.forEach((line, index) => { if (line.startsWith('FROM')) { Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`); } }); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if ( - (pullmergeRequestId && secret.isPRMRSecret) || - (!pullmergeRequestId && !secret.isPRMRSecret) - ) { - Dockerfile.forEach((line, index) => { - if (line.startsWith('FROM')) { - Dockerfile.splice(index + 1, 0, `ARG ${secret.name}=${secret.value}`); - } - }); + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.forEach((line, index) => { + if (line.startsWith('FROM')) { + Dockerfile.splice(index + 1, 0, env); } - } + }); }); } - - await fs.writeFile(`${workdir}${dockerFileLocation}`, Dockerfile.join('\n')); + await fs.writeFile(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n')); await buildImage(data); } diff --git a/apps/api/src/lib/buildPacks/nextjs.ts b/apps/api/src/lib/buildPacks/nextjs.ts index 90a70f89b..957dc5bce 100644 --- a/apps/api/src/lib/buildPacks/nextjs.ts +++ b/apps/api/src/lib/buildPacks/nextjs.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; const createDockerfile = async (data, image): Promise => { @@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { diff --git a/apps/api/src/lib/buildPacks/node.ts b/apps/api/src/lib/buildPacks/node.ts index 546942542..8ccfcc68e 100644 --- a/apps/api/src/lib/buildPacks/node.ts +++ b/apps/api/src/lib/buildPacks/node.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage, checkPnpm } from './common'; const createDockerfile = async (data, image): Promise => { @@ -20,21 +21,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { diff --git a/apps/api/src/lib/buildPacks/nuxtjs.ts b/apps/api/src/lib/buildPacks/nuxtjs.ts index 90a70f89b..957dc5bce 100644 --- a/apps/api/src/lib/buildPacks/nuxtjs.ts +++ b/apps/api/src/lib/buildPacks/nuxtjs.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; const createDockerfile = async (data, image): Promise => { @@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { diff --git a/apps/api/src/lib/buildPacks/php.ts b/apps/api/src/lib/buildPacks/php.ts index eaf97d1b1..abfd7af4f 100644 --- a/apps/api/src/lib/buildPacks/php.ts +++ b/apps/api/src/lib/buildPacks/php.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; const createDockerfile = async (data, image, htaccessFound): Promise => { @@ -13,21 +14,8 @@ const createDockerfile = async (data, image, htaccessFound): Promise => { Dockerfile.push(`FROM ${image}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } Dockerfile.push('WORKDIR /app'); diff --git a/apps/api/src/lib/buildPacks/python.ts b/apps/api/src/lib/buildPacks/python.ts index 36d707f16..56294660f 100644 --- a/apps/api/src/lib/buildPacks/python.ts +++ b/apps/api/src/lib/buildPacks/python.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; const createDockerfile = async (data, image): Promise => { @@ -18,21 +19,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (pythonWSGI?.toLowerCase() === 'gunicorn') { diff --git a/apps/api/src/lib/buildPacks/static.ts b/apps/api/src/lib/buildPacks/static.ts index 160098654..19b13cef3 100644 --- a/apps/api/src/lib/buildPacks/static.ts +++ b/apps/api/src/lib/buildPacks/static.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildCacheImageWithNode, buildImage } from './common'; const createDockerfile = async (data, image): Promise => { @@ -25,21 +26,8 @@ const createDockerfile = async (data, image): Promise => { } Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}=${isSecretFound[0].value}`); - } else { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}=${secret.value}`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (buildCommand) { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 52ab2031c..b39be7c1d 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -19,9 +19,10 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common import { scheduler } from './scheduler'; import type { ExecaChildProcess } from 'execa'; -export const version = '3.12.0'; +export const version = '3.12.9'; export const isDev = process.env.NODE_ENV === 'development'; -export const sentryDSN = 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; +export const sentryDSN = + 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; const algorithm = 'aes-256-ctr'; const customConfig: Config = { dictionaries: [adjectives, colors, animals], @@ -92,7 +93,7 @@ export const asyncExecShellStream = async ({ line: `${line.replace('\n', '')}`, buildId, applicationId - } + }; logs.push(log); if (debug) { await saveBuildLog(log); @@ -109,7 +110,7 @@ export const asyncExecShellStream = async ({ line: `${line.replace('\n', '')}`, buildId, applicationId - } + }; logs.push(log); if (debug) { await saveBuildLog(log); @@ -393,7 +394,6 @@ export function generateTimestamp(): string { return `${day().format('HH:mm:ss.SSS')}`; } - export const supportedDatabaseTypesAndVersions = [ { name: 'mongodb', @@ -509,20 +509,19 @@ export async function createRemoteEngineConfiguration(id: string) { } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } }); await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 }); const config = sshConfig.parse(''); - const Host = `${remoteIpAddress}-remote` + const Host = `${remoteIpAddress}-remote`; try { await executeCommand({ command: `ssh-keygen -R ${Host}` }); await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` }); await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` }); - } catch (error) { } - + } catch (error) {} const found = config.find({ Host }); const foundIp = config.find({ Host: remoteIpAddress }); - if (found) config.remove({ Host }) - if (foundIp) config.remove({ Host: remoteIpAddress }) + if (found) config.remove({ Host }); + if (foundIp) config.remove({ Host: remoteIpAddress }); config.append({ Host, @@ -543,15 +542,35 @@ export async function createRemoteEngineConfiguration(id: string) { } return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)); } -export async function executeCommand({ command, dockerId = null, sshCommand = false, shell = false, stream = false, buildId, applicationId, debug }: { command: string, sshCommand?: boolean, shell?: boolean, stream?: boolean, dockerId?: string, buildId?: string, applicationId?: string, debug?: boolean }): Promise> { - const { execa, execaCommand } = await import('execa') - const { parse } = await import('shell-quote') +export async function executeCommand({ + command, + dockerId = null, + sshCommand = false, + shell = false, + stream = false, + buildId, + applicationId, + debug +}: { + command: string; + sshCommand?: boolean; + shell?: boolean; + stream?: boolean; + dockerId?: string; + buildId?: string; + applicationId?: string; + debug?: boolean; +}): Promise> { + const { execa, execaCommand } = await import('execa'); + const { parse } = await import('shell-quote'); const parsedCommand = parse(command); const dockerCommand = parsedCommand[0]; const dockerArgs = parsedCommand.slice(1); if (dockerId) { - let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) + let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ + where: { id: dockerId } + }); if (remoteEngine) { await createRemoteEngineConfiguration(dockerId); engine = `ssh://${remoteIpAddress}-remote`; @@ -591,7 +610,7 @@ export async function executeCommand({ command, dockerId = null, sshCommand = fa line: `${line.replace('\n', '')}`, buildId, applicationId - } + }; logs.push(log); if (debug) { await saveBuildLog(log); @@ -608,7 +627,7 @@ export async function executeCommand({ command, dockerId = null, sshCommand = fa line: `${line.replace('\n', '')}`, buildId, applicationId - } + }; logs.push(log); if (debug) { await saveBuildLog(log); @@ -628,7 +647,7 @@ export async function executeCommand({ command, dockerId = null, sshCommand = fa reject(code); } }); - }) + }); } else { if (shell) { return await execaCommand(command, { @@ -640,7 +659,6 @@ export async function executeCommand({ command, dockerId = null, sshCommand = fa }); } } - } else { if (shell) { return execaCommand(command, { shell: true }); @@ -650,8 +668,13 @@ export async function executeCommand({ command, dockerId = null, sshCommand = fa } export async function startTraefikProxy(id: string): Promise { - const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } }) - const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true }); + const { engine, network, remoteEngine, remoteIpAddress } = + await prisma.destinationDocker.findUnique({ where: { id } }); + const { found } = await checkContainer({ + dockerId: id, + container: 'coolify-proxy', + remove: true + }); const { id: settingsId, ipv4, ipv6 } = await listSettings(); if (!found) { @@ -768,9 +791,12 @@ export async function listSettings(): Promise { } export function generateToken() { - return jsonwebtoken.sign({ - nbf: Math.floor(Date.now() / 1000) - 30, - }, process.env['COOLIFY_SECRET_KEY']) + return jsonwebtoken.sign( + { + nbf: Math.floor(Date.now() / 1000) - 30 + }, + process.env['COOLIFY_SECRET_KEY'] + ); } export function generatePassword({ length = 24, @@ -790,109 +816,102 @@ export function generatePassword({ return password; } -type DatabaseConfiguration = { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MYSQL_DATABASE: string; - MYSQL_PASSWORD: string; - MYSQL_ROOT_USER: string; - MYSQL_USER: string; - MYSQL_ROOT_PASSWORD: string; - }; -} +type DatabaseConfiguration = | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MONGO_INITDB_ROOT_USERNAME?: string; - MONGO_INITDB_ROOT_PASSWORD?: string; - MONGODB_ROOT_USER?: string; - MONGODB_ROOT_PASSWORD?: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MYSQL_DATABASE: string; + MYSQL_PASSWORD: string; + MYSQL_ROOT_USER: string; + MYSQL_USER: string; + MYSQL_ROOT_PASSWORD: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MARIADB_ROOT_USER: string; - MARIADB_ROOT_PASSWORD: string; - MARIADB_USER: string; - MARIADB_PASSWORD: string; - MARIADB_DATABASE: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MONGO_INITDB_ROOT_USERNAME?: string; + MONGO_INITDB_ROOT_PASSWORD?: string; + MONGODB_ROOT_USER?: string; + MONGODB_ROOT_PASSWORD?: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - POSTGRES_PASSWORD?: string; - POSTGRES_USER?: string; - POSTGRES_DB?: string; - POSTGRESQL_POSTGRES_PASSWORD?: string; - POSTGRESQL_USERNAME?: string; - POSTGRESQL_PASSWORD?: string; - POSTGRESQL_DATABASE?: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MARIADB_ROOT_USER: string; + MARIADB_ROOT_PASSWORD: string; + MARIADB_USER: string; + MARIADB_PASSWORD: string; + MARIADB_DATABASE: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - REDIS_AOF_ENABLED: string; - REDIS_PASSWORD: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + POSTGRES_PASSWORD?: string; + POSTGRES_USER?: string; + POSTGRES_DB?: string; + POSTGRESQL_POSTGRES_PASSWORD?: string; + POSTGRESQL_USERNAME?: string; + POSTGRESQL_PASSWORD?: string; + POSTGRESQL_DATABASE?: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - COUCHDB_PASSWORD: string; - COUCHDB_USER: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + REDIS_AOF_ENABLED: string; + REDIS_PASSWORD: string; + }; + } | { - volume: string; - image: string; - command?: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - EDGEDB_SERVER_PASSWORD: string; - EDGEDB_SERVER_USER: string; - EDGEDB_SERVER_DATABASE: string; - EDGEDB_SERVER_TLS_CERT_MODE: string; - }; - } + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + COUCHDB_PASSWORD: string; + COUCHDB_USER: string; + }; + } + | { + volume: string; + image: string; + command?: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + EDGEDB_SERVER_PASSWORD: string; + EDGEDB_SERVER_USER: string; + EDGEDB_SERVER_DATABASE: string; + EDGEDB_SERVER_TLS_CERT_MODE: string; + }; + }; export function generateDatabaseConfiguration(database: any, arch: string): DatabaseConfiguration { - const { - id, - dbUser, - dbUserPassword, - rootUser, - rootUserPassword, - defaultDatabase, - version, - type, - } = database; + const { id, dbUser, dbUserPassword, rootUser, rootUserPassword, defaultDatabase, version, type } = + database; const baseImage = getDatabaseImage(type, arch); if (type === 'mysql') { const configuration = { @@ -972,7 +991,9 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data } return configuration; } else if (type === 'redis') { - const { settings: { appendOnly } } = database; + const { + settings: { appendOnly } + } = database; const configuration: DatabaseConfiguration = { privatePort: 6379, command: undefined, @@ -986,8 +1007,9 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data }; if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/data`; - configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no' - } --requirepass ${dbUserPassword}`; + configuration.command = `/usr/local/bin/redis-server --appendonly ${ + appendOnly ? 'yes' : 'no' + } --requirepass ${dbUserPassword}`; } return configuration; } else if (type === 'couchdb') { @@ -1004,7 +1026,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data if (isARM(arch)) { configuration.volume = `${id}-${type}-data:/opt/couchdb/data`; } - return configuration + return configuration; } else if (type === 'edgedb') { const configuration: DatabaseConfiguration = { privatePort: 5656, @@ -1018,7 +1040,7 @@ export function generateDatabaseConfiguration(database: any, arch: string): Data volume: `${id}-${type}-data:/var/lib/edgedb/data`, ulimits: {} }; - return configuration + return configuration; } } export function isARM(arch: string) { @@ -1071,12 +1093,12 @@ export type ComposeFileService = { command?: string; ports?: string[]; build?: - | { - context: string; - dockerfile: string; - args?: Record; - } - | string; + | { + context: string; + dockerfile: string; + args?: Record; + } + | string; deploy?: { restart_policy?: { condition?: string; @@ -1141,13 +1163,13 @@ export const createDirectories = async ({ repository: string; buildId: string; }): Promise<{ workdir: string; repodir: string }> => { - if (repository) repository = repository.replaceAll(' ', '') + if (repository) repository = repository.replaceAll(' ', ''); const repodir = `/tmp/build-sources/${repository}/`; const workdir = `/tmp/build-sources/${repository}/${buildId}`; let workdirFound = false; try { workdirFound = !!(await fs.stat(workdir)); - } catch (error) { } + } catch (error) {} if (workdirFound) { await executeCommand({ command: `rm -fr ${workdir}` }); } @@ -1254,19 +1276,45 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { } } } -export async function checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, engine: string, remoteEngine: boolean, remoteIpAddress?: string }) { +export async function checkExposedPort({ + id, + configuredPort, + exposePort, + engine, + remoteEngine, + remoteIpAddress +}: { + id: string; + configuredPort?: number; + exposePort: number; + engine: string; + remoteEngine: boolean; + remoteIpAddress?: string; +}) { if (exposePort < 1024 || exposePort > 65535) { throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` }; } if (configuredPort) { if (configuredPort !== exposePort) { - const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); + const availablePort = await getFreeExposedPort( + id, + exposePort, + engine, + remoteEngine, + remoteIpAddress + ); if (availablePort.toString() !== exposePort.toString()) { throw { status: 500, message: `Port ${exposePort} is already in use.` }; } } } else { - const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress); + const availablePort = await getFreeExposedPort( + id, + exposePort, + engine, + remoteEngine, + remoteIpAddress + ); if (availablePort.toString() !== exposePort.toString()) { throw { status: 500, message: `Port ${exposePort} is already in use.` }; } @@ -1277,25 +1325,33 @@ export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, r if (remoteEngine) { const applicationUsed = await ( await prisma.application.findMany({ - where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + where: { + exposePort: { not: null }, + id: { not: id }, + destinationDocker: { remoteIpAddress } + }, select: { exposePort: true } }) ).map((a) => a.exposePort); const serviceUsed = await ( await prisma.service.findMany({ - where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + where: { + exposePort: { not: null }, + id: { not: id }, + destinationDocker: { remoteIpAddress } + }, select: { exposePort: true } }) ).map((a) => a.exposePort); const usedPorts = [...applicationUsed, ...serviceUsed]; if (usedPorts.includes(exposePort)) { - return false + return false; } const found = await checkPort(exposePort, { host: remoteIpAddress }); if (!found) { - return exposePort + return exposePort; } - return false + return false; } else { const applicationUsed = await ( await prisma.application.findMany({ @@ -1311,13 +1367,13 @@ export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, r ).map((a) => a.exposePort); const usedPorts = [...applicationUsed, ...serviceUsed]; if (usedPorts.includes(exposePort)) { - return false + return false; } const found = await checkPort(exposePort, { host: 'localhost' }); if (!found) { - return exposePort + return exposePort; } - return false + return false; } } export function generateRangeArray(start, end) { @@ -1330,38 +1386,54 @@ export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddr if (remoteEngine) { const dbUsed = await ( await prisma.database.findMany({ - where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } }, + where: { + publicPort: { not: null }, + id: { not: id }, + destinationDocker: { remoteIpAddress } + }, select: { publicPort: true } }) ).map((a) => a.publicPort); const wpFtpUsed = await ( await prisma.wordpress.findMany({ - where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + where: { + ftpPublicPort: { not: null }, + id: { not: id }, + service: { destinationDocker: { remoteIpAddress } } + }, select: { ftpPublicPort: true } }) ).map((a) => a.ftpPublicPort); const wpUsed = await ( await prisma.wordpress.findMany({ - where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + where: { + mysqlPublicPort: { not: null }, + id: { not: id }, + service: { destinationDocker: { remoteIpAddress } } + }, select: { mysqlPublicPort: true } }) ).map((a) => a.mysqlPublicPort); const minioUsed = await ( await prisma.minio.findMany({ - where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } }, + where: { + publicPort: { not: null }, + id: { not: id }, + service: { destinationDocker: { remoteIpAddress } } + }, select: { publicPort: true } }) ).map((a) => a.publicPort); const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; - const range = generateRangeArray(minPort, maxPort) - const availablePorts = range.filter(port => !usedPorts.includes(port)) + const range = generateRangeArray(minPort, maxPort); + const availablePorts = range.filter((port) => !usedPorts.includes(port)); for (const port of availablePorts) { - const found = await isReachable(port, { host: remoteIpAddress }) + const found = await isReachable(port, { host: remoteIpAddress }); if (!found) { - return port + return port; } } - return false + return false; } else { const dbUsed = await ( await prisma.database.findMany({ @@ -1371,32 +1443,44 @@ export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddr ).map((a) => a.publicPort); const wpFtpUsed = await ( await prisma.wordpress.findMany({ - where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + where: { + ftpPublicPort: { not: null }, + id: { not: id }, + service: { destinationDocker: { engine } } + }, select: { ftpPublicPort: true } }) ).map((a) => a.ftpPublicPort); const wpUsed = await ( await prisma.wordpress.findMany({ - where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + where: { + mysqlPublicPort: { not: null }, + id: { not: id }, + service: { destinationDocker: { engine } } + }, select: { mysqlPublicPort: true } }) ).map((a) => a.mysqlPublicPort); const minioUsed = await ( await prisma.minio.findMany({ - where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } }, + where: { + publicPort: { not: null }, + id: { not: id }, + service: { destinationDocker: { engine } } + }, select: { publicPort: true } }) ).map((a) => a.publicPort); const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; - const range = generateRangeArray(minPort, maxPort) - const availablePorts = range.filter(port => !usedPorts.includes(port)) + const range = generateRangeArray(minPort, maxPort); + const availablePorts = range.filter((port) => !usedPorts.includes(port)); for (const port of availablePorts) { - const found = await isReachable(port, { host: 'localhost' }) + const found = await isReachable(port, { host: 'localhost' }); if (!found) { - return port + return port; } } - return false + return false; } } @@ -1502,11 +1586,11 @@ export async function getServiceFromDB({ serviceSecret: true, serviceSetting: true, wordpress: true, - plausibleAnalytics: true, + plausibleAnalytics: true } }); if (!body) { - return null + return null; } // body.type = fixType(body.type); @@ -1523,7 +1607,6 @@ export async function getServiceFromDB({ return { ...body, settings }; } - export function fixType(type) { return type?.replaceAll(' ', '').toLowerCase() || null; } @@ -1610,7 +1693,7 @@ export async function stopBuild(buildId, applicationId) { } } count++; - } catch (error) { } + } catch (error) {} }, 100); }); } @@ -1640,16 +1723,22 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { images = images.trim(); if (images) { - await executeCommand({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r`, shell: true }); + await executeCommand({ + dockerId, + command: `docker rmi -f ${images}" -q | xargs -r`, + shell: true + }); } - } catch (error) { } + } catch (error) {} if (lowDiskSpace || force) { // Cleanup images that are not used try { await executeCommand({ dockerId, command: `docker image prune -f` }); - } catch (error) { } + } catch (error) {} - const { numberOfDockerImagesKeptLocally } = await prisma.setting.findUnique({ where: { id: '0' } }) + const { numberOfDockerImagesKeptLocally } = await prisma.setting.findUnique({ + where: { id: '0' } + }); const { stdout: images } = await executeCommand({ dockerId, command: `docker images|grep -v ""|grep -v REPOSITORY|awk '{print $1, $2}'`, @@ -1657,22 +1746,29 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { }); const imagesArray = images.trim().replaceAll(' ', ':').split('\n'); const imagesSet = new Set(imagesArray.map((image) => image.split(':')[0])); - let deleteImage = [] + let deleteImage = []; for (const image of imagesSet) { - let keepImage = [] + let keepImage = []; for (const image2 of imagesArray) { if (image2.startsWith(image)) { + if (force) { + deleteImage.push(image2); + continue; + } if (keepImage.length >= numberOfDockerImagesKeptLocally) { - deleteImage.push(image2) + deleteImage.push(image2); } else { - keepImage.push(image2) + keepImage.push(image2); } } - } } for (const image of deleteImage) { - await executeCommand({ dockerId, command: `docker image rm -f ${image}` }); + try { + await executeCommand({ dockerId, command: `docker image rm -f ${image}` }); + } catch (error) { + console.log(error); + } } // Prune coolify managed containers @@ -1681,12 +1777,12 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }); - } catch (error) { } + } catch (error) {} // Cleanup build caches try { await executeCommand({ dockerId, command: `docker builder prune -a -f` }); - } catch (error) { } + } catch (error) {} } } @@ -1768,16 +1864,90 @@ export function decryptApplication(application: any) { } } -export async function pushToRegistry(application: any, workdir: string, tag: string, imageName: string, customTag: string) { - const location = `${workdir}/.docker` - const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}` - const pushCommand = `docker --config ${location} push ${imageName}:${customTag}` +export async function pushToRegistry( + application: any, + workdir: string, + tag: string, + imageName: string, + customTag: string +) { + const location = `${workdir}/.docker`; + const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}`; + const pushCommand = `docker --config ${location} push ${imageName}:${customTag}`; await executeCommand({ dockerId: application.destinationDockerId, command: tagCommand - }) + }); await executeCommand({ dockerId: application.destinationDockerId, command: pushCommand - }) -} \ No newline at end of file + }); +} + +export function generateSecrets( + secrets: Array, + pullmergeRequestId: string, + isBuild = false, + port = null +): Array { + const envs = []; + const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret); + const normalSecrets = secrets.filter((s) => !s.isPRMRSecret); + if (pullmergeRequestId && isPRMRSecret.length > 0) { + isPRMRSecret.forEach((secret) => { + if (isBuild && !secret.isBuildSecret) { + return; + } + const build = isBuild && secret.isBuildSecret; + if (secret.value.includes('$')) { + secret.value = secret.value.replaceAll('$', '$$$$'); + } + if (secret.value.includes(' ') || secret.value.includes('\\n')) { + if (build) { + envs.push(`ARG ${secret.name}='${secret.value}'`); + } else { + envs.push(`${secret.name}='${secret.value}'`); + } + } else { + if (build) { + envs.push(`ARG ${secret.name}=${secret.value}`); + } else { + envs.push(`${secret.name}=${secret.value}`); + } + } + }); + } + if (!pullmergeRequestId && normalSecrets.length > 0) { + normalSecrets.forEach((secret) => { + if (isBuild && !secret.isBuildSecret) { + return; + } + if (secret.value.includes('$')) { + secret.value = secret.value.replaceAll('$', '$$$$'); + } + const build = isBuild && secret.isBuildSecret; + if (secret.value.includes(' ') || secret.value.includes('\\n')) { + if (build) { + envs.push(`ARG ${secret.name}='${secret.value}'`); + } else { + envs.push(`${secret.name}='${secret.value}'`); + } + } else { + if (build) { + envs.push(`ARG ${secret.name}=${secret.value}`); + } else { + envs.push(`${secret.name}=${secret.value}`); + } + } + }); + } + const portFound = envs.filter((env) => env.startsWith('PORT')); + if (portFound.length === 0 && port && !isBuild) { + envs.push(`PORT=${port}`); + } + const nodeEnv = envs.filter((env) => env.startsWith('NODE_ENV')); + if (nodeEnv.length === 0 && !isBuild) { + envs.push(`NODE_ENV=production`); + } + return envs; +} diff --git a/apps/api/src/lib/importers/gitlab.ts b/apps/api/src/lib/importers/gitlab.ts index eef9b62ef..d75c3bbef 100644 --- a/apps/api/src/lib/importers/gitlab.ts +++ b/apps/api/src/lib/importers/gitlab.ts @@ -12,7 +12,8 @@ export default async function ({ buildId, privateSshKey, customPort, - forPublic + forPublic, + customUser, }: { applicationId: string; workdir: string; @@ -25,6 +26,7 @@ export default async function ({ privateSshKey: string; customPort: number; forPublic: boolean; + customUser: string; }): Promise { const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); if (!forPublic) { @@ -53,7 +55,7 @@ export default async function ({ } else { await executeCommand({ command: - `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true + `git clone -q -b ${branch} ${customUser}@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true } ); } diff --git a/apps/api/src/lib/services.ts b/apps/api/src/lib/services.ts index b423cbe9c..19e3094eb 100644 --- a/apps/api/src/lib/services.ts +++ b/apps/api/src/lib/services.ts @@ -1,51 +1,47 @@ -import { isARM, isDev } from "./common"; +import { isARM, isDev } from './common'; import fs from 'fs/promises'; export async function getTemplates() { - const templatePath = isDev ? './templates.json' : '/app/templates.json'; - const open = await fs.open(templatePath, 'r'); - try { - let data = await open.readFile({ encoding: 'utf-8' }); - let jsonData = JSON.parse(data) - if (isARM(process.arch)) { - jsonData = jsonData.filter(d => d.arch !== 'amd64') - } - return jsonData; - } catch (error) { - return [] - } finally { - await open?.close() - } + const templatePath = isDev ? './templates.json' : '/app/templates.json'; + const open = await fs.open(templatePath, 'r'); + try { + let data = await open.readFile({ encoding: 'utf-8' }); + let jsonData = JSON.parse(data); + if (isARM(process.arch)) { + jsonData = jsonData.filter((d) => d.arch !== 'amd64'); + } + return jsonData; + } catch (error) { + return []; + } finally { + await open?.close(); + } } const compareSemanticVersions = (a: string, b: string) => { - const a1 = a.split('.'); - const b1 = b.split('.'); - const len = Math.min(a1.length, b1.length); - for (let i = 0; i < len; i++) { - const a2 = +a1[i] || 0; - const b2 = +b1[i] || 0; - if (a2 !== b2) { - return a2 > b2 ? 1 : -1; - } - } - return b1.length - a1.length; + const a1 = a.split('.'); + const b1 = b.split('.'); + const len = Math.min(a1.length, b1.length); + for (let i = 0; i < len; i++) { + const a2 = +a1[i] || 0; + const b2 = +b1[i] || 0; + if (a2 !== b2) { + return a2 > b2 ? 1 : -1; + } + } + return b1.length - a1.length; }; export async function getTags(type: string) { - - try { - if (type) { - const tagsPath = isDev ? './tags.json' : '/app/tags.json'; - const data = await fs.readFile(tagsPath, 'utf8') - let tags = JSON.parse(data) - if (tags) { - tags = tags.find((tag: any) => tag.name.includes(type)) - tags.tags = tags.tags.sort(compareSemanticVersions).reverse(); - return tags - } - } - } catch (error) { - return [] - - } - - + try { + if (type) { + const tagsPath = isDev ? './tags.json' : '/app/tags.json'; + const data = await fs.readFile(tagsPath, 'utf8'); + let tags = JSON.parse(data); + if (tags) { + tags = tags.find((tag: any) => tag.name.includes(type)); + tags.tags = tags.tags.sort(compareSemanticVersions).reverse(); + return tags; + } + } + } catch (error) { + return []; + } } diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index f43a5c536..d9abbd852 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -1,5 +1,5 @@ import cuid from 'cuid'; -import crypto from 'node:crypto' +import crypto from 'node:crypto'; import jsonwebtoken from 'jsonwebtoken'; import { FastifyReply } from 'fastify'; import fs from 'fs/promises'; @@ -7,1601 +7,1837 @@ import yaml from 'js-yaml'; import csv from 'csvtojson'; import { day } from '../../../../lib/dayjs'; -import { saveDockerRegistryCredentials, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { + saveDockerRegistryCredentials, + setDefaultBaseImage, + setDefaultConfiguration +} from '../../../../lib/buildPacks/common'; +import { + checkDomainsIsValidInDNS, + checkExposedPort, + createDirectories, + decrypt, + defaultComposeConfiguration, + encrypt, + errorHandler, + executeCommand, + generateSecrets, + generateSshKeyPair, + getContainerUsage, + getDomain, + isDev, + isDomainConfigured, + listSettings, + prisma, + stopBuild, + uniqueName +} from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker'; import type { FastifyRequest } from 'fastify'; -import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds, RestartApplication } from './types'; +import type { + GetImages, + CancelDeployment, + CheckDNS, + CheckRepository, + DeleteApplication, + DeleteSecret, + DeleteStorage, + GetApplicationLogs, + GetBuildIdLogs, + SaveApplication, + SaveApplicationSettings, + SaveApplicationSource, + SaveDeployKey, + SaveDestination, + SaveSecret, + SaveStorage, + DeployApplication, + CheckDomain, + StopPreviewApplication, + RestartPreviewApplication, + GetBuilds, + RestartApplication +} from './types'; import { OnlyId } from '../../../../types'; function filterObject(obj, callback) { - return Object.fromEntries(Object.entries(obj). - filter(([key, val]) => callback(val, key))); + return Object.fromEntries(Object.entries(obj).filter(([key, val]) => callback(val, key))); } export async function listApplications(request: FastifyRequest) { - try { - const { teamId } = request.user - const applications = await prisma.application.findMany({ - where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { teams: true, destinationDocker: true, settings: true } - }); - const settings = await prisma.setting.findFirst() - return { - applications, - settings - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { teamId } = request.user; + const applications = await prisma.application.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { teams: true, destinationDocker: true, settings: true } + }); + const settings = await prisma.setting.findFirst(); + return { + applications, + settings + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getImages(request: FastifyRequest) { - try { - const { buildPack, deploymentType } = request.body - let publishDirectory = undefined; - let port = undefined - const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( - buildPack, deploymentType - ); - if (buildPack === 'nextjs') { - if (deploymentType === 'static') { - publishDirectory = 'out' - port = '80' - } else { - publishDirectory = '' - port = '3000' - } - } - if (buildPack === 'nuxtjs') { - if (deploymentType === 'static') { - publishDirectory = 'dist' - port = '80' - } else { - publishDirectory = '' - port = '3000' - } - } + try { + const { buildPack, deploymentType } = request.body; + let publishDirectory = undefined; + let port = undefined; + const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( + buildPack, + deploymentType + ); + if (buildPack === 'nextjs') { + if (deploymentType === 'static') { + publishDirectory = 'out'; + port = '80'; + } else { + publishDirectory = ''; + port = '3000'; + } + } + if (buildPack === 'nuxtjs') { + if (deploymentType === 'static') { + publishDirectory = 'dist'; + port = '80'; + } else { + publishDirectory = ''; + port = '3000'; + } + } - return { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + return { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function cleanupUnconfiguredApplications(request: FastifyRequest) { - try { - const teamId = request.user.teamId - let applications = await prisma.application.findMany({ - where: { teams: { some: { id: teamId === "0" ? undefined : teamId } } }, - include: { settings: true, destinationDocker: true, teams: true }, - }); - for (const application of applications) { - if (!application.buildPack || !application.destinationDockerId || !application.branch || (!application.settings?.isBot && !application?.fqdn)) { - if (application?.destinationDockerId && application.destinationDocker?.network) { - const { stdout: containers } = await executeCommand({ - dockerId: application.destinationDocker.id, - command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.id} --format '{{json .}}'` - }) - if (containers) { - const containersArray = containers.trim().split('\n'); - for (const container of containersArray) { - const containerObj = JSON.parse(container); - const id = containerObj.ID; - await removeContainer({ id, dockerId: application.destinationDocker.id }); - } - } - } - await prisma.applicationSettings.deleteMany({ where: { applicationId: application.id } }); - await prisma.buildLog.deleteMany({ where: { applicationId: application.id } }); - await prisma.build.deleteMany({ where: { applicationId: application.id } }); - await prisma.secret.deleteMany({ where: { applicationId: application.id } }); - await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: application.id } }); - await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: application.id } }); - await prisma.application.deleteMany({ where: { id: application.id } }); - } - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const teamId = request.user.teamId; + let applications = await prisma.application.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true } + }); + for (const application of applications) { + if ( + !application.buildPack || + !application.destinationDockerId || + !application.branch || + (!application.settings?.isBot && !application?.fqdn) + ) { + if (application?.destinationDockerId && application.destinationDocker?.network) { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.id} --format '{{json .}}'` + }); + if (containers) { + const containersArray = containers.trim().split('\n'); + for (const container of containersArray) { + const containerObj = JSON.parse(container); + const id = containerObj.ID; + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } + } + } + await prisma.applicationSettings.deleteMany({ where: { applicationId: application.id } }); + await prisma.buildLog.deleteMany({ where: { applicationId: application.id } }); + await prisma.build.deleteMany({ where: { applicationId: application.id } }); + await prisma.secret.deleteMany({ where: { applicationId: application.id } }); + await prisma.applicationPersistentStorage.deleteMany({ + where: { applicationId: application.id } + }); + await prisma.applicationConnectedDatabase.deleteMany({ + where: { applicationId: application.id } + }); + await prisma.application.deleteMany({ where: { id: application.id } }); + } + } + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getApplicationStatus(request: FastifyRequest) { - try { - const { id } = request.params - const { teamId } = request.user - let payload = [] - const application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - if (application.buildPack === 'compose') { - const { stdout: containers } = await executeCommand({ - dockerId: application.destinationDocker.id, - command: - `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` - }); - const containersArray = containers.trim().split('\n'); - if (containersArray.length > 0 && containersArray[0] !== '') { - for (const container of containersArray) { - let isRunning = false; - let isExited = false; - let isRestarting = false; - const containerObj = JSON.parse(container); - const status = containerObj.State - if (status === 'running') { - isRunning = true; - } - if (status === 'exited') { - isExited = true; - } - if (status === 'restarting') { - isRestarting = true; - } - payload.push({ - name: containerObj.Names, - status: { - isRunning, - isExited, - isRestarting - } - }) - } - } - } else { - let isRunning = false; - let isExited = false; - let isRestarting = false; - const status = await checkContainer({ dockerId: application.destinationDocker.id, container: id }); - if (status?.found) { - isRunning = status.status.isRunning; - isExited = status.status.isExited; - isRestarting = status.status.isRestarting - payload.push({ - name: id, - status: { - isRunning, - isExited, - isRestarting - } - }) - - } - } - } - return payload - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { teamId } = request.user; + let payload = []; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + if (application.buildPack === 'compose') { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` + }); + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + for (const container of containersArray) { + let isRunning = false; + let isExited = false; + let isRestarting = false; + const containerObj = JSON.parse(container); + const status = containerObj.State; + if (status === 'running') { + isRunning = true; + } + if (status === 'exited') { + isExited = true; + } + if (status === 'restarting') { + isRestarting = true; + } + payload.push({ + name: containerObj.Names, + status: { + isRunning, + isExited, + isRestarting + } + }); + } + } + } else { + let isRunning = false; + let isExited = false; + let isRestarting = false; + const status = await checkContainer({ + dockerId: application.destinationDocker.id, + container: id + }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting; + payload.push({ + name: id, + status: { + isRunning, + isExited, + isRestarting + } + }); + } + } + } + return payload; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getApplication(request: FastifyRequest) { - try { - const { id } = request.params - const { teamId } = request.user - const appId = process.env['COOLIFY_APP_ID']; - const application: any = await getApplicationFromDB(id, teamId); - const settings = await listSettings(); - return { - application, - appId, - settings - }; - - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { teamId } = request.user; + const appId = process.env['COOLIFY_APP_ID']; + const application: any = await getApplicationFromDB(id, teamId); + const settings = await listSettings(); + return { + application, + appId, + settings + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function newApplication(request: FastifyRequest, reply: FastifyReply) { - try { - const name = uniqueName(); - const { teamId } = request.user - const { id } = await prisma.application.create({ - data: { - name, - teams: { connect: { id: teamId } }, - settings: { create: { debug: false, previews: false } } - } - }); - return reply.code(201).send({ id }); - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const name = uniqueName(); + const { teamId } = request.user; + const { id } = await prisma.application.create({ + data: { + name, + teams: { connect: { id: teamId } }, + settings: { create: { debug: false, previews: false } } + } + }); + return reply.code(201).send({ id }); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } function decryptApplication(application: any) { - if (application) { - if (application?.gitSource?.githubApp?.clientSecret) { - application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null; - } - if (application?.gitSource?.githubApp?.webhookSecret) { - application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null; - } - if (application?.gitSource?.githubApp?.privateKey) { - application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null; - } - if (application?.gitSource?.gitlabApp?.appSecret) { - application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null; - } - if (application?.secrets.length > 0) { - application.secrets = application.secrets.map((s: any) => { - s.value = decrypt(s.value) || null - return s; - }); - } + if (application) { + if (application?.gitSource?.githubApp?.clientSecret) { + application.gitSource.githubApp.clientSecret = + decrypt(application.gitSource.githubApp.clientSecret) || null; + } + if (application?.gitSource?.githubApp?.webhookSecret) { + application.gitSource.githubApp.webhookSecret = + decrypt(application.gitSource.githubApp.webhookSecret) || null; + } + if (application?.gitSource?.githubApp?.privateKey) { + application.gitSource.githubApp.privateKey = + decrypt(application.gitSource.githubApp.privateKey) || null; + } + if (application?.gitSource?.gitlabApp?.appSecret) { + application.gitSource.gitlabApp.appSecret = + decrypt(application.gitSource.gitlabApp.appSecret) || null; + } + if (application?.secrets.length > 0) { + application.secrets = application.secrets.map((s: any) => { + s.value = decrypt(s.value) || null; + return s; + }); + } - return application; - } + return application; + } } export async function getApplicationFromDB(id: string, teamId: string) { - try { - let application = await prisma.application.findFirst({ - where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, - include: { - destinationDocker: true, - settings: true, - gitSource: { include: { githubApp: true, gitlabApp: true } }, - secrets: true, - persistentStorage: true, - connectedDatabase: true, - previewApplication: true, - dockerRegistry: true - } - }); - if (!application) { - throw { status: 404, message: 'Application not found.' }; - } - application = decryptApplication(application); - const buildPack = application?.buildPack || null; - const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( - buildPack - ); + try { + let application = await prisma.application.findFirst({ + where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { + destinationDocker: true, + settings: true, + gitSource: { include: { githubApp: true, gitlabApp: true } }, + secrets: true, + persistentStorage: true, + connectedDatabase: true, + previewApplication: true, + dockerRegistry: true + } + }); + if (!application) { + throw { status: 404, message: 'Application not found.' }; + } + application = decryptApplication(application); + const buildPack = application?.buildPack || null; + const { baseImage, baseBuildImage, baseBuildImages, baseImages } = + setDefaultBaseImage(buildPack); - // Set default build images - if (!application.baseImage) { - application.baseImage = baseImage; - } - if (!application.baseBuildImage) { - application.baseBuildImage = baseBuildImage; - } - return { ...application, baseBuildImages, baseImages }; - - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + // Set default build images + if (!application.baseImage) { + application.baseImage = baseImage; + } + if (!application.baseBuildImage) { + application.baseBuildImage = baseBuildImage; + } + return { ...application, baseBuildImages, baseImages }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getApplicationFromDBWebhook(projectId: number, branch: string) { - try { - let applications = await prisma.application.findMany({ - where: { projectId, branch, settings: { autodeploy: true } }, - include: { - destinationDocker: true, - settings: true, - gitSource: { include: { githubApp: true, gitlabApp: true } }, - secrets: true, - persistentStorage: true, - connectedDatabase: true - } - }); - if (applications.length === 0) { - throw { status: 500, message: 'Application not configured.', type: 'webhook' } - } - applications = applications.map((application: any) => { - application = decryptApplication(application); - const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( - application.buildPack - ); + try { + let applications = await prisma.application.findMany({ + where: { projectId, branch, settings: { autodeploy: true } }, + include: { + destinationDocker: true, + settings: true, + gitSource: { include: { githubApp: true, gitlabApp: true } }, + secrets: true, + persistentStorage: true, + connectedDatabase: true + } + }); + if (applications.length === 0) { + throw { status: 500, message: 'Application not configured.', type: 'webhook' }; + } + applications = applications.map((application: any) => { + application = decryptApplication(application); + const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( + application.buildPack + ); - // Set default build images - if (!application.baseImage) { - application.baseImage = baseImage; - } - if (!application.baseBuildImage) { - application.baseBuildImage = baseBuildImage; - } - application.baseBuildImages = baseBuildImages; - application.baseImages = baseImages; - return application - }) + // Set default build images + if (!application.baseImage) { + application.baseImage = baseImage; + } + if (!application.baseBuildImage) { + application.baseBuildImage = baseBuildImage; + } + application.baseBuildImages = baseBuildImages; + application.baseImages = baseImages; + return application; + }); - return applications; - - } catch ({ status, message, type }) { - return errorHandler({ status, message, type }) - } + return applications; + } catch ({ status, message, type }) { + return errorHandler({ status, message, type }); + } } -export async function saveApplication(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - let { - name, - buildPack, - fqdn, - port, - exposePort, - installCommand, - buildCommand, - startCommand, - baseDirectory, - publishDirectory, - pythonWSGI, - pythonModule, - pythonVariable, - dockerFileLocation, - denoMainFile, - denoOptions, - gitCommitHash, - baseImage, - baseBuildImage, - deploymentType, - baseDatabaseBranch, - dockerComposeFile, - dockerComposeFileLocation, - dockerComposeConfiguration, - simpleDockerfile, - dockerRegistryImageName - } = request.body - if (port) port = Number(port); - if (exposePort) { - exposePort = Number(exposePort); - } - const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) - if (denoOptions) denoOptions = denoOptions.trim(); - const defaultConfiguration = await setDefaultConfiguration({ - buildPack, - port, - installCommand, - startCommand, - buildCommand, - publishDirectory, - baseDirectory, - dockerFileLocation, - dockerComposeFileLocation, - denoMainFile - }); - if (baseDatabaseBranch) { - await prisma.application.update({ - where: { id }, - data: { - name, - fqdn, - exposePort, - pythonWSGI, - pythonModule, - pythonVariable, - denoOptions, - baseImage, - gitCommitHash, - baseBuildImage, - deploymentType, - dockerComposeFile, - dockerComposeFileLocation, - dockerComposeConfiguration, - simpleDockerfile, - dockerRegistryImageName, - ...defaultConfiguration, - connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } - } - }); - } else { - await prisma.application.update({ - where: { id }, - data: { - name, - fqdn, - exposePort, - pythonWSGI, - pythonModule, - gitCommitHash, - pythonVariable, - denoOptions, - baseImage, - baseBuildImage, - deploymentType, - dockerComposeFile, - dockerComposeFileLocation, - dockerComposeConfiguration, - simpleDockerfile, - dockerRegistryImageName, - ...defaultConfiguration - } - }); - } - - return reply.code(201).send(); - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function saveApplication( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + let { + name, + buildPack, + fqdn, + port, + exposePort, + installCommand, + buildCommand, + startCommand, + baseDirectory, + publishDirectory, + pythonWSGI, + pythonModule, + pythonVariable, + dockerFileLocation, + denoMainFile, + denoOptions, + gitCommitHash, + baseImage, + baseBuildImage, + deploymentType, + baseDatabaseBranch, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, + simpleDockerfile, + dockerRegistryImageName + } = request.body; + if (port) port = Number(port); + if (exposePort) { + exposePort = Number(exposePort); + } + const { + destinationDocker: { engine, remoteEngine, remoteIpAddress }, + exposePort: configuredPort + } = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + if (exposePort) + await checkExposedPort({ + id, + configuredPort, + exposePort, + engine, + remoteEngine, + remoteIpAddress + }); + if (denoOptions) denoOptions = denoOptions.trim(); + const defaultConfiguration = await setDefaultConfiguration({ + buildPack, + port, + installCommand, + startCommand, + buildCommand, + publishDirectory, + baseDirectory, + dockerFileLocation, + dockerComposeFileLocation, + denoMainFile + }); + if (baseDatabaseBranch) { + await prisma.application.update({ + where: { id }, + data: { + name, + fqdn, + exposePort, + pythonWSGI, + pythonModule, + pythonVariable, + denoOptions, + baseImage, + gitCommitHash, + baseBuildImage, + deploymentType, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, + simpleDockerfile, + dockerRegistryImageName, + ...defaultConfiguration, + connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } } + } + }); + } else { + await prisma.application.update({ + where: { id }, + data: { + name, + fqdn, + exposePort, + pythonWSGI, + pythonModule, + gitCommitHash, + pythonVariable, + denoOptions, + baseImage, + baseBuildImage, + deploymentType, + dockerComposeFile, + dockerComposeFileLocation, + dockerComposeConfiguration, + simpleDockerfile, + dockerRegistryImageName, + ...defaultConfiguration + } + }); + } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function saveApplicationSettings(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body - await prisma.application.update({ - where: { id }, - data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } }, - include: { destinationDocker: true } - }); - return reply.code(201).send(); - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function saveApplicationSettings( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + const { + debug, + previews, + dualCerts, + autodeploy, + branch, + projectId, + isBot, + isDBBranching, + isCustomSSL + } = request.body; + await prisma.application.update({ + where: { id }, + data: { + fqdn: isBot ? null : undefined, + settings: { + update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } + } + }, + include: { destinationDocker: true } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function stopPreviewApplication(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { pullmergeRequestId } = request.body - const { teamId } = request.user - const application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - const container = `${id}-${pullmergeRequestId}` - const { id: dockerId } = application.destinationDocker; - const { found } = await checkContainer({ dockerId, container }); - if (found) { - await removeContainer({ id: container, dockerId: application.destinationDocker.id }); - } - await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } }) - } - return reply.code(201).send(); - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function stopPreviewApplication( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + const { pullmergeRequestId } = request.body; + const { teamId } = request.user; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const container = `${id}-${pullmergeRequestId}`; + const { id: dockerId } = application.destinationDocker; + const { found } = await checkContainer({ dockerId, container }); + if (found) { + await removeContainer({ id: container, dockerId: application.destinationDocker.id }); + } + await prisma.previewApplication.deleteMany({ + where: { applicationId: application.id, pullmergeRequestId } + }); + } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function restartApplication(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { imageId = null } = request.body - const { teamId } = request.user - let application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - const buildId = cuid(); - const { id: dockerId, network } = application.destinationDocker; - const { dockerRegistry, secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; - let location = null; - const envs = [ - `PORT=${port}` - ]; - if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } - } - }); - } - const { workdir } = await createDirectories({ repository, buildId }); - const labels = [] - let image = null - if (imageId) { - image = imageId - } else { - const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` }) - const containersArray = container.trim().split('\n'); - for (const container of containersArray) { - const containerObj = formatLabelsOnDocker(container); - image = containerObj[0].Image - Object.keys(containerObj[0].Labels).forEach(function (key) { - if (key.startsWith('coolify')) { - labels.push(`${key}=${containerObj[0].Labels[key]}`) - } - }) - } - } - if (dockerRegistry) { - const { url, username, password } = dockerRegistry - location = await saveDockerRegistryCredentials({ url, username, password, workdir }) - } +export async function restartApplication( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + const { imageId = null } = request.body; + const { teamId } = request.user; + let application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const buildId = cuid(); + const { id: dockerId, network } = application.destinationDocker; + const { + dockerRegistry, + secrets, + pullmergeRequestId, + port, + repository, + persistentStorage, + id: applicationId, + buildPack, + exposePort + } = application; + let location = null; - let imageFoundLocally = false; - try { - await executeCommand({ - dockerId, - command: `docker image inspect ${image}` - }) - imageFoundLocally = true; - } catch (error) { - // - } - let imageFoundRemotely = false; - try { - await executeCommand({ - dockerId, - command: `docker ${location ? `--config ${location}` : ''} pull ${image}` - }) - imageFoundRemotely = true; - } catch (error) { - // - } + let envs = []; + if (secrets.length > 0) { + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; + } - if (!imageFoundLocally && !imageFoundRemotely) { - throw { status: 500, message: 'Image not found, cannot restart application.' } - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + const { workdir } = await createDirectories({ repository, buildId }); + const labels = []; + let image = null; + if (imageId) { + image = imageId; + } else { + const { stdout: container } = await executeCommand({ + dockerId, + command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` + }); + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image; + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`); + } + }); + } + } + if (dockerRegistry) { + const { url, username, password } = dockerRegistry; + location = await saveDockerRegistryCredentials({ url, username, password, workdir }); + } - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - const volumes = - persistentStorage?.map((storage) => { - return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' - }${storage.path}`; - }) || []; - const composeVolumes = volumes.map((volume) => { - return { - [`${volume.split(':')[0]}`]: { - name: volume.split(':')[0] - } - }; - }); - const composeFile = { - version: '3.8', - services: { - [applicationId]: { - image, - container_name: applicationId, - volumes, - env_file: envFound ? [`${workdir}/.env`] : [], - labels, - depends_on: [], - expose: [port], - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: Object.assign({}, ...composeVolumes) - }; - await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - try { - await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }) - await executeCommand({ dockerId, command: `docker rm ${id}` }) - } catch (error) { - // - } + let imageFoundLocally = false; + try { + await executeCommand({ + dockerId, + command: `docker image inspect ${image}` + }); + imageFoundLocally = true; + } catch (error) { + // + } + let imageFoundRemotely = false; + try { + await executeCommand({ + dockerId, + command: `docker ${location ? `--config ${location}` : ''} pull ${image}` + }); + imageFoundRemotely = true; + } catch (error) { + // + } - await executeCommand({ dockerId, command: `docker compose --project-directory ${workdir} up -d` }) - return reply.code(201).send(); - } - throw { status: 500, message: 'Application cannot be restarted.' } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + if (!imageFoundLocally && !imageFoundRemotely) { + throw { status: 500, message: 'Image not found, cannot restart application.' }; + } + + const volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ + buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [applicationId]: { + image, + container_name: applicationId, + volumes, + environment: envs, + labels, + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network) + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + try { + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }); + await executeCommand({ dockerId, command: `docker rm ${id}` }); + } catch (error) { + // + } + + await executeCommand({ + dockerId, + command: `docker compose --project-directory ${workdir} up -d` + }); + return reply.code(201).send(); + } + throw { status: 500, message: 'Application cannot be restarted.' }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function stopApplication(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { teamId } = request.user - const application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - const { id: dockerId } = application.destinationDocker; - if (application.buildPack === 'compose') { - const { stdout: containers } = await executeCommand({ - dockerId: application.destinationDocker.id, - command: - `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` - }); - const containersArray = containers.trim().split('\n'); - if (containersArray.length > 0 && containersArray[0] !== '') { - for (const container of containersArray) { - const containerObj = JSON.parse(container); - await removeContainer({ id: containerObj.ID, dockerId: application.destinationDocker.id }); - } - } - return - } - const { found } = await checkContainer({ dockerId, container: id }); - if (found) { - await removeContainer({ id, dockerId: application.destinationDocker.id }); - } - } - return reply.code(201).send(); - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { teamId } = request.user; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const { id: dockerId } = application.destinationDocker; + if (application.buildPack === 'compose') { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` + }); + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + for (const container of containersArray) { + const containerObj = JSON.parse(container); + await removeContainer({ + id: containerObj.ID, + dockerId: application.destinationDocker.id + }); + } + } + return; + } + const { found } = await checkContainer({ dockerId, container: id }); + if (found) { + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } + } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function deleteApplication(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { force } = request.body +export async function deleteApplication( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + const { force } = request.body; - const { teamId } = request.user - const application = await prisma.application.findUnique({ - where: { id }, - include: { destinationDocker: true } - }); - if (!force && application?.destinationDockerId && application.destinationDocker?.network) { - const { stdout: containers } = await executeCommand({ - dockerId: application.destinationDocker.id, - command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` - }) - if (containers) { - const containersArray = containers.trim().split('\n'); - for (const container of containersArray) { - const containerObj = JSON.parse(container); - const id = containerObj.ID; - await removeContainer({ id, dockerId: application.destinationDocker.id }); - } - } - } - await prisma.applicationSettings.deleteMany({ where: { application: { id } } }); - await prisma.buildLog.deleteMany({ where: { applicationId: id } }); - await prisma.build.deleteMany({ where: { applicationId: id } }); - await prisma.secret.deleteMany({ where: { applicationId: id } }); - await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); - await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } }); - if (teamId === '0') { - await prisma.application.deleteMany({ where: { id } }); - } else { - await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const { teamId } = request.user; + const application = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + if (!force && application?.destinationDockerId && application.destinationDocker?.network) { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` + }); + if (containers) { + const containersArray = containers.trim().split('\n'); + for (const container of containersArray) { + const containerObj = JSON.parse(container); + const id = containerObj.ID; + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } + } + } + await prisma.applicationSettings.deleteMany({ where: { application: { id } } }); + await prisma.buildLog.deleteMany({ where: { applicationId: id } }); + await prisma.build.deleteMany({ where: { applicationId: id } }); + await prisma.secret.deleteMany({ where: { applicationId: id } }); + await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); + await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } }); + if (teamId === '0') { + await prisma.application.deleteMany({ where: { id } }); + } else { + await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); + } + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function checkDomain(request: FastifyRequest) { - try { - const { id } = request.params - const { domain } = request.query - const { fqdn, settings: { dualCerts } } = await prisma.application.findUnique({ where: { id }, include: { settings: true } }) - return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { domain } = request.query; + const { + fqdn, + settings: { dualCerts } + } = await prisma.application.findUnique({ where: { id }, include: { settings: true } }); + return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function checkDNS(request: FastifyRequest) { - try { - const { id } = request.params - let { exposePort, fqdn, forceSave, dualCerts } = request.body - if (!fqdn) { - return {} - } else { - fqdn = fqdn.toLowerCase(); - } - if (exposePort) exposePort = Number(exposePort); + try { + const { id } = request.params; + let { exposePort, fqdn, forceSave, dualCerts } = request.body; + if (!fqdn) { + return {}; + } else { + fqdn = fqdn.toLowerCase(); + } + if (exposePort) exposePort = Number(exposePort); - const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }) - const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); + const { + destinationDocker: { engine, remoteIpAddress, remoteEngine }, + exposePort: configuredPort + } = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + const { isDNSCheckEnabled } = await prisma.setting.findFirst({}); - const found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); - if (found) { - throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` } - } - if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }) - if (isDNSCheckEnabled && !isDev && !forceSave) { - let hostname = request.hostname.split(':')[0]; - if (remoteEngine) hostname = remoteIpAddress; - return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const found = await isDomainConfigured({ id, fqdn, remoteIpAddress }); + if (found) { + throw { + status: 500, + message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` + }; + } + if (exposePort) + await checkExposedPort({ + id, + configuredPort, + exposePort, + engine, + remoteEngine, + remoteIpAddress + }); + if (isDNSCheckEnabled && !isDev && !forceSave) { + let hostname = request.hostname.split(':')[0]; + if (remoteEngine) hostname = remoteIpAddress; + return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); + } + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getUsage(request) { - try { - const { id } = request.params - const teamId = request.user?.teamId; - let usage = {}; + try { + const { id } = request.params; + const teamId = request.user?.teamId; + let usage = {}; - const application: any = await getApplicationFromDB(id, teamId); - if (application.destinationDockerId) { - [usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]); - } - return { - usage - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const application: any = await getApplicationFromDB(id, teamId); + if (application.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]); + } + return { + usage + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getDockerImages(request) { - try { - const { id } = request.params - const teamId = request.user?.teamId; - const application: any = await getApplicationFromDB(id, teamId); - let imagesAvailables = []; - try { - const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}' | grep -i ${id} | grep -v cache`, shell: true }); - const { stdout: runningImage } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}` }); - const images = stdout.trim().split('\n'); + try { + const { id } = request.params; + const teamId = request.user?.teamId; + const application: any = await getApplicationFromDB(id, teamId); + let imagesAvailables = []; + try { + const { stdout } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}' | grep -i ${id} | grep -v cache`, + shell: true + }); + const { stdout: runningImage } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}` + }); + const images = stdout.trim().split('\n'); - for (const image of images) { - const [repository, tag, createdAt] = image.split('#'); - if (tag.includes('-')) { - continue; - } - const [year, time] = createdAt.split(' '); - imagesAvailables.push({ - repository, - tag, - createdAt: day(year + time).unix() - }) - } + for (const image of images) { + const [repository, tag, createdAt] = image.split('#'); + if (tag.includes('-')) { + continue; + } + const [year, time] = createdAt.split(' '); + imagesAvailables.push({ + repository, + tag, + createdAt: day(year + time).unix() + }); + } - imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag); + imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag); - return { - imagesAvailables, - runningImage - } - } catch (error) { - return { - imagesAvailables, - } - } - - } catch ({ status, message }) { - - return errorHandler({ status, message }) - } + return { + imagesAvailables, + runningImage + }; + } catch (error) { + return { + imagesAvailables + }; + } + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getUsageByContainer(request) { - try { - const { id, containerId } = request.params - const teamId = request.user?.teamId; - let usage = {}; + try { + const { id, containerId } = request.params; + const teamId = request.user?.teamId; + let usage = {}; - const application: any = await getApplicationFromDB(id, teamId); - if (application.destinationDockerId) { - [usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, containerId)]); - } - return { - usage - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const application: any = await getApplicationFromDB(id, teamId); + if (application.destinationDockerId) { + [usage] = await Promise.all([ + getContainerUsage(application.destinationDocker.id, containerId) + ]); + } + return { + usage + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function deployApplication(request: FastifyRequest) { - try { - const { id } = request.params - const teamId = request.user?.teamId; - const { pullmergeRequestId = null, branch, forceRebuild } = request.body - const buildId = cuid(); - const application = await getApplicationFromDB(id, teamId); - if (application) { - if (!application?.configHash) { - const configHash = crypto.createHash('sha256') - .update( - JSON.stringify({ - buildPack: application.buildPack, - port: application.port, - exposePort: application.exposePort, - installCommand: application.installCommand, - buildCommand: application.buildCommand, - startCommand: application.startCommand - }) - ) - .digest('hex'); - await prisma.application.update({ where: { id }, data: { configHash } }); - } - await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } }); - if (application.gitSourceId) { - await prisma.build.create({ - data: { - id: buildId, - applicationId: id, - sourceBranch: branch, - branch: application.branch, - pullmergeRequestId: pullmergeRequestId?.toString(), - forceRebuild, - destinationDockerId: application.destinationDocker?.id, - gitSourceId: application.gitSource?.id, - githubAppId: application.gitSource?.githubApp?.id, - gitlabAppId: application.gitSource?.gitlabApp?.id, - status: 'queued', - type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual' - } - }); - } else { - await prisma.build.create({ - data: { - id: buildId, - applicationId: id, - branch: 'latest', - forceRebuild, - destinationDockerId: application.destinationDocker?.id, - status: 'queued', - type: 'manual' - } - }); - } + try { + const { id } = request.params; + const teamId = request.user?.teamId; + const { pullmergeRequestId = null, branch, forceRebuild } = request.body; + const buildId = cuid(); + const application = await getApplicationFromDB(id, teamId); + if (application) { + if (!application?.configHash) { + const configHash = crypto + .createHash('sha256') + .update( + JSON.stringify({ + buildPack: application.buildPack, + port: application.port, + exposePort: application.exposePort, + installCommand: application.installCommand, + buildCommand: application.buildCommand, + startCommand: application.startCommand + }) + ) + .digest('hex'); + await prisma.application.update({ where: { id }, data: { configHash } }); + } + await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } }); + if (application.gitSourceId) { + await prisma.build.create({ + data: { + id: buildId, + applicationId: id, + sourceBranch: branch, + branch: application.branch, + pullmergeRequestId: pullmergeRequestId?.toString(), + forceRebuild, + destinationDockerId: application.destinationDocker?.id, + gitSourceId: application.gitSource?.id, + githubAppId: application.gitSource?.githubApp?.id, + gitlabAppId: application.gitSource?.gitlabApp?.id, + status: 'queued', + type: pullmergeRequestId + ? application.gitSource?.githubApp?.id + ? 'manual_pr' + : 'manual_mr' + : 'manual' + } + }); + } else { + await prisma.build.create({ + data: { + id: buildId, + applicationId: id, + branch: 'latest', + forceRebuild, + destinationDockerId: application.destinationDocker?.id, + status: 'queued', + type: 'manual' + } + }); + } - return { - buildId - }; - } - throw { status: 500, message: 'Application not found!' } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + return { + buildId + }; + } + throw { status: 500, message: 'Application not found!' }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } +export async function saveApplicationSource( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + const { gitSourceId, forPublic, type, simpleDockerfile } = request.body; + if (forPublic) { + const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); + await prisma.application.update({ + where: { id }, + data: { gitSource: { connect: { id: publicGit.id } } } + }); + } + if (simpleDockerfile) { + await prisma.application.update({ + where: { id }, + data: { simpleDockerfile, settings: { update: { autodeploy: false } } } + }); + } + if (gitSourceId) { + await prisma.application.update({ + where: { id }, + data: { gitSource: { connect: { id: gitSourceId } } } + }); + } -export async function saveApplicationSource(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { gitSourceId, forPublic, type, simpleDockerfile } = request.body - if (forPublic) { - const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); - await prisma.application.update({ - where: { id }, - data: { gitSource: { connect: { id: publicGit.id } } } - }); - } - if (simpleDockerfile) { - await prisma.application.update({ - where: { id }, - data: { simpleDockerfile, settings: { update: { autodeploy: false } } } - }); - } - if (gitSourceId) { - await prisma.application.update({ - where: { id }, - data: { gitSource: { connect: { id: gitSourceId } } } - }); - } - - - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getGitHubToken(request: FastifyRequest, reply: FastifyReply) { - try { - const { default: got } = await import('got') - const { id } = request.params - const { teamId } = request.user - const application: any = await getApplicationFromDB(id, teamId); - const payload = { - iat: Math.round(new Date().getTime() / 1000), - exp: Math.round(new Date().getTime() / 1000 + 60), - iss: application.gitSource.githubApp.appId - }; - const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, { - algorithm: 'RS256' - }); - const { token } = await got.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, { - headers: { - 'Authorization': `Bearer ${githubToken}`, - } - }).json() - return reply.code(201).send({ - token - }) - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { default: got } = await import('got'); + const { id } = request.params; + const { teamId } = request.user; + const application: any = await getApplicationFromDB(id, teamId); + const payload = { + iat: Math.round(new Date().getTime() / 1000), + exp: Math.round(new Date().getTime() / 1000 + 60), + iss: application.gitSource.githubApp.appId + }; + const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, { + algorithm: 'RS256' + }); + const { token } = await got + .post( + `${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, + { + headers: { + Authorization: `Bearer ${githubToken}` + } + } + ) + .json(); + return reply.code(201).send({ + token + }); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function checkRepository(request: FastifyRequest) { - try { - const { id } = request.params - const { repository, branch } = request.query - const application = await prisma.application.findUnique({ - where: { id }, - include: { gitSource: true } - }); - const found = await prisma.application.findFirst({ - where: { branch, repository, gitSource: { type: application.gitSource.type }, id: { not: id } } - }); - return { - used: found ? true : false - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { repository, branch } = request.query; + const application = await prisma.application.findUnique({ + where: { id }, + include: { gitSource: true } + }); + const found = await prisma.application.findFirst({ + where: { + branch, + repository, + gitSource: { type: application.gitSource.type }, + id: { not: id } + } + }); + return { + used: found ? true : false + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveRepository(request, reply) { - try { - const { id } = request.params - let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body + try { + const { id } = request.params; + let { + repository, + branch, + projectId, + autodeploy, + webhookToken, + isPublicRepository = false + } = request.body; - repository = repository.toLowerCase(); + repository = repository.toLowerCase(); - projectId = Number(projectId); - if (webhookToken) { - await prisma.application.update({ - where: { id }, - data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy, isPublicRepository } } } - }); - } else { - await prisma.application.update({ - where: { id }, - data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } } - }); - } - // if (!isPublicRepository) { - // const isDouble = await checkDoubleBranch(branch, projectId); - // if (isDouble) { - // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) - // } - // } - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + projectId = Number(projectId); + if (webhookToken) { + await prisma.application.update({ + where: { id }, + data: { + repository, + branch, + projectId, + gitSource: { + update: { + gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } + } + }, + settings: { update: { autodeploy, isPublicRepository } } + } + }); + } else { + await prisma.application.update({ + where: { id }, + data: { + repository, + branch, + projectId, + settings: { update: { autodeploy, isPublicRepository } } + } + }); + } + // if (!isPublicRepository) { + // const isDouble = await checkDoubleBranch(branch, projectId); + // if (isDouble) { + // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) + // } + // } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function saveDestination(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { destinationId } = request.body - await prisma.application.update({ - where: { id }, - data: { destinationDocker: { connect: { id: destinationId } } } - }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function saveDestination( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + const { destinationId } = request.body; + await prisma.application.update({ + where: { id }, + data: { destinationDocker: { connect: { id: destinationId } } } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getBuildPack(request) { - try { - const { id } = request.params - const teamId = request.user?.teamId; - const application: any = await getApplicationFromDB(id, teamId); - return { - type: application.gitSource?.type || 'dockerRegistry', - projectId: application.projectId, - repository: application.repository, - branch: application.branch, - apiUrl: application.gitSource?.apiUrl || null, - isPublicRepository: application.settings.isPublicRepository - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const teamId = request.user?.teamId; + const application: any = await getApplicationFromDB(id, teamId); + return { + type: application.gitSource?.type || 'dockerRegistry', + projectId: application.projectId, + repository: application.repository, + branch: application.branch, + apiUrl: application.gitSource?.apiUrl || null, + isPublicRepository: application.settings.isPublicRepository + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveRegistry(request, reply) { - try { - const { id } = request.params - const { registryId } = request.body - await prisma.application.update({ where: { id }, data: { dockerRegistry: { connect: { id: registryId } } } }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { registryId } = request.body; + await prisma.application.update({ + where: { id }, + data: { dockerRegistry: { connect: { id: registryId } } } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveBuildPack(request, reply) { - try { - const { id } = request.params - const { buildPack } = request.body - const { baseImage, baseBuildImage } = setDefaultBaseImage( - buildPack - ); - await prisma.application.update({ where: { id }, data: { buildPack, baseImage, baseBuildImage } }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { buildPack } = request.body; + const { baseImage, baseBuildImage } = setDefaultBaseImage(buildPack); + await prisma.application.update({ + where: { id }, + data: { buildPack, baseImage, baseBuildImage } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveConnectedDatabase(request, reply) { - try { - const { id } = request.params - const { databaseId, type } = request.body - await prisma.application.update({ where: { id }, data: { connectedDatabase: { upsert: { create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } } } } }) - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { databaseId, type } = request.body; + await prisma.application.update({ + where: { id }, + data: { + connectedDatabase: { + upsert: { + create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, + update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } + } + } + } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getSecrets(request: FastifyRequest) { - try { - const { id } = request.params + try { + const { id } = request.params; - let secrets = await prisma.secret.findMany({ - where: { applicationId: id, isPRMRSecret: false }, - orderBy: { createdAt: 'asc' } - }); - let previewSecrets = await prisma.secret.findMany({ - where: { applicationId: id, isPRMRSecret: true }, - orderBy: { createdAt: 'asc' } - }); + let secrets = await prisma.secret.findMany({ + where: { applicationId: id, isPRMRSecret: false }, + orderBy: { createdAt: 'asc' } + }); + let previewSecrets = await prisma.secret.findMany({ + where: { applicationId: id, isPRMRSecret: true }, + orderBy: { createdAt: 'asc' } + }); - secrets = secrets.map((secret) => { - secret.value = decrypt(secret.value); - return secret; - }); - previewSecrets = previewSecrets.map((secret) => { - secret.value = decrypt(secret.value); - return secret; - }); + secrets = secrets.map((secret) => { + secret.value = decrypt(secret.value); + return secret; + }); + previewSecrets = previewSecrets.map((secret) => { + secret.value = decrypt(secret.value); + return secret; + }); - return { - previewSecrets: previewSecrets.sort((a, b) => { - return ('' + a.name).localeCompare(b.name); - }), - secrets: secrets.sort((a, b) => { - return ('' + a.name).localeCompare(b.name); - }) - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + return { + previewSecrets: previewSecrets.sort((a, b) => { + return ('' + a.name).localeCompare(b.name); + }), + secrets: secrets.sort((a, b) => { + return ('' + a.name).localeCompare(b.name); + }) + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function updatePreviewSecret(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - let { name, value } = request.body - if (value) { - value = encrypt(value.trim()) - } else { - value = '' - } - await prisma.secret.updateMany({ - where: { applicationId: id, name, isPRMRSecret: true }, - data: { value } - }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function updatePreviewSecret( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id } = request.params; + let { name, value } = request.body; + if (value) { + value = encrypt(value.trim()); + } else { + value = ''; + } + await prisma.secret.updateMany({ + where: { applicationId: id, name, isPRMRSecret: true }, + data: { value } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function updateSecret(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { name, value, isBuildSecret = undefined } = request.body - await prisma.secret.updateMany({ - where: { applicationId: id, name }, - data: { value: encrypt(value.trim()), isBuildSecret } - }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { name, value, isBuildSecret = undefined } = request.body; + await prisma.secret.updateMany({ + where: { applicationId: id, name }, + data: { value: encrypt(value.trim()), isBuildSecret } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveSecret(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { name, value, isBuildSecret = false } = request.body - const found = await prisma.secret.findMany({ where: { applicationId: id, name } }) - if (found.length > 0) { - throw ({ message: 'Secret already exists.' }) - } - await prisma.secret.create({ - data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } } - }); - await prisma.secret.create({ - data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: true, application: { connect: { id } } } - }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { name, value, isBuildSecret = false } = request.body; + const found = await prisma.secret.findMany({ where: { applicationId: id, name } }); + if (found.length > 0) { + throw { message: 'Secret already exists.' }; + } + await prisma.secret.create({ + data: { + name, + value: encrypt(value.trim()), + isBuildSecret, + isPRMRSecret: false, + application: { connect: { id } } + } + }); + await prisma.secret.create({ + data: { + name, + value: encrypt(value.trim()), + isBuildSecret, + isPRMRSecret: true, + application: { connect: { id } } + } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function deleteSecret(request: FastifyRequest) { - try { - const { id } = request.params - const { name } = request.body - await prisma.secret.deleteMany({ where: { applicationId: id, name } }); - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { name } = request.body; + await prisma.secret.deleteMany({ where: { applicationId: id, name } }); + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getStorages(request: FastifyRequest) { - try { - const { id } = request.params - const persistentStorages = await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } }); - return { - persistentStorages - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const persistentStorages = await prisma.applicationPersistentStorage.findMany({ + where: { applicationId: id } + }); + return { + persistentStorages + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveStorage(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const { path, newStorage, storageId } = request.body + try { + const { id } = request.params; + const { path, newStorage, storageId } = request.body; - if (newStorage) { - await prisma.applicationPersistentStorage.create({ - data: { path, application: { connect: { id } } } - }); - } else { - await prisma.applicationPersistentStorage.update({ - where: { id: storageId }, - data: { path } - }); - } - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + if (newStorage) { + await prisma.applicationPersistentStorage.create({ + data: { path, application: { connect: { id } } } + }); + } else { + await prisma.applicationPersistentStorage.update({ + where: { id: storageId }, + data: { path } + }); + } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function deleteStorage(request: FastifyRequest) { - try { - const { id } = request.params - const { path } = request.body - await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } }); - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const { path } = request.body; + await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } }); + return {}; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function restartPreview(request: FastifyRequest, reply: FastifyReply) { - try { - const { id, pullmergeRequestId } = request.params - const { teamId } = request.user - let application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - const buildId = cuid(); - const { id: dockerId, network } = application.destinationDocker; - const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; +export async function restartPreview( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { id, pullmergeRequestId } = request.params; + const { teamId } = request.user; + let application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const buildId = cuid(); + const { id: dockerId, network } = application.destinationDocker; + const { + secrets, + port, + repository, + persistentStorage, + id: applicationId, + buildPack, + exposePort + } = application; - const envs = [ - `PORT=${port}` - ]; - if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); - } else { - envs.push(`${secret.name}=${secret.value}`); - } - } else { - if (!secret.isPRMRSecret) { - envs.push(`${secret.name}=${secret.value}`); - } - } - }); - } - const { workdir } = await createDirectories({ repository, buildId }); - const labels = [] - let image = null - const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` }) - const containersArray = container.trim().split('\n'); - for (const container of containersArray) { - const containerObj = formatLabelsOnDocker(container); - image = containerObj[0].Image - Object.keys(containerObj[0].Labels).forEach(function (key) { - if (key.startsWith('coolify')) { - labels.push(`${key}=${containerObj[0].Labels[key]}`) - } - }) - } - let imageFound = false; - try { - await executeCommand({ - dockerId, - command: `docker image inspect ${image}` - }) - imageFound = true; - } catch (error) { - // - } - if (!imageFound) { - throw { status: 500, message: 'Image not found, cannot restart application.' } - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + let envs = []; + if (secrets.length > 0) { + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; + } + const { workdir } = await createDirectories({ repository, buildId }); + const labels = []; + let image = null; + const { stdout: container } = await executeCommand({ + dockerId, + command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` + }); + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image; + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`); + } + }); + } + let imageFound = false; + try { + await executeCommand({ + dockerId, + command: `docker image inspect ${image}` + }); + imageFound = true; + } catch (error) { + // + } + if (!imageFound) { + throw { status: 500, message: 'Image not found, cannot restart application.' }; + } - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - const volumes = - persistentStorage?.map((storage) => { - return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' - }${storage.path}`; - }) || []; - const composeVolumes = volumes.map((volume) => { - return { - [`${volume.split(':')[0]}`]: { - name: volume.split(':')[0] - } - }; - }); - const composeFile = { - version: '3.8', - services: { - [`${applicationId}-${pullmergeRequestId}`]: { - image, - container_name: `${applicationId}-${pullmergeRequestId}`, - volumes, - env_file: envFound ? [`${workdir}/.env`] : [], - labels, - depends_on: [], - expose: [port], - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: Object.assign({}, ...composeVolumes) - }; - await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeCommand({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` }) - await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` }) - await executeCommand({ dockerId, command: `docker compose --project-directory ${workdir} up -d` }) - return reply.code(201).send(); - } - throw { status: 500, message: 'Application cannot be restarted.' } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ + buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [`${applicationId}-${pullmergeRequestId}`]: { + image, + container_name: `${applicationId}-${pullmergeRequestId}`, + volumes, + environment: envs, + labels, + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network) + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` }); + await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` }); + await executeCommand({ + dockerId, + command: `docker compose --project-directory ${workdir} up -d` + }); + return reply.code(201).send(); + } + throw { status: 500, message: 'Application cannot be restarted.' }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getPreviewStatus(request: FastifyRequest) { - try { - const { id, pullmergeRequestId } = request.params - const { teamId } = request.user - let isRunning = false; - let isExited = false; - let isRestarting = false; - let isBuilding = false - const application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` }); - if (status?.found) { - isRunning = status.status.isRunning; - isExited = status.status.isExited; - isRestarting = status.status.isRestarting - } - const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } }) - isBuilding = building.length > 0 - } - return { - isBuilding, - isRunning, - isRestarting, - isExited, - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id, pullmergeRequestId } = request.params; + const { teamId } = request.user; + let isRunning = false; + let isExited = false; + let isRestarting = false; + let isBuilding = false; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const status = await checkContainer({ + dockerId: application.destinationDocker.id, + container: `${id}-${pullmergeRequestId}` + }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting; + } + const building = await prisma.build.findMany({ + where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } + }); + isBuilding = building.length > 0; + } + return { + isBuilding, + isRunning, + isRestarting, + isExited + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function loadPreviews(request: FastifyRequest) { - try { - const { id } = request.params - const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); - const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) - if (stdout === '') { - throw { status: 500, message: 'No previews found.' } - } - const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application') + try { + const { id } = request.params; + const application = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + const { stdout } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` + }); + if (stdout === '') { + throw { status: 500, message: 'No previews found.' }; + } + const containers = formatLabelsOnDocker(stdout).filter( + (container) => + container.Labels['coolify.configuration'] && + container.Labels['coolify.type'] === 'standalone-application' + ); - const jsonContainers = containers - .map((container) => - JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) - ) - .filter((container) => { - return container.pullmergeRequestId && container.applicationId === id; - }); - for (const container of jsonContainers) { - const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } }) - if (found.length === 0) { - await prisma.previewApplication.create({ - data: { - pullmergeRequestId: container.pullmergeRequestId, - sourceBranch: container.branch, - customDomain: container.fqdn, - application: { connect: { id: container.applicationId } } - } - }) - } - } - return { - previews: await prisma.previewApplication.findMany({ where: { applicationId: id } }) - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const jsonContainers = containers + .map((container) => + JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) + ) + .filter((container) => { + return container.pullmergeRequestId && container.applicationId === id; + }); + for (const container of jsonContainers) { + const found = await prisma.previewApplication.findMany({ + where: { + applicationId: container.applicationId, + pullmergeRequestId: container.pullmergeRequestId + } + }); + if (found.length === 0) { + await prisma.previewApplication.create({ + data: { + pullmergeRequestId: container.pullmergeRequestId, + sourceBranch: container.branch, + customDomain: container.fqdn, + application: { connect: { id: container.applicationId } } + } + }); + } + } + return { + previews: await prisma.previewApplication.findMany({ where: { applicationId: id } }) + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getPreviews(request: FastifyRequest) { - try { - const { id } = request.params - const { teamId } = request.user - let secrets = await prisma.secret.findMany({ - where: { applicationId: id }, - orderBy: { createdAt: 'desc' } - }); - secrets = secrets.map((secret) => { - secret.value = decrypt(secret.value); - return secret; - }); + try { + const { id } = request.params; + const { teamId } = request.user; + let secrets = await prisma.secret.findMany({ + where: { applicationId: id }, + orderBy: { createdAt: 'desc' } + }); + secrets = secrets.map((secret) => { + secret.value = decrypt(secret.value); + return secret; + }); - const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret); - const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret); - return { - applicationSecrets: applicationSecrets.sort((a, b) => { - return ('' + a.name).localeCompare(b.name); - }), - PRMRSecrets: PRMRSecrets.sort((a, b) => { - return ('' + a.name).localeCompare(b.name); - }) - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret); + const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret); + return { + applicationSecrets: applicationSecrets.sort((a, b) => { + return ('' + a.name).localeCompare(b.name); + }), + PRMRSecrets: PRMRSecrets.sort((a, b) => { + return ('' + a.name).localeCompare(b.name); + }) + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getApplicationLogs(request: FastifyRequest) { - try { - const { id, containerId } = request.params; - let { since = 0 } = request.query - if (since !== 0) { - since = day(since).unix(); - } - const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.application.findUnique({ - where: { id }, - include: { destinationDocker: true } - }); - if (destinationDockerId) { - try { - const { default: ansi } = await import('strip-ansi') - const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` }) - const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); - const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); - const logs = stripLogsStderr.concat(stripLogsStdout) - const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1)) - return { logs: sortedLogs } - // } - } catch (error) { - const { statusCode, stderr } = error; - if (stderr.startsWith('Error: No such container')) { - return { logs: [], noContainer: true } - } - if (statusCode === 404) { - return { - logs: [] - }; - } - } - } - return { - message: 'No logs found.' - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id, containerId } = request.params; + let { since = 0 } = request.query; + if (since !== 0) { + since = day(since).unix(); + } + const { + destinationDockerId, + destinationDocker: { id: dockerId } + } = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + if (destinationDockerId) { + try { + const { default: ansi } = await import('strip-ansi'); + const { stdout, stderr } = await executeCommand({ + dockerId, + command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` + }); + const stripLogsStdout = stdout + .toString() + .split('\n') + .map((l) => ansi(l)) + .filter((a) => a); + const stripLogsStderr = stderr + .toString() + .split('\n') + .map((l) => ansi(l)) + .filter((a) => a); + const logs = stripLogsStderr.concat(stripLogsStdout); + const sortedLogs = logs.sort((a, b) => + day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1 + ); + return { logs: sortedLogs }; + // } + } catch (error) { + const { statusCode, stderr } = error; + if (stderr.startsWith('Error: No such container')) { + return { logs: [], noContainer: true }; + } + if (statusCode === 404) { + return { + logs: [] + }; + } + } + } + return { + message: 'No logs found.' + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getBuilds(request: FastifyRequest) { - try { - const { id } = request.params - let { buildId, skip = 0 } = request.query - if (typeof skip !== 'number') { - skip = Number(skip) - } + try { + const { id } = request.params; + let { buildId, skip = 0 } = request.query; + if (typeof skip !== 'number') { + skip = Number(skip); + } - let builds = []; + let builds = []; - const buildCount = await prisma.build.count({ where: { applicationId: id } }); - if (buildId) { - builds = await prisma.build.findMany({ where: { applicationId: id, id: buildId } }); - } else { - builds = await prisma.build.findMany({ - where: { applicationId: id }, - orderBy: { createdAt: 'desc' }, - take: 5 + skip - }); - } - builds = builds.map((build) => { - if (build.status === 'running') { - build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0); - } - return build - }) - return { - builds, - buildCount - }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + const buildCount = await prisma.build.count({ where: { applicationId: id } }); + if (buildId) { + builds = await prisma.build.findMany({ where: { applicationId: id, id: buildId } }); + } else { + builds = await prisma.build.findMany({ + where: { applicationId: id }, + orderBy: { createdAt: 'desc' }, + take: 5 + skip + }); + } + builds = builds.map((build) => { + if (build.status === 'running') { + build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0); + } + return build; + }); + return { + builds, + buildCount + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getBuildIdLogs(request: FastifyRequest) { - try { - // TODO: Fluentbit could still hold the logs, so we need to check if the logs are done - const { buildId, id } = request.params - let { sequence = 0 } = request.query - if (typeof sequence !== 'number') { - sequence = Number(sequence) - } - let file = `/app/logs/${id}_buildlog_${buildId}.csv` - if (isDev) { - file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv` - } - const data = await prisma.build.findFirst({ where: { id: buildId } }); - const createdAt = day(data.createdAt).utc(); - try { - await fs.stat(file) - } catch (error) { - let logs = await prisma.buildLog.findMany({ - where: { buildId, time: { gt: sequence } }, - orderBy: { time: 'asc' } - }); - const data = await prisma.build.findFirst({ where: { id: buildId } }); - const createdAt = day(data.createdAt).utc(); - return { - logs: logs.map(log => { - log.time = Number(log.time) - return log - }), - fromDb: true, - took: day().diff(createdAt) / 1000, - status: data?.status || 'queued' - } - } - let fileLogs = (await fs.readFile(file)).toString() - let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs) - let logs = decryptedLogs.map(log => { - const parsed = { - time: log['field1'], - line: decrypt(log['field2'] + '","' + log['field3']) - } - return parsed - }).filter(log => log.time > sequence) - return { - logs, - fromDb: false, - took: day().diff(createdAt) / 1000, - status: data?.status || 'queued' - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + // TODO: Fluentbit could still hold the logs, so we need to check if the logs are done + const { buildId, id } = request.params; + let { sequence = 0 } = request.query; + if (typeof sequence !== 'number') { + sequence = Number(sequence); + } + let file = `/app/logs/${id}_buildlog_${buildId}.csv`; + if (isDev) { + file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`; + } + const data = await prisma.build.findFirst({ where: { id: buildId } }); + const createdAt = day(data.createdAt).utc(); + try { + await fs.stat(file); + } catch (error) { + let logs = await prisma.buildLog.findMany({ + where: { buildId, time: { gt: sequence } }, + orderBy: { time: 'asc' } + }); + const data = await prisma.build.findFirst({ where: { id: buildId } }); + const createdAt = day(data.createdAt).utc(); + return { + logs: logs.map((log) => { + log.time = Number(log.time); + return log; + }), + fromDb: true, + took: day().diff(createdAt) / 1000, + status: data?.status || 'queued' + }; + } + let fileLogs = (await fs.readFile(file)).toString(); + let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs); + let logs = decryptedLogs + .map((log) => { + const parsed = { + time: log['field1'], + line: decrypt(log['field2'] + '","' + log['field3']) + }; + return parsed; + }) + .filter((log) => log.time > sequence); + return { + logs, + fromDb: false, + took: day().diff(createdAt) / 1000, + status: data?.status || 'queued' + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getGitLabSSHKey(request: FastifyRequest) { - try { - const { id } = request.params - const application = await prisma.application.findUnique({ - where: { id }, - include: { gitSource: { include: { gitlabApp: true } } } - }); - return { publicKey: application.gitSource.gitlabApp.publicSshKey }; - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const application = await prisma.application.findUnique({ + where: { id }, + include: { gitSource: { include: { gitlabApp: true } } } + }); + return { publicKey: application.gitSource.gitlabApp.publicSshKey }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveGitLabSSHKey(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - const application = await prisma.application.findUnique({ - where: { id }, - include: { gitSource: { include: { gitlabApp: true } } } - }); - if (!application.gitSource?.gitlabApp?.privateSshKey) { - const keys = await generateSshKeyPair(); - const encryptedPrivateKey = encrypt(keys.privateKey); - await prisma.gitlabApp.update({ - where: { id: application.gitSource.gitlabApp.id }, - data: { privateSshKey: encryptedPrivateKey, publicSshKey: keys.publicKey } - }); - return reply.code(201).send({ publicKey: keys.publicKey }) - } - return { message: 'SSH key already exists' } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + try { + const { id } = request.params; + const application = await prisma.application.findUnique({ + where: { id }, + include: { gitSource: { include: { gitlabApp: true } } } + }); + if (!application.gitSource?.gitlabApp?.privateSshKey) { + const keys = await generateSshKeyPair(); + const encryptedPrivateKey = encrypt(keys.privateKey); + await prisma.gitlabApp.update({ + where: { id: application.gitSource.gitlabApp.id }, + data: { privateSshKey: encryptedPrivateKey, publicSshKey: keys.publicKey } + }); + return reply.code(201).send({ publicKey: keys.publicKey }); + } + return { message: 'SSH key already exists' }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function saveDeployKey(request: FastifyRequest, reply: FastifyReply) { - try { - const { id } = request.params - let { deployKeyId } = request.body; + try { + const { id } = request.params; + let { deployKeyId } = request.body; - deployKeyId = Number(deployKeyId); - const application = await prisma.application.findUnique({ - where: { id }, - include: { gitSource: { include: { gitlabApp: true } } } - }); - await prisma.gitlabApp.update({ - where: { id: application.gitSource.gitlabApp.id }, - data: { deployKeyId } - }); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + deployKeyId = Number(deployKeyId); + const application = await prisma.application.findUnique({ + where: { id }, + include: { gitSource: { include: { gitlabApp: true } } } + }); + await prisma.gitlabApp.update({ + where: { id: application.gitSource.gitlabApp.id }, + data: { deployKeyId } + }); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } -export async function cancelDeployment(request: FastifyRequest, reply: FastifyReply) { - try { - const { buildId, applicationId } = request.body; - if (!buildId) { - throw { status: 500, message: 'buildId is required' } - - } - await stopBuild(buildId, applicationId); - return reply.code(201).send() - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function cancelDeployment( + request: FastifyRequest, + reply: FastifyReply +) { + try { + const { buildId, applicationId } = request.body; + if (!buildId) { + throw { status: 500, message: 'buildId is required' }; + } + await stopBuild(buildId, applicationId); + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } - -export async function createdBranchDatabase(database: any, baseDatabaseBranch: string, pullmergeRequestId: string) { - try { - if (!baseDatabaseBranch) return - const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database; - if (destinationDockerId) { - if (type === 'postgresql') { - const decryptedRootUserPassword = decrypt(rootUserPassword); - await executeCommand({ - dockerId: destinationDockerId, - command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump` - }) - await executeCommand({ - dockerId: destinationDockerId, - command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"` - }) - await executeCommand({ - dockerId: destinationDockerId, - command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump` - }) - await executeCommand({ - dockerId: destinationDockerId, - command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"` - }) - } - } - - - } catch ({ status, message }) { - return errorHandler({ status, message }) - } +export async function createdBranchDatabase( + database: any, + baseDatabaseBranch: string, + pullmergeRequestId: string +) { + try { + if (!baseDatabaseBranch) return; + const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database; + if (destinationDockerId) { + if (type === 'postgresql') { + const decryptedRootUserPassword = decrypt(rootUserPassword); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump` + }); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"` + }); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump` + }); + await executeCommand({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"` + }); + } + } + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function removeBranchDatabase(database: any, pullmergeRequestId: string) { - try { - const { id, type, destinationDockerId, rootUser, rootUserPassword } = database; - if (destinationDockerId) { - if (type === 'postgresql') { - const decryptedRootUserPassword = decrypt(rootUserPassword); - // Terminate all connections to the database - await executeCommand({ - dockerId: destinationDockerId, - command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"` - }) + try { + const { id, type, destinationDockerId, rootUser, rootUserPassword } = database; + if (destinationDockerId) { + if (type === 'postgresql') { + const decryptedRootUserPassword = decrypt(rootUserPassword); + // Terminate all connections to the database + await executeCommand({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"` + }); - await executeCommand({ - dockerId: destinationDockerId, - command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"` - }) - } - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} \ No newline at end of file + await executeCommand({ + dockerId: destinationDockerId, + command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"` + }); + } + } + } catch ({ status, message }) { + return errorHandler({ status, message }); + } +} diff --git a/apps/api/src/routes/api/v1/base/index.ts b/apps/api/src/routes/api/v1/base/index.ts index 170a138f3..d631df3d8 100644 --- a/apps/api/src/routes/api/v1/base/index.ts +++ b/apps/api/src/routes/api/v1/base/index.ts @@ -1,31 +1,31 @@ import { FastifyPluginAsync } from 'fastify'; -import { errorHandler, listSettings, version } from '../../../../lib/common'; +import { errorHandler, isARM, listSettings, version } from '../../../../lib/common'; const root: FastifyPluginAsync = async (fastify): Promise => { - fastify.addHook('onRequest', async (request) => { - try { - await request.jwtVerify() - } catch(error) { - return - } - }); - fastify.get('/', async (request) => { - const teamId = request.user?.teamId; - const settings = await listSettings() - try { - return { - ipv4: teamId ? settings.ipv4 : null, - ipv6: teamId ? settings.ipv6 : null, - version, - whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true', - whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON, - isRegistrationEnabled: settings.isRegistrationEnabled, - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } - }); - + fastify.addHook('onRequest', async (request) => { + try { + await request.jwtVerify(); + } catch (error) { + return; + } + }); + fastify.get('/', async (request) => { + const teamId = request.user?.teamId; + const settings = await listSettings(); + try { + return { + ipv4: teamId ? settings.ipv4 : null, + ipv6: teamId ? settings.ipv6 : null, + version, + whiteLabeled: process.env.COOLIFY_WHITE_LABELED === 'true', + whiteLabeledIcon: process.env.COOLIFY_WHITE_LABELED_ICON, + isRegistrationEnabled: settings.isRegistrationEnabled, + isARM: isARM(process.arch) + }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } + }); }; export default root; diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 32333afc5..f090689b1 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -263,14 +263,15 @@ export async function parseAndFindServiceTemplates(service: any, workdir?: strin for (const secret of service.serviceSecret) { let { name, value } = secret name = name.toLowerCase() - const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}\"`, 'gi') - const regex = new RegExp(`\\$\\$secret_${name}\"`, 'gi') + const regexHashed = new RegExp(`\\$\\$hashed\\$\\$secret_${name}`, 'gi') + const regex = new RegExp(`\\$\\$secret_${name}`, 'gi') + console.log({value, name, regex, regexHashed}) if (value) { - strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10) + '"') - strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"") + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, bcrypt.hashSync(value.replaceAll("\"", "\\\""), 10)) + strParsedTemplate = strParsedTemplate.replaceAll(regex, value.replaceAll("\"", "\\\"")) } else { - strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '' + '"') - strParsedTemplate = strParsedTemplate.replaceAll(regex, '' + '"') + strParsedTemplate = strParsedTemplate.replaceAll(regexHashed, '') + strParsedTemplate = strParsedTemplate.replaceAll(regex, '') } } } diff --git a/apps/api/src/routes/api/v1/sources/handlers.ts b/apps/api/src/routes/api/v1/sources/handlers.ts index e33652571..61849526a 100644 --- a/apps/api/src/routes/api/v1/sources/handlers.ts +++ b/apps/api/src/routes/api/v1/sources/handlers.ts @@ -22,11 +22,11 @@ export async function listSources(request: FastifyRequest) { export async function saveSource(request, reply) { try { const { id } = request.params - let { name, htmlUrl, apiUrl, customPort, isSystemWide } = request.body + let { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } = request.body if (customPort) customPort = Number(customPort) await prisma.gitSource.update({ where: { id }, - data: { name, htmlUrl, apiUrl, customPort, isSystemWide } + data: { name, htmlUrl, apiUrl, customPort, customUser, isSystemWide } }); return reply.code(201).send() } catch ({ status, message }) { @@ -48,6 +48,7 @@ export async function getSource(request: FastifyRequest) { apiUrl: null, organization: null, customPort: 22, + customUser: 'git', }, settings } @@ -133,7 +134,7 @@ export async function saveGitLabSource(request: FastifyRequest try { const { id } = request.params const { teamId } = request.user - let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort } = + let { type, name, htmlUrl, apiUrl, oauthId, appId, appSecret, groupName, customPort, customUser } = request.body if (oauthId) oauthId = Number(oauthId); @@ -142,7 +143,7 @@ export async function saveGitLabSource(request: FastifyRequest if (id === 'new') { const newId = cuid() - await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, teams: { connect: { id: teamId } } } }); + await prisma.gitSource.create({ data: { id: newId, type, apiUrl, htmlUrl, name, customPort, customUser, teams: { connect: { id: teamId } } } }); await prisma.gitlabApp.create({ data: { teams: { connect: { id: teamId } }, @@ -158,7 +159,7 @@ export async function saveGitLabSource(request: FastifyRequest id: newId } } else { - await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort } }); + await prisma.gitSource.update({ where: { id }, data: { type, apiUrl, htmlUrl, name, customPort, customUser } }); await prisma.gitlabApp.update({ where: { id }, data: { diff --git a/apps/api/src/routes/api/v1/sources/types.ts b/apps/api/src/routes/api/v1/sources/types.ts index bb65c7d85..bbccd2e7b 100644 --- a/apps/api/src/routes/api/v1/sources/types.ts +++ b/apps/api/src/routes/api/v1/sources/types.ts @@ -21,6 +21,7 @@ export interface SaveGitLabSource extends OnlyId { appSecret: string, groupName: string, customPort: number, + customUser: string, } } export interface CheckGitLabOAuthId extends OnlyId { diff --git a/apps/api/tags.json b/apps/api/tags.json index 0d84f6caa..bc5273fc0 100644 --- a/apps/api/tags.json +++ b/apps/api/tags.json @@ -1 +1 @@ -[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.8","v1.8.6","v1.8.4","v1.8.2","v1.8.10","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24","v1.5.22"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.2","1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_8e5cff30d95963eaee2587488d351e0d658c8195","SHA_8cabe2817ce7ccaf2f0a9fdbb1b5d3411de87f81","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_255c30ed012fc4c39ffc97efc1d3b00425b17c72","SHA_17f92b16ef790003338f0926fc8d791a9a61333c"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.24.2","5.24.1","5.24.0","5.23.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0","5.17.2","5.17.1","5.17.0","5.16.2"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0","v1.0.8"]},{"name":"grafana","image":"grafana/grafana","tags":["9.3.1","9.3.0","9.2.7","9.2.6","9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.15.2","v2.15.1","v2.15.0","v2.14.1","v2.14.0","v2.13.2","v2.13.1","v2.13.0","v2.12.1","v2.12.0","v2.11.3"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.7","v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.30.0","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0","v0.10.0","0.14.1"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-11-29T23-40-49Z.fips","RELEASE.2022-11-26T22-43-32Z.fips","RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.hotfix.ce525fdaf","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips","RELEASE.2022-08-08T18-34-09Z.fips","RELEASE.2022-08-05T23-27-09Z.fips","RELEASE.2022-08-02T23-59-16Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.99.2","0.99.0","0.98.3","0.98.1","0.97.0","0.96.3","0.96.1","0.92.4","0.92.0","0.91.8","0.91.6","0.91.1","0.90.8","0.90.5","0.90.3","0.90.11","0.90.1","0.9.9","0.9.7","0.9.43","0.9.41","0.9.39","0.9.37","0.9.35","0.9.33","0.9.31","0.9.29","0.9.27","0.9.25","0.9.22"]},{"name":"plausibleanalytics-arm","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.0-rc.2","v1.5.0-rc.1","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master","loadtest","latest","1.5.0-rc.0"]},{"name":"pocketbase","image":"coollabsio/pocketbase","tags":["0.8.0-arm64","0.8.0-amd64","0.8.0-aarch64","0.8.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.12.02-ffb72dfd","2022.12.02-890d63b9","2022.12.02-4970db05","2022.12.02-317fe0a2","2022.11.30-f19837cf","2022.11.30-44d4a171","2022.11.30-0361f836","2022.11.29-a8359dd4","2022.11.29-82af2f44","2022.11.29-768659f2","2022.11.29-5b19f892","2022.11.29-3579a38a","2022.11.29-1b2f1c17","2022.11.25-5ca6868c","2022.11.25-28ae469f","2022.11.25-1314c1c5","2022.11.19-b5371b7a","2022.11.18-fe8b0472","2022.11.18-1cdadf4b","2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff","2022.11.07-d3949269","2022.11.07-8f19bdaf","2022.11.06-ae54c7d5","2022.11.06-2dc5c0e1","2022.11.05-e9f42e1c","2022.11.05-d764d94a","2022.11.05-d3a7399e"]},{"name":"trilium","image":"zadam/trilium","tags":["0.57.2","0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6","0.44.4"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-12-01-0295bd44d4d9da0e0836b9152319fba173a0825e","edge-2022-11-28-f28431a1e78f88bf49ccf539fbc00afe0925542d","edge-2022-11-26-558811de16025b83de43d2747f1fe209a5b829f1","edge-2022-11-23-4a1fe25c7b70e49156e02183a8deec3b357b9030","edge-2022-11-22-9a178e7f5c2e387329592a1dd7700671f64f6682","edge-2022-11-21-eb741ebad70211ecb1babdfd23e4f43c5a59fc7b","edge-2022-11-21-4580d37f616650cf5b0851fee051651f785e8852","edge-2022-11-21-0f74d6c4d3777dbf28affd09b45c69c85ed01d84","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6","edge-2022-11-02-e4171e0c5657ca38341cce8ac31f5cbdf25389eb","edge-2022-11-02-6d886c40cd62eb23d21f7c0a1840b4a7a4c51ad0","edge-2022-11-01-608df4dd95a2d1f76c15cddd9e116bb4c3229168","edge-2022-11-01-54957be78eb76f602ceae50c0b01b64b20402b2a","edge-2022-10-31-c55c7302a6c82a160ee9d711893c12d67ecd3b27","edge-2022-10-26-c69cfdd83ed1fad4a4d57398552b8c70894a6586","edge-2022-10-26-410b3aff37de5bbfacbc47642ce28b2518bee506","edge-2022-10-25-e09e2c29ed3748eb0fa248453635dd27768e8dd9"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file +[{"name":"appsmith","image":"appsmith/appsmith-ce","tags":["v1.8.9","v1.8.7","v1.8.5","v1.8.3","v1.8.12","v1.8.10","v1.8.0","v1.7.8","v1.7.6","v1.7.4","v1.7.2","v1.7.13","v1.7.11","v1.7.1","v1.6.9","v1.6.7","v1.6.5","v1.6.3","v1.6.22","v1.6.20","v1.6.19","v1.6.17","v1.6.15","v1.6.13","v1.6.11","v1.6.1","v1.5.30","v1.5.28","v1.5.26","v1.5.24"]},{"name":"appwrite","image":"appwrite/appwrite","tags":["1.1.2","1.1.0","1.0.3","1.0.1","1.0.0","0.9.3","0.9.1","0.8.0","0.7.1","0.6.2","0.6.0","0.5.2","0.5.0","0.3.1","0.2.0","0.15.2","0.15.0","0.14.2","0.14.0","0.13.4","0.13.2","0.13.0","0.12.3","0.12.1","0.12.0","0.11.2","0.11.0","0.10.4","0.10.2","0.10.0"]},{"name":"fider","image":"getfider/fider","tags":["stable","master","main","dev","SHA_ee6e83cfaadadaa56ab76e089e01f5631af3506f","SHA_deb4f9b4f561d890d8a80e6872fea9a98a265cc6","SHA_d5cc307909d43447200483d76b5db74d8ed8349e","SHA_d1674476577a7fd3c88fc29f91c3f35f5bd6a260","SHA_d107cbb157abca6576110080736213efe0955cff","SHA_c9c55b2f5b33a76015241b97e03cfac1254b42a7","SHA_bcf451a3cb02d5c8a489fd30309249296057b084","SHA_bbfe419639514f949a042807addf0fde7d4de225","SHA_adc3afc4c7bcf96931a5f90cab65c282d860dbfd","SHA_ab5283ae95334f10b5041402dce79e333c472015","SHA_a3f4cb5ed0a4ee2d726705fc426636364aac17a1","SHA_a18224142bf51bc6463c3d22f45f62287902e9a6","SHA_8e5cff30d95963eaee2587488d351e0d658c8195","SHA_8cabe2817ce7ccaf2f0a9fdbb1b5d3411de87f81","SHA_7851f9da566132d87fa2a63004e78c3bc9c09c6c","SHA_6c0f2bed1754e9d579eb9575129a6e3dbc529c32","SHA_603508c8790d6a6fb1e852df1a58ead8e5b3ea6c","SHA_55efacf164a4749b50ee68ae8925e7dc9dfa3a0c","SHA_4bdd291ce61e5f5dfc063fa1b2d9be8c9ff1d4c4","SHA_3fba9cb6a9ceab0c78c6cff3220610f591f657cb","SHA_3d635b57606a9885babe91fe975b11429e0f2c38","SHA_3b794edbd9789a8aa38ecd3714bc536a675d3058","SHA_3570c454ad3252b690608f7bf8051737d8519f8a","SHA_263e2709fd145f3ea511e5557e170102899995b0","SHA_255c30ed012fc4c39ffc97efc1d3b00425b17c72","SHA_17f92b16ef790003338f0926fc8d791a9a61333c"]},{"name":"ghost-mariadb","image":"bitnami/ghost","tags":["5.7.1","5.7.0","5.5.0","5.4.1","5.4.0","5.3.1","5.3.0","5.25.3","5.25.2","5.25.1","5.25.0","5.24.2","5.24.1","5.24.0","5.23.0","5.22.9","5.22.8","5.22.7","5.22.6","5.22.5","5.22.4","5.22.3","5.22.2","5.22.11","5.22.10","5.22.1","5.22.0","5.21.0","5.20.0","5.2.4","4.48.8"]},{"name":"ghost-mysql","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.25.3","5.25.2","5.25.1","5.25.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0"]},{"name":"ghost-only","image":"library/ghost","tags":["5.9.4","5.8.3","5.8.2","5.7.1","5.7.0","5.5.0","5.4.1","5.3.1","5.3.0","5.25.3","5.25.2","5.25.1","5.25.0","5.24.2","5.24.1","5.23.0","5.22.9","5.22.8","5.22.4","5.22.11","5.22.10","5.22.1","5.20.0","5.2.4","5.2.3","5.2.2","5.2.1","5.19.3","5.19.0","5.18.0"]},{"name":"gitea","image":"gitea/gitea","tags":["1.9.6","1.9.5","1.9.4","1.9.3","1.9.2","1.9.0","1.8.3","1.8.1","1.8.0","1.7.5","1.7.3","1.7.1","1.7.0","1.6.3","1.6.1","1.6.0","1.5.3","1.5.1","1.5.0","1.4.3","1.4.1","1.4.0","1.3.3","1.3.1","1.3.0","1.2.3","1.2.1","1.2.0","1.17.3","1.17.2"]},{"name":"glitchtip","image":"glitchtip/glitchtip","tags":["v3.0.0","v2.0.7","v2.0.5","v2.0.2","v2.0.0","v1.9.2","v1.9.0","v1.8.4","v1.8.2","v1.8.0","v1.7.1","v1.6.4","v1.6.2","v1.6.0","v1.5.3","v1.5.1","v1.4.1","v1.3.3","v1.3.1","v1.2.6","v1.2.4","v1.2.2","v1.2.0","v1.12.4","v1.12.2","v1.12.0","v1.10.3","v1.10.1","v1.1.2","v1.1.0"]},{"name":"grafana","image":"grafana/grafana","tags":["9.3.1","9.3.0","9.2.7","9.2.6","9.2.5","9.2.4","9.2.3","9.2.2","9.2.1","9.2.0","9.1.8","9.1.7","9.1.6","9.1.5","9.1.4","9.1.3","9.1.2","9.1.1","9.1.0","9.0.9","9.0.8","9.0.7","9.0.6","9.0.5","9.0.4","9.0.3","9.0.2","9.0.1","9.0.0","8.5.9"]},{"name":"hasura","image":"hasura/graphql-engine","tags":["v2.9.0","v2.8.4","v2.8.3","v2.8.2","v2.8.1","v2.8.0","v2.7.0","v2.6.2","v2.6.1","v2.6.0","v2.5.2","v2.5.1","v2.5.0","v2.4.0","v2.3.1","v2.3.0","v2.2.2","v2.2.1","v2.2.0","v2.16.0","v2.15.2","v2.14.1","v2.13.2","v2.12.1","v2.11.3","v2.10.2","v2.1.1","v2.1.0","v2.0.9","v2.0.8"]},{"name":"keycloak","image":"quay.io/keycloak/keycloak","tags":["9.0.3","9.0.0","8.0.1","7.0.0","6.0.1","6.0.0","20.0.1","20.0.0","19.0.3","19.0.1","19.0.0","18.0.1","18.0.0","17.0.1","17.0.0","16.1.0","15.1.1","15.0.2","15.0.0","13.0.1","12.0.4","12.0.2","12.0.0","11.0.2","11.0.0","10.0.1"]},{"name":"languagetool","image":"silviof/docker-languagetool","tags":["latest","5.8","5.7","5.6","5.5","5.4","5.3"]},{"name":"lavalink","image":"fredboat/lavalink","tags":["v3.7","v3.6","v3-vda0b3a4b3916a7b1a2b79702de1143c3a6939810-SNAPSHOT","v3-vc92690c425390bd20f6c51643c67ba79ab85b7e0-SNAPSHOT","v3-vab81dcd46adf3e8a961dd57eacd2a1bde1233e6c-SNAPSHOT","v3-v9c9432704d6a4badfcbd06a57597c54bed8f4326-SNAPSHOT","v3-v3.0","v3-v3","v3-v124f8fae7dab299f9cdf1cb4c1715be455497286-SNAPSHOT","v3-","v3","v2.0.1","v2.0","v2","update-udpqueue-vb4a439d6147dbd8641ea4f265e8efc9f1e16e2d3-SNAPSHOT","update-udpqueue-","update-udpqueue","revert-713-fix-error-for-loading-jda-nas","refactor-github-actions","patch-update-github-actions","patch-more-configurable-github-actions","patch-lavaplayer-update","patch-lavaplayer-bump","patch-build-number","next-api-vd4db194cac7a839a3899857f1f6d7b910369309d-SNAPSHOT","next-api-vc2e018d5ffef54b2d17244b3d213e31723a084d6-SNAPSHOT","next-api-v42cb5f7c58e98d1911e87bffb35aee0a235b85f8-SNAPSHOT","next-api-v31a243bda80badbd7d643f68fc1f87e99639060f-SNAPSHOT","next-api-v17f6884434c2d70d1704b2322a951d9f07af8865-SNAPSHOT","next-api-"]},{"name":"meilisearch","image":"getmeili/meilisearch","tags":["v0.9.0","v0.8.3","v0.8.1","v0.30.1","v0.30.0","v0.29.3","v0.29.1","v0.29.0","v0.28.1","v0.28.0","v0.27.1","v0.27.0","v0.26.1","v0.26.0","v0.25.1","v0.25.0","v0.23.1","v0.23.0","v0.21.1","v0.21.0","v0.20.0","v0.19.0","v0.18.1","v0.18.0","v0.17.0","v0.16.0","v0.14.1","v0.14.0","v0.12.0","v0.11.0"]},{"name":"minio","image":"minio/minio","tags":["RELEASE.2022-12-12T19-27-27Z.fips","RELEASE.2022-12-07T00-56-37Z.fips","RELEASE.2022-12-02T19-19-22Z.fips","RELEASE.2022-11-29T23-40-49Z.fips","RELEASE.2022-11-26T22-43-32Z.fips","RELEASE.2022-11-17T23-20-09Z.fips","RELEASE.2022-11-11T03-44-20Z.fips","RELEASE.2022-11-10T18-20-21Z.fips","RELEASE.2022-11-08T05-27-07Z.fips","RELEASE.2022-10-29T06-21-33Z.fips","RELEASE.2022-10-24T18-35-07Z.hotfix.ce525fdaf","RELEASE.2022-10-24T18-35-07Z.fips","RELEASE.2022-10-21T22-37-48Z.fips","RELEASE.2022-10-20T00-55-09Z.fips","RELEASE.2022-10-15T19-57-03Z.fips","RELEASE.2022-10-08T20-11-00Z.fips","RELEASE.2022-10-05T14-58-27Z.fips","RELEASE.2022-10-02T19-29-29Z.fips","RELEASE.2022-09-25T15-44-53Z.fips","RELEASE.2022-09-22T18-57-27Z.fips","RELEASE.2022-09-17T00-09-45Z.hotfix.fc6d6fdbd","RELEASE.2022-09-17T00-09-45Z.hotfix.4bb22d5cd","RELEASE.2022-09-17T00-09-45Z","RELEASE.2022-09-07T22-25-02Z","RELEASE.2022-09-01T23-53-36Z","RELEASE.2022-08-26T19-53-15Z","RELEASE.2022-08-25T07-17-05Z","RELEASE.2022-08-22T23-53-06Z.fips","RELEASE.2022-08-13T21-54-44Z.fips","RELEASE.2022-08-11T04-37-28Z.fips"]},{"name":"n8n","image":"n8nio/n8n","tags":["0.99.1","0.99.0","0.98.0","0.97.0","0.96.0","0.95.1","0.95.0","0.94.1","0.94.0","0.93.0","0.92.0","0.91.0","0.9.0","0.89.2","0.88.1","0.88.0","0.87.2","0.87.1","0.87.0","0.86.1","0.86.0","0.85.0","0.84.4","0.84.3","0.84.1","0.84.0","0.83.0","0.82.1","0.82.0","0.81.0"]},{"name":"nocodb","image":"nocodb/nocodb","tags":["0.99.1","0.98.4","0.98.2","0.98.0","0.96.4","0.96.2","0.96.0","0.92.3","0.91.9","0.91.7","0.91.10","0.91.0","0.90.7","0.90.4","0.90.2","0.90.10","0.90.0","0.9.8","0.9.6","0.9.42","0.9.40","0.9.38","0.9.36","0.9.34","0.9.32","0.9.30","0.9.28","0.9.26","0.9.24","0.9.21"]},{"name":"plausibleanalytics-arm","image":"plausible/analytics","tags":["v1.5.1","v1.5.0-rc.2","v1.5.0-rc.1","v1.5.0","v1.5","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master"]},{"name":"plausibleanalytics","image":"plausible/analytics","tags":["v1.5.1","v1.5.0-rc.2","v1.5.0-rc.1","v1.5.0","v1.5","v1.4.4","v1.4.3","v1.4.2","v1.4.1","v1.4.0.rc.0","v1.4.0-rc.0","v1.4.0","v1.4","v1.3.0-rc.1","v1.3.0-rc.0","v1.3.0","v1.3","v1.2.1","v1.2.0","v1.2-rc.1","v1.2-rc.0","v1.2","v1.1.1","v1.1.0","v1.1","v1.0.0","v1.0","v1","stable","master"]},{"name":"pocketbase","image":"coollabsio/pocketbase","tags":["0.8.0-arm64","0.8.0-amd64","0.8.0-aarch64","0.8.0"]},{"name":"searxng","image":"searxng/searxng","tags":["2022.12.12-966e9c3c","2022.12.09-abb33bd7","2022.12.09-a6d870d5","2022.12.09-6479b67c","2022.12.09-3df1a983","2022.12.09-0b1f09fa","2022.12.05-67eea86b","2022.12.02-ffb72dfd","2022.12.02-890d63b9","2022.12.02-4970db05","2022.12.02-317fe0a2","2022.11.30-f19837cf","2022.11.30-44d4a171","2022.11.30-0361f836","2022.11.29-a8359dd4","2022.11.29-82af2f44","2022.11.29-768659f2","2022.11.29-5b19f892","2022.11.29-3579a38a","2022.11.29-1b2f1c17","2022.11.25-5ca6868c","2022.11.25-28ae469f","2022.11.25-1314c1c5","2022.11.19-b5371b7a","2022.11.18-fe8b0472","2022.11.18-1cdadf4b","2022.11.11-e6345758","2022.11.11-3a765113","2022.11.10-117f69fa","2022.11.09-ee4475ff"]},{"name":"trilium","image":"zadam/trilium","tags":["0.57.4","0.57.2","0.56.1","0.55.1","0.54.2","0.53.2","0.52.4","0.52.2","0.51.2","0.50.3","0.50.1","0.49.5","0.49.3","0.48.9","0.48.7","0.48.4","0.48.2","0.47.8","0.47.6","0.47.4","0.47.2","0.46.7","0.46.5","0.45.9","0.45.7","0.45.5","0.45.3","0.45.10","0.44.8","0.44.6"]},{"name":"umami-postgresql","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"umami","image":"ghcr.io/umami-software/umami","tags":["postgresql-v1.39.5","postgresql-v1.39.4","postgresql-v1.39.3","postgresql-v1.39.2","postgresql-v1.39.1","postgresql-v1.39.0","postgresql-v1.38.0","postgresql-v1.37.0","postgresql-v1.36.1","postgresql-v1.36.0","postgresql-v1.35.0","postgresql-v1.34.0","postgresql-v1.33.3","postgresql-latest","mysql-v1.39.5","mysql-v1.39.4","mysql-v1.39.3","mysql-v1.39.2","mysql-v1.39.1","mysql-v1.39.0","mysql-v1.38.0","mysql-v1.37.0","mysql-v1.36.1","mysql-v1.36.0","mysql-v1.35.0","mysql-v1.34.0","mysql-v1.33.3","mysql-latest"]},{"name":"uptimekuma","image":"louislam/uptime-kuma","tags":["1.9.2","1.9.1","1.9.0","1.8.0","1.7.3","1.7.1","1.7.0","1.6.3","1.6.2","1.6.1","1.6.0","1.5.3","1.5.2","1.5.0","1.3.1","1.2.0","1.18.5","1.18.4","1.18.3","1.18.2","1.18.1","1.18.0","1.17.1","1.17.0","1.16.1","1.16.0","1.15.1","1.15.0","1.14.1","1.14.0"]},{"name":"vaultwarden","image":"vaultwarden/server","tags":["1.26.0","1.25.2","1.25.1","1.25.0","1.24.0","1.23.1","1.23.0","1.22.2","1.22.1","1.22.0","1.21.0"]},{"name":"vscodeserver","image":"codercom/code-server","tags":["4.9.0","4.8.3","4.8.2","4.8.1","4.8.0","4.7.0","4.6.0","4.5.1","4.4.0","4.2.0","4.0.2","3.9.3","3.9.1","3.8.1","3.7.4","3.7.2","3.7.0","3.6.1","3.5.0","3.4.0","3.3.0","3.2.0","3.11.1","3.10.2","3.10.0","3.1.1","3.1.0","3.0.2","3.0.0"]},{"name":"weblate","image":"weblate/weblate","tags":["latest","edge-2022-12-12-c0db193a3baacd107c5f2c28c6e0af89c3d5afa3","edge-2022-12-09-647d40c67cf405870ba71a01584a42cfaec5915f","edge-2022-12-09-5f92b1c3243ef445a9b5929eda8bd0307584f36c","edge-2022-12-08-d1801c18970ee45fcd6c5a691d79613110537152","edge-2022-12-08-c66eb8190d1e55fe6e53d3fa87f101c189c635d4","edge-2022-12-07-6b4c53cba19003d5cf15dcbaa8edb8304fb1ae6c","edge-2022-12-06-f7644c880ee17e917d5fb72497aa5b75ecca7dd2","edge-2022-12-05-d3bf38faf4599fe6c7c86c20f33f4dd7a98de645","edge-2022-12-01-0295bd44d4d9da0e0836b9152319fba173a0825e","edge-2022-11-28-f28431a1e78f88bf49ccf539fbc00afe0925542d","edge-2022-11-26-558811de16025b83de43d2747f1fe209a5b829f1","edge-2022-11-23-4a1fe25c7b70e49156e02183a8deec3b357b9030","edge-2022-11-22-9a178e7f5c2e387329592a1dd7700671f64f6682","edge-2022-11-21-eb741ebad70211ecb1babdfd23e4f43c5a59fc7b","edge-2022-11-21-4580d37f616650cf5b0851fee051651f785e8852","edge-2022-11-21-0f74d6c4d3777dbf28affd09b45c69c85ed01d84","edge-2022-11-15-cad0a043b32c1ee61611ab258db0f01c5e6d718f","edge-2022-11-10-bf41db3afbab22384e103718094738dcfdc1a270","edge-2022-11-09-9bc90ce8b873778d2f486eccd0163bb1bb65ca6e","edge-2022-11-08-36e221037ff7097f8cd2c88d779135b6c7d3f363","edge-2022-11-08-3568e3c6759a9e9b779d98cb98393526d451466a","edge-2022-11-08-261d197970ca0679514d32ff783467972e807061","edge-2022-11-05-fa5cb203d854a11cc7850868a2890168afa3e7da","edge-2022-11-05-d93ae789eef8f065240f9fb6feb3edb236a7e6f8","edge-2022-11-05-8fc2be8e9d22e5ca2da2773488da7f72c5927ec3","edge-2022-11-05-85da67e88a113bed65530f0695ad4cddec0ed05a","edge-2022-11-05-3f4d77b6f2cb16bf008a4ef587e843ccb9c0c5d0","edge-2022-11-05-226eed520a2b32c3583c6e3247109ec8950764e7","edge-2022-11-03-487f3255cb89415fbe0769fa4b7bd2a9209deca6"]},{"name":"wordpress-only","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]},{"name":"wordpress","image":"library/wordpress","tags":["php8.1-fpm-alpine","php8.1-fpm","php8.1-apache","php8.1","php8.0-fpm-alpine","php8.0-fpm","php8.0-apache","php8.0","php7.4-fpm-alpine","php7.4-fpm","php7.4-apache","php7.4","php7.3-fpm-alpine","php7.3-fpm","php7.3-apache","php7.3","php7.2-fpm-alpine","php7.2-fpm","php7.2-apache","php7.2","php7.1-fpm-alpine","php7.1-fpm","php7.1-apache","php7.1","php7.0-fpm-alpine","php7.0-fpm","php7.0-apache","php7.0","php5.6-fpm-alpine","php5.6-fpm"]}] \ No newline at end of file diff --git a/apps/api/templates.json b/apps/api/templates.json index 256cca609..5d726ed45 100644 --- a/apps/api/templates.json +++ b/apps/api/templates.json @@ -1 +1 @@ -[{"templateVersion":"1.0.0","defaultVersion":"0.8.0","documentation":"https://pocketbase.io/docs/","type":"pocketbase","name":"Pocketbase","description":"Open Source realtime backend in 1 file","services":{"$$id":{"image":"coollabsio/pocketbase:$$core_version","volumes":["$$id-data:/app/pb_data"],"ports":["8080"]}}},{"templateVersion":"1.0.0","defaultVersion":"1.5.0-rc.0","documentation":"https://plausible.io/doc/","type":"plausibleanalytics-arm","name":"Plausible Analytics (ARM)","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DB=$$config_postgres_db"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"clickhouse/clickhouse-server:22.6-alpine","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"1.17","documentation":"https://docs.gitea.io","type":"gitea","name":"Gitea","description":"Gitea is a community managed lightweight code hosting solution written in Go.","labels":["storage","git"],"services":{"$$id":{"name":"Gitea","documentation":"https://docs.gitea.io","image":"gitea/gitea:$$core_version","volumes":["$$id-data:/data","/etc/timezone:/etc/timezone:ro","/etc/localtime:/etc/localtime:ro"],"environment":["USER_UID=1000","USER_GID=1000","DOMAIN=$$config_domain","SSH_DOMAIN=$$config_ssh_domain","ROOT_URL=$$config_root_url","SECRET_KEY=$$secret_secret_key","INTERNAL_TOKEN=$$secret_internal_token","SSH_PORT=22","START_SSH_SERVER=$$config_start_ssh_server"],"ports":["3000","22"],"proxy":[{"port":"22","hostPort":"$$config_hostport_ssh"}]}},"variables":[{"id":"$$config_hostport_ssh","name":"SSH_PORT","label":"SSH Port","defaultValue":"8022","description":"","required":true},{"id":"$$config_domain","name":"DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ssh_domain","name":"SSH_DOMAIN","label":"SSH Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_start_ssh_server","name":"START_SSH_SERVER","label":"Start SSH Server","defaultValue":"true","description":""},{"id":"$$config_root_url","name":"ROOT_URL","label":"Root URL of Gitea","defaultValue":"$$generate_fqdn_slash","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(32)","description":""},{"id":"$$secret_internal_token","name":"INTERNAL_TOKEN","label":"Internal JWT Token","defaultValue":"$$generate_token","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"20.0","documentation":"https://www.keycloak.org/documentation","type":"keycloak","name":"Keycloak","description":"Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more.","labels":["authentication","authorization","oidconnect","saml2"],"services":{"$$id":{"name":"Keycloak","command":"start --db=postgres --features=token-exchange --import-realm","depends_on":["$$id-postgresql"],"image":"quay.io/keycloak/keycloak:$$core_version","volumes":["$$id-import:/opt/keycloak/data/import"],"environment":["KC_HEALTH_ENABLED=true","KC_PROXY=edge","KC_DB=postgres","KC_HOSTNAME=$$config_keycloak_domain","KEYCLOAK_ADMIN=$$config_admin_user","KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password","KC_DB_PASSWORD=$$secret_postgres_password","KC_DB_USERNAME=$$config_postgres_user","KC_DB_URL=$$secret_keycloak_database_url"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_keycloak_domain","name":"KEYCLOAK_DOMAIN","label":"Keycloak Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_keycloak_database_url","name":"KEYCLOAK_DATABASE_URL","label":"Keycloak Database Url","defaultValue":"jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$config_admin_user","name":"KEYCLOAK_ADMIN","label":"Keycloak Admin User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_keycloak_admin_password","name":"KEYCLOAK_ADMIN_PASSWORD","label":"Keycloak Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"keycloak","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v3.6","documentation":"https://github.com/freyacodes/Lavalink","description":"Standalone audio sending node based on Lavaplayer.","type":"lavalink","name":"Lavalink","labels":["discord","discord bot","audio","lavalink","jda"],"services":{"$$id":{"name":"Lavalink","image":"fredboat/lavalink:$$core_version","environment":[],"volumes":["$$id-lavalink:/lavalink"],"ports":["2333"],"files":[{"location":"/opt/Lavalink/application.yml","content":"server:\n port: $$config_port\n address: 0.0.0.0\nlavalink:\n server:\n password: \"$$secret_password\"\n sources:\n youtube: true\n bandcamp: true\n soundcloud: true\n twitch: true\n vimeo: true\n http: true\n local: false\n\nlogging:\n file:\n path: ./logs/\n\n level:\n root: INFO\n lavalink: INFO\n\n logback:\n rollingpolicy:\n max-file-size: 1GB\n max-history: 30"}]}},"variables":[{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"2333","required":true},{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"v1.8.6","documentation":"https://docs.appsmith.com/getting-started/setup/instance-configuration/","type":"appsmith","name":"Appsmith","description":"Fastest way to build internal apps over any database or API.","services":{"$$id":{"image":"appsmith/appsmith-ce:$$core_version","environment":["APPSMITH_MAIL_ENABLED=$$config_appsmith_mail_enabled","APPSMITH_DISABLE_TELEMETRY=$$config_appsmith_disable_telemetry","APPSMITH_DISABLE_INTERCOM=$$config_appsmith_disable_intercom"],"volumes":["$$id-stacks-data:/appsmith-stacks"],"ports":["80"]}},"variables":[{"id":"$$config_appsmith_mail_enabled","name":"APPSMITH_MAIL_ENABLED","label":"Enable Mail","defaultValue":"false","description":""},{"id":"$$config_appsmith_disable_telemetry","name":"APPSMITH_DISABLE_TELEMETRY","label":"Disable Telemetry","defaultValue":"true","description":""},{"id":"$$config_appsmith_disable_intercom","name":"APPSMITH_DISABLE_INTERCOM","label":"Disable Intercom","defaultValue":"true","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.56.2","documentation":"https://hub.docker.com/r/zadam/trilium","description":"A hierarchical note taking application with focus on building large personal knowledge bases.","labels":["personal","knowledge","notes","wiki"],"type":"trilium","name":"Trilium Notes","services":{"$$id":{"image":"zadam/trilium:$$core_version","environment":[],"volumes":["$$id-trilium:/home/node/trilium-data"],"ports":["8080"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.9.2","documentation":"https://hub.docker.com/r/louislam/uptime-kuma","description":"A free & fancy self-hosted monitoring tool.","labels":["uptime"],"type":"uptimekuma","name":"UptimeKuma","services":{"$$id":{"image":"louislam/uptime-kuma:$$core_version","environment":[],"volumes":["$$id-uptimekuma:/app/data"],"ports":["3001"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"5.8","documentation":"https://hub.docker.com/r/silviof/docker-languagetool","description":"A multilingual grammar, style and spell checker.","type":"languagetool","name":"LanguageTool","services":{"$$id":{"image":"silviof/docker-languagetool:$$core_version","environment":[],"volumes":["$$id-ngrams:/ngrams"],"ports":["8010"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.26.0","documentation":"https://hub.docker.com/r/vaultwarden/server","description":"Bitwarden compatible server written in Rust.","type":"vaultwarden","name":"VaultWarden","labels":["bitwarden","password manager"],"services":{"$$id":{"image":"vaultwarden/server:$$core_version","environment":[],"volumes":["$$id-data:/data"],"ports":["80"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"9.2.3","documentation":"https://hub.docker.com/r/grafana/grafana","type":"grafana","name":"Grafana","description":"Grafana allows you to query, visualize, alert on and understand your metrics.","labels":["monitoring","metrics","dashboard"],"services":{"$$id":{"image":"grafana/grafana:$$core_version","environment":[],"volumes":["$$id-config:/etc/grafana","$$id-grafana:/var/lib/grafana"],"ports":["3000"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.0.3","documentation":"https://appwrite.io/docs","type":"appwrite","name":"Appwrite","description":"Secure Backend Server for Web, Mobile & Flutter Developers.","labels":["serverless","backend","storage","api"],"services":{"$$id":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_LOCALE=$$config__app_locale","_APP_CONSOLE_WHITELIST_ROOT=$$config__app_console_whitelist_root","_APP_CONSOLE_WHITELIST_EMAILS=$$config__app_console_whitelist_emails","_APP_CONSOLE_WHITELIST_IPS=$$config__app_console_whitelist_ips","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_SYSTEM_RESPONSE_FORMAT=$$config__app_system_response_format","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPTIONS_FORCE_HTTPS=$$config__app_options_force_https","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_STORAGE_LIMIT=$$config__app_storage_limit","_APP_STORAGE_PREVIEW_LIMIT=$$config__app_storage_preview_limit","_APP_STORAGE_ANTIVIRUS=$$config__app_storage_antivirus_enabled","_APP_STORAGE_ANTIVIRUS_HOST=$$config__app_storage_antivirus_host","_APP_STORAGE_ANTIVIRUS_PORT=$$config__app_storage_antivirus_port","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_FUNCTIONS_SIZE_LIMIT=$$config__app_functions_size_limit","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STATSD_HOST=$$config__app_statsd_host","_APP_STATSD_PORT=$$config__app_statsd_port","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-config:/storage/config","$$id-certificates:/storage/certificates","$$id-functions:/storage/functions"],"ports":["80"],"proxy":[{"port":"80"}]},"$$id-executor":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_INACTIVE_THRESHOLD=$$config__app_functions_inactive_threshold","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-functions:/storage/functions","$$id-builds:/storage/builds","/var/run/docker.sock:/var/run/docker.sock"],"entrypoint":"executor"},"$$id-influxdb":{"image":"appwrite/influxdb:1.5.0","environment":[],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-maintenance":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"maintenance"},"$$id-mariadb":{"image":"mariadb:10.7","command":"--innodb-flush-method fsync","environment":["MARIADB_ROOT_PASSWORD=$$secret__app_db_root_pass","MARIADB_DATABASE=$$config__app_db_schema","MARIADB_USER=$$config__app_db_user","MARIADB_PASSWORD=$$secret__app_db_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-mariadb:/var/lib/mysql"]},"$$id-realtime":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"realtime","proxy":[{"port":"80","pathPrefix":"/v1/realtime"}]},"$$id-redis":{"image":"redis:7.0.4-alpine","command":"--maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5","environment":[],"volumes":["$$id-redis:/data"]},"$$id-schedule":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"schedule"},"$$id-telegraf":{"image":"appwrite/telegraf:1.4.0","environment":["_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-usage-database":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type database"},"$$id-usage":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type timeseries"},"$$id-worker-audits":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-audits"},"$$id-worker-builds":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-builds"},"$$id-worker-certificates":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-config:/storage/config","$$id-certificates:/storage/certificates"],"entrypoint":"worker-certificates"},"$$id-worker-databases":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-databases"},"$$id-worker-deletes":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-functions:/storage/functions","$$id-builds:/storage/builds","$$id-certificates:/storage/certificates"],"entrypoint":"worker-deletes"},"$$id-worker-functions":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_USAGE_STATS=$$config__app_usage_stats","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-functions"},"$$id-worker-mails":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-mails"},"$$id-worker-messaging":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-messaging"},"$$id-worker-webhooks":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-webhooks"}},"variables":[{"id":"$$config__app_influxdb_host","name":"_APP_INFLUXDB_HOST","label":"InfluxDB | _APP_INFLUXDB_HOST","defaultValue":"$$id-influxdb","description":""},{"id":"$$config__app_influxdb_port","name":"_APP_INFLUXDB_PORT","label":"InfluxDB | _APP_INFLUXDB_PORT","defaultValue":"8086","description":"InfluxDB server TCP port."},{"id":"$$config__app_env","name":"_APP_ENV","label":"General | _APP_ENV","defaultValue":"production","description":"Set your server running environment."},{"id":"$$config__app_worker_per_core","name":"_APP_WORKER_PER_CORE","label":"General | _APP_WORKER_PER_CORE","defaultValue":"6","description":"Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance."},{"id":"$$config__app_locale","name":"_APP_LOCALE","label":"General | _APP_LOCALE","defaultValue":"en","description":"Set your Appwrite's locale. By default, the locale is set to 'en'."},{"id":"$$config__app_console_whitelist_root","name":"_APP_CONSOLE_WHITELIST_ROOT","label":"General | _APP_CONSOLE_WHITELIST_ROOT","defaultValue":"enabled","description":"This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled."},{"id":"$$config__app_console_whitelist_emails","name":"_APP_CONSOLE_WHITELIST_EMAILS","label":"General | _APP_CONSOLE_WHITELIST_EMAILS","defaultValue":"","description":"This option allows you to limit creation of new users on the Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma."},{"id":"$$config__app_console_whitelist_ips","name":"_APP_CONSOLE_WHITELIST_IPS","label":"General | _APP_CONSOLE_WHITELIST_IPS","defaultValue":"","description":"This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\\n\\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma."},{"id":"$$config__app_system_email_name","name":"_APP_SYSTEM_EMAIL_NAME","label":"General | _APP_SYSTEM_EMAIL_NAME","defaultValue":"Appwrite","description":"This is the sender name value that will appear on email messages sent to developers from the Appwrite console. You can use url encoded strings for spaces and special chars."},{"id":"$$config__app_system_email_address","name":"_APP_SYSTEM_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_EMAIL_ADDRESS","defaultValue":"team@appwrite.io","description":"This is the sender email address that will appear on email messages sent to developers from the Appwrite console. You should choose an email address that is allowed to be used from your SMTP server to avoid the server email ending in the users' SPAM folders."},{"id":"$$config__app_system_security_email_address","name":"_APP_SYSTEM_SECURITY_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_SECURITY_EMAIL_ADDRESS","defaultValue":"certs@appwrite.io","description":"This is the email address used to issue SSL certificates for custom domains or the user agent in your webhooks payload."},{"id":"$$config__app_system_response_format","name":"_APP_SYSTEM_RESPONSE_FORMAT","label":"General | _APP_SYSTEM_RESPONSE_FORMAT","defaultValue":"","description":"Use this environment variable to set the default Appwrite HTTP response format to support an older version of Appwrite. This option is useful to overcome breaking changes between versions. You can also use the X-Appwrite-Response-Format HTTP request header to overwrite the response for a specific request. This variable accepts any valid Appwrite version. To use the current version format, leave the value of the variable empty."},{"id":"$$config__app_options_abuse","name":"_APP_OPTIONS_ABUSE","label":"General | _APP_OPTIONS_ABUSE","defaultValue":"enabled","description":"Allows you to disable abuse checks and API rate limiting. By default, set to 'enabled'. To cancel the abuse checking, set to 'disabled'. It is not recommended to disable this check-in a production environment."},{"id":"$$config__app_options_force_https","name":"_APP_OPTIONS_FORCE_HTTPS","label":"General | _APP_OPTIONS_FORCE_HTTPS","defaultValue":"disabled","description":"Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the 'Strict-Transport-Security' header to all HTTP responses."},{"id":"$$secret__app_openssl_key_v1","name":"_APP_OPENSSL_KEY_V1","label":"General | _APP_OPENSSL_KEY_V1","defaultValue":"$$generate_hex(256)","description":"This is your server private secret key that is used to encrypt all sensitive data on your server. Appwrite server encrypts all secret data on your server like webhooks, HTTP passwords, user sessions, and storage files. Keep it a secret and have a backup for it."},{"id":"$$config__app_domain","name":"_APP_DOMAIN","label":"General | _APP_DOMAIN","defaultValue":"$$generate_domain","description":"Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is 'localhost'."},{"id":"$$config__app_domain_target","name":"_APP_DOMAIN_TARGET","label":"General | _APP_DOMAIN_TARGET","defaultValue":"$$generate_fqdn","description":"A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite '_APP_DOMAIN' variable. The default value is 'localhost'."},{"id":"$$config__app_redis_host","name":"_APP_REDIS_HOST","label":"Redis | _APP_REDIS_HOST","defaultValue":"$$id-redis","description":""},{"id":"$$config__app_redis_port","name":"_APP_REDIS_PORT","label":"Redis | _APP_REDIS_PORT","defaultValue":"6379","description":"Redis server TCP port."},{"id":"$$config__app_redis_user","name":"_APP_REDIS_USER","label":"Redis | _APP_REDIS_USER","defaultValue":"","description":"Redis server user. This is an optional variable. Default value is an empty string."},{"id":"$$secret__app_redis_pass","name":"_APP_REDIS_PASS","label":"Redis | _APP_REDIS_PASS","defaultValue":"","description":"Redis server password. This is an optional variable. Default value is an empty string."},{"id":"$$config__app_db_host","name":"_APP_DB_HOST","label":"MariaDB | _APP_DB_HOST","defaultValue":"$$id-mariadb","description":""},{"id":"$$config__app_db_port","name":"_APP_DB_PORT","label":"MariaDB | _APP_DB_PORT","defaultValue":"3306","description":"MariaDB server TCP port."},{"id":"$$config__app_db_schema","name":"_APP_DB_SCHEMA","label":"MariaDB | _APP_DB_SCHEMA","defaultValue":"appwrite","description":"MariaDB server database schema."},{"id":"$$config__app_db_user","name":"_APP_DB_USER","label":"MariaDB | _APP_DB_USER","defaultValue":"user","description":"MariaDB server user name."},{"id":"$$secret__app_db_root_pass","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB | MARIADB_ROOT_PASSWORD","defaultValue":"$$generate_hex(16)","description":"MariaDB server root user password."},{"id":"$$secret__app_db_pass","name":"_APP_DB_PASS","label":"MariaDB | _APP_DB_PASS","defaultValue":"$$generate_hex(16)","description":"MariaDB server user password."},{"id":"$$config__app_smtp_host","name":"_APP_SMTP_HOST","label":"SMTP | _APP_SMTP_HOST","defaultValue":"","description":"SMTP server host name address. Use an empty string to disable all mail sending from the server. The default value for this variable is an empty string."},{"id":"$$config__app_smtp_port","name":"_APP_SMTP_PORT","label":"SMTP | _APP_SMTP_PORT","defaultValue":"","description":"SMTP server TCP port. Empty by default."},{"id":"$$config__app_smtp_secure","name":"_APP_SMTP_SECURE","label":"SMTP | _APP_SMTP_SECURE","defaultValue":"","description":"SMTP secure connection protocol. Empty by default, change to 'tls' if running on a secure connection."},{"id":"$$config__app_smtp_username","name":"_APP_SMTP_USERNAME","label":"SMTP | _APP_SMTP_USERNAME","defaultValue":"","description":"SMTP server user name. Empty by default."},{"id":"$$secret__app_smtp_password","name":"_APP_SMTP_PASSWORD","label":"SMTP | _APP_SMTP_PASSWORD","defaultValue":"","description":"SMTP server user password. Empty by default."},{"id":"$$config__app_usage_stats","name":"_APP_USAGE_STATS","label":"General | _APP_USAGE_STATS","defaultValue":"enabled","description":"This variable allows you to disable the collection and displaying of usage stats. This value is set to 'enabled' by default, to disable the usage stats set the value to 'disabled'. When disabled, it's recommended to turn off the Worker Usage, Influxdb and Telegraf containers for better resource usage."},{"id":"$$config__app_storage_limit","name":"_APP_STORAGE_LIMIT","label":"Storage | _APP_STORAGE_LIMIT","defaultValue":"30000000","description":"Maximum file size allowed for file upload. The default value is 30MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_preview_limit","name":"_APP_STORAGE_PREVIEW_LIMIT","label":"Storage | _APP_STORAGE_PREVIEW_LIMIT","defaultValue":"20000000","description":"Maximum file size allowed for file image preview. The default value is 20MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_antivirus_enabled","name":"_APP_STORAGE_ANTIVIRUS","label":"Storage | _APP_STORAGE_ANTIVIRUS","defaultValue":"disabled","description":"This variable allows you to disable the internal anti-virus scans. This value is set to 'disabled' by default, to enable the scans set the value to 'enabled'. Before enabling, you must add the ClamAV service and depend on it on main Appwrite service."},{"id":"$$config__app_storage_antivirus_host","name":"_APP_STORAGE_ANTIVIRUS_HOST","label":"Storage | _APP_STORAGE_ANTIVIRUS_HOST","defaultValue":"clamav","description":"ClamAV server host name address."},{"id":"$$config__app_storage_antivirus_port","name":"_APP_STORAGE_ANTIVIRUS_PORT","label":"Storage | _APP_STORAGE_ANTIVIRUS_PORT","defaultValue":"3310","description":"ClamAV server TCP port."},{"id":"$$config__app_storage_device","name":"_APP_STORAGE_DEVICE","label":"Storage | _APP_STORAGE_DEVICE","defaultValue":"Local","description":"Select default storage device. The default value is 'Local'. List of supported adapters are 'Local', 'S3', 'DOSpaces', 'Backblaze', 'Linode' and 'Wasabi'."},{"id":"$$secret__app_storage_s3_access_key","name":"_APP_STORAGE_S3_ACCESS_KEY","label":"Storage | _APP_STORAGE_S3_ACCESS_KEY","defaultValue":"","description":"AWS S3 storage access key. Required when the storage adapter is set to S3. You can get your access key from your AWS console."},{"id":"$$secret__app_storage_s3_secret","name":"_APP_STORAGE_S3_SECRET","label":"Storage | _APP_STORAGE_S3_SECRET","defaultValue":"","description":"AWS S3 storage secret key. Required when the storage adapter is set to S3. You can get your secret key from your AWS console."},{"id":"$$config__app_storage_s3_region","name":"_APP_STORAGE_S3_REGION","label":"Storage | _APP_STORAGE_S3_REGION","defaultValue":"us-east-1","description":"AWS S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from AWS console."},{"id":"$$config__app_storage_s3_bucket","name":"_APP_STORAGE_S3_BUCKET","label":"Storage | _APP_STORAGE_S3_BUCKET","defaultValue":"","description":"AWS S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your AWS console."},{"id":"$$secret__app_storage_do_spaces_access_key","name":"_APP_STORAGE_DO_SPACES_ACCESS_KEY","label":"Storage | _APP_STORAGE_DO_SPACES_ACCESS_KEY","defaultValue":"","description":"DigitalOcean spaces access key. Required when the storage adapter is set to DOSpaces. You can get your access key from your DigitalOcean console."},{"id":"$$secret__app_storage_do_spaces_secret","name":"_APP_STORAGE_DO_SPACES_SECRET","label":"Storage | _APP_STORAGE_DO_SPACES_SECRET","defaultValue":"","description":"DigitalOcean spaces secret key. Required when the storage adapter is set to DOSpaces. You can get your secret key from your DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_region","name":"_APP_STORAGE_DO_SPACES_REGION","label":"Storage | _APP_STORAGE_DO_SPACES_REGION","defaultValue":"us-east-1","description":"DigitalOcean spaces region. Required when storage adapter is set to DOSpaces. You can find your region info for your space from DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_bucket","name":"_APP_STORAGE_DO_SPACES_BUCKET","label":"Storage | _APP_STORAGE_DO_SPACES_BUCKET","defaultValue":"","description":"DigitalOcean spaces bucket. Required when storage adapter is set to DOSpaces. You can create spaces in your DigitalOcean console."},{"id":"$$secret__app_storage_backblaze_access_key","name":"_APP_STORAGE_BACKBLAZE_ACCESS_KEY","label":"Storage | _APP_STORAGE_BACKBLAZE_ACCESS_KEY","defaultValue":"","description":"Backblaze access key. Required when the storage adapter is set to Backblaze. Your Backblaze keyID will be your access key. You can get your keyID from your Backblaze console."},{"id":"$$secret__app_storage_backblaze_secret","name":"_APP_STORAGE_BACKBLAZE_SECRET","label":"Storage | _APP_STORAGE_BACKBLAZE_SECRET","defaultValue":"","description":"Backblaze secret key. Required when the storage adapter is set to Backblaze. Your Backblaze applicationKey will be your secret key. You can get your applicationKey from your Backblaze console."},{"id":"$$config__app_storage_backblaze_region","name":"_APP_STORAGE_BACKBLAZE_REGION","label":"Storage | _APP_STORAGE_BACKBLAZE_REGION","defaultValue":"us-west-004","description":"Backblaze region. Required when storage adapter is set to Backblaze. You can find your region info from your Backblaze console."},{"id":"$$config__app_storage_backblaze_bucket","name":"_APP_STORAGE_BACKBLAZE_BUCKET","label":"Storage | _APP_STORAGE_BACKBLAZE_BUCKET","defaultValue":"","description":"Backblaze bucket. Required when storage adapter is set to Backblaze. You can create your bucket from your Backblaze console."},{"id":"$$secret__app_storage_linode_access_key","name":"_APP_STORAGE_LINODE_ACCESS_KEY","label":"Storage | _APP_STORAGE_LINODE_ACCESS_KEY","defaultValue":"","description":"Linode object storage access key. Required when the storage adapter is set to Linode. You can get your access key from your Linode console."},{"id":"$$secret__app_storage_linode_secret","name":"_APP_STORAGE_LINODE_SECRET","label":"Storage | _APP_STORAGE_LINODE_SECRET","defaultValue":"","description":"Linode object storage secret key. Required when the storage adapter is set to Linode. You can get your secret key from your Linode console."},{"id":"$$config__app_storage_linode_region","name":"_APP_STORAGE_LINODE_REGION","label":"Storage | _APP_STORAGE_LINODE_REGION","defaultValue":"eu-central-1","description":"Linode object storage region. Required when storage adapter is set to Linode. You can find your region info from your Linode console."},{"id":"$$config__app_storage_linode_bucket","name":"_APP_STORAGE_LINODE_BUCKET","label":"Storage | _APP_STORAGE_LINODE_BUCKET","defaultValue":"","description":"Linode object storage bucket. Required when storage adapter is set to Linode. You can create buckets in your Linode console."},{"id":"$$secret__app_storage_wasabi_access_key","name":"_APP_STORAGE_WASABI_ACCESS_KEY","label":"Storage | _APP_STORAGE_WASABI_ACCESS_KEY","defaultValue":"","description":"Wasabi access key. Required when the storage adapter is set to Wasabi. You can get your access key from your Wasabi console."},{"id":"$$secret__app_storage_wasabi_secret","name":"_APP_STORAGE_WASABI_SECRET","label":"Storage | _APP_STORAGE_WASABI_SECRET","defaultValue":"","description":"Wasabi secret key. Required when the storage adapter is set to Wasabi. You can get your secret key from your Wasabi console."},{"id":"$$config__app_storage_wasabi_region","name":"_APP_STORAGE_WASABI_REGION","label":"Storage | _APP_STORAGE_WASABI_REGION","defaultValue":"eu-central-1","description":"Wasabi region. Required when storage adapter is set to Wasabi. You can find your region info from your Wasabi console."},{"id":"$$config__app_storage_wasabi_bucket","name":"_APP_STORAGE_WASABI_BUCKET","label":"Storage | _APP_STORAGE_WASABI_BUCKET","defaultValue":"","description":"Wasabi bucket. Required when storage adapter is set to Wasabi. You can create buckets in your Wasabi console."},{"id":"$$config__app_functions_size_limit","name":"_APP_FUNCTIONS_SIZE_LIMIT","label":"Functions | _APP_FUNCTIONS_SIZE_LIMIT","defaultValue":"30000000","description":"The maximum size deployment in bytes. The default value is 30MB."},{"id":"$$config__app_functions_timeout","name":"_APP_FUNCTIONS_TIMEOUT","label":"Functions | _APP_FUNCTIONS_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when creating a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_build_timeout","name":"_APP_FUNCTIONS_BUILD_TIMEOUT","label":"Functions | _APP_FUNCTIONS_BUILD_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_containers","name":"_APP_FUNCTIONS_CONTAINERS","label":"Functions | _APP_FUNCTIONS_CONTAINERS","defaultValue":"10","description":"The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10."},{"id":"$$config__app_functions_cpus","name":"_APP_FUNCTIONS_CPUS","label":"Functions | _APP_FUNCTIONS_CPUS","defaultValue":"","description":"The maximum number of CPU core a single cloud function is allowed to use. Please note that setting a value higher than available cores will result in a function error, which might result in an error. The default value is empty. When it's empty, CPU limit will be disabled."},{"id":"$$config__app_functions_memory_allocated","name":"_APP_FUNCTIONS_MEMORY","label":"Functions | _APP_FUNCTIONS_MEMORY","defaultValue":"","description":"The maximum amount of memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, memory limit will be disabled."},{"id":"$$config__app_functions_memory_swap","name":"_APP_FUNCTIONS_MEMORY_SWAP","label":"Functions | _APP_FUNCTIONS_MEMORY_SWAP","defaultValue":"","description":"The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, swap memory limit will be disabled."},{"id":"$$config__app_functions_runtimes","name":"_APP_FUNCTIONS_RUNTIMES","label":"Functions | _APP_FUNCTIONS_RUNTIMES","defaultValue":"node-18.0","description":"This option allows you to limit the available environments for cloud functions. This option is very useful for low-cost servers to safe disk space.\nTo enable/activate this option, pass a list of allowed environments separated by a comma.\nCurrently, supported environments are: node-14.5, node-16.0, node-18.0, php-8.0, php-8.1, ruby-3.0, ruby-3.1, python-3.8, python-3.9, python-3.10, deno-1.21, deno-1.24, dart-2.15, dart-2.16, dart-2.17, dotnet-3.1, dotnet-6.0, java-8.0, java-11.0, java-17.0, java-18.0, swift-5.5, kotlin-1.6, cpp-17.0"},{"id":"$$secret__app_executor_secret","name":"_APP_EXECUTOR_SECRET","label":"Functions | _APP_EXECUTOR_SECRET","defaultValue":"$$generate_hex(16)","description":"The secret key used by Appwrite to communicate with the function executor."},{"id":"$$config__app_executor_host","name":"_APP_EXECUTOR_HOST","label":"","defaultValue":"http://$$id-executor/v1","description":""},{"id":"$$config__app_logging_provider","name":"_APP_LOGGING_PROVIDER","label":"General | _APP_LOGGING_PROVIDER","defaultValue":"","description":"This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of 'sentry', 'raygun', 'appsignal', 'logowl'"},{"id":"$$config__app_logging_config","name":"_APP_LOGGING_CONFIG","label":"General | _APP_LOGGING_CONFIG","defaultValue":"","description":"This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be 'SENTRY_API_KEY;SENTRY_APP_ID'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key. If using LogOwl, this should be LogOwl Service Ticket."},{"id":"$$config__app_statsd_host","name":"_APP_STATSD_HOST","label":"","defaultValue":"$$id-telegraf","description":""},{"id":"$$config__app_statsd_port","name":"_APP_STATSD_PORT","label":"StatsD | _APP_STATSD_PORT","defaultValue":"8125","description":"StatsD server TCP port."},{"id":"$$config__app_maintenance_interval","name":"_APP_MAINTENANCE_INTERVAL","label":"Functions | _APP_MAINTENANCE_INTERVAL","defaultValue":"86400","description":"Interval value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_execution","name":"_APP_MAINTENANCE_RETENTION_EXECUTION","label":"Functions | _APP_MAINTENANCE_RETENTION_EXECUTION","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_maintenance_retention_cache","name":"_APP_MAINTENANCE_RETENTION_CACHE","label":"Functions | _APP_MAINTENANCE_RETENTION_CACHE","defaultValue":"2592000","description":"The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days)."},{"id":"$$config__app_maintenance_retention_abuse","name":"_APP_MAINTENANCE_RETENTION_ABUSE","label":"Functions | _APP_MAINTENANCE_RETENTION_ABUSE","defaultValue":"86400","description":"The maximum duration (in seconds) upto which to retain abuse logs. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_audit","name":"_APP_MAINTENANCE_RETENTION_AUDIT","label":"Functions | _APP_MAINTENANCE_RETENTION_AUDIT","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain audit logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_sms_provider","name":"_APP_SMS_PROVIDER","label":"Phone | _APP_SMS_PROVIDER","defaultValue":"","description":"Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. Available providers are twilio, text-magic, telesign, msg91, and vonage."},{"id":"$$config__app_sms_from","name":"_APP_SMS_FROM","label":"Phone | _APP_SMS_FROM","defaultValue":"","description":"Phone number used for sending out messages. Must start with a leading '+' and maximum of 15 digits without spaces (+123456789)."},{"id":"$$config__app_functions_inactive_threshold","name":"_APP_FUNCTIONS_INACTIVE_THRESHOLD","label":"Functions | _APP_FUNCTIONS_INACTIVE_THRESHOLD","defaultValue":"60","description":"The minimum time a function can be inactive before it's container is shutdown and put to sleep. The default value is 60 seconds"},{"id":"$$config_open_runtimes_network","name":"OPEN_RUNTIMES_NETWORK","label":"","defaultValue":"$$generate_network","description":""},{"id":"$$config_dockerhub_pull_username","name":"DOCKERHUB_PULL_USERNAME","label":"Functions | DOCKERHUB_PULL_USERNAME","defaultValue":"","description":"The username for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$secret_dockerhub_pull_password","name":"DOCKERHUB_PULL_PASSWORD","label":"Functions | DOCKERHUB_PULL_PASSWORD","defaultValue":"","description":"The password for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$config__app_usage_timeseries_interval","name":"_APP_USAGE_TIMESERIES_INTERVAL","label":"General | _APP_USAGE_TIMESERIES_INTERVAL","defaultValue":"30","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds."},{"id":"$$config__app_usage_database_interval","name":"_APP_USAGE_DATABASE_INTERVAL","label":"General | _APP_USAGE_DATABASE_INTERVAL","defaultValue":"900","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats from data in Appwrite Database. The default value is 15 minutes."}]},{"templateVersion":"1.0.0","defaultVersion":"latest","documentation":"https://docs.weblate.org/en/latest/admin/install/docker.html","description":"A copylefted libre software web-based continuous localization system.","type":"weblate","name":"Weblate","labels":["translate","localization"],"services":{"$$id":{"name":"Weblate","depends_on":["$$id-postgresql","$$id-redis"],"image":"weblate/weblate:$$core_version","volumes":["$$id-data:/app/data"],"environment":["WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain","WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DATABASE=$$config_postgres_db","POSTGRES_HOST=$$id-postgresql","POSTGRES_PORT=5432","REDIS_HOST=$$id-redis"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_weblate_site_domain","name":"WEBLATE_SITE_DOMAIN","label":"Weblate Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_weblate_admin_password","name":"WEBLATE_ADMIN_PASSWORD","label":"Weblate Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"weblate","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"2022.10.14-1a5b0965","documentation":"https://docs.searxng.org/","type":"searxng","name":"SearXNG","description":"Free internet metasearch engine which aggregates results from more than 70 search services.","services":{"$$id":{"name":"SearXNG","depends_on":["$$id-redis"],"image":"searxng/searxng:$$core_version","volumes":["$$id-searxng:/etc/searxng"],"environment":["SEARXNG_BASE_URL=$$config_searxng_base_url"],"ports":["8080"],"cap_drop":["ALL"],"cap_add":["CHOWN","SETGID","SETUID","DAC_OVERRIDE"],"files":[{"location":"/etc/searxng/settings.yml","content":"\n # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings\n use_default_settings: true\n server:\n secret_key: $$secret_secret_key\n limiter: true\n image_proxy: true\n ui:\n static_use_hash: true\n redis:\n url: redis://:$$secret_redis_password@$$id-redis:6379/0"}]},"$$id-redis":{"name":"Redis","command":"redis-server --requirepass $$secret_redis_password --save \"\" --appendonly \"no\"","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":["REDIS_PASSWORD=$$secret_redis_password"],"ports":[],"cap_drop":["ALL"],"cap_add":["SETGID","SETUID","DAC_OVERRIDE"]}},"variables":[{"id":"$$config_searxng_base_url","name":"SEARXNG_BASE_URL","label":"SearXNG Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$secret_redis_password","name":"REDIS_PASSWORD","label":"Redis Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.0.6","documentation":"https://glitchtip.com/documentation","type":"glitchtip","name":"GlitchTip","description":"Simple, open source error tracking.","labels":["sentry","bugsnag"],"services":{"$$id":{"name":"GlitchTip","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","volumes":[],"environment":["PORT=$$config_port","GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","EMAIL_HOST=$$config_email_host","EMAIL_PORT=$$config_email_port","EMAIL_HOST_USER=$$config_email_host_user","EMAIL_HOST_PASSWORD=$$secret_email_host_password","EMAIL_USE_TLS=$$config_email_use_tls","EMAIL_USE_SSL=$$config_email_use_ssl","EMAIL_BACKEND=$$config_email_backend","MAILGUN_API_KEY=$$secret_mailgun_api_key","SENDGRID_API_KEY=$$secret_sendgrid_api_key","ENABLE_OPEN_USER_REGISTRATION=$$config_enable_open_user_registration","DJANGO_SUPERUSER_EMAIL=$$config_django_superuser_email","DJANGO_SUPERUSER_PASSWORD=$$secret_django_superuser_password","DJANGO_SUPERUSER_USERNAME=$$config_django_superuser_username","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":["8000"]},"$$id-worker":{"name":"Celery Worker","command":"./bin/run-celery-with-beat.sh","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":[]},"$$id-migrate":{"exclude":true,"name":"Migrate","command":"./manage.py migrate","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url"],"ports":[]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-postgresql-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_django_superuser_username","name":"DJANGO_SUPERUSER_USERNAME","label":"Django Superuser Username","defaultValue":"$$generate_username","description":""},{"id":"$$secret_django_superuser_password","name":"DJANGO_SUPERUSER_PASSWORD","label":"Django Superuser Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_port","name":"PORT","label":"GlitchTip Port","defaultValue":"8000","description":""},{"id":"$$config_celery_worker_concurrency","main":"$$id-worker","name":"CELERY_WORKER_CONCURRENCY","label":"Celery Worker Concurrency","defaultValue":"2","description":""},{"id":"$$config_glitchtip_domain","name":"GLITCHTIP_DOMAIN","label":"GlitchTip Domain","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_email_url","name":"EMAIL_URL","label":"SMTP Email URL","defaultValue":"smtp://$$config_email_host_user:$$secret_email_host_password@$$config_email_host:$$config_email_port","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_redis_url","name":"REDIS_URL","label":"Redis URL","defaultValue":"redis://$$id-redis:6379/0","description":""},{"id":"$$config_default_from_email","name":"DEFAULT_FROM_EMAIL","label":"Default Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$config_email_host","name":"EMAIL_HOST","label":"Email SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_port","name":"EMAIL_PORT","label":"Email SMTP Port","defaultValue":"25","description":""},{"id":"$$config_email_host_user","name":"EMAIL_HOST_USER","label":"Email SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_host_password","name":"EMAIL_HOST_PASSWORD","label":"Email SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_use_tls","name":"EMAIL_USE_TLS","label":"Email Use TLS","defaultValue":"false","description":""},{"id":"$$config_email_use_ssl","name":"EMAIL_USE_SSL","label":"Email Use SSL","defaultValue":"false","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_backend","name":"EMAIL_BACKEND","label":"Email Backend","defaultValue":"","description":""},{"id":"$$secret_mailgun_api_key","name":"MAILGUN_API_KEY","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$secret_sendgrid_api_key","name":"SENDGRID_API_KEY","label":"Sendgrid API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_enable_open_user_registration","name":"ENABLE_OPEN_USER_REGISTRATION","label":"Enable Open User Registration","defaultValue":"true","description":""},{"id":"$$config_django_superuser_email","name":"DJANGO_SUPERUSER_EMAIL","label":"Django Superuser Email","defaultValue":"noreply@example.com","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"glitchtip","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.13.0","documentation":"https://hasura.io/docs/latest/index/","type":"hasura","name":"Hasura","description":"Instant realtime GraphQL APIs on any Postgres application, existing or new.","labels":["graphql","database"],"services":{"$$id":{"name":"Hasura","depends_on":["$$id-postgresql"],"image":"hasura/graphql-engine:$$core_version","volumes":[],"environment":["HASURA_GRAPHQL_ENABLE_CONSOLE=$$config_hasura_graphql_enable_console","HASURA_GRAPHQL_METADATA_DATABASE_URL=$$secret_hasura_graphql_metadata_database_url","HASURA_GRAPHQL_ADMIN_SECRET=$$secret_hasura_graphql_admin_secret"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_hasura_graphql_enable_console","name":"HASURA_GRAPHQL_ENABLE_CONSOLE","label":"Enable Hasura Console","defaultValue":"true","description":""},{"id":"$$secret_hasura_graphql_metadata_database_url","name":"HASURA_GRAPHQL_METADATA_DATABASE_URL","label":"Hasura Metadata Database URL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hasura_graphql_admin_secret","name":"HASURA_GRAPHQL_ADMIN_SECRET","label":"Hasura Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"hasura","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"postgresql-v1.38.0","documentation":"https://umami.is/docs/getting-started","type":"umami-postgresql","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"user_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n \n PRIMARY KEY (\"event_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n \n PRIMARY KEY (\"view_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n \n PRIMARY KEY (\"session_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"website_id\")\n );\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n \n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n \n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n \n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"postgresql-v1.38.0","documentation":"https://umami.is/docs/getting-started","type":"umami","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"user_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n \n PRIMARY KEY (\"event_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n \n PRIMARY KEY (\"view_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n \n PRIMARY KEY (\"session_id\")\n );\n \n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \n PRIMARY KEY (\"website_id\")\n );\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n \n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n \n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n \n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n \n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n \n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n \n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n \n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"v0.29.1","documentation":"https://docs.meilisearch.com/learn/getting_started/quick_start.html","type":"meilisearch","name":"MeiliSearch","description":"A lightning Fast, Ultra Relevant, and Typo-Tolerant Search Engine.","services":{"$$id":{"name":"MeiliSearch","documentation":"https://docs.meilisearch.com/","depends_on":[],"image":"getmeili/meilisearch:$$core_version","volumes":["$$id-datams:/meili_data/data.ms","$$id-data:/meili_data","$$id-snapshot:/snapshot","$$id-dump:/dumps"],"environment":["MEILI_MASTER_KEY=$$secret_meili_master_key"],"ports":["7700"]}},"variables":[{"id":"$$secret_meili_master_key","name":"MEILI_MASTER_KEY","label":"Master Key","defaultValue":"$$generate_hex(64)","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"latest","documentation":"https://docs.ghost.org","arch":"amd64","type":"ghost-mariadb","name":"Ghost","subname":"(MariaDB)","description":"Free and open source blogging platform.","labels":["cms","blog"],"services":{"$$id":{"name":"Ghost","depends_on":["$$id-mariadb"],"image":"bitnami/ghost:$$core_version","volumes":["$$id-ghost:/bitnami/ghost"],"environment":["url=$$config_url","GHOST_HOST=$$config_ghost_host","GHOST_ENABLE_HTTPS=$$config_ghost_enable_https","GHOST_EMAIL=$$config_ghost_email","GHOST_PASSWORD=$$secret_ghost_password","GHOST_DATABASE_HOST=$$config_ghost_database_host","GHOST_DATABASE_USER=$$config_mariadb_user","GHOST_DATABASE_PASSWORD=$$secret_ghost_database_password","GHOST_DATABASE_NAME=$$config_mariadb_database","GHOST_DATABASE_PORT_NUMBER=3306"],"ports":["2368"]},"$$id-mariadb":{"name":"MariaDB","depends_on":[],"image":"bitnami/mariadb:latest","volumes":["$$id-mariadb:/bitnami/mariadb"],"environment":["MARIADB_USER=$$config_mariadb_user","MARIADB_PASSWORD=$$secret_mariadb_password","MARIADB_DATABASE=$$config_mariadb_database","MARIADB_ROOT_USER=$$config_mariadb_root_user","MARIADB_ROOT_PASSWORD=$$secret_mariadb_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_ghost_host","name":"GHOST_HOST","label":"Ghost Host","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ghost_enable_https","name":"GHOST_ENABLE_HTTPS","label":"Ghost Enable HTTPS","defaultValue":"no","description":""},{"id":"$$config_ghost_email","name":"GHOST_EMAIL","label":"Ghost Default Email","defaultValue":"admin@example.com","description":""},{"id":"$$secret_ghost_password","name":"GHOST_PASSWORD","label":"Ghost Default Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_ghost_database_host","name":"GHOST_DATABASE_HOST","label":"Ghost Database Host","defaultValue":"$$id-mariadb","description":""},{"id":"$$config_ghost_database_user","name":"GHOST_DATABASE_USER","label":"MariaDB User","defaultValue":"$$config_mariadb_user","description":""},{"id":"$$secret_ghost_database_password","name":"GHOST_DATABASE_PASSWORD","label":"MariaDB Password","defaultValue":"$$secret_mariadb_password","description":""},{"id":"$$config_ghost_database_name","name":"GHOST_DATABASE_NAME","label":"MariaDB Database","defaultValue":"$$config_mariadb_database","description":""},{"id":"$$config_mariadb_user","name":"MARIADB_USER","label":"MariaDB User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_password","name":"MARIADB_PASSWORD","label":"MariaDB Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mariadb_database","name":"MARIADB_DATABASE","label":"MariaDB Database","defaultValue":"ghost","description":""},{"id":"$$config_mariadb_root_user","name":"MARIADB_ROOT_USER","label":"MariaDB Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_root_password","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"5.22","documentation":"https://docs.ghost.org","type":"ghost-only","name":"Ghost","subname":"(without Database)","description":"Free and open source blogging platform.","services":{"$$id":{"name":"Ghost","image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_database__connection__user","database__connection__password=$$secret_database__connection__password","database__connection__database=$$config_database__connection__database"],"ports":["2368"]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","required":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"","description":"","required":true,"placeholder":"db.coolify.io"},{"id":"$$config_database__connection__user","name":"database__connection__user","label":"Database User","defaultValue":"","description":"","placeholder":"ghost","required":true},{"id":"$$secret_database__connection__password","name":"database__connection__password","label":"Database Password","defaultValue":"","description":"","placeholder":"superSecretP4ssword","showOnConfiguration":true,"required":true},{"id":"$$config_database__connection__database","name":"database__connection__database","label":"Database Name","defaultValue":"","description":"","placeholder":"ghost_db","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"5.22","documentation":"https://docs.ghost.org","type":"ghost-mysql","name":"Ghost","subname":"(MySQL)","description":"Ghost is a free and open source blogging platform.","services":{"$$id":{"name":"Ghost","depends_on":["$$id-mysql"],"image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_mysql_user","database__connection__password=$$secret_mysql_password","database__connection__database=$$config_mysql_database"],"ports":["2368"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"mysql:8.0","volumes":["$$id-mysql:/var/lib/mysql"],"environment":["MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password","MYSQL_DATABASE=$$config_mysql_database","MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","readOnly":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"$$id-mysql","description":""},{"id":"$$config_mysql_user","main":"$$id-mysql","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mysql_password","main":"$$id-mysql","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mysql_database","main":"$$id-mysql","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"ghost","description":""},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress","name":"WordPress","subname":"(MySQL)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","depends_on":["$$id-mysql"],"image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_USER=$$config_mysql_user","WORDPRESS_DB_PASSWORD=$$secret_mysql_password","WORDPRESS_DB_NAME=$$config_mysql_database","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"bitnami/mysql:5.7","imageArm":"mysql:8.0","volumes":["$$id-mysql-data:/bitnami/mysql/data"],"volumesArm":["$$id-mysql-data:/var/lib/mysql"],"environment":["MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password","MYSQL_ROOT_USER=$$config_mysql_root_user","MYSQL_DATABASE=$$config_mysql_database","MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"$$id-mysql","description":"","readOnly":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"WordPress Config Extra","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":"","readOnly":true},{"id":"$$config_mysql_root_user","name":"MYSQL_ROOT_USER","label":"MySQL Root User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$config_mysql_database","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"wordpress","description":"","readOnly":true},{"id":"$$config_mysql_user","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$secret_mysql_password","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":"","readOnly":true}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress-only","name":"WordPress","subname":"(without DB)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_PORT=$$config_wordpress_db_port","WORDPRESS_DB_USER=$$config_wordpress_db_user","WORDPRESS_DB_PASSWORD=$$secret_wordpress_db_password","WORDPRESS_DB_NAME=$$config_wordpress_db_name","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"","description":"","placeholder":"db.coollabs.io","required":true},{"id":"$$config_wordpress_db_port","name":"WORDPRESS_DB_PORT","label":"Database Port","defaultValue":"","description":"","placeholder":"3306","required":true},{"id":"$$config_wordpress_db_user","name":"WORDPRESS_DB_USER","label":"Database User","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$secret_wordpress_db_password","name":"WORDPRESS_DB_PASSWORD","label":"Database Password","defaultValue":"","description":"","placeholder":"supers3cr3tpassw0rd!","required":true,"showOnConfiguration":true},{"id":"$$config_wordpress_db_name","name":"WORDPRESS_DB_NAME","label":"Database Name","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"Extra Config","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"}]},{"templateVersion":"1.0.0","defaultVersion":"4.7.1","documentation":"https://coder.com/docs/coder-oss/latest","type":"vscodeserver","name":"VSCode Server","description":"Visual Studio Code on a remote server, accessible through the browser.","labels":["vscode","ide"],"services":{"$$id":{"name":"VSCode Server","depends_on":[],"image":"codercom/code-server:$$core_version","volumes":["$$id-config-data:/home/coder/.local/share/code-server","$$id-vscodeserver-data:/home/coder","$$id-keys-directory:/root/.ssh","$$id-theme-and-plugin-directory:/root/.local/share/code-server"],"environment":["PASSWORD=$$secret_password"],"ports":["8080"]}},"variables":[{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"RELEASE.2022-10-15T19-57-03Z","documentation":"https://min.io/docs/minio","type":"minio","name":"MinIO","description":"A cloud storage server compatible with Amazon S3.","labels":["storage","s3"],"services":{"$$id":{"name":"MinIO","command":"server /data --console-address :9001","depends_on":[],"image":"minio/minio:$$core_version","volumes":["$$id-minio-data:/data","$$id-data-write:/files"],"environment":["MINIO_SERVER_URL=$$config_coolify_fqdn_minio_console","MINIO_BROWSER_REDIRECT_URL=$$config_minio_browser_redirect_url","MINIO_DOMAIN=$$config_minio_domain","MINIO_ROOT_USER=$$config_minio_root_user","MINIO_ROOT_PASSWORD=$$secret_minio_root_password"],"ports":["9000","9001"],"proxy":[{"port":"9000","domain":"$$config_coolify_fqdn_minio_console"},{"port":"9001"}]}},"variables":[{"id":"$$config_coolify_fqdn_minio_console","name":"MINIO_SERVER_URL","label":"MinIO Server URL","defaultValue":"","description":"Specify the URL hostname the MinIO Console should use for connecting to the MinIO Server.","required":true},{"id":"$$config_minio_browser_redirect_url","name":"MINIO_BROWSER_REDIRECT_URL","label":"Browser Redirect URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_minio_domain","name":"MINIO_DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_minio_root_user","name":"MINIO_ROOT_USER","label":"Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_minio_root_password","name":"MINIO_ROOT_PASSWORD","label":"Root User Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"0.21.1","documentation":"https://fider.io/docs","type":"fider","name":"Fider","description":"A platform to collect and organize customer feedback.","labels":["suggestion","feedback"],"services":{"$$id":{"name":"Fider","image":"getfider/fider:$$core_version","depends_on":["$$id-postgresql"],"environment":["BASE_URL=$$config_base_url","DATABASE_URL=$$secret_database_url","JWT_SECRET=$$secret_jwt_secret","EMAIL_NOREPLY=$$config_email_noreply","EMAIL_MAILGUN_API=$$secret_email_mailgun_api","EMAIL_MAILGUN_REGION=$$config_email_mailgun_region","EMAIL_MAILGUN_DOMAIN=$$config_email_mailgun_domain","EMAIL_SMTP_HOST=$$config_email_smtp_host","EMAIL_SMTP_PORT=$$config_email_smtp_port","EMAIL_SMTP_USER=$$config_email_smtp_user","EMAIL_SMTP_PASSWORD=$$secret_email_smtp_password","EMAIL_SMTP_ENABLE_STARTTLS=$$config_email_smtp_enable_starttls"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db?sslmode=disable","description":""},{"id":"$$secret_jwt_secret","name":"JWT_SECRET","label":"JWT Secret","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_email_noreply","name":"EMAIL_NOREPLY","label":"No Reply Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$secret_email_mailgun_api","name":"EMAIL_MAILGUN_API","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_mailgun_region","name":"EMAIL_MAILGUN_REGION","label":"Mailgun Region","defaultValue":"EU","description":""},{"id":"$$config_email_mailgun_domain","name":"EMAIL_MAILGUN_DOMAIN","label":"Mailgun Domain","defaultValue":"","description":""},{"id":"$$config_email_smtp_host","name":"EMAIL_SMTP_HOST","label":"SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_smtp_port","name":"EMAIL_SMTP_PORT","label":"SMTP Port","defaultValue":"587","description":""},{"id":"$$config_email_smtp_user","name":"EMAIL_SMTP_USER","label":"SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_smtp_enable_starttls","name":"EMAIL_SMTP_ENABLE_STARTTLS","label":"SMTP Enable StartTLS","defaultValue":"false","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"$$generate_username","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.198.1","documentation":"https://docs.n8n.io","type":"n8n","name":"n8n.io","description":"A free and open node based Workflow Automation Tool.","labels":["workflow","automation","ifttt","zapier","nodered"],"services":{"$$id":{"name":"N8n","depends_on":[],"image":"n8nio/n8n:$$core_version","volumes":["$$id-data:/root/.n8n","$$id-data-write:/files","/var/run/docker.sock:/var/run/docker.sock"],"environment":["WEBHOOK_URL=$$config_webhook_url"],"ports":["5678"]}},"variables":[{"id":"$$config_webhook_url","name":"WEBHOOK_URL","label":"Webhook URL","defaultValue":"$$generate_fqdn","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"stable","documentation":"https://plausible.io/doc/","arch":"amd64","type":"plausibleanalytics","name":"Plausible Analytics","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"bitnami/postgresql:13","volumes":["$$id-postgresql-data:/bitnami/postgresql"],"environment":["POSTGRESQL_PASSWORD=$$secret_postgresql_password","POSTGRESQL_USERNAME=$$config_postgresql_username","POSTGRESQL_DATABASE=$$config_postgresql_database"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"clickhouse/clickhouse-server:22.6-alpine","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgresql_username","main":"$$id-postgresql","name":"POSTGRESQL_USERNAME","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgresql_password","main":"$$id-postgresql","name":"POSTGRESQL_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgresql_database","main":"$$id-postgresql","name":"POSTGRESQL_DATABASE","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"0.98.1","documentation":"https://docs.nocodb.com","type":"nocodb","name":"NocoDB","description":"Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","labels":["database","airtable","spreadsheet"],"services":{"$$id":{"name":"NocoDB","image":"nocodb/nocodb:$$core_version","environment":["PORT=$$config_port","NC_DB=$$config_nc_db","DATABASE_URL=$$secret_database_url","NC_PUBLIC_URL=$$config_public_url","NC_AUTH_JWT_SECRET=$$secret_auth_jwt_secret","NC_SENTRY_DSN=$$secret_sentry_dsn","NC_CONNECT_TO_EXTERNAL_DB_DISABLED=$$config_connect_to_external_db_disabled","NC_DISABLE_TELE=$$config_disable_tele"],"volumes":["$$id-data:/usr/app/data"],"ports":["8080"]}},"variables":[{"id":"$$config_nc_db","name":"NC_DB","label":"Database","defaultValue":"","description":"MySQL, PostgreSQL and MSSQL connection urls supported. If absent: A local SQLite will be created in root folder."},{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"8080","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL","defaultValue":"","description":"JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku deployment."},{"id":"$$config_public_url","name":"NC_PUBLIC_URL","label":"Public URL","defaultValue":"","description":"Used for sending Email invitations. If absent: Best guess from http request params."},{"id":"$$secret_auth_jwt_secret","name":"NC_AUTH_JWT_SECRET","label":"Auth JWT Secret","defaultValue":"$$generate_hex(64)","description":"JWT secret used for auth and storing other secrets. If absent: A Random secret will be generated."},{"id":"$$secret_sentry_dsn","name":"NC_SENTRY_DSN","label":"Sentry DSN","defaultValue":"","description":"For Sentry monitoring."},{"id":"$$config_connect_to_external_db_disabled","name":"NC_CONNECT_TO_EXTERNAL_DB_DISABLED","label":"Disable External Database","defaultValue":"0","description":"Disable Project creation with external database. (Enter \"1\" to disable)."},{"id":"$$config_disable_tele","name":"NC_DISABLE_TELE","label":"NocoDB Disable Telemetry","defaultValue":"1","description":"Disable telemetry (Enter \"1\" to disable)."}]}] \ No newline at end of file +[{"templateVersion":"1.0.0","defaultVersion":"0.10.2","documentation":"https://pocketbase.io/docs/","type":"pocketbase","name":"Pocketbase","description":"Open Source realtime backend in 1 file","services":{"$$id":{"image":"coollabsio/pocketbase:$$core_version","volumes":["$$id-data:/app/pb_data"],"ports":["8080"]}}},{"templateVersion":"1.0.0","defaultVersion":"v1.5.1","documentation":"https://plausible.io/doc/","type":"plausibleanalytics-arm","name":"Plausible Analytics (ARM)","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DB=$$config_postgres_db"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"clickhouse/clickhouse-server:22.6-alpine","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"1.17","documentation":"https://docs.gitea.io","type":"gitea","name":"Gitea","description":"Gitea is a community managed lightweight code hosting solution written in Go.","labels":["storage","git"],"services":{"$$id":{"name":"Gitea","documentation":"https://docs.gitea.io","image":"gitea/gitea:$$core_version","volumes":["$$id-data:/data","/etc/timezone:/etc/timezone:ro","/etc/localtime:/etc/localtime:ro"],"environment":["USER_UID=1000","USER_GID=1000","DOMAIN=$$config_domain","SSH_DOMAIN=$$config_ssh_domain","ROOT_URL=$$config_root_url","SECRET_KEY=$$secret_secret_key","INTERNAL_TOKEN=$$secret_internal_token","SSH_PORT=22","START_SSH_SERVER=$$config_start_ssh_server"],"ports":["3000","22"],"proxy":[{"port":"22","hostPort":"$$config_hostport_ssh"}]}},"variables":[{"id":"$$config_hostport_ssh","name":"SSH_PORT","label":"SSH Port","defaultValue":"8022","description":"","required":true},{"id":"$$config_domain","name":"DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ssh_domain","name":"SSH_DOMAIN","label":"SSH Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_start_ssh_server","name":"START_SSH_SERVER","label":"Start SSH Server","defaultValue":"true","description":""},{"id":"$$config_root_url","name":"ROOT_URL","label":"Root URL of Gitea","defaultValue":"$$generate_fqdn_slash","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(32)","description":""},{"id":"$$secret_internal_token","name":"INTERNAL_TOKEN","label":"Internal JWT Token","defaultValue":"$$generate_token","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"20.0","documentation":"https://www.keycloak.org/documentation","type":"keycloak","name":"Keycloak","description":"Keycloak provides user federation, strong authentication, user management, fine-grained authorization, and more.","labels":["authentication","authorization","oidconnect","saml2"],"services":{"$$id":{"name":"Keycloak","command":"start --db=postgres --features=token-exchange --import-realm","depends_on":["$$id-postgresql"],"image":"quay.io/keycloak/keycloak:$$core_version","volumes":["$$id-import:/opt/keycloak/data/import"],"environment":["KC_HEALTH_ENABLED=true","KC_PROXY=edge","KC_DB=postgres","KC_HOSTNAME=$$config_keycloak_domain","KEYCLOAK_ADMIN=$$config_admin_user","KEYCLOAK_ADMIN_PASSWORD=$$secret_keycloak_admin_password","KC_DB_PASSWORD=$$secret_postgres_password","KC_DB_USERNAME=$$config_postgres_user","KC_DB_URL=$$secret_keycloak_database_url"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_keycloak_domain","name":"KEYCLOAK_DOMAIN","label":"Keycloak Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_keycloak_database_url","name":"KEYCLOAK_DATABASE_URL","label":"Keycloak Database Url","defaultValue":"jdbc:postgresql://$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$config_admin_user","name":"KEYCLOAK_ADMIN","label":"Keycloak Admin User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_keycloak_admin_password","name":"KEYCLOAK_ADMIN_PASSWORD","label":"Keycloak Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"keycloak","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v3.7","documentation":"https://github.com/freyacodes/Lavalink","description":"Standalone audio sending node based on Lavaplayer.","type":"lavalink","name":"Lavalink","labels":["discord","discord bot","audio","lavalink","jda"],"services":{"$$id":{"name":"Lavalink","image":"fredboat/lavalink:$$core_version","environment":[],"volumes":["$$id-lavalink:/lavalink"],"ports":["2333"],"files":[{"location":"/opt/Lavalink/application.yml","content":"server:\n port: $$config_port\n address: 0.0.0.0\nlavalink:\n server:\n password: \"$$secret_password\"\n sources:\n youtube: true\n bandcamp: true\n soundcloud: true\n twitch: true\n vimeo: true\n http: true\n local: false\n\nlogging:\n file:\n path: ./logs/\n\n level:\n root: INFO\n lavalink: INFO\n\n logback:\n rollingpolicy:\n max-file-size: 1GB\n max-history: 30"}]}},"variables":[{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"2333","required":true},{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"v1.8.9","documentation":"https://docs.appsmith.com/getting-started/setup/instance-configuration/","type":"appsmith","name":"Appsmith","description":"Fastest way to build internal apps over any database or API.","services":{"$$id":{"image":"appsmith/appsmith-ce:$$core_version","environment":["APPSMITH_MAIL_ENABLED=$$config_appsmith_mail_enabled","APPSMITH_DISABLE_TELEMETRY=$$config_appsmith_disable_telemetry","APPSMITH_DISABLE_INTERCOM=$$config_appsmith_disable_intercom"],"volumes":["$$id-stacks-data:/appsmith-stacks"],"ports":["80"]}},"variables":[{"id":"$$config_appsmith_mail_enabled","name":"APPSMITH_MAIL_ENABLED","label":"Enable Mail","defaultValue":"false","description":""},{"id":"$$config_appsmith_disable_telemetry","name":"APPSMITH_DISABLE_TELEMETRY","label":"Disable Telemetry","defaultValue":"true","description":""},{"id":"$$config_appsmith_disable_intercom","name":"APPSMITH_DISABLE_INTERCOM","label":"Disable Intercom","defaultValue":"true","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.57.4","documentation":"https://hub.docker.com/r/zadam/trilium","description":"A hierarchical note taking application with focus on building large personal knowledge bases.","labels":["personal","knowledge","notes","wiki"],"type":"trilium","name":"Trilium Notes","services":{"$$id":{"image":"zadam/trilium:$$core_version","environment":[],"volumes":["$$id-trilium:/home/node/trilium-data"],"ports":["8080"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.18.5","documentation":"https://hub.docker.com/r/louislam/uptime-kuma","description":"A free & fancy self-hosted monitoring tool.","labels":["uptime"],"type":"uptimekuma","name":"UptimeKuma","services":{"$$id":{"image":"louislam/uptime-kuma:$$core_version","environment":[],"volumes":["$$id-uptimekuma:/app/data"],"ports":["3001"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"5.8","documentation":"https://hub.docker.com/r/silviof/docker-languagetool","description":"A multilingual grammar, style and spell checker.","type":"languagetool","name":"LanguageTool","services":{"$$id":{"image":"silviof/docker-languagetool:$$core_version","environment":[],"volumes":["$$id-ngrams:/ngrams"],"ports":["8010"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.26.0","documentation":"https://hub.docker.com/r/vaultwarden/server","description":"Bitwarden compatible server written in Rust.","type":"vaultwarden","name":"VaultWarden","labels":["bitwarden","password manager"],"services":{"$$id":{"image":"vaultwarden/server:$$core_version","environment":[],"volumes":["$$id-data:/data"],"ports":["80"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"9.3.1","documentation":"https://hub.docker.com/r/grafana/grafana","type":"grafana","name":"Grafana","description":"Grafana allows you to query, visualize, alert on and understand your metrics.","labels":["monitoring","metrics","dashboard"],"services":{"$$id":{"image":"grafana/grafana:$$core_version","environment":[],"volumes":["$$id-config:/etc/grafana","$$id-grafana:/var/lib/grafana"],"ports":["3000"]}},"variables":[]},{"templateVersion":"1.0.0","defaultVersion":"1.1.2","documentation":"https://appwrite.io/docs","type":"appwrite","name":"Appwrite","description":"Secure Backend Server for Web, Mobile & Flutter Developers.","labels":["serverless","backend","storage","api"],"services":{"$$id":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_LOCALE=$$config__app_locale","_APP_CONSOLE_WHITELIST_ROOT=$$config__app_console_whitelist_root","_APP_CONSOLE_WHITELIST_EMAILS=$$config__app_console_whitelist_emails","_APP_CONSOLE_WHITELIST_IPS=$$config__app_console_whitelist_ips","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_SYSTEM_RESPONSE_FORMAT=$$config__app_system_response_format","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPTIONS_FORCE_HTTPS=$$config__app_options_force_https","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_STORAGE_LIMIT=$$config__app_storage_limit","_APP_STORAGE_PREVIEW_LIMIT=$$config__app_storage_preview_limit","_APP_STORAGE_ANTIVIRUS=$$config__app_storage_antivirus_enabled","_APP_STORAGE_ANTIVIRUS_HOST=$$config__app_storage_antivirus_host","_APP_STORAGE_ANTIVIRUS_PORT=$$config__app_storage_antivirus_port","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_FUNCTIONS_SIZE_LIMIT=$$config__app_functions_size_limit","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STATSD_HOST=$$config__app_statsd_host","_APP_STATSD_PORT=$$config__app_statsd_port","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-config:/storage/config","$$id-certificates:/storage/certificates","$$id-functions:/storage/functions"],"ports":["80"],"proxy":[{"port":"80"}]},"$$id-executor":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_FUNCTIONS_BUILD_TIMEOUT=$$config__app_functions_build_timeout","_APP_FUNCTIONS_CONTAINERS=$$config__app_functions_containers","_APP_FUNCTIONS_RUNTIMES=$$config__app_functions_runtimes","_APP_FUNCTIONS_CPUS=$$config__app_functions_cpus","_APP_FUNCTIONS_MEMORY=$$config__app_functions_memory_allocated","_APP_FUNCTIONS_MEMORY_SWAP=$$config__app_functions_memory_swap","_APP_FUNCTIONS_INACTIVE_THRESHOLD=$$config__app_functions_inactive_threshold","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-functions:/storage/functions","$$id-builds:/storage/builds","/var/run/docker.sock:/var/run/docker.sock","/tmp:/tmp:rw"],"entrypoint":"executor"},"$$id-influxdb":{"image":"appwrite/influxdb:1.5.0","environment":[],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-maintenance":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_MAINTENANCE_INTERVAL=$$config__app_maintenance_interval","_APP_MAINTENANCE_RETENTION_EXECUTION=$$config__app_maintenance_retention_execution","_APP_MAINTENANCE_RETENTION_CACHE=$$config__app_maintenance_retention_cache","_APP_MAINTENANCE_RETENTION_ABUSE=$$config__app_maintenance_retention_abuse","_APP_MAINTENANCE_RETENTION_AUDIT=$$config__app_maintenance_retention_audit","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"maintenance"},"$$id-mariadb":{"image":"mariadb:10.7","command":"--innodb-flush-method fsync","environment":["MARIADB_ROOT_PASSWORD=$$secret__app_db_root_pass","MARIADB_DATABASE=$$config__app_db_schema","MARIADB_USER=$$config__app_db_user","MARIADB_PASSWORD=$$secret__app_db_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-mariadb:/var/lib/mysql"]},"$$id-realtime":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_WORKER_PER_CORE=$$config__app_worker_per_core","_APP_OPTIONS_ABUSE=$$config__app_options_abuse","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_USAGE_STATS=$$config__app_usage_stats","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"realtime","proxy":[{"port":"80","pathPrefix":"/v1/realtime"}]},"$$id-redis":{"image":"redis:7.0.4-alpine","command":"--maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5","environment":[],"volumes":["$$id-redis:/data"]},"$$id-schedule":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"schedule"},"$$id-telegraf":{"image":"appwrite/telegraf:1.4.0","environment":["_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-influxdb:/var/lib/influxdb"]},"$$id-usage-database":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type database"},"$$id-usage":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_INFLUXDB_HOST=$$config__app_influxdb_host","_APP_INFLUXDB_PORT=$$config__app_influxdb_port","_APP_USAGE_TIMESERIES_INTERVAL=$$config__app_usage_timeseries_interval","_APP_USAGE_DATABASE_INTERVAL=$$config__app_usage_database_interval","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"usage --type timeseries"},"$$id-worker-audits":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-audits"},"$$id-worker-builds":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-builds"},"$$id-worker-certificates":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_DOMAIN=$$config__app_domain","_APP_DOMAIN_TARGET=$$config__app_domain_target","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-config:/storage/config","$$id-certificates:/storage/certificates"],"entrypoint":"worker-certificates"},"$$id-worker-databases":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-databases"},"$$id-worker-deletes":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_STORAGE_DEVICE=$$config__app_storage_device","_APP_STORAGE_S3_ACCESS_KEY=$$secret__app_storage_s3_access_key","_APP_STORAGE_S3_SECRET=$$secret__app_storage_s3_secret","_APP_STORAGE_S3_REGION=$$config__app_storage_s3_region","_APP_STORAGE_S3_BUCKET=$$config__app_storage_s3_bucket","_APP_STORAGE_DO_SPACES_ACCESS_KEY=$$secret__app_storage_do_spaces_access_key","_APP_STORAGE_DO_SPACES_SECRET=$$secret__app_storage_do_spaces_secret","_APP_STORAGE_DO_SPACES_REGION=$$config__app_storage_do_spaces_region","_APP_STORAGE_DO_SPACES_BUCKET=$$config__app_storage_do_spaces_bucket","_APP_STORAGE_BACKBLAZE_ACCESS_KEY=$$secret__app_storage_backblaze_access_key","_APP_STORAGE_BACKBLAZE_SECRET=$$secret__app_storage_backblaze_secret","_APP_STORAGE_BACKBLAZE_REGION=$$config__app_storage_backblaze_region","_APP_STORAGE_BACKBLAZE_BUCKET=$$config__app_storage_backblaze_bucket","_APP_STORAGE_LINODE_ACCESS_KEY=$$secret__app_storage_linode_access_key","_APP_STORAGE_LINODE_SECRET=$$secret__app_storage_linode_secret","_APP_STORAGE_LINODE_REGION=$$config__app_storage_linode_region","_APP_STORAGE_LINODE_BUCKET=$$config__app_storage_linode_bucket","_APP_STORAGE_WASABI_ACCESS_KEY=$$secret__app_storage_wasabi_access_key","_APP_STORAGE_WASABI_SECRET=$$secret__app_storage_wasabi_secret","_APP_STORAGE_WASABI_REGION=$$config__app_storage_wasabi_region","_APP_STORAGE_WASABI_BUCKET=$$config__app_storage_wasabi_bucket","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":["$$id-uploads:/storage/uploads","$$id-cache:/storage/cache","$$id-functions:/storage/functions","$$id-builds:/storage/builds","$$id-certificates:/storage/certificates"],"entrypoint":"worker-deletes"},"$$id-worker-functions":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_DB_HOST=$$config__app_db_host","_APP_DB_PORT=$$config__app_db_port","_APP_DB_SCHEMA=$$config__app_db_schema","_APP_DB_USER=$$config__app_db_user","_APP_DB_PASS=$$secret__app_db_pass","_APP_FUNCTIONS_TIMEOUT=$$config__app_functions_timeout","_APP_EXECUTOR_SECRET=$$secret__app_executor_secret","_APP_EXECUTOR_HOST=$$config__app_executor_host","_APP_USAGE_STATS=$$config__app_usage_stats","DOCKERHUB_PULL_USERNAME=$$config_dockerhub_pull_username","DOCKERHUB_PULL_PASSWORD=$$secret_dockerhub_pull_password","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-functions"},"$$id-worker-mails":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_EMAIL_NAME=$$config__app_system_email_name","_APP_SYSTEM_EMAIL_ADDRESS=$$config__app_system_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMTP_HOST=$$config__app_smtp_host","_APP_SMTP_PORT=$$config__app_smtp_port","_APP_SMTP_SECURE=$$config__app_smtp_secure","_APP_SMTP_USERNAME=$$config__app_smtp_username","_APP_SMTP_PASSWORD=$$secret__app_smtp_password","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-mails"},"$$id-worker-messaging":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_SMS_PROVIDER=$$config__app_sms_provider","_APP_SMS_FROM=$$config__app_sms_from","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-messaging"},"$$id-worker-webhooks":{"image":"appwrite/appwrite:$$core_version","environment":["_APP_ENV=$$config__app_env","_APP_OPENSSL_KEY_V1=$$secret__app_openssl_key_v1","_APP_SYSTEM_SECURITY_EMAIL_ADDRESS=$$config__app_system_security_email_address","_APP_REDIS_HOST=$$config__app_redis_host","_APP_REDIS_PORT=$$config__app_redis_port","_APP_REDIS_USER=$$config__app_redis_user","_APP_REDIS_PASS=$$secret__app_redis_pass","_APP_LOGGING_PROVIDER=$$config__app_logging_provider","_APP_LOGGING_CONFIG=$$config__app_logging_config","OPEN_RUNTIMES_NETWORK=$$config_open_runtimes_network"],"volumes":[],"entrypoint":"worker-webhooks"}},"variables":[{"id":"$$config__app_influxdb_host","name":"_APP_INFLUXDB_HOST","label":"InfluxDB | _APP_INFLUXDB_HOST","defaultValue":"$$id-influxdb","description":""},{"id":"$$config__app_influxdb_port","name":"_APP_INFLUXDB_PORT","label":"InfluxDB | _APP_INFLUXDB_PORT","defaultValue":"8086","description":"InfluxDB server TCP port."},{"id":"$$config__app_env","name":"_APP_ENV","label":"General | _APP_ENV","defaultValue":"production","description":"Set your server running environment."},{"id":"$$config__app_worker_per_core","name":"_APP_WORKER_PER_CORE","label":"General | _APP_WORKER_PER_CORE","defaultValue":"6","description":"Internal Worker per core for the API, Realtime and Executor containers. Can be configured to optimize performance."},{"id":"$$config__app_locale","name":"_APP_LOCALE","label":"General | _APP_LOCALE","defaultValue":"en","description":"Set your Appwrite's locale. By default, the locale is set to 'en'."},{"id":"$$config__app_console_whitelist_root","name":"_APP_CONSOLE_WHITELIST_ROOT","label":"General | _APP_CONSOLE_WHITELIST_ROOT","defaultValue":"enabled","description":"This option allows you to disable the creation of new users on the Appwrite console. When enabled only 1 user will be able to use the registration form. New users can be added by inviting them to your project. By default this option is enabled."},{"id":"$$config__app_console_whitelist_emails","name":"_APP_CONSOLE_WHITELIST_EMAILS","label":"General | _APP_CONSOLE_WHITELIST_EMAILS","defaultValue":"","description":"This option allows you to limit creation of new users on the Appwrite console. This option is very useful for small teams or sole developers. To enable it, pass a list of allowed email addresses separated by a comma."},{"id":"$$config__app_console_whitelist_ips","name":"_APP_CONSOLE_WHITELIST_IPS","label":"General | _APP_CONSOLE_WHITELIST_IPS","defaultValue":"","description":"This last option allows you to limit creation of users in Appwrite console for users sharing the same set of IP addresses. This option is very useful for team working with a VPN service or a company IP.\\n\\nTo enable/activate this option, pass a list of allowed IP addresses separated by a comma."},{"id":"$$config__app_system_email_name","name":"_APP_SYSTEM_EMAIL_NAME","label":"General | _APP_SYSTEM_EMAIL_NAME","defaultValue":"Appwrite","description":"This is the sender name value that will appear on email messages sent to developers from the Appwrite console. You can use url encoded strings for spaces and special chars."},{"id":"$$config__app_system_email_address","name":"_APP_SYSTEM_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_EMAIL_ADDRESS","defaultValue":"team@appwrite.io","description":"This is the sender email address that will appear on email messages sent to developers from the Appwrite console. You should choose an email address that is allowed to be used from your SMTP server to avoid the server email ending in the users' SPAM folders."},{"id":"$$config__app_system_security_email_address","name":"_APP_SYSTEM_SECURITY_EMAIL_ADDRESS","label":"General | _APP_SYSTEM_SECURITY_EMAIL_ADDRESS","defaultValue":"certs@appwrite.io","description":"This is the email address used to issue SSL certificates for custom domains or the user agent in your webhooks payload."},{"id":"$$config__app_system_response_format","name":"_APP_SYSTEM_RESPONSE_FORMAT","label":"General | _APP_SYSTEM_RESPONSE_FORMAT","defaultValue":"","description":"Use this environment variable to set the default Appwrite HTTP response format to support an older version of Appwrite. This option is useful to overcome breaking changes between versions. You can also use the X-Appwrite-Response-Format HTTP request header to overwrite the response for a specific request. This variable accepts any valid Appwrite version. To use the current version format, leave the value of the variable empty."},{"id":"$$config__app_options_abuse","name":"_APP_OPTIONS_ABUSE","label":"General | _APP_OPTIONS_ABUSE","defaultValue":"enabled","description":"Allows you to disable abuse checks and API rate limiting. By default, set to 'enabled'. To cancel the abuse checking, set to 'disabled'. It is not recommended to disable this check-in a production environment."},{"id":"$$config__app_options_force_https","name":"_APP_OPTIONS_FORCE_HTTPS","label":"General | _APP_OPTIONS_FORCE_HTTPS","defaultValue":"disabled","description":"Allows you to force HTTPS connection to your API. This feature redirects any HTTP call to HTTPS and adds the 'Strict-Transport-Security' header to all HTTP responses."},{"id":"$$secret__app_openssl_key_v1","name":"_APP_OPENSSL_KEY_V1","label":"General | _APP_OPENSSL_KEY_V1","defaultValue":"$$generate_hex(256)","description":"This is your server private secret key that is used to encrypt all sensitive data on your server. Appwrite server encrypts all secret data on your server like webhooks, HTTP passwords, user sessions, and storage files. Keep it a secret and have a backup for it."},{"id":"$$config__app_domain","name":"_APP_DOMAIN","label":"General | _APP_DOMAIN","defaultValue":"$$generate_domain","description":"Your Appwrite domain address. When setting a public suffix domain, Appwrite will attempt to issue a valid SSL certificate automatically. When used with a dev domain, Appwrite will assign a self-signed SSL certificate. The default value is 'localhost'."},{"id":"$$config__app_domain_target","name":"_APP_DOMAIN_TARGET","label":"General | _APP_DOMAIN_TARGET","defaultValue":"$$generate_fqdn","description":"A DNS A record hostname to serve as a CNAME target for your Appwrite custom domains. You can use the same value as used for the Appwrite '_APP_DOMAIN' variable. The default value is 'localhost'."},{"id":"$$config__app_redis_host","name":"_APP_REDIS_HOST","label":"Redis | _APP_REDIS_HOST","defaultValue":"$$id-redis","description":""},{"id":"$$config__app_redis_port","name":"_APP_REDIS_PORT","label":"Redis | _APP_REDIS_PORT","defaultValue":"6379","description":"Redis server TCP port."},{"id":"$$config__app_redis_user","name":"_APP_REDIS_USER","label":"Redis | _APP_REDIS_USER","defaultValue":"","description":"Redis server user. This is an optional variable. Default value is an empty string."},{"id":"$$secret__app_redis_pass","name":"_APP_REDIS_PASS","label":"Redis | _APP_REDIS_PASS","defaultValue":"","description":"Redis server password. This is an optional variable. Default value is an empty string."},{"id":"$$config__app_db_host","name":"_APP_DB_HOST","label":"MariaDB | _APP_DB_HOST","defaultValue":"$$id-mariadb","description":""},{"id":"$$config__app_db_port","name":"_APP_DB_PORT","label":"MariaDB | _APP_DB_PORT","defaultValue":"3306","description":"MariaDB server TCP port."},{"id":"$$config__app_db_schema","name":"_APP_DB_SCHEMA","label":"MariaDB | _APP_DB_SCHEMA","defaultValue":"appwrite","description":"MariaDB server database schema."},{"id":"$$config__app_db_user","name":"_APP_DB_USER","label":"MariaDB | _APP_DB_USER","defaultValue":"user","description":"MariaDB server user name."},{"id":"$$secret__app_db_root_pass","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB | MARIADB_ROOT_PASSWORD","defaultValue":"$$generate_hex(16)","description":"MariaDB server root user password."},{"id":"$$secret__app_db_pass","name":"_APP_DB_PASS","label":"MariaDB | _APP_DB_PASS","defaultValue":"$$generate_hex(16)","description":"MariaDB server user password."},{"id":"$$config__app_smtp_host","name":"_APP_SMTP_HOST","label":"SMTP | _APP_SMTP_HOST","defaultValue":"","description":"SMTP server host name address. Use an empty string to disable all mail sending from the server. The default value for this variable is an empty string."},{"id":"$$config__app_smtp_port","name":"_APP_SMTP_PORT","label":"SMTP | _APP_SMTP_PORT","defaultValue":"","description":"SMTP server TCP port. Empty by default."},{"id":"$$config__app_smtp_secure","name":"_APP_SMTP_SECURE","label":"SMTP | _APP_SMTP_SECURE","defaultValue":"","description":"SMTP secure connection protocol. Empty by default, change to 'tls' if running on a secure connection."},{"id":"$$config__app_smtp_username","name":"_APP_SMTP_USERNAME","label":"SMTP | _APP_SMTP_USERNAME","defaultValue":"","description":"SMTP server user name. Empty by default."},{"id":"$$secret__app_smtp_password","name":"_APP_SMTP_PASSWORD","label":"SMTP | _APP_SMTP_PASSWORD","defaultValue":"","description":"SMTP server user password. Empty by default."},{"id":"$$config__app_usage_stats","name":"_APP_USAGE_STATS","label":"General | _APP_USAGE_STATS","defaultValue":"enabled","description":"This variable allows you to disable the collection and displaying of usage stats. This value is set to 'enabled' by default, to disable the usage stats set the value to 'disabled'. When disabled, it's recommended to turn off the Worker Usage, Influxdb and Telegraf containers for better resource usage."},{"id":"$$config__app_storage_limit","name":"_APP_STORAGE_LIMIT","label":"Storage | _APP_STORAGE_LIMIT","defaultValue":"30000000","description":"Maximum file size allowed for file upload. The default value is 30MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_preview_limit","name":"_APP_STORAGE_PREVIEW_LIMIT","label":"Storage | _APP_STORAGE_PREVIEW_LIMIT","defaultValue":"20000000","description":"Maximum file size allowed for file image preview. The default value is 20MB. You should pass your size limit value in bytes."},{"id":"$$config__app_storage_antivirus_enabled","name":"_APP_STORAGE_ANTIVIRUS","label":"Storage | _APP_STORAGE_ANTIVIRUS","defaultValue":"disabled","description":"This variable allows you to disable the internal anti-virus scans. This value is set to 'disabled' by default, to enable the scans set the value to 'enabled'. Before enabling, you must add the ClamAV service and depend on it on main Appwrite service."},{"id":"$$config__app_storage_antivirus_host","name":"_APP_STORAGE_ANTIVIRUS_HOST","label":"Storage | _APP_STORAGE_ANTIVIRUS_HOST","defaultValue":"clamav","description":"ClamAV server host name address."},{"id":"$$config__app_storage_antivirus_port","name":"_APP_STORAGE_ANTIVIRUS_PORT","label":"Storage | _APP_STORAGE_ANTIVIRUS_PORT","defaultValue":"3310","description":"ClamAV server TCP port."},{"id":"$$config__app_storage_device","name":"_APP_STORAGE_DEVICE","label":"Storage | _APP_STORAGE_DEVICE","defaultValue":"Local","description":"Select default storage device. The default value is 'Local'. List of supported adapters are 'Local', 'S3', 'DOSpaces', 'Backblaze', 'Linode' and 'Wasabi'."},{"id":"$$secret__app_storage_s3_access_key","name":"_APP_STORAGE_S3_ACCESS_KEY","label":"Storage | _APP_STORAGE_S3_ACCESS_KEY","defaultValue":"","description":"AWS S3 storage access key. Required when the storage adapter is set to S3. You can get your access key from your AWS console."},{"id":"$$secret__app_storage_s3_secret","name":"_APP_STORAGE_S3_SECRET","label":"Storage | _APP_STORAGE_S3_SECRET","defaultValue":"","description":"AWS S3 storage secret key. Required when the storage adapter is set to S3. You can get your secret key from your AWS console."},{"id":"$$config__app_storage_s3_region","name":"_APP_STORAGE_S3_REGION","label":"Storage | _APP_STORAGE_S3_REGION","defaultValue":"us-east-1","description":"AWS S3 storage region. Required when storage adapter is set to S3. You can find your region info for your bucket from AWS console."},{"id":"$$config__app_storage_s3_bucket","name":"_APP_STORAGE_S3_BUCKET","label":"Storage | _APP_STORAGE_S3_BUCKET","defaultValue":"","description":"AWS S3 storage bucket. Required when storage adapter is set to S3. You can create buckets in your AWS console."},{"id":"$$secret__app_storage_do_spaces_access_key","name":"_APP_STORAGE_DO_SPACES_ACCESS_KEY","label":"Storage | _APP_STORAGE_DO_SPACES_ACCESS_KEY","defaultValue":"","description":"DigitalOcean spaces access key. Required when the storage adapter is set to DOSpaces. You can get your access key from your DigitalOcean console."},{"id":"$$secret__app_storage_do_spaces_secret","name":"_APP_STORAGE_DO_SPACES_SECRET","label":"Storage | _APP_STORAGE_DO_SPACES_SECRET","defaultValue":"","description":"DigitalOcean spaces secret key. Required when the storage adapter is set to DOSpaces. You can get your secret key from your DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_region","name":"_APP_STORAGE_DO_SPACES_REGION","label":"Storage | _APP_STORAGE_DO_SPACES_REGION","defaultValue":"us-east-1","description":"DigitalOcean spaces region. Required when storage adapter is set to DOSpaces. You can find your region info for your space from DigitalOcean console."},{"id":"$$config__app_storage_do_spaces_bucket","name":"_APP_STORAGE_DO_SPACES_BUCKET","label":"Storage | _APP_STORAGE_DO_SPACES_BUCKET","defaultValue":"","description":"DigitalOcean spaces bucket. Required when storage adapter is set to DOSpaces. You can create spaces in your DigitalOcean console."},{"id":"$$secret__app_storage_backblaze_access_key","name":"_APP_STORAGE_BACKBLAZE_ACCESS_KEY","label":"Storage | _APP_STORAGE_BACKBLAZE_ACCESS_KEY","defaultValue":"","description":"Backblaze access key. Required when the storage adapter is set to Backblaze. Your Backblaze keyID will be your access key. You can get your keyID from your Backblaze console."},{"id":"$$secret__app_storage_backblaze_secret","name":"_APP_STORAGE_BACKBLAZE_SECRET","label":"Storage | _APP_STORAGE_BACKBLAZE_SECRET","defaultValue":"","description":"Backblaze secret key. Required when the storage adapter is set to Backblaze. Your Backblaze applicationKey will be your secret key. You can get your applicationKey from your Backblaze console."},{"id":"$$config__app_storage_backblaze_region","name":"_APP_STORAGE_BACKBLAZE_REGION","label":"Storage | _APP_STORAGE_BACKBLAZE_REGION","defaultValue":"us-west-004","description":"Backblaze region. Required when storage adapter is set to Backblaze. You can find your region info from your Backblaze console."},{"id":"$$config__app_storage_backblaze_bucket","name":"_APP_STORAGE_BACKBLAZE_BUCKET","label":"Storage | _APP_STORAGE_BACKBLAZE_BUCKET","defaultValue":"","description":"Backblaze bucket. Required when storage adapter is set to Backblaze. You can create your bucket from your Backblaze console."},{"id":"$$secret__app_storage_linode_access_key","name":"_APP_STORAGE_LINODE_ACCESS_KEY","label":"Storage | _APP_STORAGE_LINODE_ACCESS_KEY","defaultValue":"","description":"Linode object storage access key. Required when the storage adapter is set to Linode. You can get your access key from your Linode console."},{"id":"$$secret__app_storage_linode_secret","name":"_APP_STORAGE_LINODE_SECRET","label":"Storage | _APP_STORAGE_LINODE_SECRET","defaultValue":"","description":"Linode object storage secret key. Required when the storage adapter is set to Linode. You can get your secret key from your Linode console."},{"id":"$$config__app_storage_linode_region","name":"_APP_STORAGE_LINODE_REGION","label":"Storage | _APP_STORAGE_LINODE_REGION","defaultValue":"eu-central-1","description":"Linode object storage region. Required when storage adapter is set to Linode. You can find your region info from your Linode console."},{"id":"$$config__app_storage_linode_bucket","name":"_APP_STORAGE_LINODE_BUCKET","label":"Storage | _APP_STORAGE_LINODE_BUCKET","defaultValue":"","description":"Linode object storage bucket. Required when storage adapter is set to Linode. You can create buckets in your Linode console."},{"id":"$$secret__app_storage_wasabi_access_key","name":"_APP_STORAGE_WASABI_ACCESS_KEY","label":"Storage | _APP_STORAGE_WASABI_ACCESS_KEY","defaultValue":"","description":"Wasabi access key. Required when the storage adapter is set to Wasabi. You can get your access key from your Wasabi console."},{"id":"$$secret__app_storage_wasabi_secret","name":"_APP_STORAGE_WASABI_SECRET","label":"Storage | _APP_STORAGE_WASABI_SECRET","defaultValue":"","description":"Wasabi secret key. Required when the storage adapter is set to Wasabi. You can get your secret key from your Wasabi console."},{"id":"$$config__app_storage_wasabi_region","name":"_APP_STORAGE_WASABI_REGION","label":"Storage | _APP_STORAGE_WASABI_REGION","defaultValue":"eu-central-1","description":"Wasabi region. Required when storage adapter is set to Wasabi. You can find your region info from your Wasabi console."},{"id":"$$config__app_storage_wasabi_bucket","name":"_APP_STORAGE_WASABI_BUCKET","label":"Storage | _APP_STORAGE_WASABI_BUCKET","defaultValue":"","description":"Wasabi bucket. Required when storage adapter is set to Wasabi. You can create buckets in your Wasabi console."},{"id":"$$config__app_functions_size_limit","name":"_APP_FUNCTIONS_SIZE_LIMIT","label":"Functions | _APP_FUNCTIONS_SIZE_LIMIT","defaultValue":"30000000","description":"The maximum size deployment in bytes. The default value is 30MB."},{"id":"$$config__app_functions_timeout","name":"_APP_FUNCTIONS_TIMEOUT","label":"Functions | _APP_FUNCTIONS_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when creating a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_build_timeout","name":"_APP_FUNCTIONS_BUILD_TIMEOUT","label":"Functions | _APP_FUNCTIONS_BUILD_TIMEOUT","defaultValue":"900","description":"The maximum number of seconds allowed as a timeout value when building a new function. The default value is 900 seconds."},{"id":"$$config__app_functions_containers","name":"_APP_FUNCTIONS_CONTAINERS","label":"Functions | _APP_FUNCTIONS_CONTAINERS","defaultValue":"10","description":"The maximum number of containers Appwrite is allowed to keep alive in the background for function environments. Running containers allow faster execution time as there is no need to recreate each container every time a function gets executed. The default value is 10."},{"id":"$$config__app_functions_cpus","name":"_APP_FUNCTIONS_CPUS","label":"Functions | _APP_FUNCTIONS_CPUS","defaultValue":"","description":"The maximum number of CPU core a single cloud function is allowed to use. Please note that setting a value higher than available cores will result in a function error, which might result in an error. The default value is empty. When it's empty, CPU limit will be disabled."},{"id":"$$config__app_functions_memory_allocated","name":"_APP_FUNCTIONS_MEMORY","label":"Functions | _APP_FUNCTIONS_MEMORY","defaultValue":"","description":"The maximum amount of memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, memory limit will be disabled."},{"id":"$$config__app_functions_memory_swap","name":"_APP_FUNCTIONS_MEMORY_SWAP","label":"Functions | _APP_FUNCTIONS_MEMORY_SWAP","defaultValue":"","description":"The maximum amount of swap memory a single cloud function is allowed to use in megabytes. The default value is empty. When it's empty, swap memory limit will be disabled."},{"id":"$$config__app_functions_runtimes","name":"_APP_FUNCTIONS_RUNTIMES","label":"Functions | _APP_FUNCTIONS_RUNTIMES","defaultValue":"node-18.0","description":"This option allows you to limit the available environments for cloud functions. This option is very useful for low-cost servers to safe disk space.\nTo enable/activate this option, pass a list of allowed environments separated by a comma.\nCurrently, supported environments are: node-14.5, node-16.0, node-18.0, php-8.0, php-8.1, ruby-3.0, ruby-3.1, python-3.8, python-3.9, python-3.10, deno-1.21, deno-1.24, dart-2.15, dart-2.16, dart-2.17, dotnet-3.1, dotnet-6.0, java-8.0, java-11.0, java-17.0, java-18.0, swift-5.5, kotlin-1.6, cpp-17.0"},{"id":"$$secret__app_executor_secret","name":"_APP_EXECUTOR_SECRET","label":"Functions | _APP_EXECUTOR_SECRET","defaultValue":"$$generate_hex(16)","description":"The secret key used by Appwrite to communicate with the function executor."},{"id":"$$config__app_executor_host","name":"_APP_EXECUTOR_HOST","label":"","defaultValue":"http://$$id-executor/v1","description":""},{"id":"$$config__app_logging_provider","name":"_APP_LOGGING_PROVIDER","label":"General | _APP_LOGGING_PROVIDER","defaultValue":"","description":"This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of 'sentry', 'raygun', 'appsignal', 'logowl'"},{"id":"$$config__app_logging_config","name":"_APP_LOGGING_CONFIG","label":"General | _APP_LOGGING_CONFIG","defaultValue":"","description":"This variable configures authentication to 3rd party error logging providers. If using Sentry, this should be 'SENTRY_API_KEY;SENTRY_APP_ID'. If using Raygun, this should be Raygun API key. If using AppSignal, this should be AppSignal API key. If using LogOwl, this should be LogOwl Service Ticket."},{"id":"$$config__app_statsd_host","name":"_APP_STATSD_HOST","label":"","defaultValue":"$$id-telegraf","description":""},{"id":"$$config__app_statsd_port","name":"_APP_STATSD_PORT","label":"StatsD | _APP_STATSD_PORT","defaultValue":"8125","description":"StatsD server TCP port."},{"id":"$$config__app_maintenance_interval","name":"_APP_MAINTENANCE_INTERVAL","label":"Functions | _APP_MAINTENANCE_INTERVAL","defaultValue":"86400","description":"Interval value containing the number of seconds that the Appwrite maintenance process should wait before executing system cleanups and optimizations. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_execution","name":"_APP_MAINTENANCE_RETENTION_EXECUTION","label":"Functions | _APP_MAINTENANCE_RETENTION_EXECUTION","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_maintenance_retention_cache","name":"_APP_MAINTENANCE_RETENTION_CACHE","label":"Functions | _APP_MAINTENANCE_RETENTION_CACHE","defaultValue":"2592000","description":"The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days)."},{"id":"$$config__app_maintenance_retention_abuse","name":"_APP_MAINTENANCE_RETENTION_ABUSE","label":"Functions | _APP_MAINTENANCE_RETENTION_ABUSE","defaultValue":"86400","description":"The maximum duration (in seconds) upto which to retain abuse logs. The default value is 86400 seconds (1 day)."},{"id":"$$config__app_maintenance_retention_audit","name":"_APP_MAINTENANCE_RETENTION_AUDIT","label":"Functions | _APP_MAINTENANCE_RETENTION_AUDIT","defaultValue":"1209600","description":"The maximum duration (in seconds) upto which to retain audit logs. The default value is 1209600 seconds (14 days)."},{"id":"$$config__app_sms_provider","name":"_APP_SMS_PROVIDER","label":"Phone | _APP_SMS_PROVIDER","defaultValue":"","description":"Provider used for delivering SMS for Phone authentication. Use the following format: 'sms://[USER]:[SECRET]@[PROVIDER]'. Available providers are twilio, text-magic, telesign, msg91, and vonage."},{"id":"$$config__app_sms_from","name":"_APP_SMS_FROM","label":"Phone | _APP_SMS_FROM","defaultValue":"","description":"Phone number used for sending out messages. Must start with a leading '+' and maximum of 15 digits without spaces (+123456789)."},{"id":"$$config__app_functions_inactive_threshold","name":"_APP_FUNCTIONS_INACTIVE_THRESHOLD","label":"Functions | _APP_FUNCTIONS_INACTIVE_THRESHOLD","defaultValue":"60","description":"The minimum time a function can be inactive before it's container is shutdown and put to sleep. The default value is 60 seconds"},{"id":"$$config_open_runtimes_network","name":"OPEN_RUNTIMES_NETWORK","label":"","defaultValue":"$$generate_network","description":""},{"id":"$$config_dockerhub_pull_username","name":"DOCKERHUB_PULL_USERNAME","label":"Functions | DOCKERHUB_PULL_USERNAME","defaultValue":"","description":"The username for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$secret_dockerhub_pull_password","name":"DOCKERHUB_PULL_PASSWORD","label":"Functions | DOCKERHUB_PULL_PASSWORD","defaultValue":"","description":"The password for hub.docker.com. This variable is used to pull images from hub.docker.com."},{"id":"$$config__app_usage_timeseries_interval","name":"_APP_USAGE_TIMESERIES_INTERVAL","label":"General | _APP_USAGE_TIMESERIES_INTERVAL","defaultValue":"30","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats and syncing it to mariadb from InfluxDB. The default value is 30 seconds."},{"id":"$$config__app_usage_database_interval","name":"_APP_USAGE_DATABASE_INTERVAL","label":"General | _APP_USAGE_DATABASE_INTERVAL","defaultValue":"900","description":"Interval value containing the number of seconds that the Appwrite usage process should wait before aggregating stats from data in Appwrite Database. The default value is 15 minutes."}]},{"templateVersion":"1.0.0","defaultVersion":"latest","documentation":"https://docs.weblate.org/en/latest/admin/install/docker.html","description":"A copylefted libre software web-based continuous localization system.","type":"weblate","name":"Weblate","labels":["translate","localization"],"services":{"$$id":{"name":"Weblate","depends_on":["$$id-postgresql","$$id-redis"],"image":"weblate/weblate:$$core_version","volumes":["$$id-data:/app/data"],"environment":["WEBLATE_SITE_DOMAIN=$$config_weblate_site_domain","WEBLATE_ADMIN_PASSWORD=$$secret_weblate_admin_password","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_USER=$$config_postgres_user","POSTGRES_DATABASE=$$config_postgres_db","POSTGRES_HOST=$$id-postgresql","POSTGRES_PORT=5432","REDIS_HOST=$$id-redis"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_weblate_site_domain","name":"WEBLATE_SITE_DOMAIN","label":"Weblate Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$secret_weblate_admin_password","name":"WEBLATE_ADMIN_PASSWORD","label":"Weblate Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"weblate","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"2022.12.12-966e9c3c","documentation":"https://docs.searxng.org/","type":"searxng","name":"SearXNG","description":"Free internet metasearch engine which aggregates results from more than 70 search services.","services":{"$$id":{"name":"SearXNG","depends_on":["$$id-redis"],"image":"searxng/searxng:$$core_version","volumes":["$$id-searxng:/etc/searxng"],"environment":["SEARXNG_BASE_URL=$$config_searxng_base_url"],"ports":["8080"],"cap_drop":["ALL"],"cap_add":["CHOWN","SETGID","SETUID","DAC_OVERRIDE"],"files":[{"location":"/etc/searxng/settings.yml","content":"\n # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings\n use_default_settings: true\n server:\n secret_key: $$secret_secret_key\n limiter: true\n image_proxy: true\n ui:\n static_use_hash: true\n redis:\n url: redis://:$$secret_redis_password@$$id-redis:6379/0"}]},"$$id-redis":{"name":"Redis","command":"redis-server --requirepass $$secret_redis_password --save \"\" --appendonly \"no\"","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-redis-data:/data"],"environment":["REDIS_PASSWORD=$$secret_redis_password"],"ports":[],"cap_drop":["ALL"],"cap_add":["SETGID","SETUID","DAC_OVERRIDE"]}},"variables":[{"id":"$$config_searxng_base_url","name":"SEARXNG_BASE_URL","label":"SearXNG Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_secret_key","name":"SECRET_KEY","label":"Secret Key","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$secret_redis_password","name":"REDIS_PASSWORD","label":"Redis Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v3.0.0","documentation":"https://glitchtip.com/documentation","type":"glitchtip","name":"GlitchTip","description":"Simple, open source error tracking.","labels":["sentry","bugsnag"],"services":{"$$id":{"name":"GlitchTip","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","volumes":[],"environment":["PORT=$$config_port","GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","EMAIL_HOST=$$config_email_host","EMAIL_PORT=$$config_email_port","EMAIL_HOST_USER=$$config_email_host_user","EMAIL_HOST_PASSWORD=$$secret_email_host_password","EMAIL_USE_TLS=$$config_email_use_tls","EMAIL_USE_SSL=$$config_email_use_ssl","EMAIL_BACKEND=$$config_email_backend","MAILGUN_API_KEY=$$secret_mailgun_api_key","SENDGRID_API_KEY=$$secret_sendgrid_api_key","ENABLE_OPEN_USER_REGISTRATION=$$config_enable_open_user_registration","DJANGO_SUPERUSER_EMAIL=$$config_django_superuser_email","DJANGO_SUPERUSER_PASSWORD=$$secret_django_superuser_password","DJANGO_SUPERUSER_USERNAME=$$config_django_superuser_username","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":["8000"]},"$$id-worker":{"name":"Celery Worker","command":"./bin/run-celery-with-beat.sh","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url","CELERY_WORKER_CONCURRENCY=$$config_celery_worker_concurrency"],"ports":[]},"$$id-migrate":{"exclude":true,"name":"Migrate","command":"./manage.py migrate","depends_on":["$$id-postgresql","$$id-redis"],"image":"glitchtip/glitchtip:$$core_version","environment":["GLITCHTIP_DOMAIN=$$config_glitchtip_domain","SECRET_KEY=$$secret_secret_key","DATABASE_URL=$$secret_database_url","REDIS_URL=$$secret_redis_url","DEFAULT_FROM_EMAIL=$$config_default_from_email","EMAIL_URL=$$secret_email_url"],"ports":[]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:14-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]},"$$id-redis":{"name":"Redis","depends_on":[],"image":"redis:7-alpine","volumes":["$$id-postgresql-redis-data:/data"],"environment":[],"ports":[]}},"variables":[{"id":"$$config_django_superuser_username","name":"DJANGO_SUPERUSER_USERNAME","label":"Django Superuser Username","defaultValue":"$$generate_username","description":""},{"id":"$$secret_django_superuser_password","name":"DJANGO_SUPERUSER_PASSWORD","label":"Django Superuser Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_port","name":"PORT","label":"GlitchTip Port","defaultValue":"8000","description":""},{"id":"$$config_celery_worker_concurrency","main":"$$id-worker","name":"CELERY_WORKER_CONCURRENCY","label":"Celery Worker Concurrency","defaultValue":"2","description":""},{"id":"$$config_glitchtip_domain","name":"GLITCHTIP_DOMAIN","label":"GlitchTip Domain","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_email_url","name":"EMAIL_URL","label":"SMTP Email URL","defaultValue":"smtp://$$config_email_host_user:$$secret_email_host_password@$$config_email_host:$$config_email_port","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_redis_url","name":"REDIS_URL","label":"Redis URL","defaultValue":"redis://$$id-redis:6379/0","description":""},{"id":"$$config_default_from_email","name":"DEFAULT_FROM_EMAIL","label":"Default Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$config_email_host","name":"EMAIL_HOST","label":"Email SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_port","name":"EMAIL_PORT","label":"Email SMTP Port","defaultValue":"25","description":""},{"id":"$$config_email_host_user","name":"EMAIL_HOST_USER","label":"Email SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_host_password","name":"EMAIL_HOST_PASSWORD","label":"Email SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_use_tls","name":"EMAIL_USE_TLS","label":"Email Use TLS","defaultValue":"false","description":""},{"id":"$$config_email_use_ssl","name":"EMAIL_USE_SSL","label":"Email Use SSL","defaultValue":"false","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":""},{"id":"$$config_email_backend","name":"EMAIL_BACKEND","label":"Email Backend","defaultValue":"","description":""},{"id":"$$secret_mailgun_api_key","name":"MAILGUN_API_KEY","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$secret_sendgrid_api_key","name":"SENDGRID_API_KEY","label":"Sendgrid API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_enable_open_user_registration","name":"ENABLE_OPEN_USER_REGISTRATION","label":"Enable Open User Registration","defaultValue":"true","description":""},{"id":"$$config_django_superuser_email","name":"DJANGO_SUPERUSER_EMAIL","label":"Django Superuser Email","defaultValue":"noreply@example.com","description":""},{"id":"$$config_postgres_user","main":"$$id-postgresql","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","main":"$$id-postgresql","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","main":"$$id-postgresql","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"glitchtip","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"v2.16.0","documentation":"https://hasura.io/docs/latest/index/","type":"hasura","name":"Hasura","description":"Instant realtime GraphQL APIs on any Postgres application, existing or new.","labels":["graphql","database"],"services":{"$$id":{"name":"Hasura","depends_on":["$$id-postgresql"],"image":"hasura/graphql-engine:$$core_version","volumes":[],"environment":["HASURA_GRAPHQL_ENABLE_CONSOLE=$$config_hasura_graphql_enable_console","HASURA_GRAPHQL_METADATA_DATABASE_URL=$$secret_hasura_graphql_metadata_database_url","HASURA_GRAPHQL_ADMIN_SECRET=$$secret_hasura_graphql_admin_secret"],"ports":["8080"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[]}},"variables":[{"id":"$$config_hasura_graphql_enable_console","name":"HASURA_GRAPHQL_ENABLE_CONSOLE","label":"Enable Hasura Console","defaultValue":"true","description":""},{"id":"$$secret_hasura_graphql_metadata_database_url","name":"HASURA_GRAPHQL_METADATA_DATABASE_URL","label":"Hasura Metadata Database URL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hasura_graphql_admin_secret","name":"HASURA_GRAPHQL_ADMIN_SECRET","label":"Hasura Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"hasura","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"postgresql-v1.39.5","documentation":"https://umami.is/docs/getting-started","type":"umami-postgresql","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n\n PRIMARY KEY (\"user_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n\n PRIMARY KEY (\"event_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n\n PRIMARY KEY (\"view_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n\n PRIMARY KEY (\"session_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n\n PRIMARY KEY (\"website_id\")\n );\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n\n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n\n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n\n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n\n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n\n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"postgresql-v1.39.5","documentation":"https://umami.is/docs/getting-started","type":"umami","name":"Umami","subname":"(PostgreSQL)","description":"A simple, easy to use, self-hosted web analytics solution.","services":{"$$id":{"name":"Umami","depends_on":["$$id-postgresql"],"image":"ghcr.io/umami-software/umami:$$core_version","volumes":[],"environment":["ADMIN_PASSWORD=$$secret_admin_password","DATABASE_URL=$$secret_database_url","DATABASE_TYPE=$$config_database_type","HASH_SALT=$$secret_hash_salt"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"],"ports":[],"files":[{"location":"/docker-entrypoint-initdb.d/schema.postgresql.sql","content":"\n -- CreateTable\n CREATE TABLE \"account\" (\n \"user_id\" SERIAL NOT NULL,\n \"username\" VARCHAR(255) NOT NULL,\n \"password\" VARCHAR(60) NOT NULL,\n \"is_admin\" BOOLEAN NOT NULL DEFAULT false,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"updated_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n\n PRIMARY KEY (\"user_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"event\" (\n \"event_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"event_type\" VARCHAR(50) NOT NULL,\n \"event_value\" VARCHAR(50) NOT NULL,\n\n PRIMARY KEY (\"event_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"pageview\" (\n \"view_id\" SERIAL NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"session_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"url\" VARCHAR(500) NOT NULL,\n \"referrer\" VARCHAR(500),\n\n PRIMARY KEY (\"view_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"session\" (\n \"session_id\" SERIAL NOT NULL,\n \"session_uuid\" UUID NOT NULL,\n \"website_id\" INTEGER NOT NULL,\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n \"hostname\" VARCHAR(100),\n \"browser\" VARCHAR(20),\n \"os\" VARCHAR(20),\n \"device\" VARCHAR(20),\n \"screen\" VARCHAR(11),\n \"language\" VARCHAR(35),\n \"country\" CHAR(2),\n\n PRIMARY KEY (\"session_id\")\n );\n\n -- CreateTable\n CREATE TABLE \"website\" (\n \"website_id\" SERIAL NOT NULL,\n \"website_uuid\" UUID NOT NULL,\n \"user_id\" INTEGER NOT NULL,\n \"name\" VARCHAR(100) NOT NULL,\n \"domain\" VARCHAR(500),\n \"share_id\" VARCHAR(64),\n \"created_at\" TIMESTAMPTZ(6) DEFAULT CURRENT_TIMESTAMP,\n\n PRIMARY KEY (\"website_id\")\n );\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"account.username_unique\" ON \"account\"(\"username\");\n\n -- CreateIndex\n CREATE INDEX \"event_created_at_idx\" ON \"event\"(\"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"event_session_id_idx\" ON \"event\"(\"session_id\");\n\n -- CreateIndex\n CREATE INDEX \"event_website_id_idx\" ON \"event\"(\"website_id\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_created_at_idx\" ON \"pageview\"(\"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_session_id_idx\" ON \"pageview\"(\"session_id\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_website_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_website_id_idx\" ON \"pageview\"(\"website_id\");\n\n -- CreateIndex\n CREATE INDEX \"pageview_website_id_session_id_created_at_idx\" ON \"pageview\"(\"website_id\", \"session_id\", \"created_at\");\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"session.session_uuid_unique\" ON \"session\"(\"session_uuid\");\n\n -- CreateIndex\n CREATE INDEX \"session_created_at_idx\" ON \"session\"(\"created_at\");\n\n -- CreateIndex\n CREATE INDEX \"session_website_id_idx\" ON \"session\"(\"website_id\");\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"website.website_uuid_unique\" ON \"website\"(\"website_uuid\");\n\n -- CreateIndex\n CREATE UNIQUE INDEX \"website.share_id_unique\" ON \"website\"(\"share_id\");\n\n -- CreateIndex\n CREATE INDEX \"website_user_id_idx\" ON \"website\"(\"user_id\");\n\n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"event\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"session_id\") REFERENCES \"session\"(\"session_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"pageview\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"session\" ADD FOREIGN KEY (\"website_id\") REFERENCES \"website\"(\"website_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n -- AddForeignKey\n ALTER TABLE \"website\" ADD FOREIGN KEY (\"user_id\") REFERENCES \"account\"(\"user_id\") ON DELETE CASCADE ON UPDATE CASCADE;\n\n insert into account (username, password, is_admin) values ('admin', '$$hashed$$secret_admin_password', true);"}]}},"variables":[{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db","description":""},{"id":"$$secret_hash_salt","name":"HASH_SALT","label":"Hash Salt","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_database_type","name":"DATABASE_TYPE","label":"Database Type","defaultValue":"postgresql","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"umami","description":""},{"id":"$$secret_admin_password","name":"ADMIN_PASSWORD","label":"Initial Admin Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"v0.30.1","documentation":"https://docs.meilisearch.com/learn/getting_started/quick_start.html","type":"meilisearch","name":"MeiliSearch","description":"A lightning Fast, Ultra Relevant, and Typo-Tolerant Search Engine.","services":{"$$id":{"name":"MeiliSearch","documentation":"https://docs.meilisearch.com/","depends_on":[],"image":"getmeili/meilisearch:$$core_version","volumes":["$$id-datams:/meili_data/data.ms","$$id-data:/meili_data","$$id-snapshot:/snapshot","$$id-dump:/dumps"],"environment":["MEILI_MASTER_KEY=$$secret_meili_master_key"],"ports":["7700"]}},"variables":[{"id":"$$secret_meili_master_key","name":"MEILI_MASTER_KEY","label":"Master Key","defaultValue":"$$generate_hex(64)","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","ignore":true,"defaultVersion":"latest","documentation":"https://docs.ghost.org","arch":"amd64","type":"ghost-mariadb","name":"Ghost","subname":"(MariaDB)","description":"Free and open source blogging platform.","labels":["cms","blog"],"services":{"$$id":{"name":"Ghost","depends_on":["$$id-mariadb"],"image":"bitnami/ghost:$$core_version","volumes":["$$id-ghost:/bitnami/ghost"],"environment":["url=$$config_url","GHOST_HOST=$$config_ghost_host","GHOST_ENABLE_HTTPS=$$config_ghost_enable_https","GHOST_EMAIL=$$config_ghost_email","GHOST_PASSWORD=$$secret_ghost_password","GHOST_DATABASE_HOST=$$config_ghost_database_host","GHOST_DATABASE_USER=$$config_mariadb_user","GHOST_DATABASE_PASSWORD=$$secret_ghost_database_password","GHOST_DATABASE_NAME=$$config_mariadb_database","GHOST_DATABASE_PORT_NUMBER=3306"],"ports":["2368"]},"$$id-mariadb":{"name":"MariaDB","depends_on":[],"image":"bitnami/mariadb:latest","volumes":["$$id-mariadb:/bitnami/mariadb"],"environment":["MARIADB_USER=$$config_mariadb_user","MARIADB_PASSWORD=$$secret_mariadb_password","MARIADB_DATABASE=$$config_mariadb_database","MARIADB_ROOT_USER=$$config_mariadb_root_user","MARIADB_ROOT_PASSWORD=$$secret_mariadb_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_ghost_host","name":"GHOST_HOST","label":"Ghost Host","defaultValue":"$$generate_domain","description":""},{"id":"$$config_ghost_enable_https","name":"GHOST_ENABLE_HTTPS","label":"Ghost Enable HTTPS","defaultValue":"no","description":""},{"id":"$$config_ghost_email","name":"GHOST_EMAIL","label":"Ghost Default Email","defaultValue":"admin@example.com","description":""},{"id":"$$secret_ghost_password","name":"GHOST_PASSWORD","label":"Ghost Default Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_ghost_database_host","name":"GHOST_DATABASE_HOST","label":"Ghost Database Host","defaultValue":"$$id-mariadb","description":""},{"id":"$$config_ghost_database_user","name":"GHOST_DATABASE_USER","label":"MariaDB User","defaultValue":"$$config_mariadb_user","description":""},{"id":"$$secret_ghost_database_password","name":"GHOST_DATABASE_PASSWORD","label":"MariaDB Password","defaultValue":"$$secret_mariadb_password","description":""},{"id":"$$config_ghost_database_name","name":"GHOST_DATABASE_NAME","label":"MariaDB Database","defaultValue":"$$config_mariadb_database","description":""},{"id":"$$config_mariadb_user","name":"MARIADB_USER","label":"MariaDB User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_password","name":"MARIADB_PASSWORD","label":"MariaDB Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mariadb_database","name":"MARIADB_DATABASE","label":"MariaDB Database","defaultValue":"ghost","description":""},{"id":"$$config_mariadb_root_user","name":"MARIADB_ROOT_USER","label":"MariaDB Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mariadb_root_password","name":"MARIADB_ROOT_PASSWORD","label":"MariaDB Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"5.25.3","documentation":"https://docs.ghost.org","type":"ghost-only","name":"Ghost","subname":"(without Database)","description":"Free and open source blogging platform.","services":{"$$id":{"name":"Ghost","image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_database__connection__user","database__connection__password=$$secret_database__connection__password","database__connection__database=$$config_database__connection__database"],"ports":["2368"]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","required":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"","description":"","required":true,"placeholder":"db.coolify.io"},{"id":"$$config_database__connection__user","name":"database__connection__user","label":"Database User","defaultValue":"","description":"","placeholder":"ghost","required":true},{"id":"$$secret_database__connection__password","name":"database__connection__password","label":"Database Password","defaultValue":"","description":"","placeholder":"superSecretP4ssword","showOnConfiguration":true,"required":true},{"id":"$$config_database__connection__database","name":"database__connection__database","label":"Database Name","defaultValue":"","description":"","placeholder":"ghost_db","required":true}]},{"templateVersion":"1.0.0","defaultVersion":"5.25.3","documentation":"https://docs.ghost.org","type":"ghost-mysql","name":"Ghost","subname":"(MySQL)","description":"Ghost is a free and open source blogging platform.","services":{"$$id":{"name":"Ghost","depends_on":["$$id-mysql"],"image":"ghost:$$core_version","volumes":["$$id-ghost:/var/lib/ghost/content"],"environment":["url=$$config_url","database__client=$$config_database__client","database__connection__host=$$config_database__connection__host","database__connection__user=$$config_mysql_user","database__connection__password=$$secret_mysql_password","database__connection__database=$$config_mysql_database"],"ports":["2368"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"mysql:8.0","volumes":["$$id-mysql:/var/lib/mysql"],"environment":["MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password","MYSQL_DATABASE=$$config_mysql_database","MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password"],"ports":[]}},"variables":[{"id":"$$config_url","name":"url","label":"URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_database__client","name":"database__client","label":"Database Client","defaultValue":"mysql","description":"","readOnly":true},{"id":"$$config_database__connection__host","name":"database__connection__host","label":"Database Host","defaultValue":"$$id-mysql","description":""},{"id":"$$config_mysql_user","main":"$$id-mysql","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_mysql_password","main":"$$id-mysql","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_mysql_database","main":"$$id-mysql","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"ghost","description":""},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress","name":"WordPress","subname":"(MySQL)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","depends_on":["$$id-mysql"],"image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_USER=$$config_mysql_user","WORDPRESS_DB_PASSWORD=$$secret_mysql_password","WORDPRESS_DB_NAME=$$config_mysql_database","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]},"$$id-mysql":{"name":"MySQL","depends_on":[],"image":"bitnami/mysql:5.7","imageArm":"mysql:8.0","volumes":["$$id-mysql-data:/bitnami/mysql/data"],"volumesArm":["$$id-mysql-data:/var/lib/mysql"],"environment":["MYSQL_ROOT_PASSWORD=$$secret_mysql_root_password","MYSQL_ROOT_USER=$$config_mysql_root_user","MYSQL_DATABASE=$$config_mysql_database","MYSQL_USER=$$config_mysql_user","MYSQL_PASSWORD=$$secret_mysql_password"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"$$id-mysql","description":"","readOnly":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"WordPress Config Extra","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"},{"id":"$$secret_mysql_root_password","name":"MYSQL_ROOT_PASSWORD","label":"MySQL Root Password","defaultValue":"$$generate_password","description":"","readOnly":true},{"id":"$$config_mysql_root_user","name":"MYSQL_ROOT_USER","label":"MySQL Root User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$config_mysql_database","name":"MYSQL_DATABASE","label":"MySQL Database","defaultValue":"wordpress","description":"","readOnly":true},{"id":"$$config_mysql_user","name":"MYSQL_USER","label":"MySQL User","defaultValue":"$$generate_username","description":"","readOnly":true},{"id":"$$secret_mysql_password","name":"MYSQL_PASSWORD","label":"MySQL Password","defaultValue":"$$generate_password","description":"","readOnly":true}]},{"templateVersion":"1.0.0","defaultVersion":"php8.1","documentation":"https://wordpress.org/","type":"wordpress-only","name":"WordPress","subname":"(without DB)","description":"A content management system based on PHP.","labels":["wordpress","php","cms"],"services":{"$$id":{"name":"WordPress","image":"wordpress:$$core_version","volumes":["$$id-wordpress-data:/var/www/html"],"environment":["WORDPRESS_DB_HOST=$$config_wordpress_db_host","WORDPRESS_DB_PORT=$$config_wordpress_db_port","WORDPRESS_DB_USER=$$config_wordpress_db_user","WORDPRESS_DB_PASSWORD=$$secret_wordpress_db_password","WORDPRESS_DB_NAME=$$config_wordpress_db_name","WORDPRESS_CONFIG_EXTRA=$$config_wordpress_config_extra"],"ports":["80"]}},"variables":[{"id":"$$config_wordpress_db_host","name":"WORDPRESS_DB_HOST","label":"Database Host","defaultValue":"","description":"","placeholder":"db.coollabs.io","required":true},{"id":"$$config_wordpress_db_port","name":"WORDPRESS_DB_PORT","label":"Database Port","defaultValue":"","description":"","placeholder":"3306","required":true},{"id":"$$config_wordpress_db_user","name":"WORDPRESS_DB_USER","label":"Database User","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$secret_wordpress_db_password","name":"WORDPRESS_DB_PASSWORD","label":"Database Password","defaultValue":"","description":"","placeholder":"supers3cr3tpassw0rd!","required":true,"showOnConfiguration":true},{"id":"$$config_wordpress_db_name","name":"WORDPRESS_DB_NAME","label":"Database Name","defaultValue":"","description":"","placeholder":"wordpress","required":true},{"id":"$$config_wordpress_config_extra","name":"WORDPRESS_CONFIG_EXTRA","label":"Extra Config","defaultValue":"","description":"","type":"textarea","placeholder":"define('WP_DEBUG', true);\ndefine('WP_DEBUG_LOG', true);\ndefine('WP_DEBUG_DISPLAY', false);\n@ini_set('display_errors', 0);\n"}]},{"templateVersion":"1.0.0","defaultVersion":"4.9.0","documentation":"https://coder.com/docs/coder-oss/latest","type":"vscodeserver","name":"VSCode Server","description":"Visual Studio Code on a remote server, accessible through the browser.","labels":["vscode","ide"],"services":{"$$id":{"name":"VSCode Server","depends_on":[],"image":"codercom/code-server:$$core_version","volumes":["$$id-config-data:/home/coder/.local/share/code-server","$$id-vscodeserver-data:/home/coder","$$id-keys-directory:/root/.ssh","$$id-theme-and-plugin-directory:/root/.local/share/code-server"],"environment":["PASSWORD=$$secret_password"],"ports":["8080"]}},"variables":[{"id":"$$secret_password","name":"PASSWORD","label":"Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"RELEASE.2022-12-12T19-27-27Z","documentation":"https://min.io/docs/minio","type":"minio","name":"MinIO","description":"A cloud storage server compatible with Amazon S3.","labels":["storage","s3"],"services":{"$$id":{"name":"MinIO","command":"server /data --console-address :9001","depends_on":[],"image":"minio/minio:$$core_version","volumes":["$$id-minio-data:/data","$$id-data-write:/files"],"environment":["MINIO_SERVER_URL=$$config_coolify_fqdn_minio_console","MINIO_BROWSER_REDIRECT_URL=$$config_minio_browser_redirect_url","MINIO_DOMAIN=$$config_minio_domain","MINIO_ROOT_USER=$$config_minio_root_user","MINIO_ROOT_PASSWORD=$$secret_minio_root_password"],"ports":["9000","9001"],"proxy":[{"port":"9000","domain":"$$config_coolify_fqdn_minio_console"},{"port":"9001"}]}},"variables":[{"id":"$$config_coolify_fqdn_minio_console","name":"MINIO_SERVER_URL","label":"MinIO Server URL","defaultValue":"","description":"Specify the URL hostname the MinIO Console should use for connecting to the MinIO Server.","required":true},{"id":"$$config_minio_browser_redirect_url","name":"MINIO_BROWSER_REDIRECT_URL","label":"Browser Redirect URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$config_minio_domain","name":"MINIO_DOMAIN","label":"Domain","defaultValue":"$$generate_domain","description":""},{"id":"$$config_minio_root_user","name":"MINIO_ROOT_USER","label":"Root User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_minio_root_password","name":"MINIO_ROOT_PASSWORD","label":"Root User Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true}]},{"templateVersion":"1.0.0","defaultVersion":"0.21.1","documentation":"https://fider.io/docs","type":"fider","name":"Fider","description":"A platform to collect and organize customer feedback.","labels":["suggestion","feedback"],"services":{"$$id":{"name":"Fider","image":"getfider/fider:$$core_version","depends_on":["$$id-postgresql"],"environment":["BASE_URL=$$config_base_url","DATABASE_URL=$$secret_database_url","JWT_SECRET=$$secret_jwt_secret","EMAIL_NOREPLY=$$config_email_noreply","EMAIL_MAILGUN_API=$$secret_email_mailgun_api","EMAIL_MAILGUN_REGION=$$config_email_mailgun_region","EMAIL_MAILGUN_DOMAIN=$$config_email_mailgun_domain","EMAIL_SMTP_HOST=$$config_email_smtp_host","EMAIL_SMTP_PORT=$$config_email_smtp_port","EMAIL_SMTP_USER=$$config_email_smtp_user","EMAIL_SMTP_PASSWORD=$$secret_email_smtp_password","EMAIL_SMTP_ENABLE_STARTTLS=$$config_email_smtp_enable_starttls"],"ports":["3000"]},"$$id-postgresql":{"name":"PostgreSQL","depends_on":[],"image":"postgres:12-alpine","volumes":["$$id-postgresql-data:/var/lib/postgresql/data"],"environment":["POSTGRES_USER=$$config_postgres_user","POSTGRES_PASSWORD=$$secret_postgres_password","POSTGRES_DB=$$config_postgres_db"]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgres_user:$$secret_postgres_password@$$id-postgresql:5432/$$config_postgres_db?sslmode=disable","description":""},{"id":"$$secret_jwt_secret","name":"JWT_SECRET","label":"JWT Secret","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_email_noreply","name":"EMAIL_NOREPLY","label":"No Reply Email Address","defaultValue":"noreply@example.com","description":""},{"id":"$$secret_email_mailgun_api","name":"EMAIL_MAILGUN_API","label":"Mailgun API Key","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_mailgun_region","name":"EMAIL_MAILGUN_REGION","label":"Mailgun Region","defaultValue":"EU","description":""},{"id":"$$config_email_mailgun_domain","name":"EMAIL_MAILGUN_DOMAIN","label":"Mailgun Domain","defaultValue":"","description":""},{"id":"$$config_email_smtp_host","name":"EMAIL_SMTP_HOST","label":"SMTP Host","defaultValue":"","description":""},{"id":"$$config_email_smtp_port","name":"EMAIL_SMTP_PORT","label":"SMTP Port","defaultValue":"587","description":""},{"id":"$$config_email_smtp_user","name":"EMAIL_SMTP_USER","label":"SMTP User","defaultValue":"","description":""},{"id":"$$secret_email_smtp_password","name":"EMAIL_SMTP_PASSWORD","label":"SMTP Password","defaultValue":"","description":"","showOnConfiguration":true},{"id":"$$config_email_smtp_enable_starttls","name":"EMAIL_SMTP_ENABLE_STARTTLS","label":"SMTP Enable StartTLS","defaultValue":"false","description":""},{"id":"$$config_postgres_user","name":"POSTGRES_USER","label":"PostgreSQL User","defaultValue":"$$generate_username","description":""},{"id":"$$secret_postgres_password","name":"POSTGRES_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":""},{"id":"$$config_postgres_db","name":"POSTGRES_DB","label":"PostgreSQL Database","defaultValue":"$$generate_username","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"0.207.0","documentation":"https://docs.n8n.io","type":"n8n","name":"n8n.io","description":"A free and open node based Workflow Automation Tool.","labels":["workflow","automation","ifttt","zapier","nodered"],"services":{"$$id":{"name":"N8n","depends_on":[],"image":"n8nio/n8n:$$core_version","volumes":["$$id-data:/root/.n8n","$$id-data-write:/files","/var/run/docker.sock:/var/run/docker.sock"],"environment":["WEBHOOK_URL=$$config_webhook_url"],"ports":["5678"]}},"variables":[{"id":"$$config_webhook_url","name":"WEBHOOK_URL","label":"Webhook URL","defaultValue":"$$generate_fqdn","description":""}]},{"templateVersion":"1.0.0","defaultVersion":"stable","documentation":"https://plausible.io/doc/","arch":"amd64","type":"plausibleanalytics","name":"Plausible Analytics","description":"A lightweight and open-source website analytics tool.","labels":["analytics","statistics","plausible","gdpr","no-cookie","google analytics"],"services":{"$$id":{"name":"Plausible Analytics","command":"sh -c \"sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run\"","depends_on":["$$id-postgresql","$$id-clickhouse"],"image":"plausible/analytics:$$core_version","environment":["ADMIN_USER_EMAIL=$$config_admin_user_email","ADMIN_USER_NAME=$$config_admin_user_name","ADMIN_USER_PWD=$$secret_admin_user_pwd","BASE_URL=$$config_base_url","SECRET_KEY_BASE=$$secret_secret_key_base","DISABLE_AUTH=$$config_disable_auth","DISABLE_REGISTRATION=$$config_disable_registration","DATABASE_URL=$$secret_database_url","CLICKHOUSE_DATABASE_URL=$$secret_clickhouse_database_url"],"ports":["8000"]},"$$id-postgresql":{"name":"PostgreSQL","image":"bitnami/postgresql:13","volumes":["$$id-postgresql-data:/bitnami/postgresql"],"environment":["POSTGRESQL_PASSWORD=$$secret_postgresql_password","POSTGRESQL_USERNAME=$$config_postgresql_username","POSTGRESQL_DATABASE=$$config_postgresql_database"]},"$$id-clickhouse":{"name":"Clickhouse","volumes":["$$id-clickhouse-data:/var/lib/clickhouse"],"image":"clickhouse/clickhouse-server:22.6-alpine","ulimits":{"nofile":{"soft":262144,"hard":262144}},"files":[{"location":"/etc/clickhouse-server/users.d/logging.xml","content":"warningtrue"},{"location":"/etc/clickhouse-server/config.d/logging.xml","content":"00"},{"location":"/docker-entrypoint-initdb.d/init.query","content":"CREATE DATABASE IF NOT EXISTS plausible;"},{"location":"/docker-entrypoint-initdb.d/init-db.sh","content":"clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query"}]}},"variables":[{"id":"$$config_base_url","name":"BASE_URL","label":"Base URL","defaultValue":"$$generate_fqdn","description":"You must set this to the FQDN of the Plausible Analytics instance. This is used to generate the links to the Plausible Analytics instance."},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL for PostgreSQL","defaultValue":"postgresql://$$config_postgresql_username:$$secret_postgresql_password@$$id-postgresql:5432/$$config_postgresql_database","description":""},{"id":"$$secret_clickhouse_database_url","name":"CLICKHOUSE_DATABASE_URL","label":"Database URL for Clickhouse","defaultValue":"http://$$id-clickhouse:8123/plausible","description":""},{"id":"$$config_admin_user_email","name":"ADMIN_USER_EMAIL","label":"Admin Email Address","defaultValue":"admin@example.com","description":"This is the admin email. Please change it."},{"id":"$$config_admin_user_name","name":"ADMIN_USER_NAME","label":"Admin User Name","defaultValue":"$$generate_username","description":"This is the admin username. Please change it."},{"id":"$$secret_admin_user_pwd","name":"ADMIN_USER_PWD","label":"Admin User Password","defaultValue":"$$generate_password","description":"This is the admin password. Please change it.","showOnConfiguration":true},{"id":"$$secret_secret_key_base","name":"SECRET_KEY_BASE","label":"Secret Key Base","defaultValue":"$$generate_hex(64)","description":""},{"id":"$$config_disable_auth","name":"DISABLE_AUTH","label":"Disable Authentication","defaultValue":"false","description":""},{"id":"$$config_disable_registration","name":"DISABLE_REGISTRATION","label":"Disable Registration","defaultValue":"true","description":""},{"id":"$$config_postgresql_username","main":"$$id-postgresql","name":"POSTGRESQL_USERNAME","label":"PostgreSQL Username","defaultValue":"postgresql","description":""},{"id":"$$secret_postgresql_password","main":"$$id-postgresql","name":"POSTGRESQL_PASSWORD","label":"PostgreSQL Password","defaultValue":"$$generate_password","description":"","showOnConfiguration":true},{"id":"$$config_postgresql_database","main":"$$id-postgresql","name":"POSTGRESQL_DATABASE","label":"PostgreSQL Database","defaultValue":"plausible","description":""},{"id":"$$config_scriptName","name":"SCRIPT_NAME","label":"Custom Script Name","defaultValue":"plausible.js","description":"This is the default script name."}]},{"templateVersion":"1.0.0","defaultVersion":"0.99.1","documentation":"https://docs.nocodb.com","type":"nocodb","name":"NocoDB","description":"Turns any MySQL, PostgreSQL, SQL Server, SQLite & MariaDB into a smart-spreadsheet.","labels":["database","airtable","spreadsheet"],"services":{"$$id":{"name":"NocoDB","image":"nocodb/nocodb:$$core_version","environment":["PORT=$$config_port","NC_DB=$$config_nc_db","DATABASE_URL=$$secret_database_url","NC_PUBLIC_URL=$$config_public_url","NC_AUTH_JWT_SECRET=$$secret_auth_jwt_secret","NC_SENTRY_DSN=$$secret_sentry_dsn","NC_CONNECT_TO_EXTERNAL_DB_DISABLED=$$config_connect_to_external_db_disabled","NC_DISABLE_TELE=$$config_disable_tele"],"volumes":["$$id-data:/usr/app/data"],"ports":["8080"]}},"variables":[{"id":"$$config_nc_db","name":"NC_DB","label":"Database","defaultValue":"","description":"MySQL, PostgreSQL and MSSQL connection urls supported. If absent: A local SQLite will be created in root folder."},{"id":"$$config_port","name":"PORT","label":"Port","defaultValue":"8080","description":""},{"id":"$$secret_database_url","name":"DATABASE_URL","label":"Database URL","defaultValue":"","description":"JDBC URL Format. Can be used instead of NC_DB. Used in 1-Click Heroku deployment."},{"id":"$$config_public_url","name":"NC_PUBLIC_URL","label":"Public URL","defaultValue":"","description":"Used for sending Email invitations. If absent: Best guess from http request params."},{"id":"$$secret_auth_jwt_secret","name":"NC_AUTH_JWT_SECRET","label":"Auth JWT Secret","defaultValue":"$$generate_hex(64)","description":"JWT secret used for auth and storing other secrets. If absent: A Random secret will be generated."},{"id":"$$secret_sentry_dsn","name":"NC_SENTRY_DSN","label":"Sentry DSN","defaultValue":"","description":"For Sentry monitoring."},{"id":"$$config_connect_to_external_db_disabled","name":"NC_CONNECT_TO_EXTERNAL_DB_DISABLED","label":"Disable External Database","defaultValue":"0","description":"Disable Project creation with external database. (Enter \"1\" to disable)."},{"id":"$$config_disable_tele","name":"NC_DISABLE_TELE","label":"NocoDB Disable Telemetry","defaultValue":"1","description":"Disable telemetry (Enter \"1\" to disable)."}]}] \ No newline at end of file diff --git a/apps/client/.eslintignore b/apps/client/.eslintignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/apps/client/.eslintignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/apps/client/.eslintrc.cjs b/apps/client/.eslintrc.cjs new file mode 100644 index 000000000..3ccf435f0 --- /dev/null +++ b/apps/client/.eslintrc.cjs @@ -0,0 +1,20 @@ +module.exports = { + root: true, + parser: '@typescript-eslint/parser', + extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'], + plugins: ['svelte3', '@typescript-eslint'], + ignorePatterns: ['*.cjs'], + overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }], + settings: { + 'svelte3/typescript': () => require('typescript') + }, + parserOptions: { + sourceType: 'module', + ecmaVersion: 2020 + }, + env: { + browser: true, + es2017: true, + node: true + } +}; diff --git a/apps/client/.gitignore b/apps/client/.gitignore new file mode 100644 index 000000000..6635cf554 --- /dev/null +++ b/apps/client/.gitignore @@ -0,0 +1,10 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example +vite.config.js.timestamp-* +vite.config.ts.timestamp-* diff --git a/apps/client/.npmrc b/apps/client/.npmrc new file mode 100644 index 000000000..b6f27f135 --- /dev/null +++ b/apps/client/.npmrc @@ -0,0 +1 @@ +engine-strict=true diff --git a/apps/client/.prettierignore b/apps/client/.prettierignore new file mode 100644 index 000000000..38972655f --- /dev/null +++ b/apps/client/.prettierignore @@ -0,0 +1,13 @@ +.DS_Store +node_modules +/build +/.svelte-kit +/package +.env +.env.* +!.env.example + +# Ignore files for PNPM, NPM and YARN +pnpm-lock.yaml +package-lock.json +yarn.lock diff --git a/apps/client/.prettierrc b/apps/client/.prettierrc new file mode 100644 index 000000000..a77fddea9 --- /dev/null +++ b/apps/client/.prettierrc @@ -0,0 +1,9 @@ +{ + "useTabs": true, + "singleQuote": true, + "trailingComma": "none", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }] +} diff --git a/apps/client/README.md b/apps/client/README.md new file mode 100644 index 000000000..d7fe004d9 --- /dev/null +++ b/apps/client/README.md @@ -0,0 +1 @@ +# SvelteKit Static site diff --git a/apps/client/package.json b/apps/client/package.json new file mode 100644 index 000000000..4704b174a --- /dev/null +++ b/apps/client/package.json @@ -0,0 +1,54 @@ +{ + "name": "client", + "description": "Coolify's SvelteKit UI", + "license": "Apache-2.0", + "private": true, + "scripts": { + "dev": "vite dev", + "build": "vite build && cp -Pr build/ ../../build/public", + "preview": "vite preview", + "test": "playwright test", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "lint": "prettier --plugin-search-dir . --check . && eslint .", + "format": "prettier --plugin-search-dir . --write ." + }, + "devDependencies": { + "@playwright/test": "1.28.1", + "@sveltejs/adapter-static": "1.0.0-next.48", + "@sveltejs/kit": "1.0.0-next.572", + "@types/js-cookie": "3.0.2", + "@typescript-eslint/eslint-plugin": "5.44.0", + "@typescript-eslint/parser": "5.44.0", + "autoprefixer": "10.4.13", + "eslint": "8.28.0", + "eslint-config-prettier": "8.5.0", + "eslint-plugin-svelte3": "4.0.0", + "postcss": "8.4.19", + "postcss-load-config": "4.0.1", + "prettier": "2.8.0", + "prettier-plugin-svelte": "2.8.1", + "svelte": "3.53.1", + "svelte-check": "2.9.2", + "svelte-preprocess": "^4.10.7", + "tailwindcss": "3.2.4", + "tslib": "2.4.1", + "typescript": "4.9.3", + "vite": "3.2.4" + }, + "type": "module", + "dependencies": { + "@trpc/client": "10.1.0", + "@trpc/server": "10.1.0", + "cuid": "2.1.8", + "daisyui": "2.41.0", + "dayjs": "1.11.6", + "flowbite-svelte": "0.28.0", + "js-cookie": "3.0.1", + "js-yaml": "4.1.0", + "p-limit": "4.0.0", + "server": "workspace:*", + "superjson": "1.11.0", + "svelte-select": "4.4.7" + } +} diff --git a/apps/client/playwright.config.ts b/apps/client/playwright.config.ts new file mode 100644 index 000000000..6ad3a7faa --- /dev/null +++ b/apps/client/playwright.config.ts @@ -0,0 +1,10 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; + +const config: PlaywrightTestConfig = { + webServer: { + command: 'npm run build && npm run preview', + port: 4173 + } +}; + +export default config; diff --git a/apps/client/pnpm-lock.yaml b/apps/client/pnpm-lock.yaml new file mode 100644 index 000000000..f9b8429e8 --- /dev/null +++ b/apps/client/pnpm-lock.yaml @@ -0,0 +1,1793 @@ +lockfileVersion: 5.4 + +specifiers: + '@playwright/test': 1.25.0 + '@sveltejs/adapter-auto': next + '@sveltejs/kit': next + '@trpc/client': ^10.1.0 + '@typescript-eslint/eslint-plugin': ^5.27.0 + '@typescript-eslint/parser': ^5.27.0 + eslint: ^8.16.0 + eslint-config-prettier: ^8.3.0 + eslint-plugin-svelte3: ^4.0.0 + prettier: ^2.6.2 + prettier-plugin-svelte: ^2.7.0 + svelte: ^3.44.0 + svelte-check: ^2.7.1 + svelte-preprocess: ^4.10.6 + tslib: ^2.3.1 + typescript: ^4.7.4 + vite: ^3.1.0 + +dependencies: + '@trpc/client': 10.1.0 + +devDependencies: + '@playwright/test': 1.25.0 + '@sveltejs/adapter-auto': 1.0.0-next.89 + '@sveltejs/kit': 1.0.0-next.560_svelte@3.53.1+vite@3.2.4 + '@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu + '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + eslint: 8.28.0 + eslint-config-prettier: 8.5.0_eslint@8.28.0 + eslint-plugin-svelte3: 4.0.0_xgu65rlhscpnxffotiaicv6m5i + prettier: 2.8.0 + prettier-plugin-svelte: 2.8.1_3ndnxlh52lolrqe4kgjgbxb3xa + svelte: 3.53.1 + svelte-check: 2.9.2_svelte@3.53.1 + svelte-preprocess: 4.10.7_7dvewpees4iyn2tkw2qzal77a4 + tslib: 2.4.1 + typescript: 4.9.3 + vite: 3.2.4 + +packages: + + /@esbuild/android-arm/0.15.15: + resolution: {integrity: sha512-JJjZjJi2eBL01QJuWjfCdZxcIgot+VoK6Fq7eKF9w4YHm9hwl7nhBR1o2Wnt/WcANk5l9SkpvrldW1PLuXxcbw==} + engines: {node: '>=12'} + cpu: [arm] + os: [android] + requiresBuild: true + dev: true + optional: true + + /@esbuild/linux-loong64/0.15.15: + resolution: {integrity: sha512-lhz6UNPMDXUhtXSulw8XlFAtSYO26WmHQnCi2Lg2p+/TMiJKNLtZCYUxV4wG6rZMzXmr8InGpNwk+DLT2Hm0PA==} + engines: {node: '>=12'} + cpu: [loong64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /@eslint/eslintrc/1.3.3: + resolution: {integrity: sha512-uj3pT6Mg+3t39fvLrj8iuCIJ38zKO9FpGtJ4BBJebJhEwjoT+KLVNCcHT5QC9NGRIEi7fZ0ZR8YRb884auB4Lg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + ajv: 6.12.6 + debug: 4.3.4 + espree: 9.4.1 + globals: 13.18.0 + ignore: 5.2.0 + import-fresh: 3.3.0 + js-yaml: 4.1.0 + minimatch: 3.1.2 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/config-array/0.11.7: + resolution: {integrity: sha512-kBbPWzN8oVMLb0hOUYXhmxggL/1cJE6ydvjDIGi9EnAGUyA7cLVKQg+d/Dsm+KZwx2czGHrCmMVLiyg8s5JPKw==} + engines: {node: '>=10.10.0'} + dependencies: + '@humanwhocodes/object-schema': 1.2.1 + debug: 4.3.4 + minimatch: 3.1.2 + transitivePeerDependencies: + - supports-color + dev: true + + /@humanwhocodes/module-importer/1.0.1: + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + dev: true + + /@humanwhocodes/object-schema/1.2.1: + resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} + dev: true + + /@jridgewell/resolve-uri/3.1.0: + resolution: {integrity: sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==} + engines: {node: '>=6.0.0'} + dev: true + + /@jridgewell/sourcemap-codec/1.4.14: + resolution: {integrity: sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==} + dev: true + + /@jridgewell/trace-mapping/0.3.17: + resolution: {integrity: sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==} + dependencies: + '@jridgewell/resolve-uri': 3.1.0 + '@jridgewell/sourcemap-codec': 1.4.14 + dev: true + + /@nodelib/fs.scandir/2.1.5: + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + run-parallel: 1.2.0 + dev: true + + /@nodelib/fs.stat/2.0.5: + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} + dev: true + + /@nodelib/fs.walk/1.2.8: + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} + dependencies: + '@nodelib/fs.scandir': 2.1.5 + fastq: 1.13.0 + dev: true + + /@playwright/test/1.25.0: + resolution: {integrity: sha512-j4EZhTTQI3dBeWblE21EV//swwmBtOpIrLdOIJIRv4uqsLdHgBg1z+JtTg+AeC5o2bAXIE26kDNW5A0TimG8Bg==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@types/node': 18.11.9 + playwright-core: 1.25.0 + dev: true + + /@polka/url/1.0.0-next.21: + resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==} + dev: true + + /@sveltejs/adapter-auto/1.0.0-next.89: + resolution: {integrity: sha512-S+sASFX4sSZD1xEKmZ3zHxQh2DGxXBUpCGAtUakKkI2MRvFIm+zYIm+7GPekofMLd19FjdFDKkuOjBKPdmA8+w==} + dependencies: + import-meta-resolve: 2.2.0 + dev: true + + /@sveltejs/kit/1.0.0-next.560_svelte@3.53.1+vite@3.2.4: + resolution: {integrity: sha512-ldZJyd+jfQWVkOkRHq25cXMffhL5MgB1Uzhhw1ngF8ezB38P/g4T+5ohP8wuk2lxPJIjbY3S6BeXN5mod9XOhA==} + engines: {node: '>=16.14'} + hasBin: true + requiresBuild: true + peerDependencies: + svelte: ^3.44.0 + vite: ^3.2.0 + dependencies: + '@sveltejs/vite-plugin-svelte': 1.3.1_svelte@3.53.1+vite@3.2.4 + '@types/cookie': 0.5.1 + cookie: 0.5.0 + devalue: 4.2.0 + kleur: 4.1.5 + magic-string: 0.26.7 + mime: 3.0.0 + sade: 1.8.1 + set-cookie-parser: 2.5.1 + sirv: 2.0.2 + svelte: 3.53.1 + tiny-glob: 0.2.9 + undici: 5.12.0 + vite: 3.2.4 + transitivePeerDependencies: + - diff-match-patch + - supports-color + dev: true + + /@sveltejs/vite-plugin-svelte/1.3.1_svelte@3.53.1+vite@3.2.4: + resolution: {integrity: sha512-2Uu2sDdIR+XQWF7QWOVSF2jR9EU6Ciw1yWfYnfLYj8HIgnNxkh/8g22Fw2pBUI8QNyW/KxtqJUWBI+8ypamSrQ==} + engines: {node: ^14.18.0 || >= 16} + peerDependencies: + diff-match-patch: ^1.0.5 + svelte: ^3.44.0 + vite: ^3.0.0 + peerDependenciesMeta: + diff-match-patch: + optional: true + dependencies: + debug: 4.3.4 + deepmerge: 4.2.2 + kleur: 4.1.5 + magic-string: 0.26.7 + svelte: 3.53.1 + svelte-hmr: 0.15.1_svelte@3.53.1 + vite: 3.2.4 + vitefu: 0.2.2_vite@3.2.4 + transitivePeerDependencies: + - supports-color + dev: true + + /@trpc/client/10.1.0: + resolution: {integrity: sha512-E7L9l2OTa5lIdM0NYvQLJf/GLapskfiVLv0Jv7t6GVxEOFd+O4THWsWQgJVUUAz9iq805iMNkY3uqSvf4GJaWg==} + peerDependencies: + '@trpc/server': 10.1.0 + dev: false + + /@types/cookie/0.5.1: + resolution: {integrity: sha512-COUnqfB2+ckwXXSFInsFdOAWQzCCx+a5hq2ruyj+Vjund94RJQd4LG2u9hnvJrTgunKAaax7ancBYlDrNYxA0g==} + dev: true + + /@types/json-schema/7.0.11: + resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==} + dev: true + + /@types/node/18.11.9: + resolution: {integrity: sha512-CRpX21/kGdzjOpFsZSkcrXMGIBWMGNIHXXBVFSH+ggkftxg+XYP20TESbh+zFvFj3EQOl5byk0HTRn1IL6hbqg==} + dev: true + + /@types/pug/2.0.6: + resolution: {integrity: sha512-SnHmG9wN1UVmagJOnyo/qkk0Z7gejYxOYYmaAwr5u2yFYfsupN3sg10kyzN8Hep/2zbHxCnsumxOoRIRMBwKCg==} + dev: true + + /@types/sass/1.43.1: + resolution: {integrity: sha512-BPdoIt1lfJ6B7rw35ncdwBZrAssjcwzI5LByIrYs+tpXlj/CAkuVdRsgZDdP4lq5EjyWzwxZCqAoFyHKFwp32g==} + dependencies: + '@types/node': 18.11.9 + dev: true + + /@types/semver/7.3.13: + resolution: {integrity: sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==} + dev: true + + /@typescript-eslint/eslint-plugin/5.44.0_fnsv2sbzcckq65bwfk7a5xwslu: + resolution: {integrity: sha512-j5ULd7FmmekcyWeArx+i8x7sdRHzAtXTkmDPthE4amxZOWKFK7bomoJ4r7PJ8K7PoMzD16U8MmuZFAonr1ERvw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + '@typescript-eslint/parser': ^5.0.0 + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/scope-manager': 5.44.0 + '@typescript-eslint/type-utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + '@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + debug: 4.3.4 + eslint: 8.28.0 + ignore: 5.2.0 + natural-compare-lite: 1.4.0 + regexpp: 3.2.0 + semver: 7.3.8 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/parser/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: + resolution: {integrity: sha512-H7LCqbZnKqkkgQHaKLGC6KUjt3pjJDx8ETDqmwncyb6PuoigYajyAwBGz08VU/l86dZWZgI4zm5k2VaKqayYyA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/scope-manager': 5.44.0 + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 + debug: 4.3.4 + eslint: 8.28.0 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/scope-manager/5.44.0: + resolution: {integrity: sha512-2pKml57KusI0LAhgLKae9kwWeITZ7IsZs77YxyNyIVOwQ1kToyXRaJLl+uDEXzMN5hnobKUOo2gKntK9H1YL8g==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/visitor-keys': 5.44.0 + dev: true + + /@typescript-eslint/type-utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: + resolution: {integrity: sha512-A1u0Yo5wZxkXPQ7/noGkRhV4J9opcymcr31XQtOzcc5nO/IHN2E2TPMECKWYpM3e6olWEM63fq/BaL1wEYnt/w==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: '*' + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 + '@typescript-eslint/utils': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a + debug: 4.3.4 + eslint: 8.28.0 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/types/5.44.0: + resolution: {integrity: sha512-Tp+zDnHmGk4qKR1l+Y1rBvpjpm5tGXX339eAlRBDg+kgZkz9Bw+pqi4dyseOZMsGuSH69fYfPJCBKBrbPCxYFQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /@typescript-eslint/typescript-estree/5.44.0_typescript@4.9.3: + resolution: {integrity: sha512-M6Jr+RM7M5zeRj2maSfsZK2660HKAJawv4Ud0xT+yauyvgrsHu276VtXlKDFnEmhG+nVEd0fYZNXGoAgxwDWJw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + typescript: '*' + peerDependenciesMeta: + typescript: + optional: true + dependencies: + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/visitor-keys': 5.44.0 + debug: 4.3.4 + globby: 11.1.0 + is-glob: 4.0.3 + semver: 7.3.8 + tsutils: 3.21.0_typescript@4.9.3 + typescript: 4.9.3 + transitivePeerDependencies: + - supports-color + dev: true + + /@typescript-eslint/utils/5.44.0_hsf322ms6xhhd4b5ne6lb74y4a: + resolution: {integrity: sha512-fMzA8LLQ189gaBjS0MZszw5HBdZgVwxVFShCO3QN+ws3GlPkcy9YuS3U4wkT6su0w+Byjq3mS3uamy9HE4Yfjw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + '@types/json-schema': 7.0.11 + '@types/semver': 7.3.13 + '@typescript-eslint/scope-manager': 5.44.0 + '@typescript-eslint/types': 5.44.0 + '@typescript-eslint/typescript-estree': 5.44.0_typescript@4.9.3 + eslint: 8.28.0 + eslint-scope: 5.1.1 + eslint-utils: 3.0.0_eslint@8.28.0 + semver: 7.3.8 + transitivePeerDependencies: + - supports-color + - typescript + dev: true + + /@typescript-eslint/visitor-keys/5.44.0: + resolution: {integrity: sha512-a48tLG8/4m62gPFbJ27FxwCOqPKxsb8KC3HkmYoq2As/4YyjQl1jDbRr1s63+g4FS/iIehjmN3L5UjmKva1HzQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + '@typescript-eslint/types': 5.44.0 + eslint-visitor-keys: 3.3.0 + dev: true + + /acorn-jsx/5.3.2_acorn@8.8.1: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + dependencies: + acorn: 8.8.1 + dev: true + + /acorn/8.8.1: + resolution: {integrity: sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==} + engines: {node: '>=0.4.0'} + hasBin: true + dev: true + + /ajv/6.12.6: + resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + dev: true + + /ansi-regex/5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + dev: true + + /ansi-styles/4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + dependencies: + color-convert: 2.0.1 + dev: true + + /anymatch/3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.1 + dev: true + + /argparse/2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + dev: true + + /array-union/2.1.0: + resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} + engines: {node: '>=8'} + dev: true + + /balanced-match/1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + dev: true + + /binary-extensions/2.2.0: + resolution: {integrity: sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==} + engines: {node: '>=8'} + dev: true + + /brace-expansion/1.1.11: + resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + dev: true + + /braces/3.0.2: + resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} + engines: {node: '>=8'} + dependencies: + fill-range: 7.0.1 + dev: true + + /buffer-crc32/0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + dev: true + + /busboy/1.6.0: + resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} + engines: {node: '>=10.16.0'} + dependencies: + streamsearch: 1.1.0 + dev: true + + /callsites/3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + dev: true + + /chalk/4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + dev: true + + /chokidar/3.5.3: + resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} + engines: {node: '>= 8.10.0'} + dependencies: + anymatch: 3.1.3 + braces: 3.0.2 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /color-convert/2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + dependencies: + color-name: 1.1.4 + dev: true + + /color-name/1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + dev: true + + /concat-map/0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + dev: true + + /cookie/0.5.0: + resolution: {integrity: sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==} + engines: {node: '>= 0.6'} + dev: true + + /cross-spawn/7.0.3: + resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + engines: {node: '>= 8'} + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + dev: true + + /debug/4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + dev: true + + /deep-is/0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + dev: true + + /deepmerge/4.2.2: + resolution: {integrity: sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==} + engines: {node: '>=0.10.0'} + dev: true + + /detect-indent/6.1.0: + resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} + engines: {node: '>=8'} + dev: true + + /devalue/4.2.0: + resolution: {integrity: sha512-mbjoAaCL2qogBKgeFxFPOXAUsZchircF+B/79LD4sHH0+NHfYm8gZpQrskKDn5gENGt35+5OI1GUF7hLVnkPDw==} + dev: true + + /dir-glob/3.0.1: + resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + engines: {node: '>=8'} + dependencies: + path-type: 4.0.0 + dev: true + + /doctrine/3.0.0: + resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} + engines: {node: '>=6.0.0'} + dependencies: + esutils: 2.0.3 + dev: true + + /es6-promise/3.3.1: + resolution: {integrity: sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg==} + dev: true + + /esbuild-android-64/0.15.15: + resolution: {integrity: sha512-F+WjjQxO+JQOva3tJWNdVjouFMLK6R6i5gjDvgUthLYJnIZJsp1HlF523k73hELY20WPyEO8xcz7aaYBVkeg5Q==} + engines: {node: '>=12'} + cpu: [x64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-android-arm64/0.15.15: + resolution: {integrity: sha512-attlyhD6Y22jNyQ0fIIQ7mnPvDWKw7k6FKnsXlBvQE6s3z6s6cuEHcSgoirquQc7TmZgVCK5fD/2uxmRN+ZpcQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [android] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-64/0.15.15: + resolution: {integrity: sha512-ohZtF8W1SHJ4JWldsPVdk8st0r9ExbAOSrBOh5L+Mq47i696GVwv1ab/KlmbUoikSTNoXEhDzVpxUR/WIO19FQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-darwin-arm64/0.15.15: + resolution: {integrity: sha512-P8jOZ5zshCNIuGn+9KehKs/cq5uIniC+BeCykvdVhx/rBXSxmtj3CUIKZz4sDCuESMbitK54drf/2QX9QHG5Ag==} + engines: {node: '>=12'} + cpu: [arm64] + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-64/0.15.15: + resolution: {integrity: sha512-KkTg+AmDXz1IvA9S1gt8dE24C8Thx0X5oM0KGF322DuP+P3evwTL9YyusHAWNsh4qLsR80nvBr/EIYs29VSwuA==} + engines: {node: '>=12'} + cpu: [x64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-freebsd-arm64/0.15.15: + resolution: {integrity: sha512-FUcML0DRsuyqCMfAC+HoeAqvWxMeq0qXvclZZ/lt2kLU6XBnDA5uKTLUd379WYEyVD4KKFctqWd9tTuk8C/96g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [freebsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-32/0.15.15: + resolution: {integrity: sha512-q28Qn5pZgHNqug02aTkzw5sW9OklSo96b5nm17Mq0pDXrdTBcQ+M6Q9A1B+dalFeynunwh/pvfrNucjzwDXj+Q==} + engines: {node: '>=12'} + cpu: [ia32] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-64/0.15.15: + resolution: {integrity: sha512-217KPmWMirkf8liO+fj2qrPwbIbhNTGNVtvqI1TnOWJgcMjUWvd677Gq3fTzXEjilkx2yWypVnTswM2KbXgoAg==} + engines: {node: '>=12'} + cpu: [x64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm/0.15.15: + resolution: {integrity: sha512-RYVW9o2yN8yM7SB1yaWr378CwrjvGCyGybX3SdzPHpikUHkME2AP55Ma20uNwkNyY2eSYFX9D55kDrfQmQBR4w==} + engines: {node: '>=12'} + cpu: [arm] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-arm64/0.15.15: + resolution: {integrity: sha512-/ltmNFs0FivZkYsTzAsXIfLQX38lFnwJTWCJts0IbCqWZQe+jjj0vYBNbI0kmXLb3y5NljiM5USVAO1NVkdh2g==} + engines: {node: '>=12'} + cpu: [arm64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-mips64le/0.15.15: + resolution: {integrity: sha512-PksEPb321/28GFFxtvL33yVPfnMZihxkEv5zME2zapXGp7fA1X2jYeiTUK+9tJ/EGgcNWuwvtawPxJG7Mmn86A==} + engines: {node: '>=12'} + cpu: [mips64el] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-ppc64le/0.15.15: + resolution: {integrity: sha512-ek8gJBEIhcpGI327eAZigBOHl58QqrJrYYIZBWQCnH3UnXoeWMrMZLeeZL8BI2XMBhP+sQ6ERctD5X+ajL/AIA==} + engines: {node: '>=12'} + cpu: [ppc64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-riscv64/0.15.15: + resolution: {integrity: sha512-H5ilTZb33/GnUBrZMNJtBk7/OXzDHDXjIzoLXHSutwwsLxSNaLxzAaMoDGDd/keZoS+GDBqNVxdCkpuiRW4OSw==} + engines: {node: '>=12'} + cpu: [riscv64] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-linux-s390x/0.15.15: + resolution: {integrity: sha512-jKaLUg78mua3rrtrkpv4Or2dNTJU7bgHN4bEjT4OX4GR7nLBSA9dfJezQouTxMmIW7opwEC5/iR9mpC18utnxQ==} + engines: {node: '>=12'} + cpu: [s390x] + os: [linux] + requiresBuild: true + dev: true + optional: true + + /esbuild-netbsd-64/0.15.15: + resolution: {integrity: sha512-aOvmF/UkjFuW6F36HbIlImJTTx45KUCHJndtKo+KdP8Dhq3mgLRKW9+6Ircpm8bX/RcS3zZMMmaBLkvGY06Gvw==} + engines: {node: '>=12'} + cpu: [x64] + os: [netbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-openbsd-64/0.15.15: + resolution: {integrity: sha512-HFFX+WYedx1w2yJ1VyR1Dfo8zyYGQZf1cA69bLdrHzu9svj6KH6ZLK0k3A1/LFPhcEY9idSOhsB2UyU0tHPxgQ==} + engines: {node: '>=12'} + cpu: [x64] + os: [openbsd] + requiresBuild: true + dev: true + optional: true + + /esbuild-sunos-64/0.15.15: + resolution: {integrity: sha512-jOPBudffG4HN8yJXcK9rib/ZTFoTA5pvIKbRrt3IKAGMq1EpBi4xoVoSRrq/0d4OgZLaQbmkHp8RO9eZIn5atA==} + engines: {node: '>=12'} + cpu: [x64] + os: [sunos] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-32/0.15.15: + resolution: {integrity: sha512-MDkJ3QkjnCetKF0fKxCyYNBnOq6dmidcwstBVeMtXSgGYTy8XSwBeIE4+HuKiSsG6I/mXEb++px3IGSmTN0XiA==} + engines: {node: '>=12'} + cpu: [ia32] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-64/0.15.15: + resolution: {integrity: sha512-xaAUIB2qllE888SsMU3j9nrqyLbkqqkpQyWVkfwSil6BBPgcPk3zOFitTTncEKCLTQy3XV9RuH7PDj3aJDljWA==} + engines: {node: '>=12'} + cpu: [x64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild-windows-arm64/0.15.15: + resolution: {integrity: sha512-ttuoCYCIJAFx4UUKKWYnFdrVpoXa3+3WWkXVI6s09U+YjhnyM5h96ewTq/WgQj9LFSIlABQvadHSOQyAVjW5xQ==} + engines: {node: '>=12'} + cpu: [arm64] + os: [win32] + requiresBuild: true + dev: true + optional: true + + /esbuild/0.15.15: + resolution: {integrity: sha512-TEw/lwK4Zzld9x3FedV6jy8onOUHqcEX3ADFk4k+gzPUwrxn8nWV62tH0udo8jOtjFodlEfc4ypsqX3e+WWO6w==} + engines: {node: '>=12'} + hasBin: true + requiresBuild: true + optionalDependencies: + '@esbuild/android-arm': 0.15.15 + '@esbuild/linux-loong64': 0.15.15 + esbuild-android-64: 0.15.15 + esbuild-android-arm64: 0.15.15 + esbuild-darwin-64: 0.15.15 + esbuild-darwin-arm64: 0.15.15 + esbuild-freebsd-64: 0.15.15 + esbuild-freebsd-arm64: 0.15.15 + esbuild-linux-32: 0.15.15 + esbuild-linux-64: 0.15.15 + esbuild-linux-arm: 0.15.15 + esbuild-linux-arm64: 0.15.15 + esbuild-linux-mips64le: 0.15.15 + esbuild-linux-ppc64le: 0.15.15 + esbuild-linux-riscv64: 0.15.15 + esbuild-linux-s390x: 0.15.15 + esbuild-netbsd-64: 0.15.15 + esbuild-openbsd-64: 0.15.15 + esbuild-sunos-64: 0.15.15 + esbuild-windows-32: 0.15.15 + esbuild-windows-64: 0.15.15 + esbuild-windows-arm64: 0.15.15 + dev: true + + /escape-string-regexp/4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + dev: true + + /eslint-config-prettier/8.5.0_eslint@8.28.0: + resolution: {integrity: sha512-obmWKLUNCnhtQRKc+tmnYuQl0pFU1ibYJQ5BGhTVB08bHe9wC8qUeG7c08dj9XX+AuPj1YSGSQIHl1pnDHZR0Q==} + hasBin: true + peerDependencies: + eslint: '>=7.0.0' + dependencies: + eslint: 8.28.0 + dev: true + + /eslint-plugin-svelte3/4.0.0_xgu65rlhscpnxffotiaicv6m5i: + resolution: {integrity: sha512-OIx9lgaNzD02+MDFNLw0GEUbuovNcglg+wnd/UY0fbZmlQSz7GlQiQ1f+yX0XvC07XPcDOnFcichqI3xCwp71g==} + peerDependencies: + eslint: '>=8.0.0' + svelte: ^3.2.0 + dependencies: + eslint: 8.28.0 + svelte: 3.53.1 + dev: true + + /eslint-scope/5.1.1: + resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==} + engines: {node: '>=8.0.0'} + dependencies: + esrecurse: 4.3.0 + estraverse: 4.3.0 + dev: true + + /eslint-scope/7.1.1: + resolution: {integrity: sha512-QKQM/UXpIiHcLqJ5AOyIW7XZmzjkzQXYE54n1++wb0u9V/abW3l9uQnxX8Z5Xd18xyKIMTUAyQ0k1e8pz6LUrw==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + dev: true + + /eslint-utils/3.0.0_eslint@8.28.0: + resolution: {integrity: sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==} + engines: {node: ^10.0.0 || ^12.0.0 || >= 14.0.0} + peerDependencies: + eslint: '>=5' + dependencies: + eslint: 8.28.0 + eslint-visitor-keys: 2.1.0 + dev: true + + /eslint-visitor-keys/2.1.0: + resolution: {integrity: sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==} + engines: {node: '>=10'} + dev: true + + /eslint-visitor-keys/3.3.0: + resolution: {integrity: sha512-mQ+suqKJVyeuwGYHAdjMFqjCyfl8+Ldnxuyp3ldiMBFKkvytrXUZWaiPCEav8qDHKty44bD+qV1IP4T+w+xXRA==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dev: true + + /eslint/8.28.0: + resolution: {integrity: sha512-S27Di+EVyMxcHiwDrFzk8dJYAaD+/5SoWKxL1ri/71CRHsnJnRDPNt2Kzj24+MT9FDupf4aqqyqPrvI8MvQ4VQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + hasBin: true + dependencies: + '@eslint/eslintrc': 1.3.3 + '@humanwhocodes/config-array': 0.11.7 + '@humanwhocodes/module-importer': 1.0.1 + '@nodelib/fs.walk': 1.2.8 + ajv: 6.12.6 + chalk: 4.1.2 + cross-spawn: 7.0.3 + debug: 4.3.4 + doctrine: 3.0.0 + escape-string-regexp: 4.0.0 + eslint-scope: 7.1.1 + eslint-utils: 3.0.0_eslint@8.28.0 + eslint-visitor-keys: 3.3.0 + espree: 9.4.1 + esquery: 1.4.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 6.0.1 + find-up: 5.0.0 + glob-parent: 6.0.2 + globals: 13.18.0 + grapheme-splitter: 1.0.4 + ignore: 5.2.0 + import-fresh: 3.3.0 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + is-path-inside: 3.0.3 + js-sdsl: 4.2.0 + js-yaml: 4.1.0 + json-stable-stringify-without-jsonify: 1.0.1 + levn: 0.4.1 + lodash.merge: 4.6.2 + minimatch: 3.1.2 + natural-compare: 1.4.0 + optionator: 0.9.1 + regexpp: 3.2.0 + strip-ansi: 6.0.1 + strip-json-comments: 3.1.1 + text-table: 0.2.0 + transitivePeerDependencies: + - supports-color + dev: true + + /espree/9.4.1: + resolution: {integrity: sha512-XwctdmTO6SIvCzd9810yyNzIrOrqNYV9Koizx4C/mRhf9uq0o4yHoCEU/670pOxOL/MSraektvSAji79kX90Vg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + dependencies: + acorn: 8.8.1 + acorn-jsx: 5.3.2_acorn@8.8.1 + eslint-visitor-keys: 3.3.0 + dev: true + + /esquery/1.4.0: + resolution: {integrity: sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==} + engines: {node: '>=0.10'} + dependencies: + estraverse: 5.3.0 + dev: true + + /esrecurse/4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + dependencies: + estraverse: 5.3.0 + dev: true + + /estraverse/4.3.0: + resolution: {integrity: sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==} + engines: {node: '>=4.0'} + dev: true + + /estraverse/5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + dev: true + + /esutils/2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + dev: true + + /fast-deep-equal/3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + dev: true + + /fast-glob/3.2.12: + resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: true + + /fast-json-stable-stringify/2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + dev: true + + /fast-levenshtein/2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + dev: true + + /fastq/1.13.0: + resolution: {integrity: sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==} + dependencies: + reusify: 1.0.4 + dev: true + + /file-entry-cache/6.0.1: + resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flat-cache: 3.0.4 + dev: true + + /fill-range/7.0.1: + resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} + engines: {node: '>=8'} + dependencies: + to-regex-range: 5.0.1 + dev: true + + /find-up/5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + dev: true + + /flat-cache/3.0.4: + resolution: {integrity: sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==} + engines: {node: ^10.12.0 || >=12.0.0} + dependencies: + flatted: 3.2.7 + rimraf: 3.0.2 + dev: true + + /flatted/3.2.7: + resolution: {integrity: sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==} + dev: true + + /fs.realpath/1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + dev: true + + /fsevents/2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + requiresBuild: true + dev: true + optional: true + + /function-bind/1.1.1: + resolution: {integrity: sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==} + dev: true + + /glob-parent/5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob-parent/6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + dependencies: + is-glob: 4.0.3 + dev: true + + /glob/7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.2 + once: 1.4.0 + path-is-absolute: 1.0.1 + dev: true + + /globals/13.18.0: + resolution: {integrity: sha512-/mR4KI8Ps2spmoc0Ulu9L7agOF0du1CZNQ3dke8yItYlyKNmGrkONemBbd6V8UTc1Wgcqn21t3WYB7dbRmh6/A==} + engines: {node: '>=8'} + dependencies: + type-fest: 0.20.2 + dev: true + + /globalyzer/0.1.0: + resolution: {integrity: sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==} + dev: true + + /globby/11.1.0: + resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} + engines: {node: '>=10'} + dependencies: + array-union: 2.1.0 + dir-glob: 3.0.1 + fast-glob: 3.2.12 + ignore: 5.2.0 + merge2: 1.4.1 + slash: 3.0.0 + dev: true + + /globrex/0.1.2: + resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} + dev: true + + /graceful-fs/4.2.10: + resolution: {integrity: sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==} + dev: true + + /grapheme-splitter/1.0.4: + resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==} + dev: true + + /has-flag/4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + dev: true + + /has/1.0.3: + resolution: {integrity: sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==} + engines: {node: '>= 0.4.0'} + dependencies: + function-bind: 1.1.1 + dev: true + + /ignore/5.2.0: + resolution: {integrity: sha512-CmxgYGiEPCLhfLnpPp1MoRmifwEIOgjcHXxOBjv7mY96c+eWScsOP9c112ZyLdWHi0FxHjI+4uVhKYp/gcdRmQ==} + engines: {node: '>= 4'} + dev: true + + /import-fresh/3.3.0: + resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + engines: {node: '>=6'} + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + dev: true + + /import-meta-resolve/2.2.0: + resolution: {integrity: sha512-CpPOtiCHxP9HdtDM5F45tNiAe66Cqlv3f5uHoJjt+KlaLrUh9/Wz9vepADZ78SlqEo62aDWZtj9ydMGXV+CPnw==} + dev: true + + /imurmurhash/0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + dev: true + + /inflight/1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + dev: true + + /inherits/2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + dev: true + + /is-binary-path/2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + dependencies: + binary-extensions: 2.2.0 + dev: true + + /is-core-module/2.11.0: + resolution: {integrity: sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==} + dependencies: + has: 1.0.3 + dev: true + + /is-extglob/2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + dev: true + + /is-glob/4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + dependencies: + is-extglob: 2.1.1 + dev: true + + /is-number/7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + dev: true + + /is-path-inside/3.0.3: + resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} + engines: {node: '>=8'} + dev: true + + /isexe/2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + dev: true + + /js-sdsl/4.2.0: + resolution: {integrity: sha512-dyBIzQBDkCqCu+0upx25Y2jGdbTGxE9fshMsCdK0ViOongpV+n5tXRcZY9v7CaVQ79AGS9KA1KHtojxiM7aXSQ==} + dev: true + + /js-yaml/4.1.0: + resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} + hasBin: true + dependencies: + argparse: 2.0.1 + dev: true + + /json-schema-traverse/0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + dev: true + + /json-stable-stringify-without-jsonify/1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + dev: true + + /kleur/4.1.5: + resolution: {integrity: sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==} + engines: {node: '>=6'} + dev: true + + /levn/0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + dev: true + + /locate-path/6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + dependencies: + p-locate: 5.0.0 + dev: true + + /lodash.merge/4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + dev: true + + /lru-cache/6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + dependencies: + yallist: 4.0.0 + dev: true + + /magic-string/0.25.9: + resolution: {integrity: sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /magic-string/0.26.7: + resolution: {integrity: sha512-hX9XH3ziStPoPhJxLq1syWuZMxbDvGNbVchfrdCtanC7D13888bMFow61x8axrx+GfHLtVeAx2kxL7tTGRl+Ow==} + engines: {node: '>=12'} + dependencies: + sourcemap-codec: 1.4.8 + dev: true + + /merge2/1.4.1: + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} + dev: true + + /micromatch/4.0.5: + resolution: {integrity: sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==} + engines: {node: '>=8.6'} + dependencies: + braces: 3.0.2 + picomatch: 2.3.1 + dev: true + + /mime/3.0.0: + resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==} + engines: {node: '>=10.0.0'} + hasBin: true + dev: true + + /min-indent/1.0.1: + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} + dev: true + + /minimatch/3.1.2: + resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + dependencies: + brace-expansion: 1.1.11 + dev: true + + /minimist/1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + dev: true + + /mkdirp/0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + dependencies: + minimist: 1.2.7 + dev: true + + /mri/1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + dev: true + + /mrmime/1.0.1: + resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==} + engines: {node: '>=10'} + dev: true + + /ms/2.1.2: + resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + dev: true + + /nanoid/3.3.4: + resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + dev: true + + /natural-compare-lite/1.4.0: + resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} + dev: true + + /natural-compare/1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + dev: true + + /normalize-path/3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + dev: true + + /once/1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + dependencies: + wrappy: 1.0.2 + dev: true + + /optionator/0.9.1: + resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + engines: {node: '>= 0.8.0'} + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.3 + dev: true + + /p-limit/3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + dependencies: + yocto-queue: 0.1.0 + dev: true + + /p-locate/5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + dependencies: + p-limit: 3.1.0 + dev: true + + /parent-module/1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + dependencies: + callsites: 3.1.0 + dev: true + + /path-exists/4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + dev: true + + /path-is-absolute/1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + dev: true + + /path-key/3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + dev: true + + /path-parse/1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + dev: true + + /path-type/4.0.0: + resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} + engines: {node: '>=8'} + dev: true + + /picocolors/1.0.0: + resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} + dev: true + + /picomatch/2.3.1: + resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} + engines: {node: '>=8.6'} + dev: true + + /playwright-core/1.25.0: + resolution: {integrity: sha512-kZ3Jwaf3wlu0GgU0nB8UMQ+mXFTqBIFz9h1svTlNduNKjnbPXFxw7mJanLVjqxHJRn62uBfmgBj93YHidk2N5Q==} + engines: {node: '>=14'} + hasBin: true + dev: true + + /postcss/8.4.19: + resolution: {integrity: sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==} + engines: {node: ^10 || ^12 || >=14} + dependencies: + nanoid: 3.3.4 + picocolors: 1.0.0 + source-map-js: 1.0.2 + dev: true + + /prelude-ls/1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + dev: true + + /prettier-plugin-svelte/2.8.1_3ndnxlh52lolrqe4kgjgbxb3xa: + resolution: {integrity: sha512-KA3K1J3/wKDnCxW7ZDRA/QL2Q67N7Xs3gOERqJ5X1qFjq1DdnN3K1R29scSKwh+kA8FF67pXbYytUpvN/i3iQw==} + peerDependencies: + prettier: ^1.16.4 || ^2.0.0 + svelte: ^3.2.0 + dependencies: + prettier: 2.8.0 + svelte: 3.53.1 + dev: true + + /prettier/2.8.0: + resolution: {integrity: sha512-9Lmg8hTFZKG0Asr/kW9Bp8tJjRVluO8EJQVfY2T7FMw9T5jy4I/Uvx0Rca/XWf50QQ1/SS48+6IJWnrb+2yemA==} + engines: {node: '>=10.13.0'} + hasBin: true + dev: true + + /punycode/2.1.1: + resolution: {integrity: sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==} + engines: {node: '>=6'} + dev: true + + /queue-microtask/1.2.3: + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + dev: true + + /readdirp/3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + dependencies: + picomatch: 2.3.1 + dev: true + + /regexpp/3.2.0: + resolution: {integrity: sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==} + engines: {node: '>=8'} + dev: true + + /resolve-from/4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + dev: true + + /resolve/1.22.1: + resolution: {integrity: sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==} + hasBin: true + dependencies: + is-core-module: 2.11.0 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + dev: true + + /reusify/1.0.4: + resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + dev: true + + /rimraf/2.7.1: + resolution: {integrity: sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rimraf/3.0.2: + resolution: {integrity: sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==} + hasBin: true + dependencies: + glob: 7.2.3 + dev: true + + /rollup/2.79.1: + resolution: {integrity: sha512-uKxbd0IhMZOhjAiD5oAFp7BqvkA4Dv47qpOCtaNvng4HBwdbWtdOh8f5nZNuk2rp51PMGk3bzfWu5oayNEuYnw==} + engines: {node: '>=10.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /run-parallel/1.2.0: + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + dependencies: + queue-microtask: 1.2.3 + dev: true + + /sade/1.8.1: + resolution: {integrity: sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==} + engines: {node: '>=6'} + dependencies: + mri: 1.2.0 + dev: true + + /sander/0.5.1: + resolution: {integrity: sha512-3lVqBir7WuKDHGrKRDn/1Ye3kwpXaDOMsiRP1wd6wpZW56gJhsbp5RqQpA6JG/P+pkXizygnr1dKR8vzWaVsfA==} + dependencies: + es6-promise: 3.3.1 + graceful-fs: 4.2.10 + mkdirp: 0.5.6 + rimraf: 2.7.1 + dev: true + + /semver/7.3.8: + resolution: {integrity: sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==} + engines: {node: '>=10'} + hasBin: true + dependencies: + lru-cache: 6.0.0 + dev: true + + /set-cookie-parser/2.5.1: + resolution: {integrity: sha512-1jeBGaKNGdEq4FgIrORu/N570dwoPYio8lSoYLWmX7sQ//0JY08Xh9o5pBcgmHQ/MbsYp/aZnOe1s1lIsbLprQ==} + dev: true + + /shebang-command/2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + dependencies: + shebang-regex: 3.0.0 + dev: true + + /shebang-regex/3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + dev: true + + /sirv/2.0.2: + resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==} + engines: {node: '>= 10'} + dependencies: + '@polka/url': 1.0.0-next.21 + mrmime: 1.0.1 + totalist: 3.0.0 + dev: true + + /slash/3.0.0: + resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} + engines: {node: '>=8'} + dev: true + + /sorcery/0.10.0: + resolution: {integrity: sha512-R5ocFmKZQFfSTstfOtHjJuAwbpGyf9qjQa1egyhvXSbM7emjrtLXtGdZsDJDABC85YBfVvrOiGWKSYXPKdvP1g==} + hasBin: true + dependencies: + buffer-crc32: 0.2.13 + minimist: 1.2.7 + sander: 0.5.1 + sourcemap-codec: 1.4.8 + dev: true + + /source-map-js/1.0.2: + resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} + engines: {node: '>=0.10.0'} + dev: true + + /sourcemap-codec/1.4.8: + resolution: {integrity: sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==} + dev: true + + /streamsearch/1.1.0: + resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} + engines: {node: '>=10.0.0'} + dev: true + + /strip-ansi/6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + dependencies: + ansi-regex: 5.0.1 + dev: true + + /strip-indent/3.0.0: + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} + dependencies: + min-indent: 1.0.1 + dev: true + + /strip-json-comments/3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + dev: true + + /supports-color/7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + dependencies: + has-flag: 4.0.0 + dev: true + + /supports-preserve-symlinks-flag/1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + dev: true + + /svelte-check/2.9.2_svelte@3.53.1: + resolution: {integrity: sha512-DRi8HhnCiqiGR2YF9ervPGvtoYrheE09cXieCTEqeTPOTJzfoa54Py8rovIBv4bH4n5HgZYIyTQ3DDLHQLl2uQ==} + hasBin: true + peerDependencies: + svelte: ^3.24.0 + dependencies: + '@jridgewell/trace-mapping': 0.3.17 + chokidar: 3.5.3 + fast-glob: 3.2.12 + import-fresh: 3.3.0 + picocolors: 1.0.0 + sade: 1.8.1 + svelte: 3.53.1 + svelte-preprocess: 4.10.7_7dvewpees4iyn2tkw2qzal77a4 + typescript: 4.9.3 + transitivePeerDependencies: + - '@babel/core' + - coffeescript + - less + - node-sass + - postcss + - postcss-load-config + - pug + - sass + - stylus + - sugarss + dev: true + + /svelte-hmr/0.15.1_svelte@3.53.1: + resolution: {integrity: sha512-BiKB4RZ8YSwRKCNVdNxK/GfY+r4Kjgp9jCLEy0DuqAKfmQtpL38cQK3afdpjw4sqSs4PLi3jIPJIFp259NkZtA==} + engines: {node: ^12.20 || ^14.13.1 || >= 16} + peerDependencies: + svelte: '>=3.19.0' + dependencies: + svelte: 3.53.1 + dev: true + + /svelte-preprocess/4.10.7_7dvewpees4iyn2tkw2qzal77a4: + resolution: {integrity: sha512-sNPBnqYD6FnmdBrUmBCaqS00RyCsCpj2BG58A1JBswNF7b0OKviwxqVrOL/CKyJrLSClrSeqQv5BXNg2RUbPOw==} + engines: {node: '>= 9.11.2'} + requiresBuild: true + peerDependencies: + '@babel/core': ^7.10.2 + coffeescript: ^2.5.1 + less: ^3.11.3 || ^4.0.0 + node-sass: '*' + postcss: ^7 || ^8 + postcss-load-config: ^2.1.0 || ^3.0.0 || ^4.0.0 + pug: ^3.0.0 + sass: ^1.26.8 + stylus: ^0.55.0 + sugarss: ^2.0.0 + svelte: ^3.23.0 + typescript: ^3.9.5 || ^4.0.0 + peerDependenciesMeta: + '@babel/core': + optional: true + coffeescript: + optional: true + less: + optional: true + node-sass: + optional: true + postcss: + optional: true + postcss-load-config: + optional: true + pug: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + typescript: + optional: true + dependencies: + '@types/pug': 2.0.6 + '@types/sass': 1.43.1 + detect-indent: 6.1.0 + magic-string: 0.25.9 + sorcery: 0.10.0 + strip-indent: 3.0.0 + svelte: 3.53.1 + typescript: 4.9.3 + dev: true + + /svelte/3.53.1: + resolution: {integrity: sha512-Q4/hHkktZogGhN5iqxqSi9sjEVoe/NbIxX4hXEHoasTxj+TxEQVAq66LnDMdAZxjmsodkoI5F3slqsS68U7FNw==} + engines: {node: '>= 8'} + dev: true + + /text-table/0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + dev: true + + /tiny-glob/0.2.9: + resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==} + dependencies: + globalyzer: 0.1.0 + globrex: 0.1.2 + dev: true + + /to-regex-range/5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + dependencies: + is-number: 7.0.0 + dev: true + + /totalist/3.0.0: + resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==} + engines: {node: '>=6'} + dev: true + + /tslib/1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + dev: true + + /tslib/2.4.1: + resolution: {integrity: sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==} + dev: true + + /tsutils/3.21.0_typescript@4.9.3: + resolution: {integrity: sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==} + engines: {node: '>= 6'} + peerDependencies: + typescript: '>=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta' + dependencies: + tslib: 1.14.1 + typescript: 4.9.3 + dev: true + + /type-check/0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + dependencies: + prelude-ls: 1.2.1 + dev: true + + /type-fest/0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + dev: true + + /typescript/4.9.3: + resolution: {integrity: sha512-CIfGzTelbKNEnLpLdGFgdyKhG23CKdKgQPOBc+OUNrkJ2vr+KSzsSV5kq5iWhEQbok+quxgGzrAtGWCyU7tHnA==} + engines: {node: '>=4.2.0'} + hasBin: true + dev: true + + /undici/5.12.0: + resolution: {integrity: sha512-zMLamCG62PGjd9HHMpo05bSLvvwWOZgGeiWlN/vlqu3+lRo3elxktVGEyLMX+IO7c2eflLjcW74AlkhEZm15mg==} + engines: {node: '>=12.18'} + dependencies: + busboy: 1.6.0 + dev: true + + /uri-js/4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + dependencies: + punycode: 2.1.1 + dev: true + + /vite/3.2.4: + resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==} + engines: {node: ^14.18.0 || >=16.0.0} + hasBin: true + peerDependencies: + '@types/node': '>= 14' + less: '*' + sass: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + sass: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + dependencies: + esbuild: 0.15.15 + postcss: 8.4.19 + resolve: 1.22.1 + rollup: 2.79.1 + optionalDependencies: + fsevents: 2.3.2 + dev: true + + /vitefu/0.2.2_vite@3.2.4: + resolution: {integrity: sha512-8CKEIWPm4B4DUDN+h+hVJa9pyNi7rzc5MYmbxhs1wcMakueGFNWB5/DL30USm9qU3xUPnL4/rrLEAwwFiD1tag==} + peerDependencies: + vite: ^3.0.0 + peerDependenciesMeta: + vite: + optional: true + dependencies: + vite: 3.2.4 + dev: true + + /which/2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + dependencies: + isexe: 2.0.0 + dev: true + + /word-wrap/1.2.3: + resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} + engines: {node: '>=0.10.0'} + dev: true + + /wrappy/1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + dev: true + + /yallist/4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + dev: true + + /yocto-queue/0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + dev: true diff --git a/apps/client/postcss.config.cjs b/apps/client/postcss.config.cjs new file mode 100644 index 000000000..fe10e55a8 --- /dev/null +++ b/apps/client/postcss.config.cjs @@ -0,0 +1,13 @@ +const tailwindcss = require('tailwindcss'); +const autoprefixer = require('autoprefixer'); + +const config = { + plugins: [ + //Some plugins, like tailwindcss/nesting, need to run before Tailwind, + tailwindcss(), + //But others, like autoprefixer, need to run after, + autoprefixer + ] +}; + +module.exports = config; diff --git a/apps/client/src/app.d.ts b/apps/client/src/app.d.ts new file mode 100644 index 000000000..b527fe7bd --- /dev/null +++ b/apps/client/src/app.d.ts @@ -0,0 +1,12 @@ +// See https://kit.svelte.dev/docs/types#app +// for information about these interfaces +// and what to do when importing types +declare namespace App { + // interface Locals {} + // interface PageData {} + // interface Error {} + // interface Platform {} +} + +declare const GITPOD_WORKSPACE_URL: string; +declare const CODESANDBOX_HOST: string; diff --git a/apps/client/src/app.html b/apps/client/src/app.html new file mode 100644 index 000000000..0061883f2 --- /dev/null +++ b/apps/client/src/app.html @@ -0,0 +1,12 @@ + + + + + + + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/client/src/app.postcss b/apps/client/src/app.postcss new file mode 100644 index 000000000..67ad7e2d9 --- /dev/null +++ b/apps/client/src/app.postcss @@ -0,0 +1,284 @@ +/* Write your global styles here, in PostCSS syntax */ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 400; + src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-regular.woff2') format('woff2'), + url('/poppins-v19-latin-ext_latin_devanagari-regular.woff') format('woff'); +} +@font-face { + font-family: 'Poppins'; + font-style: normal; + font-weight: 500; + src: local(''), url('/poppins-v19-latin-ext_latin_devanagari-500.woff2') format('woff2'), + url('/poppins-v19-latin-ext_latin_devanagari-500.woff') format('woff'); +} + +button { + @apply text-sm !important; +} +html { + @apply h-full min-h-full overflow-y-scroll; +} +body { + @apply min-h-screen overflow-x-hidden bg-coolblack text-sm text-white scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200; +} + +input, +.input { + @apply h-12 w-96 rounded border border-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-200 disabled:bg-transparent disabled:bg-coolblack md:text-sm; +} +textarea { + @apply min-w-[14rem] rounded border border-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-200 disabled:bg-transparent md:text-sm; +} + +#svelte .custom-select-wrapper .selectContainer.disabled input { + @apply placeholder:text-stone-600; +} + +#svelte .custom-select-wrapper .selectContainer input { + @apply text-white; +} + +#svelte .custom-select-wrapper .selectContainer { + @apply h-12 rounded bg-coolgray-200 p-2 px-0 text-xs tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm; +} + +#svelte .listContainer { + @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200; +} +#svelte .selectedItem { + @apply pl-2; +} + +#svelte .item.hover { + @apply bg-coollabs text-white !important; +} +#svelte .item.active { + @apply bg-coolgray-100 text-white; +} + +select { + @apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm; +} +.custom-select-wrapper { + --background: rgb(32 32 32); + --inputColor: white; + --multiItemPadding: 0; + --multiSelectPadding: 0 0.5rem 0 0.5rem; + --border: none; + --placeholderColor: rgb(87 83 78); + --listBackground: rgb(32 32 32); + --itemColor: white; + --itemHoverBG: rgb(107 22 237); + --multiItemBG: rgb(32 32 32); + --multiClearHoverBG: transparent; + --multiClearHoverFill: rgb(239 68 68); + --multiItemActiveBG: transparent; + --multiClearBG: transparent; + --clearSelectFocusColor: white; + --clearSelectHoverColor: rgb(239 68 68); + --multiItemBorderRadius: 0.25rem; + --listShadow: none; +} + +label { + @apply inline-block; +} +.btn { + @apply text-white text-base min-w-fit no-animation; +} + +a { + @apply underline hover:text-white; +} + +.content { + @apply p-2 px-4; +} + +.title { + @apply text-lg lg:text-2xl font-bold; +} +.subtitle { + @apply text-lg lg:text-xl font-bold text-indigo-300; +} +.label { + @apply text-sm leading-6 font-semibold text-sky-500 dark:text-sky-400; +} +.card { + @apply border bg-coolgray-100 border-coolgray-200 rounded p-2 space-y-2 sticky top-4 mb-2 items-center; +} +.icon-holder { + overflow: hidden; + height: 30px; + border-radius: 5px; + margin-right: 8px; + background: linear-gradient(0deg, #999, #ddd); +} +.instance-status-running { + box-shadow: 1px 4px 5px #3df721; +} +.instance-status-stopped { + box-shadow: 1px 4px 5px rgb(110, 191, 225); +} +.instance-status-error { + box-shadow: 1px 4px 5px #fb00ff; +} +.instance-status-degraded { + box-shadow: 1px 4px 5px #f7b121; +} +.badge-status-healthy, +.badge-status-running { + @apply text-green-500; +} +.badge-status-degraded { + @apply text-green-500; +} +.badge-status-stopped { + @apply text-sky-500; +} +.delete-button { + @apply bg-red-600; +} +.delete-button:hover { + @apply bg-red-500; +} +/* Interchange menu position */ +.menu-left { + display: flex; + flex-direction: row; +} +.menu-left .menu-bar { + display: flex; + flex-direction: column; +} +.menu-left .menu-bar > * { + display: flex; + flex-direction: column; +} +.menu-top { + display: flex; + flex-direction: column; +} +.menu-top .menu-bar { + display: flex; + flex-direction: row; +} +.menu-top .menu-bar > * { + display: flex; + flex-direction: row; +} + +.nav-main { + @apply fixed top-0 left-0 min-h-screen w-16 min-w-[4rem] overflow-hidden border-r border-stone-800 bg-coolgray-200 scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200 xl:overflow-visible; +} + +.nav-side { + @apply absolute right-0 top-0 z-50 m-5 flex flex-wrap items-center justify-end space-x-2 bg-coolblack/40 text-white; +} + +.add-icon { + @apply rounded p-1 transition duration-200; +} + +.icons { + @apply rounded p-2 transition duration-200 hover:bg-coolgray-500 disabled:bg-coolblack disabled:text-coolgray-500 !important; +} + +.arrow-right-applications { + @apply -ml-6 px-2 font-bold text-green-500; +} + +.border-gradient { + border-bottom: 2px solid transparent; + -o-border-image: linear-gradient( + 0.25turn, + rgba(255, 249, 34), + rgba(255, 0, 128), + rgba(56, 2, 155, 0) + ); + border-image: linear-gradient( + 0.25turn, + rgba(255, 249, 34), + rgba(255, 0, 128), + rgba(56, 2, 155, 0) + ); + border-image-slice: 1; +} +.border-gradient-full { + border: 4px solid transparent; + -o-border-image: linear-gradient( + 0.25turn, + rgba(255, 249, 34), + rgba(255, 0, 128), + rgba(56, 2, 155, 0) + ); + border-image: linear-gradient( + 0.25turn, + rgba(255, 249, 34), + rgba(255, 0, 128), + rgba(56, 2, 155, 0) + ); + border-image-slice: 1; +} + +.box-selection { + @apply min-w-[16rem] justify-center rounded border-transparent bg-coolgray-200 p-6 hover:border-transparent hover:bg-coolgray-400; +} + +.lds-heart { + animation: lds-heart 1.2s infinite cubic-bezier(0.215, 0.61, 0.355, 1); +} +@keyframes lds-heart { + 0% { + transform: scale(1); + } + 5% { + transform: scale(1.2); + } + 39% { + transform: scale(0.85); + } + 45% { + transform: scale(1); + } + 60% { + transform: scale(0.95); + } + 100% { + transform: scale(0.9); + } +} + +.sub-menu { + @apply w-48 text-base font-bold hover:bg-coolgray-500 rounded p-2 hover:text-white text-stone-200 cursor-pointer; +} + +.sub-menu-active { + @apply bg-coolgray-500 text-white; +} + +.table tbody td, +.table tbody th, +.table thead th { + background-color: transparent; +} +.table * { + border: none; +} + +.header { + @apply flex flex-row z-10 w-full py-5 px-5; +} +.burger { + @apply block m-[2px] h-[3px] w-5 rounded; +} + +.bg-coollabs-gradient { + @apply bg-gradient-to-r from-purple-500 via-pink-500 to-red-500; +} diff --git a/apps/client/src/lib/common.ts b/apps/client/src/lib/common.ts new file mode 100644 index 000000000..8c2361a0c --- /dev/null +++ b/apps/client/src/lib/common.ts @@ -0,0 +1,201 @@ +import { dev } from '$app/environment'; +import { addToast } from './store'; +import Cookies from 'js-cookie'; +export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); + +export function errorNotification(error: any | { message: string }): void { + if (error instanceof Error) { + console.error(error.message) + addToast({ + message: error.message, + type: 'error' + }); + } else { + console.error(error) + addToast({ + message: error, + type: 'error' + }); + } +} +export function getRndInteger(min: number, max: number) { + return Math.floor(Math.random() * (max - min + 1)) + min; +} + +export function getDomain(domain: string) { + return domain?.replace('https://', '').replace('http://', ''); +} + +export const notNodeDeployments = ['php', 'docker', 'rust', 'python', 'deno', 'laravel', 'heroku']; +export const staticDeployments = [ + 'react', + 'vuejs', + 'static', + 'svelte', + 'gatsby', + 'php', + 'astro', + 'eleventy' +]; + +export function getAPIUrl() { + if (GITPOD_WORKSPACE_URL) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + return newURL; + } + if (CODESANDBOX_HOST) { + return `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; + } + return dev ? `http://${window.location.hostname}:3001` : 'http://localhost:3000'; +} +export function getWebhookUrl(type: string) { + if (GITPOD_WORKSPACE_URL) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, ''); + if (type === 'github') { + return `${newURL}/webhooks/github/events`; + } + if (type === 'gitlab') { + return `${newURL}/webhooks/gitlab/events`; + } + } + if (CODESANDBOX_HOST) { + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`; + if (type === 'github') { + return `${newURL}/webhooks/github/events`; + } + if (type === 'gitlab') { + return `${newURL}/webhooks/gitlab/events`; + } + } + return `https://webhook.site/0e5beb2c-4e9b-40e2-a89e-32295e570c21/events`; +} + +async function send({ + method, + path, + data = null, + headers, + timeout = 120000 +}: { + method: string; + path: string; + data?: any; + headers?: any; + timeout?: number; +}): Promise> { + const token = Cookies.get('token'); + const controller = new AbortController(); + const id = setTimeout(() => controller.abort(), timeout); + const opts: any = { method, headers: {}, body: null, signal: controller.signal }; + if (data && Object.keys(data).length > 0) { + const parsedData = data; + for (const [key, value] of Object.entries(data)) { + if (value === '') { + parsedData[key] = null; + } + } + if (parsedData) { + opts.headers['Content-Type'] = 'application/json'; + opts.body = JSON.stringify(parsedData); + } + } + + if (headers) { + opts.headers = { + ...opts.headers, + ...headers + }; + } + if (token && !path.startsWith('https://')) { + opts.headers = { + ...opts.headers, + Authorization: `Bearer ${token}` + }; + } + if (!path.startsWith('https://')) { + path = `/api/v1${path}`; + } + + if (dev && !path.startsWith('https://')) { + path = `${getAPIUrl()}${path}`; + } + if (method === 'POST' && data && !opts.body) { + opts.body = data; + } + const response = await fetch(`${path}`, opts); + + clearTimeout(id); + + const contentType = response.headers.get('content-type'); + + let responseData = {}; + if (contentType) { + if (contentType?.indexOf('application/json') !== -1) { + responseData = await response.json(); + } else if (contentType?.indexOf('text/plain') !== -1) { + responseData = await response.text(); + } else { + return {}; + } + } else { + return {}; + } + if (!response.ok) { + if ( + response.status === 401 && + !path.startsWith('https://api.github') && + !path.includes('/v4/') + ) { + Cookies.remove('token'); + } + + throw responseData; + } + return responseData; +} + +export function get(path: string, headers?: Record): Promise> { + return send({ method: 'GET', path, headers }); +} + +export function del( + path: string, + data: Record, + headers?: Record +): Promise> { + return send({ method: 'DELETE', path, data, headers }); +} + +export function post( + path: string, + data: Record | FormData, + headers?: Record +): Promise> { + return send({ method: 'POST', path, data, headers }); +} + +export function put( + path: string, + data: Record, + headers?: Record +): Promise> { + return send({ method: 'PUT', path, data, headers }); +} +export function changeQueryParams(buildId: string) { + const queryParams = new URLSearchParams(window.location.search); + queryParams.set('buildId', buildId); + // @ts-ignore + return history.pushState(null, null, '?' + queryParams.toString()); +} + +export const dateOptions: any = { + year: 'numeric', + month: 'short', + day: '2-digit', + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + hour12: false +}; \ No newline at end of file diff --git a/apps/client/src/lib/components/Beta.svelte b/apps/client/src/lib/components/Beta.svelte new file mode 100644 index 000000000..279401fcf --- /dev/null +++ b/apps/client/src/lib/components/Beta.svelte @@ -0,0 +1 @@ + BETA \ No newline at end of file diff --git a/apps/client/src/lib/components/CopyPasswordField.svelte b/apps/client/src/lib/components/CopyPasswordField.svelte new file mode 100644 index 000000000..a0a474750 --- /dev/null +++ b/apps/client/src/lib/components/CopyPasswordField.svelte @@ -0,0 +1,156 @@ + + +
+ {#if !isPasswordField || showPassword} + {#if textarea} + + {:else} + + {/if} + {:else} + + {/if} + +
+
+ {#if isPasswordField} + +
(showPassword = !showPassword)}> + {#if showPassword} + + + + {:else} + + + + + {/if} +
+ {/if} + {#if value && isHttps} + +
+ + + + + +
+ {/if} +
+
+
diff --git a/apps/client/src/lib/components/Explainer.svelte b/apps/client/src/lib/components/Explainer.svelte new file mode 100644 index 000000000..924ce70d6 --- /dev/null +++ b/apps/client/src/lib/components/Explainer.svelte @@ -0,0 +1,38 @@ + + +
+ + + + + +
diff --git a/apps/client/src/lib/components/Setting.svelte b/apps/client/src/lib/components/Setting.svelte new file mode 100644 index 000000000..555323b37 --- /dev/null +++ b/apps/client/src/lib/components/Setting.svelte @@ -0,0 +1,87 @@ + + +
+
+ + +
+
+
+ +
+ Use setting + + + + +
+
+ +{#if dataTooltip} + {dataTooltip} +{/if} diff --git a/apps/client/src/lib/components/Toast.svelte b/apps/client/src/lib/components/Toast.svelte new file mode 100644 index 000000000..bb34929cd --- /dev/null +++ b/apps/client/src/lib/components/Toast.svelte @@ -0,0 +1,64 @@ + + + +
dispatch('click')} + on:mouseover={() => dispatch('pause')} + on:focus={() => dispatch('pause')} + on:mouseout={() => dispatch('resume')} + on:blur={() => dispatch('resume')} + class={` flex flex-row justify-center alert shadow-lg text-white hover:scale-105 transition-all duration-100 cursor-pointer rounded ${success()}`} + class:alert-error={type === 'error'} + class:alert-info={type === 'info'} +> + {#if type === 'success'} + + {:else if type === 'error'} + + {:else if type === 'info'} + + {/if} + +
diff --git a/apps/client/src/lib/components/Toasts.svelte b/apps/client/src/lib/components/Toasts.svelte new file mode 100644 index 000000000..929189bcb --- /dev/null +++ b/apps/client/src/lib/components/Toasts.svelte @@ -0,0 +1,25 @@ + + +{#if $toasts.length > 0} +
+ +
+{/if} + + diff --git a/apps/client/src/lib/components/Tooltip.svelte b/apps/client/src/lib/components/Tooltip.svelte new file mode 100644 index 000000000..e0591a9d4 --- /dev/null +++ b/apps/client/src/lib/components/Tooltip.svelte @@ -0,0 +1,10 @@ + + + diff --git a/apps/client/src/lib/components/UpdateAvailable.svelte b/apps/client/src/lib/components/UpdateAvailable.svelte new file mode 100644 index 000000000..bda1f02cc --- /dev/null +++ b/apps/client/src/lib/components/UpdateAvailable.svelte @@ -0,0 +1,206 @@ + + +
+ {#if $appSession.teamId === '0'} + {#if $isUpdateAvailable} + + New Version Available! + {/if} + {/if} +
diff --git a/apps/client/src/lib/components/icons/Delete.svelte b/apps/client/src/lib/components/icons/Delete.svelte new file mode 100644 index 000000000..f04b3952c --- /dev/null +++ b/apps/client/src/lib/components/icons/Delete.svelte @@ -0,0 +1,17 @@ + + + + + + + + diff --git a/apps/client/src/lib/components/icons/RemoteLink.svelte b/apps/client/src/lib/components/icons/RemoteLink.svelte new file mode 100644 index 000000000..7622822a2 --- /dev/null +++ b/apps/client/src/lib/components/icons/RemoteLink.svelte @@ -0,0 +1,10 @@ + + + diff --git a/apps/client/src/lib/components/icons/applications/ApplicationIcons.svelte b/apps/client/src/lib/components/icons/applications/ApplicationIcons.svelte new file mode 100644 index 000000000..5e4f83e94 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/ApplicationIcons.svelte @@ -0,0 +1,47 @@ + + +{#if application.buildPack?.toLowerCase() === 'rust'} + +{:else if application.buildPack?.toLowerCase() === 'node'} + +{:else if application.buildPack?.toLowerCase() === 'react'} + +{:else if application.buildPack?.toLowerCase() === 'svelte'} + +{:else if application.buildPack?.toLowerCase() === 'vuejs'} + +{:else if application.buildPack?.toLowerCase() === 'php'} + +{:else if application.buildPack?.toLowerCase() === 'python'} + +{:else if application.buildPack?.toLowerCase() === 'static'} + +{:else if application.buildPack?.toLowerCase() === 'nestjs'} + +{:else if application.buildPack?.toLowerCase() === 'nuxtjs'} + +{:else if application.buildPack?.toLowerCase() === 'nextjs'} + +{:else if application.buildPack?.toLowerCase() === 'gatsby'} + +{:else if application.buildPack?.toLowerCase() === 'docker'} + +{:else if application.buildPack?.toLowerCase() === 'astro'} + +{:else if application.buildPack?.toLowerCase() === 'eleventy'} + +{:else if application.buildPack?.toLowerCase() === 'deno'} + +{:else if application.buildPack?.toLowerCase() === 'laravel'} + +{:else if application.buildPack?.toLowerCase() === 'heroku'} + +{:else if application.buildPack?.toLowerCase() === 'compose'} + +{:else if application.simpleDockerfile} + +{/if} diff --git a/apps/client/src/lib/components/icons/applications/Astro.svelte b/apps/client/src/lib/components/icons/applications/Astro.svelte new file mode 100644 index 000000000..2344372ab --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Astro.svelte @@ -0,0 +1,25 @@ + + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Compose.svelte b/apps/client/src/lib/components/icons/applications/Compose.svelte new file mode 100644 index 000000000..f0482e776 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Compose.svelte @@ -0,0 +1,9 @@ + + +docker compose logo diff --git a/apps/client/src/lib/components/icons/applications/Deno.svelte b/apps/client/src/lib/components/icons/applications/Deno.svelte new file mode 100644 index 000000000..25eee8132 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Deno.svelte @@ -0,0 +1,30 @@ + + + diff --git a/apps/client/src/lib/components/icons/applications/Docker.svelte b/apps/client/src/lib/components/icons/applications/Docker.svelte new file mode 100644 index 000000000..74ba0ebf0 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Docker.svelte @@ -0,0 +1,9 @@ + + + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Eleventy.svelte b/apps/client/src/lib/components/icons/applications/Eleventy.svelte new file mode 100644 index 000000000..b2d8d6122 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Eleventy.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Gatsby.svelte b/apps/client/src/lib/components/icons/applications/Gatsby.svelte new file mode 100644 index 000000000..d67a63417 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Gatsby.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Heroku.svelte b/apps/client/src/lib/components/icons/applications/Heroku.svelte new file mode 100644 index 000000000..dff845bc2 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Heroku.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Laravel.svelte b/apps/client/src/lib/components/icons/applications/Laravel.svelte new file mode 100644 index 000000000..d13694a8c --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Laravel.svelte @@ -0,0 +1,14 @@ + + +Logomark diff --git a/apps/client/src/lib/components/icons/applications/Nestjs.svelte b/apps/client/src/lib/components/icons/applications/Nestjs.svelte new file mode 100644 index 000000000..ac0f8af3f --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Nestjs.svelte @@ -0,0 +1,13 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Nextjs.svelte b/apps/client/src/lib/components/icons/applications/Nextjs.svelte new file mode 100644 index 000000000..9ed0227d1 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Nextjs.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Nodejs.svelte b/apps/client/src/lib/components/icons/applications/Nodejs.svelte new file mode 100644 index 000000000..93140f08f --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Nodejs.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/client/src/lib/components/icons/applications/Nuxtjs.svelte b/apps/client/src/lib/components/icons/applications/Nuxtjs.svelte new file mode 100644 index 000000000..cb2a66ff4 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Nuxtjs.svelte @@ -0,0 +1,24 @@ + + + + + + + + + diff --git a/apps/client/src/lib/components/icons/applications/PHP.svelte b/apps/client/src/lib/components/icons/applications/PHP.svelte new file mode 100644 index 000000000..d52ab0dd5 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/PHP.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Python.svelte b/apps/client/src/lib/components/icons/applications/Python.svelte new file mode 100644 index 000000000..17abb0f6d --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Python.svelte @@ -0,0 +1,57 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/React.svelte b/apps/client/src/lib/components/icons/applications/React.svelte new file mode 100644 index 000000000..c0867ffc8 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/React.svelte @@ -0,0 +1,16 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Rust.svelte b/apps/client/src/lib/components/icons/applications/Rust.svelte new file mode 100644 index 000000000..97bcee903 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Rust.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Static.svelte b/apps/client/src/lib/components/icons/applications/Static.svelte new file mode 100644 index 000000000..14cbb0ce8 --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Static.svelte @@ -0,0 +1,34 @@ + + + diff --git a/apps/client/src/lib/components/icons/applications/Svelte.svelte b/apps/client/src/lib/components/icons/applications/Svelte.svelte new file mode 100644 index 000000000..cfa96c59d --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Svelte.svelte @@ -0,0 +1,25 @@ + + + + + + diff --git a/apps/client/src/lib/components/icons/applications/Vuejs.svelte b/apps/client/src/lib/components/icons/applications/Vuejs.svelte new file mode 100644 index 000000000..5ead6229d --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/Vuejs.svelte @@ -0,0 +1,21 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/applications/index.ts b/apps/client/src/lib/components/icons/applications/index.ts new file mode 100644 index 000000000..7bbe7b55b --- /dev/null +++ b/apps/client/src/lib/components/icons/applications/index.ts @@ -0,0 +1,20 @@ +//@ts-nocheck +export { default as Rust } from './Rust.svelte'; +export { default as Nodejs } from './Nodejs.svelte'; +export { default as React } from './React.svelte'; +export { default as Svelte } from './Svelte.svelte'; +export { default as Vuejs } from './Vuejs.svelte'; +export { default as Php } from './PHP.svelte'; +export { default as Python } from './Python.svelte'; +export { default as Static } from './Static.svelte'; +export { default as Nestjs } from './Nestjs.svelte'; +export { default as Nuxtjs } from './Nuxtjs.svelte'; +export { default as Nextjs } from './Nextjs.svelte'; +export { default as Gatsby } from './Gatsby.svelte'; +export { default as Docker } from './Docker.svelte'; +export { default as Astro } from './Astro.svelte'; +export { default as Eleventy } from './Eleventy.svelte'; +export { default as Deno } from './Deno.svelte'; +export { default as Laravel } from './Laravel.svelte'; +export { default as Heroku } from './Heroku.svelte'; +export { default as Compose } from './Compose.svelte'; diff --git a/apps/client/src/lib/components/icons/databases/Clickhouse.svelte b/apps/client/src/lib/components/icons/databases/Clickhouse.svelte new file mode 100644 index 000000000..dd237a48c --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/Clickhouse.svelte @@ -0,0 +1,13 @@ + + + diff --git a/apps/client/src/lib/components/icons/databases/CouchDB.svelte b/apps/client/src/lib/components/icons/databases/CouchDB.svelte new file mode 100644 index 000000000..411c4928d --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/CouchDB.svelte @@ -0,0 +1,18 @@ + + + diff --git a/apps/client/src/lib/components/icons/databases/DatabaseIcons.svelte b/apps/client/src/lib/components/icons/databases/DatabaseIcons.svelte new file mode 100644 index 000000000..a9ebf475b --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/DatabaseIcons.svelte @@ -0,0 +1,21 @@ + + +{#if type === 'mysql'} + +{:else if type === 'postgresql'} + +{:else if type === 'mongodb'} + +{:else if type === 'mariadb'} + +{:else if type === 'redis'} + +{:else if type === 'couchdb'} + +{:else if type === 'edgedb'} + +{/if} diff --git a/apps/client/src/lib/components/icons/databases/EdgeDB.svelte b/apps/client/src/lib/components/icons/databases/EdgeDB.svelte new file mode 100644 index 000000000..57fdebed5 --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/EdgeDB.svelte @@ -0,0 +1,22 @@ + + + diff --git a/apps/client/src/lib/components/icons/databases/MariaDB.svelte b/apps/client/src/lib/components/icons/databases/MariaDB.svelte new file mode 100644 index 000000000..5bf504bcc --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/MariaDB.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/databases/MongoDB.svelte b/apps/client/src/lib/components/icons/databases/MongoDB.svelte new file mode 100644 index 000000000..fbb261aa2 --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/MongoDB.svelte @@ -0,0 +1,90 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/databases/MySQL.svelte b/apps/client/src/lib/components/icons/databases/MySQL.svelte new file mode 100644 index 000000000..095093214 --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/MySQL.svelte @@ -0,0 +1,17 @@ + + + diff --git a/apps/client/src/lib/components/icons/databases/PostgreSQL.svelte b/apps/client/src/lib/components/icons/databases/PostgreSQL.svelte new file mode 100644 index 000000000..3021508a6 --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/PostgreSQL.svelte @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + diff --git a/apps/client/src/lib/components/icons/databases/Redis.svelte b/apps/client/src/lib/components/icons/databases/Redis.svelte new file mode 100644 index 000000000..24a7dc797 --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/Redis.svelte @@ -0,0 +1,34 @@ + + + diff --git a/apps/client/src/lib/components/icons/databases/index.ts b/apps/client/src/lib/components/icons/databases/index.ts new file mode 100644 index 000000000..e200b5311 --- /dev/null +++ b/apps/client/src/lib/components/icons/databases/index.ts @@ -0,0 +1,11 @@ +//@ts-nocheck +export { default as Clickhouse } from './Clickhouse.svelte'; +export { default as CouchDB } from './CouchDB.svelte'; +export { default as MariaDB } from './MariaDB.svelte'; +export { default as MongoDB } from './MongoDB.svelte'; +export { default as MySQL } from './MySQL.svelte'; +export { default as PostgreSQL } from './PostgreSQL.svelte'; +export { default as Redis } from './Redis.svelte'; +export { default as EdgeDB } from './EdgeDB.svelte'; + + diff --git a/apps/client/src/lib/components/icons/destinations/LocalDocker.svelte b/apps/client/src/lib/components/icons/destinations/LocalDocker.svelte new file mode 100644 index 000000000..f3ab3be56 --- /dev/null +++ b/apps/client/src/lib/components/icons/destinations/LocalDocker.svelte @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/apps/client/src/lib/components/icons/destinations/RemoteDocker.svelte b/apps/client/src/lib/components/icons/destinations/RemoteDocker.svelte new file mode 100644 index 000000000..1d00a6900 --- /dev/null +++ b/apps/client/src/lib/components/icons/destinations/RemoteDocker.svelte @@ -0,0 +1,16 @@ + + + + + + + diff --git a/apps/client/src/lib/components/icons/destinations/index.ts b/apps/client/src/lib/components/icons/destinations/index.ts new file mode 100644 index 000000000..f39255325 --- /dev/null +++ b/apps/client/src/lib/components/icons/destinations/index.ts @@ -0,0 +1,2 @@ +export { default as LocalDocker } from './LocalDocker.svelte'; +export { default as RemoteDocker } from './RemoteDocker.svelte'; diff --git a/apps/client/src/lib/components/icons/index.ts b/apps/client/src/lib/components/icons/index.ts new file mode 100644 index 000000000..7ccdd45c4 --- /dev/null +++ b/apps/client/src/lib/components/icons/index.ts @@ -0,0 +1,6 @@ +export { default as RemoteLink } from './RemoteLink.svelte'; +export { default as Delete } from './Delete.svelte'; +export * as Applications from './applications'; +export * as Sources from './sources'; +export * as Destinations from './destinations'; +export * as Databases from './databases'; diff --git a/apps/client/src/lib/components/icons/services/ServiceIcons.svelte b/apps/client/src/lib/components/icons/services/ServiceIcons.svelte new file mode 100644 index 000000000..7f146e3fb --- /dev/null +++ b/apps/client/src/lib/components/icons/services/ServiceIcons.svelte @@ -0,0 +1,59 @@ + + +{#if name} + {`Icon +{/if} diff --git a/apps/client/src/lib/components/icons/sources/Github.svelte b/apps/client/src/lib/components/icons/sources/Github.svelte new file mode 100644 index 000000000..38ef50829 --- /dev/null +++ b/apps/client/src/lib/components/icons/sources/Github.svelte @@ -0,0 +1,15 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/sources/Gitlab.svelte b/apps/client/src/lib/components/icons/sources/Gitlab.svelte new file mode 100644 index 000000000..7ddfa21c1 --- /dev/null +++ b/apps/client/src/lib/components/icons/sources/Gitlab.svelte @@ -0,0 +1,25 @@ + + + + + diff --git a/apps/client/src/lib/components/icons/sources/index.ts b/apps/client/src/lib/components/icons/sources/index.ts new file mode 100644 index 000000000..3a0d7cee3 --- /dev/null +++ b/apps/client/src/lib/components/icons/sources/index.ts @@ -0,0 +1,2 @@ +export { default as GitHub } from './Github.svelte'; +export { default as GitLab } from './Gitlab.svelte'; diff --git a/apps/client/src/lib/dayjs.ts b/apps/client/src/lib/dayjs.ts new file mode 100644 index 000000000..9ff5b0a1a --- /dev/null +++ b/apps/client/src/lib/dayjs.ts @@ -0,0 +1,7 @@ +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc.js'; +import relativeTime from 'dayjs/plugin/relativeTime.js'; +dayjs.extend(utc); +dayjs.extend(relativeTime); + +export { dayjs as day }; diff --git a/apps/client/src/lib/store.ts b/apps/client/src/lib/store.ts new file mode 100644 index 000000000..87afa0034 --- /dev/null +++ b/apps/client/src/lib/store.ts @@ -0,0 +1,173 @@ +import { writable, readable, type Writable } from 'svelte/store'; +import superjson from 'superjson'; +import type { AppRouter } from 'server/src/trpc'; +import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; +import { browser, dev } from '$app/environment'; +import Cookies from 'js-cookie'; +import cuid from 'cuid'; + +export const serverBaseUrl = dev ? `http://${browser && window.location.hostname}:2022` : ''; +export let token: string = Cookies.get('token') || ''; +export const trpc = createTRPCProxyClient({ + transformer: superjson, + links: [ + httpBatchLink({ + url: `${serverBaseUrl}/trpc`, + headers() { + return { + Authorization: token + }; + } + }) + ] +}); +export const disabledButton: Writable = writable(false); +export const location: Writable = writable(null) +interface AppSession { + isRegistrationEnabled: boolean; + token?: string; + ipv4: string | null; + ipv6: string | null; + version: string | null; + userId: string | null; + teamId: string | null; + permission: string; + isAdmin: boolean; + whiteLabeled: boolean; + whiteLabeledDetails: { + icon: string | null; + }; + tokens: { + github: string | null; + gitlab: string | null; + }; + pendingInvitations: Array; +} + +export const appSession: Writable = writable({ + isRegistrationEnabled: false, + ipv4: null, + ipv6: null, + version: null, + userId: null, + teamId: null, + permission: 'read', + isAdmin: false, + whiteLabeled: false, + whiteLabeledDetails: { + icon: null + }, + tokens: { + github: null, + gitlab: null + }, + pendingInvitations: [] +}); + +interface AddToast { + type?: 'info' | 'success' | 'error'; + message: string; + timeout?: number | undefined; +} +export const toasts: any = writable([]); + +export const dismissToast = (id: string) => { + toasts.update((all: any) => all.filter((t: any) => t.id !== id)); +}; +export const pauseToast = (id: string) => { + toasts.update((all: any) => { + const index = all.findIndex((t: any) => t.id === id); + if (index > -1) clearTimeout(all[index].timeoutInterval); + return all; + }); +}; +export const resumeToast = (id: string) => { + toasts.update((all: any) => { + const index = all.findIndex((t: any) => t.id === id); + if (index > -1) { + all[index].timeoutInterval = setTimeout(() => { + dismissToast(id); + }, all[index].timeout); + } + return all; + }); +}; + +export const addToast = (toast: AddToast) => { + const id = cuid(); + const defaults = { + id, + type: 'info', + timeout: 2000 + }; + let t: any = { ...defaults, ...toast }; + if (t.timeout) t.timeoutInterval = setTimeout(() => dismissToast(id), t.timeout); + toasts.update((all: any) => [t, ...all]); +}; + +export const features = readable({ + beta: browser && window.localStorage.getItem('beta') === 'true', + latestVersion: browser && window.localStorage.getItem('latestVersion') +}); + +export const updateLoading: Writable = writable(false); +export const isUpdateAvailable: Writable = writable(false); +export const latestVersion: Writable = writable('latest'); +export const loginEmail: Writable = writable(); +export const search: any = writable(''); + +export const isDeploymentEnabled: Writable = writable(false); +export const status: Writable = writable({ + application: { + statuses: [], + overallStatus: 'stopped', + loading: false, + restarting: false, + initialLoading: true + }, + service: { + statuses: [], + overallStatus: 'stopped', + loading: false, + startup: {}, + initialLoading: true + }, + database: { + isRunning: false, + isExited: false, + loading: false, + initialLoading: true, + isPublic: false + } +}); + +export function checkIfDeploymentEnabledApplications(isAdmin: boolean, application: any) { + return !!( + (isAdmin && application.buildPack === 'compose') || + ((application.fqdn || application.settings.isBot) && + ((application.gitSource && application.repository && application.buildPack) || + application.simpleDockerfile) && + application.destinationDocker) + ); +} +export const setLocation = (resource: any, settings?: any) => { + if (resource.settings.isBot && resource.exposePort) { + disabledButton.set(false); + return location.set(`http://${dev ? 'localhost' : settings.ipv4}:${resource.exposePort}`); + } + if (GITPOD_WORKSPACE_URL && resource.exposePort) { + const { href } = new URL(GITPOD_WORKSPACE_URL); + const newURL = href.replace('https://', `https://${resource.exposePort}-`).replace(/\/$/, ''); + return location.set(newURL); + } else if (CODESANDBOX_HOST) { + const newURL = `https://${CODESANDBOX_HOST.replace(/\$PORT/, resource.exposePort)}`; + return location.set(newURL); + } + if (resource.fqdn) { + return location.set(resource.fqdn); + } else { + location.set(null); + disabledButton.set(false); + } +}; +export const selectedBuildId: any = writable(null) diff --git a/apps/client/src/routes/+error.svelte b/apps/client/src/routes/+error.svelte new file mode 100644 index 000000000..eef31e906 --- /dev/null +++ b/apps/client/src/routes/+error.svelte @@ -0,0 +1,14 @@ + + +
+
Ooops, are you lost?
+ Go back + {#if $page.error.message !== 'Not Found'} +
+
{$page
+					.error.message}
+
+ {/if} +
diff --git a/apps/client/src/routes/+layout.svelte b/apps/client/src/routes/+layout.svelte new file mode 100644 index 000000000..cd5ab4cd1 --- /dev/null +++ b/apps/client/src/routes/+layout.svelte @@ -0,0 +1,417 @@ + + + + {#if !$appSession.whiteLabeled} + Coolify + + {:else if $appSession.whiteLabeledDetails.icon} + Coolify + + {/if} + + +
+ +
+ {#if $appSession.userId} + IAM + Settings + Documentation + Logout + + {#if $appSession.whiteLabeled} + Powered by Coolify + {/if} + {/if} + +
+
+ +
+
+
+ +
diff --git a/apps/client/src/routes/+layout.ts b/apps/client/src/routes/+layout.ts new file mode 100644 index 000000000..6a2608cac --- /dev/null +++ b/apps/client/src/routes/+layout.ts @@ -0,0 +1,43 @@ +import { error } from '@sveltejs/kit'; +import { trpc } from '$lib/store'; +import type { LayoutLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; +import Cookies from 'js-cookie'; +export const ssr = false; + +export const load: LayoutLoad = async ({ url }) => { + const { pathname } = new URL(url); + + try { + if (pathname === '/login' || pathname === '/register') { + const baseSettings = await trpc.settings.getBaseSettings.query(); + return { + settings: { + ...baseSettings + } + }; + } + const settings = await trpc.settings.getInstanceSettings.query(); + if (settings.data.token) { + Cookies.set('token', settings.data.token); + } + return { + settings: { + ...settings + } + }; + } catch (err) { + if (err?.data?.httpStatus == 401) { + throw redirect(307, '/login'); + } + if (err instanceof Error) { + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + '

' + err.message + }); + } + + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + }); + } +}; diff --git a/apps/client/src/routes/+page.svelte b/apps/client/src/routes/+page.svelte new file mode 100644 index 000000000..bee821f5b --- /dev/null +++ b/apps/client/src/routes/+page.svelte @@ -0,0 +1,1652 @@ + + + +
+ {#if applications.length !== 0 || destinations.length !== 0 || databases.length !== 0 || services.length !== 0 || gitSources.length !== 0 || destinations.length !== 0} +
+ + + + + +
+
+
+ +
doSearch('')} + > + + + + + +
+ + doSearch()} + /> +
+ +
+ {/if} + {#if (filtered.applications.length > 0 && applications.length > 0) || filtered.otherApplications.length > 0} +
+

Applications

+ + {#if foundUnconfiguredApplication} + + {/if} +
+ {/if} + {#if filtered.applications.length > 0 && applications.length > 0} +
+ + {/if} + {#if filtered.otherApplications.length > 0} + {#if filtered.applications.length > 0} +
+ {/if} + {/if} + {#if filtered.otherApplications.length > 0} + + {/if} + {#if (filtered.services.length > 0 && services.length > 0) || filtered.otherServices.length > 0} +
+

Services

+ + {#if foundUnconfiguredService} + + {/if} +
+ {/if} + {#if filtered.services.length > 0 && services.length > 0} +
+ + {/if} + {#if filtered.otherServices.length > 0} + {#if filtered.services.length > 0} +
+ {/if} + {/if} + {#if filtered.otherServices.length > 0} + + {/if} + {#if (filtered.databases.length > 0 && databases.length > 0) || filtered.otherDatabases.length > 0} +
+

Databases

+ + {#if foundUnconfiguredDatabase} + + {/if} +
+ {/if} + {#if filtered.databases.length > 0 && databases.length > 0} +
+ + {/if} + {#if filtered.otherDatabases.length > 0} + {#if filtered.databases.length > 0} +
+ {/if} + {/if} + {#if filtered.otherDatabases.length > 0} + + {/if} + {#if (filtered.gitSources.length > 0 && gitSources.length > 0) || filtered.otherGitSources.length > 0} +
+

Git Sources

+
+ {/if} + {#if filtered.gitSources.length > 0 && gitSources.length > 0} +
+
+ {#if filtered.gitSources.length > 0} + {#each filtered.gitSources as source} + {#key source.id} + + + {/if} + {#if filtered.otherGitSources.length > 0} + {#if filtered.gitSources.length > 0} +
+ {/if} + {/if} + {#if filtered.otherGitSources.length > 0} +
+ {#each filtered.otherGitSources as source} + {#key source.id} + + + {/if} + {#if (filtered.destinations.length > 0 && destinations.length > 0) || filtered.otherDestinations.length > 0} +
+

Destinations

+
+ {/if} + {#if filtered.destinations.length > 0 && destinations.length > 0} +
+ + {/if} + {#if filtered.otherDestinations.length > 0} + {#if filtered.destinations.length > 0} +
+ {/if} + {/if} + {#if filtered.otherDestinations.length > 0} + + {/if} + + {#if filtered.applications.length === 0 && filtered.destinations.length === 0 && filtered.databases.length === 0 && filtered.services.length === 0 && filtered.gitSources.length === 0 && filtered.destinations.length === 0 && $search} +
+

+ Nothing found with {$search}. +

+
+ {/if} + {#if applications.length === 0 && destinations.length === 0 && databases.length === 0 && services.length === 0 && gitSources.length === 0 && destinations.length === 0} +
+
+
+

+ Hey +

+

It looks like you did not configure anything yet.

+ +
+
+
+ {/if} +
+
diff --git a/apps/client/src/routes/+page.ts b/apps/client/src/routes/+page.ts new file mode 100644 index 000000000..3465b727e --- /dev/null +++ b/apps/client/src/routes/+page.ts @@ -0,0 +1,13 @@ +import { error } from '@sveltejs/kit'; +import { trpc } from '$lib/store'; +export const ssr = false; + +export const load = async () => { + try { + return await trpc.dashboard.resources.query(); + } catch (err) { + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + }); + } +}; diff --git a/apps/client/src/routes/applications/[id]/+layout.svelte b/apps/client/src/routes/applications/[id]/+layout.svelte new file mode 100644 index 000000000..8edaae1b8 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/+layout.svelte @@ -0,0 +1,114 @@ + + +
+ +
+ {#if $status.application.initialLoading} + + {:else if $status.application.overallStatus === 'degraded'} + (stopping = true)} + on:stopped={() => (stopping = false)} + /> + {:else if $status.application.overallStatus === 'healthy'} + + {:else if $status.application.overallStatus === 'stopped'} + + {/if} +
+
+
+ {#if !isConfigurationView} + + {/if} +
+ +
+
diff --git a/apps/client/src/routes/applications/[id]/+layout.ts b/apps/client/src/routes/applications/[id]/+layout.ts new file mode 100644 index 000000000..3bbc66f62 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/+layout.ts @@ -0,0 +1,56 @@ +import { error } from '@sveltejs/kit'; +import { trpc } from '$lib/store'; +import type { LayoutLoad } from './$types'; +import { redirect } from '@sveltejs/kit'; + +function checkConfiguration(application: any): string | null { + let configurationPhase = null; + if (!application.gitSourceId && !application.simpleDockerfile) { + return (configurationPhase = 'source'); + } + if (application.simpleDockerfile) { + if (!application.destinationDockerId) { + configurationPhase = 'destination'; + } + return configurationPhase; + } else if (!application.repository && !application.branch) { + configurationPhase = 'repository'; + } else if (!application.destinationDockerId) { + configurationPhase = 'destination'; + } else if (!application.buildPack) { + configurationPhase = 'buildpack'; + } + return configurationPhase; +} + +export const load: LayoutLoad = async ({ params, url }) => { + const { pathname } = new URL(url); + const { id } = params; + try { + const application = await trpc.applications.getApplicationById.query({ id }); + if (!application) { + throw redirect(307, '/applications'); + } + const configurationPhase = checkConfiguration(application); + console.log({ configurationPhase }); + // if ( + // configurationPhase && + // pathname !== `/applications/${params.id}/configuration/${configurationPhase}` + // ) { + // throw redirect(302, `/applications/${params.id}/configuration/${configurationPhase}`); + // } + return { + application + }; + } catch (err) { + if (err instanceof Error) { + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + '

' + err.message + }); + } + + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + }); + } +}; diff --git a/apps/client/src/routes/applications/[id]/+page.svelte b/apps/client/src/routes/applications/[id]/+page.svelte new file mode 100644 index 000000000..be04741aa --- /dev/null +++ b/apps/client/src/routes/applications/[id]/+page.svelte @@ -0,0 +1,1240 @@ + + +
+
handleSubmit()}> +
+
+
General
+ {#if $appSession.isAdmin} + + {/if} +
+
+
+ + +
+ {#if !isSimpleDockerfile} +
+ + {#if isDisabled || application.settings?.isPublicRepository} + + {:else} + + {/if} +
+
+ + +
+
+ + {#if isDisabled || application.settings?.isPublicRepository} + + {:else} + + {/if} +
+ {/if} +
+ + {#if isDisabled} + + {:else} + + + {/if} +
+ {#if application.dockerRegistry?.id && application.gitSourceId} +
+ + +
+ {/if} + {#if !isSimpleDockerfile} +
+ + {#if isDisabled} + + {:else} + + + {/if} +
+ {/if} +
+ +
+ +
+
+ {#if application.buildPack !== 'compose'} +
+ changeSettings('isBot')} + title="Is your application a bot?" + description="You can deploy applications without domains or make them to listen on the Exposed Port.

Useful to host Twitch bots, regular jobs, or anything that does not require an incoming HTTP connection." + disabled={isDisabled} + /> +
+ {/if} + {#if !isBot && application.buildPack !== 'compose'} +
+ +
+ + {#if forceSave} +
+ {#if isNonWWWDomainOK} + + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if} +
+
+
+ !isDisabled && changeSettings('dualCerts')} + /> +
+ {#if isHttps && application.buildPack !== 'compose'} +
+ changeSettings('isCustomSSL')} + /> +
+ {/if} + {/if} +
+ {#if isSimpleDockerfile} +
+ Configuration +
+ +
+
+ +
+