diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 000000000..0505d67e3 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,16 @@ +# See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/javascript-node/.devcontainer/base.Dockerfile + +# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster +ARG VARIANT="16-bullseye" +FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT} + +# [Optional] Uncomment this section to install additional OS packages. +# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ +# && apt-get -y install --no-install-recommends + +# [Optional] Uncomment if you want to install an additional version of node using nvm +# ARG EXTRA_NODE_VERSION=10 +# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}" + +# [Optional] Uncomment if you want to install more global node modules +RUN su node -c "npm install -g pnpm" diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 000000000..f898e8827 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,28 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.233.0/containers/javascript-node +{ + "name": "Node.js", + "build": { + "dockerfile": "Dockerfile", + // Update 'VARIANT' to pick a Node version: 18, 16, 14. + // Append -bullseye or -buster to pin to an OS version. + // Use -bullseye variants on local arm64/Apple Silicon. + "args": { + "VARIANT": "16-bullseye" + } + }, + // Set *default* container specific settings.json values on container create. + "settings": {}, + // Add the IDs of extensions you want installed when the container is created. + "extensions": ["dbaeumer.vscode-eslint", "svelte.svelte-vscode"], + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000], + // Use 'postCreateCommand' to run commands after the container is created. + "postCreateCommand": "cp .env.template .env && pnpm install && pnpm db:push && pnpm db:seed", + // Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "node", + "features": { + "docker-in-docker": "20.10", + "github-cli": "latest" + } +} diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 49618c8e7..326d434b3 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,3 +3,6 @@ contact_links: - name: 🤔 Questions and Help url: https://discord.com/invite/6rDM4fkymF about: Reach out to us on discord or our github discussions page. + - name: 🙋‍♂️ service request + url: https://feedback.coolify.io/ + about: want to request a new service? for e.g wordpress, hasura, appwrite etc... diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2be3c4ff5..62f919153 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,7 +15,13 @@ ## 🙋 Want to help? ## 👋 Introduction -🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. +### Setup with github codespaces + +If you have github codespaces enabled then you can just create a codespace and run `pnpm dev` to run your the dev environment. All the required dependencies and packages has been configured for you already. + +### Setup locally in your machine + +> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. 💡 Although windows users can use github codespaces for development #### Recommended Pull Request Guideline @@ -35,20 +41,16 @@ # How to start after you set up your local fork? You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. -#### Setup a local development environment +#### Steps for local setup -- Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. -- Install dependencies with `pnpm install`. -- Need to create a local SQlite database with `pnpm db:push`. - - This will apply all migrations at `db/dev.db`. -- Seed the database with base entities with `pnpm db:seed` -- You can start coding after starting `pnpm dev`. +1. Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. +2. Install dependencies with `pnpm install`. +3. Need to create a local SQlite database with `pnpm db:push`. -#### How to start after you set up your local fork? + This will apply all migrations at `db/dev.db`. -This repository works better with [pnpm](https://pnpm.io) due to the lock file. I recommend you to give it a try and use `pnpm` as well because it is cool and efficient! - -You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. +4. Seed the database with base entities with `pnpm db:seed` +5. You can start coding after starting `pnpm dev`. ## 🧑‍💻 Developer contribution diff --git a/README.md b/README.md index f4b7af6ef..da200223a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ ## Live Demo (If it is unresponsive, that means someone overloaded the server. 🙃) ## Feedback + If you have a new service / build pack you would like to add, raise an idea [here](https://feedback.coolify.io/) to get feedback from the community! ## How to install @@ -62,6 +63,7 @@ ### Applications - Rust - Docker - Python +- Deno ### Databases diff --git a/data/traefik/docker-compose-tcp.yaml b/data/traefik/docker-compose-tcp.yaml new file mode 100644 index 000000000..110630d2e --- /dev/null +++ b/data/traefik/docker-compose-tcp.yaml @@ -0,0 +1,23 @@ +version: '3.5' + +services: + ${ID}: + container_name: proxy-for-${PORT} + image: traefik:v2.6 + command: + - --api.insecure=true + - --entrypoints.web.address=:${PORT} + - --providers.docker=false + - --providers.docker.exposedbydefault=false + - --providers.http.endpoint=http://host.docker.internal:3000/traefik.json?id=${ID} + - --providers.http.pollTimeout=5s + - --log.level=error + ports: + - '${PORT}:${PORT}' + networks: + - ${NETWORK} + +networks: + net: + external: false + name: ${NETWORK} diff --git a/docker-compose-traefik.yaml b/docker-compose-traefik.yaml new file mode 100644 index 000000000..09fd32525 --- /dev/null +++ b/docker-compose-traefik.yaml @@ -0,0 +1,29 @@ +version: '3.8' + +services: + proxy: + image: traefik:v2.6 + command: + - --api.insecure=true + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --providers.docker=false + - --providers.docker.exposedbydefault=false + - --providers.http.endpoint=http://host.docker.internal:3000/traefik.json + - --providers.http.pollTimeout=5s + - --log.level=error + ports: + - '80:80' + - '443:443' + - '8080:8080' + volumes: + - /var/run/docker.sock:/var/run/docker.sock + extra_hosts: + - 'host.docker.internal:host-gateway' + networks: + - coolify-infra + +networks: + coolify-infra: + attachable: true + name: coolify-infra diff --git a/docker-compose.yaml b/docker-compose.yaml index 6f3cd69f0..39866b5e2 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -39,3 +39,5 @@ volumes: name: coolify-ssl-certs coolify-letsencrypt: name: coolify-letsencrypt + coolify-traefik-letsencrypt: + name: coolify-traefik-letsencrypt diff --git a/package.json b/package.json index 0c6bde272..5dbad34ca 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.8.2", + "version": "2.9.8", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0", @@ -30,10 +30,11 @@ }, "devDependencies": { "@sveltejs/adapter-node": "1.0.0-next.73", - "@sveltejs/kit": "1.0.0-next.326", + "@sveltejs/adapter-static": "1.0.0-next.31", + "@sveltejs/kit": "1.0.0-next.334", "@types/js-cookie": "3.0.2", "@types/js-yaml": "4.0.5", - "@types/node": "17.0.31", + "@types/node": "17.0.34", "@types/node-forge": "1.0.2", "@typescript-eslint/eslint-plugin": "4.31.1", "@typescript-eslint/parser": "4.31.1", @@ -49,10 +50,10 @@ "postcss": "8.4.13", "prettier": "2.6.2", "prettier-plugin-svelte": "2.7.0", - "prettier-plugin-tailwindcss": "0.1.10", + "prettier-plugin-tailwindcss": "0.1.11", "prisma": "3.11.1", "svelte": "3.48.0", - "svelte-check": "2.7.0", + "svelte-check": "2.7.1", "svelte-preprocess": "4.10.6", "svelte-select": "4.4.7", "sveltekit-i18n": "2.2.1", @@ -67,7 +68,7 @@ "@prisma/client": "3.11.1", "@sentry/node": "6.19.7", "bcryptjs": "2.4.3", - "bullmq": "1.81.4", + "bullmq": "1.82.2", "compare-versions": "4.1.3", "cookie": "0.5.0", "cuid": "2.1.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e98d0f2e8..b626b4fbd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,17 +5,18 @@ specifiers: '@prisma/client': 3.11.1 '@sentry/node': 6.19.7 '@sveltejs/adapter-node': 1.0.0-next.73 - '@sveltejs/kit': 1.0.0-next.326 + '@sveltejs/adapter-static': 1.0.0-next.31 + '@sveltejs/kit': 1.0.0-next.334 '@types/js-cookie': 3.0.2 '@types/js-yaml': 4.0.5 - '@types/node': 17.0.31 + '@types/node': 17.0.34 '@types/node-forge': 1.0.2 '@typescript-eslint/eslint-plugin': 4.31.1 '@typescript-eslint/parser': 4.31.1 '@zerodevx/svelte-toast': 0.7.1 autoprefixer: 10.4.7 bcryptjs: 2.4.3 - bullmq: 1.81.4 + bullmq: 1.82.2 compare-versions: 4.1.3 cookie: 0.5.0 cross-env: 7.0.3 @@ -43,10 +44,10 @@ specifiers: postcss: 8.4.13 prettier: 2.6.2 prettier-plugin-svelte: 2.7.0 - prettier-plugin-tailwindcss: 0.1.10 + prettier-plugin-tailwindcss: 0.1.11 prisma: 3.11.1 svelte: 3.48.0 - svelte-check: 2.7.0 + svelte-check: 2.7.1 svelte-kit-cookie-session: 2.1.4 svelte-preprocess: 4.10.6 svelte-select: 4.4.7 @@ -63,7 +64,7 @@ dependencies: '@prisma/client': 3.11.1_prisma@3.11.1 '@sentry/node': 6.19.7 bcryptjs: 2.4.3 - bullmq: 1.81.4 + bullmq: 1.82.2 compare-versions: 4.1.3 cookie: 0.5.0 cuid: 2.1.8 @@ -87,10 +88,11 @@ dependencies: devDependencies: '@sveltejs/adapter-node': 1.0.0-next.73 - '@sveltejs/kit': 1.0.0-next.326_svelte@3.48.0 + '@sveltejs/adapter-static': 1.0.0-next.31 + '@sveltejs/kit': 1.0.0-next.334_svelte@3.48.0 '@types/js-cookie': 3.0.2 '@types/js-yaml': 4.0.5 - '@types/node': 17.0.31 + '@types/node': 17.0.34 '@types/node-forge': 1.0.2 '@typescript-eslint/eslint-plugin': 4.31.1_lii63oz3usekbu5ehvrcuwn5jy '@typescript-eslint/parser': 4.31.1_e4zyhrvfnqudwdx5bevnvkluy4 @@ -106,15 +108,15 @@ devDependencies: postcss: 8.4.13 prettier: 2.6.2 prettier-plugin-svelte: 2.7.0_kkjbqzpydplecjtkxrgomroeru - prettier-plugin-tailwindcss: 0.1.10_prettier@2.6.2 + prettier-plugin-tailwindcss: 0.1.11_prettier@2.6.2 prisma: 3.11.1 svelte: 3.48.0 - svelte-check: 2.7.0_f2ke6qjyzu5axsjd6yk3u4tn7a + svelte-check: 2.7.1_f2ke6qjyzu5axsjd6yk3u4tn7a svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu svelte-select: 4.4.7 sveltekit-i18n: 2.2.1_svelte@3.48.0 tailwindcss: 3.0.24_ts-node@10.7.0 - ts-node: 10.7.0_l47be6km5p57gglrggidw5gsgm + ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa tslib: 2.4.0 typescript: 4.6.4 @@ -214,6 +216,31 @@ packages: } dev: false + /@jridgewell/resolve-uri/3.0.7: + resolution: + { + integrity: sha512-8cXDaBBHOr2pQ7j77Y6Vp5VDT2sIqWyWQ56TjEq4ih/a4iST3dItRe8Q9fp0rrIl9DoKhWQtUQz/YpOxLkXbNA== + } + engines: { node: '>=6.0.0' } + dev: true + + /@jridgewell/sourcemap-codec/1.4.13: + resolution: + { + integrity: sha512-GryiOJmNcWbovBxTfZSF71V/mXbgcV3MewDe3kIMCLyIh5e7SKAeUZs+rMnJ8jkMolZ/4/VsdBmMrw3l+VdZ3w== + } + dev: true + + /@jridgewell/trace-mapping/0.3.13: + resolution: + { + integrity: sha512-o1xbKhp9qnIAoHJSWd6KlCZfqslL4valSF81H8ImioOAxluWYWOpWkpyktY2vnt4tbrX9XYaxovq6cgowaJp2w== + } + dependencies: + '@jridgewell/resolve-uri': 3.0.7 + '@jridgewell/sourcemap-codec': 1.4.13 + dev: true + /@nodelib/fs.scandir/2.1.5: resolution: { @@ -380,12 +407,21 @@ packages: tiny-glob: 0.2.9 dev: true - /@sveltejs/kit/1.0.0-next.326_svelte@3.48.0: + /@sveltejs/adapter-static/1.0.0-next.31: resolution: { - integrity: sha512-prJqmXZ2H1wmFfnMw7wDujfbkcA8vuubuqUkpVVmXhfh2+SEzQscPTNwxoE5EJxb5sywtLWEvYx3hv1gPS4Lvg== + integrity: sha512-d9RNA/de5ljb+gN8mKA3YfmfJoTbYFdH96NYDD8u4Lu9O/ZnseUxOAcAmD4/LKbLXOY/oYhRpt029xT2owyI3Q== } - engines: { node: '>=14.13' } + dependencies: + tiny-glob: 0.2.9 + dev: true + + /@sveltejs/kit/1.0.0-next.334_svelte@3.48.0: + resolution: + { + integrity: sha512-HPMF1oYBfyOG6wfU0Y6F4SID8jphue9yF+PXJqVTDBL5Z2WBG2ogum6MavE8aWhq+g2H6w5y0jNT8+8DO2KTCA== + } + engines: { node: '>=16' } hasBin: true peerDependencies: svelte: ^3.44.0 @@ -495,7 +531,7 @@ packages: dependencies: '@types/http-cache-semantics': 4.0.1 '@types/keyv': 3.1.3 - '@types/node': 17.0.31 + '@types/node': 17.0.34 '@types/responselike': 1.0.0 dev: false @@ -533,7 +569,7 @@ packages: integrity: sha512-FXCJgyyN3ivVgRoml4h94G/p3kY+u/B86La+QptcqJaWtBWtmc6TtkNfS40n9bIvyLteHh7zXOtgbobORKPbDg== } dependencies: - '@types/node': 17.0.31 + '@types/node': 17.0.34 dev: false /@types/node-forge/1.0.2: @@ -542,13 +578,13 @@ packages: integrity: sha512-J1OkeZGaW0y9Y7xD49Ja8O82B9l5nZDeoYuGWqIOYPAf9LR+xF23k9ILdzv8dH+2H033fx3D5oiA0GlmicI+sg== } dependencies: - '@types/node': 17.0.31 + '@types/node': 17.0.34 dev: true - /@types/node/17.0.31: + /@types/node/17.0.34: resolution: { - integrity: sha512-AR0x5HbXGqkEx9CadRH3EBYx/VkiUgZIhP4wvPn/+5KIsgpNoyFaRlVe0Zlx9gRtg8fA06a9tskE2MSN7TcG4Q== + integrity: sha512-XImEz7XwTvDBtzlTnm8YvMqGW/ErMWBsKZ+hMTvnDIjGCKxwK5Xpc+c/oQjOauwq8M4OS11hEkpjX8rrI/eEgA== } /@types/pug/2.0.5: @@ -564,7 +600,7 @@ packages: integrity: sha512-85Y2BjiufFzaMIlvJDvTTB8Fxl2xfLo4HgmHzVBz08w4wDePCTjYw66PdrolO0kzli3yam/YCgRufyo1DdQVTA== } dependencies: - '@types/node': 17.0.31 + '@types/node': 17.0.34 dev: false /@types/sass/1.16.1: @@ -573,7 +609,7 @@ packages: integrity: sha512-iZUcRrGuz/Tbg3loODpW7vrQJkUtpY2fFSf4ELqqkApcS2TkZ1msk7ie8iZPB86lDOP8QOTTmuvWjc5S0R9OjQ== } dependencies: - '@types/node': 17.0.31 + '@types/node': 17.0.34 dev: true /@typescript-eslint/eslint-plugin/4.31.1_lii63oz3usekbu5ehvrcuwn5jy: @@ -1689,10 +1725,10 @@ packages: ieee754: 1.2.1 dev: false - /bullmq/1.81.4: + /bullmq/1.82.2: resolution: { - integrity: sha512-sUEWOMKZnWlh1/XNqYAoSwXW6P8nZN7uJiHKZ8XlZCiIxWlEGjFtlugkkiCZ0lsTI2nNRHdxfpn78x9K3L1utQ== + integrity: sha512-pDmMl6HmL/7B41ldBK4lnmGUcobkI/n/a0T3d/volMWC0ULxsaZ6R6fDePk23LwH9Fxu4o9Ny+zurCL3vG7lbg== } dependencies: cron-parser: 4.2.1 @@ -4150,7 +4186,7 @@ packages: dependencies: lilconfig: 2.0.5 postcss: 8.4.13 - ts-node: 10.7.0_l47be6km5p57gglrggidw5gsgm + ts-node: 10.7.0_3smuweqyuzdazdnyhhezld6mfa yaml: 1.10.2 dev: true @@ -4218,10 +4254,10 @@ packages: svelte: 3.48.0 dev: true - /prettier-plugin-tailwindcss/0.1.10_prettier@2.6.2: + /prettier-plugin-tailwindcss/0.1.11_prettier@2.6.2: resolution: { - integrity: sha512-ooDGNuXUjgCXfShliVYQ6+0iXqUFXn+zdNInPe0WZN9qINt9srbLGFGY5jeVL4MXtY20/4S8JaBcd8l6N6NfCQ== + integrity: sha512-a28+1jvpIZQdZ/W97wOXb6VqI762MKE/TxMMuibMEHhyYsSxQA8Ek30KObd5kJI2HF1ldtSYprFayXJXi3pz8Q== } engines: { node: '>=12.17.0' } peerDependencies: @@ -4708,14 +4744,6 @@ packages: engines: { node: '>=0.10.0' } dev: true - /source-map/0.7.3: - resolution: - { - integrity: sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ== - } - engines: { node: '>= 8' } - dev: true - /sourcemap-codec/1.4.8: resolution: { @@ -4888,21 +4916,21 @@ packages: engines: { node: '>= 0.4' } dev: true - /svelte-check/2.7.0_f2ke6qjyzu5axsjd6yk3u4tn7a: + /svelte-check/2.7.1_f2ke6qjyzu5axsjd6yk3u4tn7a: resolution: { - integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A== + integrity: sha512-vHVu2+SQ6ibt77iTQaq2oiOjBgGL48qqcg0ZdEOsP5pPOjgeyR9QbnaEdzdBs9nsVYBc/42haKtzb2uFqS8GVw== } hasBin: true peerDependencies: svelte: ^3.24.0 dependencies: + '@jridgewell/trace-mapping': 0.3.13 chokidar: 3.5.3 fast-glob: 3.2.11 import-fresh: 3.3.0 picocolors: 1.0.0 sade: 1.7.4 - source-map: 0.7.3 svelte: 3.48.0 svelte-preprocess: 4.10.6_nq4dx2skq5drra53vttuo4lltu typescript: 4.6.4 @@ -5143,7 +5171,7 @@ packages: engines: { node: '>=0.10.0' } dev: true - /ts-node/10.7.0_l47be6km5p57gglrggidw5gsgm: + /ts-node/10.7.0_3smuweqyuzdazdnyhhezld6mfa: resolution: { integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== @@ -5165,7 +5193,7 @@ packages: '@tsconfig/node12': 1.0.9 '@tsconfig/node14': 1.0.1 '@tsconfig/node16': 1.0.2 - '@types/node': 17.0.31 + '@types/node': 17.0.34 acorn: 8.5.0 acorn-walk: 8.2.0 arg: 4.1.3 diff --git a/prisma/migrations/20220517081328_traefik/migration.sql b/prisma/migrations/20220517081328_traefik/migration.sql new file mode 100644 index 000000000..a83281fa4 --- /dev/null +++ b/prisma/migrations/20220517081328_traefik/migration.sql @@ -0,0 +1,24 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "proxyPassword" TEXT NOT NULL, + "proxyUser" TEXT NOT NULL, + "proxyHash" TEXT, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "isDNSCheckEnabled" BOOLEAN NOT NULL DEFAULT true, + "isTraefikUsed" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isAutoUpdateEnabled", "isDNSCheckEnabled", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/migrations/20220519095648_minio_apifqdn/migration.sql b/prisma/migrations/20220519095648_minio_apifqdn/migration.sql new file mode 100644 index 000000000..a44712864 --- /dev/null +++ b/prisma/migrations/20220519095648_minio_apifqdn/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Minio" ADD COLUMN "apiFqdn" TEXT; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b0c2e233a..7cef6adda 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -20,6 +20,7 @@ model Setting { proxyHash String? isAutoUpdateEnabled Boolean @default(false) isDNSCheckEnabled Boolean @default(true) + isTraefikUsed Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } @@ -334,6 +335,7 @@ model Minio { rootUser String rootUserPassword String publicPort Int? + apiFqdn String? serviceId String @unique service Service @relation(fields: [serviceId], references: [id]) createdAt DateTime @default(now()) diff --git a/prisma/seed.cjs b/prisma/seed.cjs index 1e6b160c6..2a76ee27c 100644 --- a/prisma/seed.cjs +++ b/prisma/seed.cjs @@ -5,6 +5,12 @@ const prisma = new PrismaClient(); const crypto = require('crypto'); const generator = require('generate-password'); const cuid = require('cuid'); +const compare = require('compare-versions'); +const { version } = require('../package.json'); +const child = require('child_process'); +const util = require('util'); + +const algorithm = 'aes-256-ctr'; function generatePassword(length = 24) { return generator.generate({ @@ -13,7 +19,7 @@ function generatePassword(length = 24) { strict: true }); } -const algorithm = 'aes-256-ctr'; +const asyncExecShell = util.promisify(child.exec); async function main() { // Enable registration for the first user @@ -64,6 +70,56 @@ async function main() { } }); } + if (settings.isTraefikUsed) { + // Force stop Coolify Proxy, as it had a bug in < 2.9.2. TrustProxy + api.insecure + try { + const { stdout } = await asyncExecShell( + `docker inspect coolify-proxy --format '{{json .Config.Cmd}}'` + ); + if ( + !stdout + .replaceAll('[', '') + .replaceAll(']', '') + .replaceAll('"', '') + .replace('\n', '') + .split(',') + .includes('--entrypoints.web.forwardedHeaders.insecure=true') + ) { + console.log('Reconfiguring Coolify Proxy (Traefik)...'); + await asyncExecShell(`docker stop -t 0 coolify-proxy && docker rm coolify-proxy`); + const { stdout: Config } = await asyncExecShell( + `docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + await asyncExecShell( + `docker run --restart always \ + --add-host 'host.docker.internal:host-gateway' \ + --add-host 'host.docker.internal:${ip}' \ + -v coolify-traefik-letsencrypt:/etc/traefik/acme \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network coolify-infra \ + -p "80:80" \ + -p "443:443" \ + --name coolify-proxy \ + -d traefik:v2.6 \ + --entrypoints.web.address=:80 \ + --entrypoints.web.forwardedHeaders.insecure=true \ + --entrypoints.websecure.address=:443 \ + --entrypoints.websecure.forwardedHeaders.insecure=true \ + --providers.docker=true \ + --providers.docker.exposedbydefault=false \ + --providers.http.endpoint=http://coolify:3000/webhooks/traefik/main.json \ + --providers.http.pollTimeout=5s \ + --certificatesresolvers.letsencrypt.acme.httpchallenge=true \ + --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ + --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ + --log.level=error` + ); + } + } catch (error) { + console.log(error); + } + } } main() .catch((e) => { diff --git a/src/app.html b/src/app.html index a0336e87d..ab9a75726 100644 --- a/src/app.html +++ b/src/app.html @@ -3,7 +3,6 @@ - Coolify %svelte.head% diff --git a/src/lib/buildPacks/nestjs.ts b/src/lib/buildPacks/nestjs.ts index b0bb6ba89..9486c9e8e 100644 --- a/src/lib/buildPacks/nestjs.ts +++ b/src/lib/buildPacks/nestjs.ts @@ -10,8 +10,7 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (isPnpm) { - Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); - Dockerfile.push('RUN pnpm add -g pnpm'); + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); } Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`); diff --git a/src/lib/buildPacks/nextjs.ts b/src/lib/buildPacks/nextjs.ts index 77f5bc5f8..5ebb30410 100644 --- a/src/lib/buildPacks/nextjs.ts +++ b/src/lib/buildPacks/nextjs.ts @@ -35,8 +35,7 @@ const createDockerfile = async (data, image): Promise => { }); } if (isPnpm) { - Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); - Dockerfile.push('RUN pnpm add -g pnpm'); + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); } Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`RUN ${installCommand}`); diff --git a/src/lib/buildPacks/node.ts b/src/lib/buildPacks/node.ts index 9d8e643ef..46043593b 100644 --- a/src/lib/buildPacks/node.ts +++ b/src/lib/buildPacks/node.ts @@ -36,8 +36,7 @@ const createDockerfile = async (data, image): Promise => { }); } if (isPnpm) { - Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); - Dockerfile.push('RUN pnpm add -g pnpm'); + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); } Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`RUN ${installCommand}`); diff --git a/src/lib/buildPacks/nuxtjs.ts b/src/lib/buildPacks/nuxtjs.ts index 9cd7e9674..e1fded7f2 100644 --- a/src/lib/buildPacks/nuxtjs.ts +++ b/src/lib/buildPacks/nuxtjs.ts @@ -35,8 +35,7 @@ const createDockerfile = async (data, image): Promise => { }); } if (isPnpm) { - Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); - Dockerfile.push('RUN pnpm add -g pnpm'); + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); } Dockerfile.push(`COPY .${baseDirectory || ''} ./`); Dockerfile.push(`RUN ${installCommand}`); diff --git a/src/lib/common.ts b/src/lib/common.ts index 61009d42f..cae9350ce 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -96,12 +96,16 @@ export const getUserDetails = async ( const userId = event?.locals?.session?.data?.userId || null; let permission = 'read'; if (teamId && userId) { - const data = await db.prisma.permission.findFirst({ - where: { teamId, userId }, - select: { permission: true }, - rejectOnNotFound: true - }); - if (data.permission) permission = data.permission; + try { + const data = await db.prisma.permission.findFirst({ + where: { teamId, userId }, + select: { permission: true }, + rejectOnNotFound: true + }); + if (data.permission) permission = data.permission; + } catch (error) { + console.log(error); + } } const payload = { diff --git a/src/lib/components/PageLoader.svelte b/src/lib/components/PageLoader.svelte new file mode 100644 index 000000000..edbbcd80c --- /dev/null +++ b/src/lib/components/PageLoader.svelte @@ -0,0 +1,35 @@ + + +
+
+
+ + diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index d42fcb726..183f6ab6a 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -62,7 +62,7 @@ export const supportedDatabaseTypesAndVersions = [ name: 'postgresql', fancyName: 'PostgreSQL', baseImage: 'bitnami/postgresql', - versions: ['14.2.0', '13.6.0', '12.10.0 ', '11.15.0', '10.20.0'] + versions: ['14.2.0', '13.6.0', '12.10.0', '11.15.0', '10.20.0'] }, { name: 'redis', @@ -219,6 +219,18 @@ export const supportedServiceTypesAndVersions = [ ports: { main: 3000 } + // }, + // { + // name: 'appwrite', + // fancyName: 'AppWrite', + // baseImage: 'appwrite/appwrite', + // images: ['appwrite/influxdb', 'appwrite/telegraf', 'mariadb:10.7', 'redis:6.0-alpine3.12'], + // versions: ['latest', '0.13.0'], + // recommendedVersion: '0.13.0', + // ports: { + // main: 3000 + // } + // } } ]; diff --git a/src/lib/database/checks.ts b/src/lib/database/checks.ts index bbe773ae5..2c1c25cd8 100644 --- a/src/lib/database/checks.ts +++ b/src/lib/database/checks.ts @@ -51,10 +51,12 @@ export async function isSecretExists({ export async function isDomainConfigured({ id, - fqdn + fqdn, + checkOwn = false }: { id: string; fqdn: string; + checkOwn?: boolean; }): Promise { const domain = getDomain(fqdn); const nakedDomain = domain.replace('www.', ''); @@ -72,12 +74,15 @@ export async function isDomainConfigured({ where: { OR: [ { fqdn: { endsWith: `//${nakedDomain}` } }, - { fqdn: { endsWith: `//www.${nakedDomain}` } } + { fqdn: { endsWith: `//www.${nakedDomain}` } }, + { minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } }, + { minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } } ], - id: { not: id } + id: { not: checkOwn ? undefined : id } }, select: { fqdn: true } }); + const coolifyFqdn = await prisma.setting.findFirst({ where: { OR: [ diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index 53551882b..c26796ea4 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -305,6 +305,12 @@ export async function getFreePort() { select: { mysqlPublicPort: true } }) ).map((a) => a.mysqlPublicPort); - const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed]; + const minioUsed = await ( + await prisma.minio.findMany({ + where: { publicPort: { not: null } }, + select: { publicPort: true } + }) + ).map((a) => a.publicPort); + const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed]; return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts }); } diff --git a/src/lib/database/destinations.ts b/src/lib/database/destinations.ts index 2f6cdb488..f378874b0 100644 --- a/src/lib/database/destinations.ts +++ b/src/lib/database/destinations.ts @@ -1,6 +1,6 @@ import { asyncExecShell, getEngine } from '$lib/common'; import { dockerInstance } from '$lib/docker'; -import { startCoolifyProxy } from '$lib/haproxy'; +import { startCoolifyProxy, startTraefikProxy } from '$lib/haproxy'; import { getDatabaseImage } from '.'; import { prisma } from './common'; import type { DestinationDocker, Service, Application, Prisma } from '@prisma/client'; @@ -125,7 +125,14 @@ export async function newLocalDestination({ } await prisma.destinationDocker.updateMany({ where: { engine }, data: { isCoolifyProxyUsed } }); } - if (isCoolifyProxyUsed) await startCoolifyProxy(engine); + if (isCoolifyProxyUsed) { + const settings = await prisma.setting.findFirst(); + if (settings?.isTraefikUsed) { + await startTraefikProxy(engine); + } else { + await startCoolifyProxy(engine); + } + } return destination.id; } export async function removeDestination({ id }: Pick): Promise { @@ -133,12 +140,14 @@ export async function removeDestination({ id }: Pick): if (destination.isCoolifyProxyUsed) { const host = getEngine(destination.engine); const { network } = destination; + const settings = await prisma.setting.findFirst(); + const containerName = settings.isTraefikUsed ? 'coolify-proxy' : 'coolify-haproxy'; const { stdout: found } = await asyncExecShell( - `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=coolify-haproxy --format '{{.}}'` + `DOCKER_HOST=${host} docker ps -a --filter network=${network} --filter name=${containerName} --format '{{.}}'` ); if (found) { await asyncExecShell( - `DOCKER_HOST="${host}" docker network disconnect ${network} coolify-haproxy` + `DOCKER_HOST="${host}" docker network disconnect ${network} ${containerName}` ); await asyncExecShell(`DOCKER_HOST="${host}" docker network rm ${network}`); } diff --git a/src/lib/database/gitSources.ts b/src/lib/database/gitSources.ts index a0c0b94ed..bed676988 100644 --- a/src/lib/database/gitSources.ts +++ b/src/lib/database/gitSources.ts @@ -67,16 +67,10 @@ export async function getSource({ return body; } export async function addGitHubSource({ id, teamId, type, name, htmlUrl, apiUrl, organization }) { - await prisma.gitSource.update({ + return await prisma.gitSource.update({ where: { id }, data: { type, name, htmlUrl, apiUrl, organization } }); - return await prisma.githubApp.create({ - data: { - teams: { connect: { id: teamId } }, - gitSource: { connect: { id } } - } - }); } export async function addGitLabSource({ id, diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index d2a7bb899..a481a8fe1 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -18,7 +18,7 @@ const include: Prisma.ServiceInclude = { hasura: true, fider: true }; -export async function listServicesWithIncludes() { +export async function listServicesWithIncludes(): Promise { return await prisma.service.findMany({ include, orderBy: { createdAt: 'desc' } @@ -360,7 +360,24 @@ export async function updateService({ }): Promise { return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } }); } - +export async function updateMinioService({ + id, + fqdn, + apiFqdn, + exposePort, + name +}: { + id: string; + fqdn: string; + apiFqdn: string; + exposePort?: number; + name: string; +}): Promise { + return await prisma.service.update({ + where: { id }, + data: { fqdn, name, exposePort, minio: { update: { apiFqdn } } } + }); +} export async function updateFiderService({ id, fqdn, @@ -418,7 +435,6 @@ export async function updateWordpress({ fqdn, name, exposePort, - ownMysql, mysqlDatabase, extraConfig, mysqlHost, @@ -459,7 +475,7 @@ export async function updateWordpress({ }); } -export async function updateMinioService({ +export async function updateMinioServicePort({ id, publicPort }: { diff --git a/src/lib/docker.ts b/src/lib/docker.ts index 0b60a4ed9..864b32b62 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -66,8 +66,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) { }); } if (isPnpm) { - Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); - Dockerfile.push('RUN pnpm add -g pnpm'); + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); } if (installCommand) { Dockerfile.push(`COPY .${baseDirectory || ''}/package.json ./`); diff --git a/src/lib/haproxy/index.ts b/src/lib/haproxy/index.ts index 5b713e343..362783b73 100644 --- a/src/lib/haproxy/index.ts +++ b/src/lib/haproxy/index.ts @@ -3,12 +3,22 @@ import { asyncExecShell, getEngine } from '$lib/common'; import got, { type Got, type Response } from 'got'; import * as db from '$lib/database'; import type { DestinationDocker } from '@prisma/client'; - +import fs from 'fs/promises'; +import yaml from 'js-yaml'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; export const defaultProxyImage = `coolify-haproxy-alpine:latest`; export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`; export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`; +export const defaultTraefikImage = `traefik:v2.6`; + +const mainTraefikEndpoint = dev + ? 'http://host.docker.internal:3000/webhooks/traefik/main.json' + : 'http://coolify:3000/webhooks/traefik/main.json'; + +const otherTraefikEndpoint = dev + ? 'http://host.docker.internal:3000/webhooks/traefik/other.json' + : 'http://coolify:3000/webhooks/traefik/other.json'; export async function haproxyInstance(): Promise { const { proxyPassword } = await db.listSettings(); @@ -98,13 +108,21 @@ export async function checkHAProxy(haproxy?: Got): Promise { } export async function stopTcpHttpProxy( + id: string, destinationDocker: DestinationDocker, - publicPort: number + publicPort: number, + forceName: string = null ): Promise<{ stdout: string; stderr: string } | Error> { const { engine } = destinationDocker; const host = getEngine(engine); - const containerName = `haproxy-for-${publicPort}`; + const settings = await db.listSettings(); + let containerName = `${id}-${publicPort}`; + if (!settings.isTraefikUsed) { + containerName = `haproxy-for-${publicPort}`; + } + if (forceName) containerName = forceName; const found = await checkContainer(engine, containerName); + try { if (found) { return await asyncExecShell( @@ -115,12 +133,77 @@ export async function stopTcpHttpProxy( return error; } } -export async function startTcpProxy( +export async function startTraefikTCPProxy( destinationDocker: DestinationDocker, id: string, publicPort: number, privatePort: number, - volume?: string + type?: string +): Promise<{ stdout: string; stderr: string } | Error> { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + const containerName = `${id}-${publicPort}`; + const found = await checkContainer(engine, containerName, true); + let dependentId = id; + if (type === 'wordpressftp') dependentId = `${id}-ftp`; + const foundDependentContainer = await checkContainer(engine, dependentId, true); + try { + if (foundDependentContainer && !found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + const tcpProxy = { + version: '3.5', + services: { + [`${id}-${publicPort}`]: { + container_name: containerName, + image: 'traefik:v2.6', + command: [ + `--entrypoints.tcp.address=:${publicPort}`, + `--entryPoints.tcp.forwardedHeaders.insecure=true`, + `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`, + '--providers.http.pollTimeout=2s', + '--log.level=error' + ], + ports: [`${publicPort}:${publicPort}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + volumes: ['/var/run/docker.sock:/var/run/docker.sock'], + networks: ['coolify-infra', network] + } + }, + networks: { + [network]: { + external: false, + name: network + }, + 'coolify-infra': { + external: false, + name: 'coolify-infra' + } + } + }; + await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` + ); + await fs.rm(`/tmp/docker-compose-${id}.yaml`); + } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } + } catch (error) { + console.log(error); + return error; + } +} +export async function startTcpProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number ): Promise<{ stdout: string; stderr: string } | Error> { const { network, engine } = destinationDocker; const host = getEngine(engine); @@ -128,7 +211,6 @@ export async function startTcpProxy( const containerName = `haproxy-for-${publicPort}`; const found = await checkContainer(engine, containerName, true); const foundDependentContainer = await checkContainer(engine, id, true); - try { if (foundDependentContainer && !found) { const { stdout: Config } = await asyncExecShell( @@ -136,9 +218,7 @@ export async function startTcpProxy( ); const ip = JSON.parse(Config)[0].Gateway; return await asyncExecShell( - `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} ${ - volume ? `-v ${volume}` : '' - } -d coollabsio/${defaultProxyImageTcp}` + `DOCKER_HOST=${host} docker run --restart always -e PORT=${publicPort} -e APP=${id} -e PRIVATE_PORT=${privatePort} --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' --network ${network} -p ${publicPort}:${publicPort} --name ${containerName} -d coollabsio/${defaultProxyImageTcp}` ); } if (!foundDependentContainer && found) { @@ -151,6 +231,76 @@ export async function startTcpProxy( } } +export async function startTraefikHTTPProxy( + destinationDocker: DestinationDocker, + id: string, + publicPort: number, + privatePort: number +): Promise<{ stdout: string; stderr: string } | Error> { + const { network, engine } = destinationDocker; + const host = getEngine(engine); + + const containerName = `${id}-${publicPort}`; + const found = await checkContainer(engine, containerName, true); + const foundDependentContainer = await checkContainer(engine, id, true); + + try { + if (foundDependentContainer && !found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + const tcpProxy = { + version: '3.5', + services: { + [`${id}-${publicPort}`]: { + container_name: containerName, + image: 'traefik:v2.6', + command: [ + `--entrypoints.http.address=:${publicPort}`, + `--entryPoints.http.forwardedHeaders.insecure=true`, + `--providers.http.endpoint=${otherTraefikEndpoint}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=http`, + '--providers.http.pollTimeout=2s', + '--certificatesresolvers.letsencrypt.acme.httpchallenge=true', + '--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json', + '--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http', + '--log.level=error' + ], + ports: [`${publicPort}:${publicPort}`], + extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal:${ip}`], + networks: ['coolify-infra', network], + volumes: ['coolify-traefik-letsencrypt:/etc/traefik/acme'] + } + }, + networks: { + [network]: { + external: false, + name: network + }, + 'coolify-infra': { + external: false, + name: 'coolify-infra' + } + }, + volumes: { + 'coolify-traefik-letsencrypt': {} + } + }; + await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f /tmp/docker-compose-${id}.yaml up -d` + ); + await fs.rm(`/tmp/docker-compose-${id}.yaml`); + } + if (!foundDependentContainer && found) { + return await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${containerName} && docker rm ${containerName}` + ); + } + } catch (error) { + return error; + } +} export async function startHttpProxy( destinationDocker: DestinationDocker, id: string, @@ -197,10 +347,50 @@ export async function startCoolifyProxy(engine: string): Promise { `DOCKER_HOST="${host}" docker run -e HAPROXY_USERNAME=${proxyUser} -e HAPROXY_PASSWORD=${proxyPassword} --restart always --add-host 'host.docker.internal:host-gateway' --add-host 'host.docker.internal:${ip}' -v coolify-ssl-certs:/usr/local/etc/haproxy/ssl --network coolify-infra -p "80:80" -p "443:443" -p "8404:8404" -p "5555:5555" -p "5000:5000" --name coolify-haproxy -d coollabsio/${defaultProxyImage}` ); await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); } await configureNetworkCoolifyProxy(engine); } +export async function startTraefikProxy(engine: string): Promise { + const host = getEngine(engine); + const found = await checkContainer(engine, 'coolify-proxy', true); + const { id, proxyPassword, proxyUser } = await db.listSettings(); + if (!found) { + const { stdout: Config } = await asyncExecShell( + `DOCKER_HOST="${host}" docker network inspect bridge --format '{{json .IPAM.Config }}'` + ); + const ip = JSON.parse(Config)[0].Gateway; + await asyncExecShell( + `DOCKER_HOST="${host}" docker run --restart always \ + --add-host 'host.docker.internal:host-gateway' \ + --add-host 'host.docker.internal:${ip}' \ + -v coolify-traefik-letsencrypt:/etc/traefik/acme \ + -v /var/run/docker.sock:/var/run/docker.sock \ + --network coolify-infra \ + -p "80:80" \ + -p "443:443" \ + --name coolify-proxy \ + -d ${defaultTraefikImage} \ + --entrypoints.web.address=:80 \ + --entrypoints.web.forwardedHeaders.insecure=true \ + --entrypoints.websecure.address=:443 \ + --entrypoints.websecure.forwardedHeaders.insecure=true \ + --providers.docker=true \ + --providers.docker.exposedbydefault=false \ + --providers.http.endpoint=${mainTraefikEndpoint} \ + --providers.http.pollTimeout=5s \ + --certificatesresolvers.letsencrypt.acme.httpchallenge=true \ + --certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \ + --certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \ + --log.level=error` + ); + await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); + } + await configureNetworkTraefikProxy(engine); +} + export async function isContainerExited(engine: string, containerName: string): Promise { let isExited = false; const host = getEngine(engine); @@ -245,6 +435,21 @@ export async function checkContainer( return containerFound; } +export async function getContainerUsage(engine: string, container: string): Promise { + const host = getEngine(engine); + try { + const { stdout } = await asyncExecShell( + `DOCKER_HOST="${host}" docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` + ); + return JSON.parse(stdout); + } catch (err) { + return { + MemUsage: 0, + CPUPerc: 0, + NetIO: 0 + }; + } +} export async function stopCoolifyProxy( engine: string ): Promise<{ stdout: string; stderr: string } | Error> { @@ -263,6 +468,24 @@ export async function stopCoolifyProxy( return error; } } +export async function stopTraefikProxy( + engine: string +): Promise<{ stdout: string; stderr: string } | Error> { + const host = getEngine(engine); + const found = await checkContainer(engine, 'coolify-proxy'); + await db.setDestinationSettings({ engine, isCoolifyProxyUsed: false }); + const { id } = await db.prisma.setting.findFirst({}); + await db.prisma.setting.update({ where: { id }, data: { proxyHash: null } }); + try { + if (found) { + await asyncExecShell( + `DOCKER_HOST="${host}" docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + ); + } + } catch (error) { + return error; + } +} export async function configureNetworkCoolifyProxy(engine: string): Promise { const host = getEngine(engine); @@ -279,3 +502,19 @@ export async function configureNetworkCoolifyProxy(engine: string): Promise { + const host = getEngine(engine); + const destinations = await db.prisma.destinationDocker.findMany({ where: { engine } }); + const { stdout: networks } = await asyncExecShell( + `DOCKER_HOST="${host}" docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'` + ); + const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(','); + for (const destination of destinations) { + if (!configuredNetworks.includes(destination.network)) { + await asyncExecShell( + `DOCKER_HOST="${host}" docker network connect ${destination.network} coolify-proxy` + ); + } + } +} diff --git a/src/lib/queues/proxy.ts b/src/lib/queues/proxy.ts index db8742e74..245fb3b02 100644 --- a/src/lib/queues/proxy.ts +++ b/src/lib/queues/proxy.ts @@ -1,4 +1,4 @@ -import { ErrorHandler } from '$lib/database'; +import { ErrorHandler, prisma } from '$lib/database'; import { configureHAProxy } from '$lib/haproxy/configuration'; export default async function (): Promise { try { - return await configureHAProxy(); + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await configureHAProxy(); + } } catch (error) { return ErrorHandler(error.response?.body || error); } diff --git a/src/lib/queues/proxyTcpHttp.ts b/src/lib/queues/proxyTcpHttp.ts index c268d2aa0..9ce239b66 100644 --- a/src/lib/queues/proxyTcpHttp.ts +++ b/src/lib/queues/proxyTcpHttp.ts @@ -1,5 +1,16 @@ import { ErrorHandler, generateDatabaseConfiguration, prisma } from '$lib/database'; -import { startCoolifyProxy, startHttpProxy, startTcpProxy } from '$lib/haproxy'; +import { + checkContainer, + startCoolifyProxy, + startHttpProxy, + startTcpProxy, + startTraefikHTTPProxy, + startTraefikProxy, + startTraefikTCPProxy, + stopCoolifyProxy, + stopTcpHttpProxy, + stopTraefikProxy +} from '$lib/haproxy'; export default async function (): Promise { try { // Coolify Proxy + const engine = '/var/run/docker.sock'; + const settings = await prisma.setting.findFirst(); const localDocker = await prisma.destinationDocker.findFirst({ - where: { engine: '/var/run/docker.sock' } + where: { engine, network: 'coolify' } }); if (localDocker && localDocker.isCoolifyProxyUsed) { - await startCoolifyProxy('/var/run/docker.sock'); + if (settings.isTraefikUsed) { + const found = await checkContainer(engine, 'coolify-haproxy'); + if (found) await stopCoolifyProxy(engine); + await startTraefikProxy(engine); + } else { + const found = await checkContainer(engine, 'coolify-proxy'); + if (found) await stopTraefikProxy(engine); + await startCoolifyProxy(engine); + } } + // TCP Proxies const databasesWithPublicPort = await prisma.database.findMany({ where: { publicPort: { not: null } }, @@ -21,8 +43,16 @@ export default async function (): Promise { try { - return await generateSSLCerts(); + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await generateSSLCerts(); + } } catch (error) { console.log(error); throw error; diff --git a/src/lib/queues/sslrenewal.ts b/src/lib/queues/sslrenewal.ts index 766b9c502..0c14b2dc9 100644 --- a/src/lib/queues/sslrenewal.ts +++ b/src/lib/queues/sslrenewal.ts @@ -1,8 +1,12 @@ import { renewSSLCerts } from '$lib/letsencrypt'; +import { prisma } from '$lib/database'; export default async function (): Promise { try { - return await renewSSLCerts(); + const settings = await prisma.setting.findFirst(); + if (!settings.isTraefikUsed) { + return await renewSSLCerts(); + } } catch (error) { console.log(error); throw error; diff --git a/src/lib/realtime.ts b/src/lib/realtime.ts new file mode 100644 index 000000000..419bb154b --- /dev/null +++ b/src/lib/realtime.ts @@ -0,0 +1,3 @@ +// import ioClient from 'socket.io-client'; +// const socket = ioClient('http://localhost:3000'); +// export const io = socket; diff --git a/src/lib/store.ts b/src/lib/store.ts index 684922bfb..9366d70b2 100644 --- a/src/lib/store.ts +++ b/src/lib/store.ts @@ -12,3 +12,14 @@ export const features: Readable<{ latestVersion: string; beta: boolean }> = read beta: browser && window.localStorage.getItem('beta') === 'true', latestVersion: browser && window.localStorage.getItem('latestVersion') }); + +export const isTraefikUsed: Writable = writable(false); + +export const status: Writable = writable({ + application: { + isRunning: false, + isExited: false, + loading: false, + initialLoading: true + } +}); diff --git a/src/routes/__layout.svelte b/src/routes/__layout.svelte index 6ead964da..492673a28 100644 --- a/src/routes/__layout.svelte +++ b/src/routes/__layout.svelte @@ -34,23 +34,30 @@ @@ -153,16 +162,16 @@ {#if loading} {:else} - {#if isExited} + {#if $status.application.isExited} {/if} - {#if isRunning} + {#if $status.application.initialLoading} + + {:else if $status.application.isRunning}
+ +
+
Application Usage
+
+
+
+
Used Memory / Memory Limit
+
+ {usage?.MemUsage} +
+
+ +
+
Used CPU
+
+ {usage?.CPUPerc} +
+
+ +
+
Network IO
+
+ {usage?.NetIO} +
+
+
+
+
@@ -347,7 +400,7 @@ value={application.destinationDocker.name} id="destination" disabled - class="bg-transparent " + class="bg-transparent" />
@@ -358,10 +411,10 @@ >
!isRunning && changeSettings('dualCerts')} + on:click={() => !$status.application.isRunning && changeSettings('dualCerts')} />
{#if application.buildPack === 'python'} @@ -531,8 +584,8 @@
{ const destinationDocker = await db.getDestinationByApplicationId({ id, teamId }); const docker = dockerInstance({ destinationDocker }); const listContainers = await docker.engine.listContainers({ - filters: { network: [destinationDocker.network] } + filters: { network: [destinationDocker.network], name: [id] } }); const containers = listContainers.filter((container) => { return ( @@ -30,11 +30,7 @@ export const get: RequestHandler = async (event) => { JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) ) .filter((container) => { - return ( - container.type !== 'manual' && - container.type !== 'webhook_commit' && - container.applicationId === id - ); + return container.pullmergeRequestId && container.applicationId === id; }); return { body: { diff --git a/src/routes/applications/[id]/previews/index.svelte b/src/routes/applications/[id]/previews/index.svelte index 152aa00d2..f343c17df 100644 --- a/src/routes/applications/[id]/previews/index.svelte +++ b/src/routes/applications/[id]/previews/index.svelte @@ -31,6 +31,7 @@ import { errorNotification } from '$lib/form'; import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; + import { goto } from '$app/navigation'; const { id } = $page.params; async function refreshSecrets() { @@ -39,11 +40,18 @@ } async function redeploy(container) { try { - await post(`/applications/${id}/deploy.json`, { + const { buildId } = await post(`/applications/${id}/deploy.json`, { pullmergeRequestId: container.pullmergeRequestId, branch: container.branch }); toast.push('Application redeployed queued.'); + if ($page.url.pathname.startsWith(`/applications/${id}/logs/build`)) { + return window.location.assign(`/applications/${id}/logs/build?buildId=${buildId}`); + } else { + return await goto(`/applications/${id}/logs/build?buildId=${buildId}`, { + replaceState: true + }); + } } catch ({ error }) { return errorNotification(error); } diff --git a/src/routes/applications/[id]/status.json.ts b/src/routes/applications/[id]/status.json.ts new file mode 100644 index 000000000..27c5df3d7 --- /dev/null +++ b/src/routes/applications/[id]/status.json.ts @@ -0,0 +1,36 @@ +import { asyncExecShell, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer, getContainerUsage, isContainerExited } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; +import { setDefaultConfiguration } from '$lib/buildPacks/common'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let isRunning = false; + let isExited = false; + try { + const application = await db.getApplication({ id, teamId }); + if (application.destinationDockerId) { + [isRunning, isExited] = await Promise.all([ + checkContainer(application.destinationDocker.engine, id), + isContainerExited(application.destinationDocker.engine, id) + ]); + } + return { + status: 200, + body: { + isRunning, + isExited + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/applications/[id]/usage.json.ts b/src/routes/applications/[id]/usage.json.ts new file mode 100644 index 000000000..ab161d81e --- /dev/null +++ b/src/routes/applications/[id]/usage.json.ts @@ -0,0 +1,30 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { getContainerUsage } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let usage = {}; + try { + const application = await db.getApplication({ id, teamId }); + if (application.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(application.destinationDocker.engine, id)]); + } + return { + status: 200, + body: { + usage + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/dashboard.json.ts b/src/routes/dashboard.json.ts index 297ce1a14..1b9528861 100644 --- a/src/routes/dashboard.json.ts +++ b/src/routes/dashboard.json.ts @@ -49,6 +49,7 @@ export const get: RequestHandler = async (event) => { where: { userId }, include: { team: { include: { _count: { select: { users: true } } } } } }); + const settings = await db.prisma.setting.findFirst(); return { body: { teams, @@ -57,7 +58,8 @@ export const get: RequestHandler = async (event) => { destinationsCount, teamsCount, databasesCount, - servicesCount + servicesCount, + settings } }; } catch (error) { diff --git a/src/routes/databases/[id]/delete.json.ts b/src/routes/databases/[id]/delete.json.ts index d731492ad..71c05130c 100644 --- a/src/routes/databases/[id]/delete.json.ts +++ b/src/routes/databases/[id]/delete.json.ts @@ -12,7 +12,7 @@ export const del: RequestHandler = async (event) => { const database = await db.getDatabase({ id, teamId }); if (database.destinationDockerId) { const everStarted = await stopDatabase(database); - if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort); + if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); } await db.removeDatabase({ id }); return { status: 200 }; diff --git a/src/routes/databases/[id]/index.svelte b/src/routes/databases/[id]/index.svelte index 3ead53529..c9eb7a4ff 100644 --- a/src/routes/databases/[id]/index.svelte +++ b/src/routes/databases/[id]/index.svelte @@ -33,10 +33,40 @@
@@ -49,4 +79,31 @@
+
+
Database Usage
+
+
+
+
Used Memory / Memory Limit
+
+ {usage?.MemUsage} +
+
+ +
+
Used CPU
+
+ {usage?.CPUPerc} +
+
+ +
+
Network IO
+
+ {usage?.NetIO} +
+
+
+
+
diff --git a/src/routes/databases/[id]/settings.json.ts b/src/routes/databases/[id]/settings.json.ts index 1f09946c6..1c2002890 100644 --- a/src/routes/databases/[id]/settings.json.ts +++ b/src/routes/databases/[id]/settings.json.ts @@ -1,7 +1,7 @@ import { getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { generateDatabaseConfiguration, ErrorHandler, getFreePort } from '$lib/database'; -import { startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; +import { startTcpProxy, startTraefikTCPProxy, stopTcpHttpProxy } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -13,6 +13,7 @@ export const post: RequestHandler = async (event) => { const publicPort = await getFreePort(); try { + const settings = await db.listSettings(); await db.setDatabase({ id, isPublic, appendOnly }); const database = await db.getDatabase({ id, teamId }); const { destinationDockerId, destinationDocker, publicPort: oldPublicPort } = database; @@ -21,10 +22,14 @@ export const post: RequestHandler = async (event) => { if (destinationDockerId) { if (isPublic) { await db.prisma.database.update({ where: { id }, data: { publicPort } }); - await startTcpProxy(destinationDocker, id, publicPort, privatePort); + if (settings.isTraefikUsed) { + await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); + } else { + await startTcpProxy(destinationDocker, id, publicPort, privatePort); + } } else { await db.prisma.database.update({ where: { id }, data: { publicPort: null } }); - await stopTcpHttpProxy(destinationDocker, oldPublicPort); + await stopTcpHttpProxy(id, destinationDocker, oldPublicPort); } } return { diff --git a/src/routes/databases/[id]/stop.json.ts b/src/routes/databases/[id]/stop.json.ts index 6bd4f4c19..15c5c6e41 100644 --- a/src/routes/databases/[id]/stop.json.ts +++ b/src/routes/databases/[id]/stop.json.ts @@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => { try { const database = await db.getDatabase({ id, teamId }); const everStarted = await stopDatabase(database); - if (everStarted) await stopTcpHttpProxy(database.destinationDocker, database.publicPort); + if (everStarted) await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); await db.setDatabase({ id, isPublic: false }); await db.prisma.database.update({ where: { id }, data: { publicPort: null } }); @@ -21,6 +21,7 @@ export const post: RequestHandler = async (event) => { status: 200 }; } catch (error) { + console.log(error); return ErrorHandler(error); } }; diff --git a/src/routes/databases/[id]/usage.json.ts b/src/routes/databases/[id]/usage.json.ts new file mode 100644 index 000000000..9ec987310 --- /dev/null +++ b/src/routes/databases/[id]/usage.json.ts @@ -0,0 +1,30 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { getContainerUsage } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let usage = {}; + try { + const database = await db.getDatabase({ id, teamId }); + if (database.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(database.destinationDocker.engine, id)]); + } + return { + status: 200, + body: { + usage + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/destinations/[id]/index.json.ts b/src/routes/destinations/[id]/index.json.ts index aa9be6aea..8c4c67cee 100644 --- a/src/routes/destinations/[id]/index.json.ts +++ b/src/routes/destinations/[id]/index.json.ts @@ -26,8 +26,12 @@ export const get: RequestHandler = async (event) => { // // await saveSshKey(destination); // payload.state = await checkContainer(engine, 'coolify-haproxy'); } else { + let containerName = 'coolify-proxy'; + if (!settings.isTraefikUsed) { + containerName = 'coolify-haproxy'; + } payload.state = - destination?.engine && (await checkContainer(destination.engine, 'coolify-haproxy')); + destination?.engine && (await checkContainer(destination.engine, containerName)); } return { status: 200, diff --git a/src/routes/destinations/[id]/restart.json.ts b/src/routes/destinations/[id]/restart.json.ts index 9d2da459a..c2cf88fa4 100644 --- a/src/routes/destinations/[id]/restart.json.ts +++ b/src/routes/destinations/[id]/restart.json.ts @@ -1,7 +1,12 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; import * as db from '$lib/database'; -import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; +import { + startCoolifyProxy, + startTraefikProxy, + stopCoolifyProxy, + stopTraefikProxy +} from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -11,9 +16,16 @@ export const post: RequestHandler = async (event) => { const { engine } = await event.request.json(); try { - await stopCoolifyProxy(engine); - await startCoolifyProxy(engine); + const settings = await db.prisma.setting.findFirst({}); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + await startTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + await startCoolifyProxy(engine); + } await db.setDestinationSettings({ engine, isCoolifyProxyUsed: true }); + return { status: 200 }; diff --git a/src/routes/destinations/[id]/start.json.ts b/src/routes/destinations/[id]/start.json.ts index f5272418a..865187542 100644 --- a/src/routes/destinations/[id]/start.json.ts +++ b/src/routes/destinations/[id]/start.json.ts @@ -1,6 +1,12 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; -import { startCoolifyProxy, stopCoolifyProxy } from '$lib/haproxy'; +import * as db from '$lib/database'; +import { + startCoolifyProxy, + startTraefikProxy, + stopCoolifyProxy, + stopTraefikProxy +} from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -8,14 +14,24 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { engine } = await event.request.json(); + const settings = await db.prisma.setting.findFirst({}); try { - await startCoolifyProxy(engine); + if (settings?.isTraefikUsed) { + await startTraefikProxy(engine); + } else { + await startCoolifyProxy(engine); + } + return { status: 200 }; } catch (error) { - await stopCoolifyProxy(engine); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + } return ErrorHandler(error); } }; diff --git a/src/routes/destinations/[id]/stop.json.ts b/src/routes/destinations/[id]/stop.json.ts index 495c7612e..a1e01d218 100644 --- a/src/routes/destinations/[id]/stop.json.ts +++ b/src/routes/destinations/[id]/stop.json.ts @@ -1,6 +1,7 @@ import { getUserDetails } from '$lib/common'; import { ErrorHandler } from '$lib/database'; -import { stopCoolifyProxy } from '$lib/haproxy'; +import * as db from '$lib/database'; +import { stopCoolifyProxy, stopTraefikProxy } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; export const post: RequestHandler = async (event) => { @@ -9,7 +10,13 @@ export const post: RequestHandler = async (event) => { const { engine } = await event.request.json(); try { - await stopCoolifyProxy(engine); + const settings = await db.prisma.setting.findFirst({}); + if (settings?.isTraefikUsed) { + await stopTraefikProxy(engine); + } else { + await stopCoolifyProxy(engine); + } + return { status: 200 }; diff --git a/src/routes/index.svelte b/src/routes/index.svelte index bd441afdb..858a0387b 100644 --- a/src/routes/index.svelte +++ b/src/routes/index.svelte @@ -23,7 +23,6 @@ import { t } from '$lib/translations'; import { get } from '$lib/api'; import { onDestroy, onMount } from 'svelte'; - import Loading from './applications/[id]/logs/_Loading.svelte'; import Trend from './_Trend.svelte'; import { session } from '$app/stores'; diff --git a/src/routes/services/[id]/_Services/_MinIO.svelte b/src/routes/services/[id]/_Services/_MinIO.svelte index 8610b0025..10d36a252 100644 --- a/src/routes/services/[id]/_Services/_MinIO.svelte +++ b/src/routes/services/[id]/_Services/_MinIO.svelte @@ -30,14 +30,16 @@ value={service.minio.rootUserPassword} />
-
- - -
+{#if !service.minio.apiFqdn} +
+ + +
+{/if} diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte index facf713c1..60371725f 100644 --- a/src/routes/services/[id]/_Services/_Services.svelte +++ b/src/routes/services/[id]/_Services/_Services.svelte @@ -1,4 +1,6 @@
@@ -86,6 +117,14 @@
+ {#if service.type === 'minio' && !service.minio.apiFqdn && isRunning} +
+ IMPORTANT! There was a small modification with + Minio in the latest version of Coolify. Now you can separate the Console URL from the API URL, + so you could use both through SSL. But this proccess cannot be done automatically, so you have + to stop your Minio instance, configure the new domain and start it back. Sorry for any inconvenience. +
+ {/if}
@@ -131,25 +170,62 @@ {/if}
-
-
- - -
+ {#if service.type === 'minio'} +
+
+ +
+ + +
+
+
+ + +
+ + +
+ {:else} +
+
+ + +
+ + +
+ {/if} - -
!isRunning && changeSettings('dualCerts')} />
-
+
{ + const { status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + const { id } = event.params; + + let { name, fqdn, exposePort } = await event.request.json(); + if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); + + try { + await db.updateService({ id, fqdn, name, exposePort }); + return { status: 201 }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/appwrite-wip/start.json.ts b/src/routes/services/[id]/appwrite-wip/start.json.ts new file mode 100644 index 000000000..70748a0a6 --- /dev/null +++ b/src/routes/services/[id]/appwrite-wip/start.json.ts @@ -0,0 +1,519 @@ +import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; +import type { ComposeFile } from '$lib/types/composeFile'; +import { getServiceMainPort } from '$lib/components/common'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; + const network = destinationDockerId && destinationDocker.network; + const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('n8n'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + variables[secret.name] = secret.value; + }); + } + + const variables = { + _APP_ENV: 'production', + _APP_VERSION: '', + _APP_LOCALE: '', + _APP_OPTIONS_ABUSE: '', + _APP_OPTIONS_FORCE_HTTPS: '', + _APP_OPENSSL_KEY_V1: '', + _APP_DOMAIN: '', + _APP_DOMAIN_TARGET: '', + _APP_CONSOLE_WHITELIST_ROOT: '', + _APP_CONSOLE_WHITELIST_EMAILS: '', + _APP_CONSOLE_WHITELIST_IPS: '', + _APP_SYSTEM_EMAIL_NAME: '', + _APP_SYSTEM_EMAIL_ADDRESS: '', + _APP_SYSTEM_RESPONSE_FORMAT: '', + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: '', + _APP_USAGE_STATS: '', + _APP_LOGGING_PROVIDER: '', + _APP_LOGGING_CONFIG: '', + _APP_USAGE_AGGREGATION_INTERVAL: '', + _APP_WORKER_PER_CORE: '', + _APP_REDIS_HOST: '', + _APP_REDIS_PORT: '', + _APP_REDIS_USER: '', + _APP_REDIS_PASS: '', + _APP_DB_HOST: '', + _APP_DB_PORT: '', + _APP_DB_SCHEMA: '', + _APP_DB_USER: '', + _APP_DB_PASS: '', + _APP_DB_ROOT_PASS: '', + _APP_INFLUXDB_HOST: '', + _APP_INFLUXDB_PORT: '', + _APP_STATSD_HOST: '', + _APP_STATSD_PORT: '', + _APP_SMTP_HOST: '', + _APP_SMTP_PORT: '', + _APP_SMTP_SECURE: '', + _APP_SMTP_USERNAME: '', + _APP_SMTP_PASSWORD: '', + _APP_STORAGE_LIMIT: '', + _APP_STORAGE_ANTIVIRUS: '', + _APP_STORAGE_ANTIVIRUS_HOST: '', + _APP_STORAGE_ANTIVIRUS_PORT: '', + _APP_STORAGE_DEVICE: '', + _APP_STORAGE_S3_ACCESS_KEY: '', + _APP_STORAGE_S3_SECRET: '', + _APP_STORAGE_S3_REGION: '', + _APP_STORAGE_S3_BUCKET: '', + _APP_STORAGE_DO_SPACES_ACCESS_KEY: '', + _APP_STORAGE_DO_SPACES_SECRET: '', + _APP_STORAGE_DO_SPACES_REGION: '', + _APP_STORAGE_DO_SPACES_BUCKET: '', + _APP_FUNCTIONS_SIZE_LIMIT: '', + _APP_FUNCTIONS_TIMEOUT: '', + _APP_FUNCTIONS_BUILD_TIMEOUT: '', + _APP_FUNCTIONS_CONTAINERS: '', + _APP_FUNCTIONS_CPUS: '', + _APP_FUNCTIONS_MEMORY: '', + _APP_FUNCTIONS_MEMORY_SWAP: '', + _APP_FUNCTIONS_RUNTIMES: '', + _APP_EXECUTOR_SECRET: '', + _APP_EXECUTOR_RUNTIME_NETWORK: '', + _APP_FUNCTIONS_ENVS: '', + _APP_FUNCTIONS_INACTIVE_THRESHOLD: '', + DOCKERHUB_PULL_USERNAME: '', + DOCKERHUB_PULL_PASSWORD: '', + DOCKERHUB_PULL_EMAIL: '', + _APP_MAINTENANCE_INTERVAL: '', + _APP_MAINTENANCE_RETENTION_EXECUTION: '', + _APP_MAINTENANCE_RETENTION_ABUSE: '', + _APP_MAINTENANCE_RETENTION_AUDIT: '' + }; + const config = { + appwrite: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-uploads:/storage/uploads`, + `${id}-appwrite-cache:/storage/cache`, + `${id}-appwrite-config:/storage/config`, + `${id}-appwrite-certificates:/storage/certificates`, + `${id}-appwrite-functions:/storage/functions` + ], + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_WORKER_PER_CORE: variables._APP_WORKER_PER_CORE, + _APP_LOCALE: variables._APP_LOCALE, + _APP_CONSOLE_WHITELIST_ROOT: variables._APP_CONSOLE_WHITELIST_ROOT, + _APP_CONSOLE_WHITELIST_EMAILS: variables._APP_CONSOLE_WHITELIST_EMAILS, + _APP_CONSOLE_WHITELIST_IPS: variables._APP_CONSOLE_WHITELIST_IPS, + _APP_SYSTEM_EMAIL_NAME: variables._APP_SYSTEM_EMAIL_NAME, + _APP_SYSTEM_EMAIL_ADDRESS: variables._APP_SYSTEM_EMAIL_ADDRESS, + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS, + _APP_SYSTEM_RESPONSE_FORMAT: variables._APP_SYSTEM_RESPONSE_FORMAT, + _APP_OPTIONS_ABUSE: variables._APP_OPTIONS_ABUSE, + _APP_OPTIONS_FORCE_HTTPS: variables._APP_OPTIONS_FORCE_HTTPS, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_DOMAIN: variables._APP_DOMAIN, + _APP_DOMAIN_TARGET: variables._APP_DOMAIN_TARGET, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_SMTP_HOST: variables._APP_SMTP_HOST, + _APP_SMTP_PORT: variables._APP_SMTP_PORT, + _APP_SMTP_SECURE: variables._APP_SMTP_SECURE, + _APP_SMTP_USERNAME: variables._APP_SMTP_USERNAME, + _APP_SMTP_PASSWORD: variables._APP_SMTP_PASSWORD, + _APP_USAGE_STATS: variables._APP_USAGE_STATS, + _APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST, + _APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT, + _APP_STORAGE_LIMIT: variables._APP_STORAGE_LIMIT, + _APP_STORAGE_ANTIVIRUS: variables._APP_STORAGE_ANTIVIRUS, + _APP_STORAGE_ANTIVIRUS_HOST: variables._APP_STORAGE_ANTIVIRUS_HOST, + _APP_STORAGE_ANTIVIRUS_PORT: variables._APP_STORAGE_ANTIVIRUS_PORT, + _APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE, + _APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY, + _APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET, + _APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION, + _APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET, + _APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY, + _APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET, + _APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION, + _APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET, + _APP_FUNCTIONS_SIZE_LIMIT: variables._APP_FUNCTIONS_SIZE_LIMIT, + _APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT, + _APP_FUNCTIONS_BUILD_TIMEOUT: variables._APP_FUNCTIONS_BUILD_TIMEOUT, + _APP_FUNCTIONS_CONTAINERS: variables._APP_FUNCTIONS_CONTAINERS, + _APP_FUNCTIONS_CPUS: variables._APP_FUNCTIONS_CPUS, + _APP_FUNCTIONS_MEMORY: variables._APP_FUNCTIONS_MEMORY, + _APP_FUNCTIONS_MEMORY_SWAP: variables._APP_FUNCTIONS_MEMORY_SWAP, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_FUNCTIONS_RUNTIMES: variables._APP_FUNCTIONS_RUNTIMES, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG, + _APP_STATSD_HOST: variables._APP_STATSD_HOST, + _APP_STATSD_PORT: variables._APP_STATSD_PORT, + _APP_MAINTENANCE_INTERVAL: variables._APP_MAINTENANCE_INTERVAL, + _APP_MAINTENANCE_RETENTION_EXECUTION: variables._APP_MAINTENANCE_RETENTION_EXECUTION, + _APP_MAINTENANCE_RETENTION_ABUSE: variables._APP_MAINTENANCE_RETENTION_ABUSE, + _APP_MAINTENANCE_RETENTION_AUDIT: variables._APP_MAINTENANCE_RETENTION_AUDIT + } + }, + appwriteRealtime: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_WORKER_PER_CORE: variables._APP_WORKER_PER_CORE, + _APP_OPTIONS_ABUSE: variables._APP_OPTIONS_ABUSE, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_USAGE_STATS: variables._APP_USAGE_STATS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteExecutor: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-functions:/storage/functions`, + `/tmp:/tmp`, + '/var/run/docker.sock:/var/run/docker.sock' + ], + environmentVariables: { + DOCKERHUB_PULL_USERNAME: variables.DOCKERHUB_PULL_USERNAME, + DOCKERHUB_PULL_PASSWORD: variables.DOCKERHUB_PULL_PASSWORD, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG, + _APP_VERSION: variables._APP_VERSION, + _APP_ENV: variables._APP_ENV, + _APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE, + _APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY, + _APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET, + _APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION, + _APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET, + _APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY, + _APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET, + _APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION, + _APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET, + _APP_FUNCTIONS_CPUS: variables._APP_FUNCTIONS_CPUS, + _APP_FUNCTIONS_MEMORY: variables._APP_FUNCTIONS_MEMORY, + _APP_FUNCTIONS_MEMORY_SWAP: variables._APP_FUNCTIONS_MEMORY_SWAP, + _APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_FUNCTIONS_RUNTIMES: variables._APP_FUNCTIONS_RUNTIMES, + _APP_FUNCTIONS_INACTIVE_THRESHOLD: variables._APP_FUNCTIONS_INACTIVE_THRESHOLD, + _APP_EXECUTOR_RUNTIME_NETWORK: variables._APP_EXECUTOR_RUNTIME_NETWORK + } + }, + appwriteWorkerDatabase: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerBuilds: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerAudits: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerWebhooks: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerDeletes: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-uploads:/storage/uploads`, + `${id}-appwrite-cache:/storage/cache`, + `${id}-appwrite-certificates:/storage/certificates` + ], + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_STORAGE_DEVICE: variables._APP_STORAGE_DEVICE, + _APP_STORAGE_S3_ACCESS_KEY: variables._APP_STORAGE_S3_ACCESS_KEY, + _APP_STORAGE_S3_SECRET: variables._APP_STORAGE_S3_SECRET, + _APP_STORAGE_S3_REGION: variables._APP_STORAGE_S3_REGION, + _APP_STORAGE_S3_BUCKET: variables._APP_STORAGE_S3_BUCKET, + _APP_STORAGE_DO_SPACES_ACCESS_KEY: variables._APP_STORAGE_DO_SPACES_ACCESS_KEY, + _APP_STORAGE_DO_SPACES_SECRET: variables._APP_STORAGE_DO_SPACES_SECRET, + _APP_STORAGE_DO_SPACES_REGION: variables._APP_STORAGE_DO_SPACES_REGION, + _APP_STORAGE_DO_SPACES_BUCKET: variables._APP_STORAGE_DO_SPACES_BUCKET, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerCertificates: { + image: `${image}:${version}`, + volumes: [ + `${id}-appwrite-config:/storage/config`, + `${id}-appwrite-certificates:/storage/certificates` + ], + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_SYSTEM_SECURITY_EMAIL_ADDRESS: variables._APP_SYSTEM_SECURITY_EMAIL_ADDRESS, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DOMAIN_TARGET: variables._APP_DOMAIN_TARGET, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteWorkerFunctions: { + image: `${image}:${version}`, + envvironmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_FUNCTIONS_TIMEOUT: variables._APP_FUNCTIONS_TIMEOUT, + _APP_EXECUTOR_SECRET: variables._APP_EXECUTOR_SECRET, + _APP_USAGE_STATS: variables._APP_USAGE_STATS, + DOCKERHUB_PULL_USERNAME: variables.DOCKERHUB_PULL_USERNAME, + DOCKERHUB_PULL_PASSWORD: variables.DOCKERHUB_PULL_PASSWORD + } + }, + appwriteWorkerMails: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_SYSTEM_EMAIL_NAME: variables._APP_SYSTEM_EMAIL_NAME, + _APP_SYSTEM_EMAIL_ADDRESS: variables._APP_SYSTEM_EMAIL_ADDRESS, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_SMTP_HOST: variables._APP_SMTP_HOST, + _APP_SMTP_PORT: variables._APP_SMTP_PORT, + _APP_SMTP_SECURE: variables._APP_SMTP_SECURE, + _APP_SMTP_USERNAME: variables._APP_SMTP_USERNAME, + _APP_SMTP_PASSWORD: variables._APP_SMTP_PASSWORD, + _APP_LOGGING_PROVIDER: variables._APP_LOGGING_PROVIDER, + _APP_LOGGING_CONFIG: variables._APP_LOGGING_CONFIG + } + }, + appwriteMaintenance: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS, + _APP_MAINTENANCE_INTERVAL: variables._APP_MAINTENANCE_INTERVAL, + _APP_MAINTENANCE_RETENTION_EXECUTION: variables._APP_MAINTENANCE_RETENTION_EXECUTION, + _APP_MAINTENANCE_RETENTION_ABUSE: variables._APP_MAINTENANCE_RETENTION_ABUSE, + _APP_MAINTENANCE_RETENTION_AUDIT: variables._APP_MAINTENANCE_RETENTION_AUDIT + } + }, + appwriteUsage: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_OPENSSL_KEY_V1: variables._APP_OPENSSL_KEY_V1, + _APP_DB_HOST: variables._APP_DB_HOST, + _APP_DB_PORT: variables._APP_DB_PORT, + _APP_DB_SCHEMA: variables._APP_DB_SCHEMA, + _APP_DB_USER: variables._APP_DB_USER, + _APP_DB_PASS: variables._APP_DB_PASS, + _APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST, + _APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT, + _APP_USAGE_AGGREGATION_INTERVAL: variables._APP_USAGE_AGGREGATION_INTERVAL, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS + } + }, + appwriteSchedule: { + image: `${image}:${version}`, + environmentVariables: { + _APP_ENV: variables._APP_ENV, + _APP_REDIS_HOST: variables._APP_REDIS_HOST, + _APP_REDIS_PORT: variables._APP_REDIS_PORT, + _APP_REDIS_USER: variables._APP_REDIS_USER, + _APP_REDIS_PASS: variables._APP_REDIS_PASS + } + }, + mariadb: { + image: 'mariadb:10.7', + volumes: [`${id}-appwrite-mariadb:/var/lib/mysql`], + environmentVariables: { + MYSQL_ROOT_PASSWORD: variables._APP_DB_ROOT_PASS, + MYSQL_DATABASE: variables._APP_DB_SCHEMA, + MYSQL_USER: variables._APP_DB_USER, + MYSQL_PASSWORD: variables._APP_DB_PASS + } + }, + redis: { + image: 'redis:6.0-alpine3.12', + volumes: [`${id}-appwrite-redis:/data`] + }, + influxdb: { + image: 'appwrite/influxdb:1.0.0', + volumes: [`${id}-appwrite-influxdb:/var/lib/influxdb`] + }, + telegraf: { + image: 'appwrite/telegraf:1.0.0', + environmentVariables: { + _APP_INFLUXDB_HOST: variables._APP_INFLUXDB_HOST, + _APP_INFLUXDB_PORT: variables._APP_INFLUXDB_PORT + } + } + }; + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + networks: [network], + volumes: [...config.appwrite.volumes], + environment: config.environmentVariables, + restart: 'always', + labels: makeLabelForServices('appwrite'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + } + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/appwrite-wip/stop.json.ts b/src/routes/services/[id]/appwrite-wip/stop.json.ts new file mode 100644 index 000000000..c604e1cc3 --- /dev/null +++ b/src/routes/services/[id]/appwrite-wip/stop.json.ts @@ -0,0 +1,35 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker, fqdn } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + const found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + } catch (error) { + console.error(error); + } + } + + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/check.json.ts b/src/routes/services/[id]/check.json.ts index 9a9193524..4201e94da 100644 --- a/src/routes/services/[id]/check.json.ts +++ b/src/routes/services/[id]/check.json.ts @@ -1,19 +1,56 @@ import { asyncExecShell, getDomain, getEngine, getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; +import { t } from '$lib/translations'; import type { RequestHandler } from '@sveltejs/kit'; +import getPort from 'get-port'; export const post: RequestHandler = async (event) => { const { status, body } = await getUserDetails(event); if (status === 401) return { status, body }; const { id } = event.params; - let { fqdn } = await event.request.json(); + let { fqdn, exposePort, otherFqdns } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (otherFqdns && otherFqdns.length > 0) otherFqdns = otherFqdns.map((f) => f.toLowerCase()); + if (exposePort) exposePort = Number(exposePort); try { - const found = await db.isDomainConfigured({ id, fqdn }); + let found = await db.isDomainConfigured({ id, fqdn }); + if (found) { + throw { + message: t.get('application.domain_already_in_use', { + domain: getDomain(fqdn).replace('www.', '') + }) + }; + } + if (otherFqdns && otherFqdns.length > 0) { + for (const ofqdn of otherFqdns) { + const domain = getDomain(ofqdn); + const nakedDomain = domain.replace('www.', ''); + found = await db.isDomainConfigured({ id, fqdn: ofqdn, checkOwn: true }); + if (found) { + throw { + message: t.get('application.domain_already_in_use', { + domain: nakedDomain + }) + }; + } + } + } + if (exposePort) { + exposePort = Number(exposePort); + + if (exposePort < 1024 || exposePort > 65535) { + throw { message: `Exposed Port needs to be between 1024 and 65535.` }; + } + + const publicPort = await getPort({ port: exposePort }); + if (publicPort !== exposePort) { + throw { message: `Port ${exposePort} is already in use.` }; + } + } return { status: found ? 500 : 200, body: { diff --git a/src/routes/services/[id]/fider/start.json.ts b/src/routes/services/[id]/fider/start.json.ts index 51e45ec3f..2adf53738 100644 --- a/src/routes/services/[id]/fider/start.json.ts +++ b/src/routes/services/[id]/fider/start.json.ts @@ -59,7 +59,7 @@ export const post: RequestHandler = async (event) => { fider: { image: `${image}:${version}`, environmentVariables: { - HOST_DOMAIN: domain, + BASE_URL: domain, DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`, JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`, EMAIL_NOREPLY: emailNoreply, diff --git a/src/routes/services/[id]/index.svelte b/src/routes/services/[id]/index.svelte index a16591266..4d231a8fb 100644 --- a/src/routes/services/[id]/index.svelte +++ b/src/routes/services/[id]/index.svelte @@ -30,19 +30,43 @@
@@ -52,6 +76,7 @@
{service.name}
+ {#if service.fqdn}
+
+
Service Usage
+
+
+
+
Used Memory / Memory Limit
+
+ {usage?.MemUsage} +
+
+
+
Used CPU
+
+ {usage?.CPUPerc} +
+
+ +
+
Network IO
+
+ {usage?.NetIO} +
+
+
+
+
diff --git a/src/routes/services/[id]/minio/index.json.ts b/src/routes/services/[id]/minio/index.json.ts index ff98ede6d..32056218d 100644 --- a/src/routes/services/[id]/minio/index.json.ts +++ b/src/routes/services/[id]/minio/index.json.ts @@ -9,12 +9,17 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn, exposePort } = await event.request.json(); + let { + name, + fqdn, + exposePort, + minio: { apiFqdn } + } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); if (exposePort) exposePort = Number(exposePort); - + if (apiFqdn) apiFqdn = apiFqdn.toLowerCase(); try { - await db.updateService({ id, fqdn, name, exposePort }); + await db.updateMinioService({ id, fqdn, apiFqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/minio/start.json.ts b/src/routes/services/[id]/minio/start.json.ts index 0c93eb3ce..9dd75bdd9 100644 --- a/src/routes/services/[id]/minio/start.json.ts +++ b/src/routes/services/[id]/minio/start.json.ts @@ -3,7 +3,6 @@ import * as db from '$lib/database'; import { promises as fs } from 'fs'; import yaml from 'js-yaml'; import type { RequestHandler } from '@sveltejs/kit'; -import { startHttpProxy } from '$lib/haproxy'; import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; @@ -35,7 +34,6 @@ export const post: RequestHandler = async (event) => { const publicPort = await getFreePort(); const consolePort = 9001; - const apiPort = 9000; const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -94,8 +92,7 @@ export const post: RequestHandler = async (event) => { try { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); - await db.updateMinioService({ id, publicPort }); - await startHttpProxy(destinationDocker, id, publicPort, apiPort); + await db.updateMinioServicePort({ id, publicPort }); return { status: 200 }; diff --git a/src/routes/services/[id]/minio/stop.json.ts b/src/routes/services/[id]/minio/stop.json.ts index 3e2340237..4ae3c106b 100644 --- a/src/routes/services/[id]/minio/stop.json.ts +++ b/src/routes/services/[id]/minio/stop.json.ts @@ -12,12 +12,7 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { - destinationDockerId, - destinationDocker, - fqdn, - minio: { publicPort } - } = service; + const { destinationDockerId, destinationDocker } = service; await db.updateMinioService({ id, publicPort: null }); if (destinationDockerId) { const engine = destinationDocker.engine; @@ -30,11 +25,6 @@ export const post: RequestHandler = async (event) => { } catch (error) { console.error(error); } - try { - await stopTcpHttpProxy(destinationDocker, publicPort); - } catch (error) { - console.log(error); - } } return { diff --git a/src/routes/services/[id]/nocodb/start.json.ts b/src/routes/services/[id]/nocodb/start.json.ts index bf3a702df..35eb4e2c5 100644 --- a/src/routes/services/[id]/nocodb/start.json.ts +++ b/src/routes/services/[id]/nocodb/start.json.ts @@ -27,6 +27,7 @@ export const post: RequestHandler = async (event) => { const config = { image: `${image}:${version}`, + volume: `${id}-nc:/usr/app/data`, environmentVariables: {} }; if (serviceSecret.length > 0) { @@ -41,6 +42,7 @@ export const post: RequestHandler = async (event) => { container_name: id, image: config.image, networks: [network], + volumes: [config.volume], environment: config.environmentVariables, restart: 'always', ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), @@ -59,6 +61,11 @@ export const post: RequestHandler = async (event) => { [network]: { external: true } + }, + volumes: { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; diff --git a/src/routes/services/[id]/plausibleanalytics/start.json.ts b/src/routes/services/[id]/plausibleanalytics/start.json.ts index 81c8f2bd8..e98e7093a 100644 --- a/src/routes/services/[id]/plausibleanalytics/start.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/start.json.ts @@ -195,7 +195,6 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; - console.log(JSON.stringify(composeFile, null, 2)); await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); await asyncExecShell( diff --git a/src/routes/services/[id]/usage.json.ts b/src/routes/services/[id]/usage.json.ts new file mode 100644 index 000000000..24e27b1c4 --- /dev/null +++ b/src/routes/services/[id]/usage.json.ts @@ -0,0 +1,30 @@ +import { getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { getContainerUsage } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + let usage = {}; + try { + const service = await db.getService({ id, teamId }); + if (service.destinationDockerId) { + [usage] = await Promise.all([getContainerUsage(service.destinationDocker.engine, id)]); + } + return { + status: 200, + body: { + usage + }, + headers: {} + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/wordpress/ftp.json.ts b/src/routes/services/[id]/wordpress/ftp.json.ts index cee85bc41..fab9cd7df 100644 --- a/src/routes/services/[id]/wordpress/ftp.json.ts +++ b/src/routes/services/[id]/wordpress/ftp.json.ts @@ -3,7 +3,12 @@ import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; import { decrypt, encrypt } from '$lib/crypto'; import * as db from '$lib/database'; import { ErrorHandler, generatePassword, getFreePort } from '$lib/database'; -import { checkContainer, startTcpProxy, stopTcpHttpProxy } from '$lib/haproxy'; +import { + checkContainer, + startTcpProxy, + startTraefikTCPProxy, + stopTcpHttpProxy +} from '$lib/haproxy'; import type { ComposeFile } from '$lib/types/composeFile'; import type { RequestHandler } from '@sveltejs/kit'; import cuid from 'cuid'; @@ -31,49 +36,51 @@ export const post: RequestHandler = async (event) => { }); const { service: { destinationDockerId, destinationDocker }, - ftpPublicPort: oldPublicPort, + ftpPublicPort, ftpUser: user, ftpPassword: savedPassword, ftpHostKey, ftpHostKeyPrivate } = data; - if (user) ftpUser = user; - if (savedPassword) ftpPassword = decrypt(savedPassword); + const { network, engine } = destinationDocker; + const settings = await db.prisma.setting.findFirst(); + const host = getEngine(engine); + if (ftpEnabled) { + if (user) ftpUser = user; + if (savedPassword) ftpPassword = decrypt(savedPassword); + + const { stdout: password } = await asyncExecShell( + `echo ${ftpPassword} | openssl passwd -1 -stdin` + ); + if (destinationDockerId) { + try { + await fs.stat(hostkeyDir); + } catch (error) { + await asyncExecShell(`mkdir -p ${hostkeyDir}`); + } + if (!ftpHostKey) { + await asyncExecShell( + `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519` + ); + const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`); + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKey: encrypt(ftpHostKey) } + }); + } else { + await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`); + } + if (!ftpHostKeyPrivate) { + await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); + const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } + }); + } else { + await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); + } - const { stdout: password } = await asyncExecShell( - `echo ${ftpPassword} | openssl passwd -1 -stdin` - ); - if (destinationDockerId) { - try { - await fs.stat(hostkeyDir); - } catch (error) { - await asyncExecShell(`mkdir -p ${hostkeyDir}`); - } - if (!ftpHostKey) { - await asyncExecShell( - `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519` - ); - const { stdout: ftpHostKey } = await asyncExecShell(`cat ${hostkeyDir}/${id}.ed25519`); - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpHostKey: encrypt(ftpHostKey) } - }); - } else { - await asyncExecShell(`echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`); - } - if (!ftpHostKeyPrivate) { - await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); - const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } - }); - } else { - await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); - } - const { network, engine } = destinationDocker; - const host = getEngine(engine); - if (ftpEnabled) { await db.prisma.wordpress.update({ where: { serviceId: id }, data: { @@ -142,24 +149,7 @@ export const post: RequestHandler = async (event) => { await asyncExecShell( `DOCKER_HOST=${host} docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d` ); - - await startTcpProxy(destinationDocker, `${id}-ftp`, publicPort, 22); - } else { - await db.prisma.wordpress.update({ - where: { serviceId: id }, - data: { ftpPublicPort: null } - }); - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` - ); - } catch (error) { - // - } - await stopTcpHttpProxy(destinationDocker, oldPublicPort); } - } - if (ftpEnabled) { return { status: 201, body: { @@ -169,6 +159,18 @@ export const post: RequestHandler = async (event) => { } }; } else { + await db.prisma.wordpress.update({ + where: { serviceId: id }, + data: { ftpPublicPort: null } + }); + try { + await asyncExecShell( + `DOCKER_HOST=${host} docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp` + ); + } catch (error) { + // + } + await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort); return { status: 200, body: {} diff --git a/src/routes/settings/index.json.ts b/src/routes/settings/index.json.ts index 21f4907f2..e57b159c6 100644 --- a/src/routes/settings/index.json.ts +++ b/src/routes/settings/index.json.ts @@ -72,8 +72,7 @@ export const post: RequestHandler = async (event) => { minPort, maxPort, isAutoUpdateEnabled, - isDNSCheckEnabled, - forceSave + isDNSCheckEnabled } = await event.request.json(); try { const { id } = await db.listSettings(); diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index c601db4ed..6eeeb2bdf 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -37,12 +37,13 @@ import { getDomain } from '$lib/components/common'; import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; - import { features } from '$lib/store'; + import { features, isTraefikUsed } from '$lib/store'; let isRegistrationEnabled = settings.isRegistrationEnabled; let dualCerts = settings.dualCerts; let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let isDNSCheckEnabled = settings.isDNSCheckEnabled; + $isTraefikUsed = settings.isTraefikUsed; let minPort = settings.minPort; let maxPort = settings.maxPort; @@ -55,7 +56,8 @@ let isFqdnSet = !!settings.fqdn; let loading = { save: false, - remove: false + remove: false, + proxyMigration: false }; async function removeFqdn() { @@ -86,6 +88,7 @@ if (name === 'isDNSCheckEnabled') { isDNSCheckEnabled = !isDNSCheckEnabled; } + await post(`/settings.json`, { isRegistrationEnabled, dualCerts, @@ -156,6 +159,20 @@ function resetView() { forceSave = false; } + async function migrateProxy(to) { + if (loading.proxyMigration) return; + try { + loading.proxyMigration = true; + await post(`/update.json`, { type: to }); + const data = await get(`/settings.json`); + $isTraefikUsed = data.settings.isTraefikUsed; + return toast.push('Proxy migration started, it takes a few seconds.'); + } catch ({ error }) { + return errorNotification(error); + } finally { + loading.proxyMigration = false; + } + }
@@ -192,6 +209,26 @@
+
+
+
+
New Proxy Available!
+ +
+
+ +
diff --git a/src/routes/sources/[id]/_Github.svelte b/src/routes/sources/[id]/_Github.svelte index b03fe8abd..b21640df1 100644 --- a/src/routes/sources/[id]/_Github.svelte +++ b/src/routes/sources/[id]/_Github.svelte @@ -84,6 +84,9 @@
General
+ {#if source.apiUrl && source.htmlUrl && source.name} + + {/if}
@@ -117,11 +120,6 @@ />
- {#if source.apiUrl && source.htmlUrl && source.name} -
- -
- {/if} {:else if source.githubApp?.installationId}
@@ -173,7 +171,7 @@
{:else}
-
diff --git a/src/routes/update.json.ts b/src/routes/update.json.ts index 19845b90e..a7c3e5c57 100644 --- a/src/routes/update.json.ts +++ b/src/routes/update.json.ts @@ -34,14 +34,14 @@ export const get: RequestHandler = async (request) => { export const post: RequestHandler = async (event) => { const { type, latestVersion } = await event.request.json(); + const settings = await db.prisma.setting.findFirst(); if (type === 'update') { try { if (!dev) { - const { isAutoUpdateEnabled } = await db.prisma.setting.findFirst(); await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); await asyncExecShell(`env | grep COOLIFY > .env`); await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` + `sed -i '/COOLIFY_AUTO_UPDATE=/c\COOLIFY_AUTO_UPDATE=${settings.isAutoUpdateEnabled}' .env` ); await asyncExecShell( `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-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"` @@ -61,6 +61,44 @@ export const post: RequestHandler = async (event) => { } catch (error) { return ErrorHandler(error); } + } else if (type === 'traefik') { + try { + // const found = await checkContainer('/var/run/docker.sock', 'coolify-haproxy'); + // if (found) { + // await asyncExecShell(`docker stop -t 0 coolify-haproxy`); + // await asyncExecShell(`docker rm coolify-haproxy`); + // } + // await startTraefikProxy('/var/run/docker.sock'); + await db.prisma.setting.update({ + where: { id: settings.id }, + data: { isTraefikUsed: true } + }); + return { + status: 200, + body: {} + }; + } catch (error) { + return ErrorHandler(error); + } + } else if (type === 'haproxy') { + try { + // const found = await checkContainer('/var/run/docker.sock', 'coolify-proxy'); + // if (found) { + // await asyncExecShell(`docker stop -t 0 coolify-proxy`); + // await asyncExecShell(`docker rm coolify-proxy`); + // } + // await startCoolifyProxy('/var/run/docker.sock'); + await db.prisma.setting.update({ + where: { id: settings.id }, + data: { isTraefikUsed: false } + }); + return { + status: 200, + body: {} + }; + } catch (error) { + return ErrorHandler(error); + } } return { status: 500 diff --git a/src/routes/webhooks/traefik/main.json.ts b/src/routes/webhooks/traefik/main.json.ts new file mode 100644 index 000000000..a34d96a4a --- /dev/null +++ b/src/routes/webhooks/traefik/main.json.ts @@ -0,0 +1,364 @@ +import { dev } from '$app/env'; +import { asyncExecShell, getDomain, getEngine } from '$lib/common'; +import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import * as db from '$lib/database'; +import { listServicesWithIncludes } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +function configureMiddleware( + { id, container, port, domain, nakedDomain, isHttps, isWWW, isDualCerts, scriptName, type }, + traefik +) { + if (isHttps) { + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + middlewares: ['redirect-to-https'] + }; + + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${container}:${port}` + } + ] + } + }; + + if (isDualCerts) { + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + } else { + if (isWWW) { + traefik.http.routers[`${id}-secure-www`] = { + entrypoints: ['websecure'], + rule: `Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${domain}` + } + }, + middlewares: ['redirect-to-www'] + }; + traefik.http.routers[`${id}`].middlewares.push('redirect-to-www'); + } else { + traefik.http.routers[`${id}-secure-www`] = { + entrypoints: ['websecure'], + rule: `Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${domain}` + } + }, + middlewares: ['redirect-to-non-www'] + }; + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${domain}\`)`, + service: `${id}`, + tls: { + certresolver: 'letsencrypt' + }, + middlewares: [] + }; + traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www'); + } + } + } else { + traefik.http.routers[id] = { + entrypoints: ['web'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + middlewares: [] + }; + + traefik.http.routers[`${id}-secure`] = { + entrypoints: ['websecure'], + rule: `Host(\`${nakedDomain}\`) || Host(\`www.${nakedDomain}\`)`, + service: `${id}`, + tls: { + domains: { + main: `${nakedDomain}` + } + }, + middlewares: ['redirect-to-http'] + }; + + traefik.http.services[id] = { + loadbalancer: { + servers: [ + { + url: `http://${container}:${port}` + } + ] + } + }; + + if (!isDualCerts) { + if (isWWW) { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-www'); + } else { + traefik.http.routers[`${id}`].middlewares.push('redirect-to-non-www'); + traefik.http.routers[`${id}-secure`].middlewares.push('redirect-to-non-www'); + } + } + } + + if (type === 'plausibleanalytics' && scriptName && scriptName !== 'plausible.js') { + if (!traefik.http.routers[`${id}`].middlewares.includes(`${id}-redir`)) { + traefik.http.routers[`${id}`].middlewares.push(`${id}-redir`); + } + if (!traefik.http.routers[`${id}-secure`].middlewares.includes(`${id}-redir`)) { + traefik.http.routers[`${id}-secure`].middlewares.push(`${id}-redir`); + } + } +} +export const get: RequestHandler = async (event) => { + const traefik = { + http: { + routers: {}, + services: {}, + middlewares: { + 'redirect-to-https': { + redirectscheme: { + scheme: 'https' + } + }, + 'redirect-to-http': { + redirectscheme: { + scheme: 'http' + } + }, + 'redirect-to-non-www': { + redirectregex: { + regex: '^https?://www\\.(.+)', + replacement: 'http://${1}' + } + }, + 'redirect-to-www': { + redirectregex: { + regex: '^https?://(?:www\\.)?(.+)', + replacement: 'http://www.${1}' + } + } + } + } + }; + const applications = await db.prisma.application.findMany({ + include: { destinationDocker: true, settings: true } + }); + const data = { + applications: [], + services: [], + coolify: [] + }; + for (const application of applications) { + const { + fqdn, + id, + port, + destinationDocker, + destinationDockerId, + settings: { previews, dualCerts } + } = application; + if (destinationDockerId) { + const { engine, network } = destinationDocker; + const isRunning = true; + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + if (isRunning) { + data.applications.push({ + id, + container: id, + port: port || 3000, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + if (previews) { + const host = getEngine(engine); + const { stdout } = await asyncExecShell( + `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` + ); + const containers = stdout + .trim() + .split('\n') + .filter((a) => a) + .map((c) => c.replace(/"/g, '')); + if (containers.length > 0) { + for (const container of containers) { + const previewDomain = `${container.split('-')[1]}.${domain}`; + const nakedDomain = previewDomain.replace(/^www\./, ''); + data.applications.push({ + id: container, + container, + port: port || 3000, + domain: previewDomain, + isRunning, + nakedDomain, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + } + } + } + } + } + const services = await listServicesWithIncludes(); + + for (const service of services) { + const { + fqdn, + id, + type, + destinationDocker, + destinationDockerId, + dualCerts, + plausibleAnalytics + } = service; + if (destinationDockerId) { + const { engine } = destinationDocker; + const found = supportedServiceTypesAndVersions.find((a) => a.name === type); + if (found) { + const port = found.ports.main; + const publicPort = service[type]?.publicPort; + const isRunning = true; + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + if (isRunning) { + // Plausible Analytics custom script + let scriptName = false; + if (type === 'plausibleanalytics' && plausibleAnalytics.scriptName !== 'plausible.js') { + scriptName = plausibleAnalytics.scriptName; + } + + let container = id; + let otherDomain = null; + let otherNakedDomain = null; + let otherIsHttps = null; + let otherIsWWW = null; + + if (type === 'minio' && service.minio.apiFqdn) { + otherDomain = getDomain(service.minio.apiFqdn); + otherNakedDomain = otherDomain.replace(/^www\./, ''); + otherIsHttps = service.minio.apiFqdn.startsWith('https://'); + otherIsWWW = service.minio.apiFqdn.includes('www.'); + } + data.services.push({ + id, + container, + type, + otherDomain, + otherNakedDomain, + otherIsHttps, + otherIsWWW, + port, + publicPort, + domain, + nakedDomain, + isRunning, + isHttps, + isWWW, + isDualCerts: dualCerts, + scriptName + }); + } + } + } + } + } + + const { fqdn, dualCerts } = await db.prisma.setting.findFirst(); + if (fqdn) { + const domain = getDomain(fqdn); + const nakedDomain = domain.replace(/^www\./, ''); + const isHttps = fqdn.startsWith('https://'); + const isWWW = fqdn.includes('www.'); + data.coolify.push({ + id: dev ? 'host.docker.internal' : 'coolify', + container: dev ? 'host.docker.internal' : 'coolify', + port: 3000, + domain, + nakedDomain, + isHttps, + isWWW, + isDualCerts: dualCerts + }); + } + for (const application of data.applications) { + configureMiddleware(application, traefik); + } + for (const service of data.services) { + const { id, scriptName } = service; + + configureMiddleware(service, traefik); + if (service.type === 'minio') { + service.id = id + '-minio'; + service.container = id; + service.domain = service.otherDomain; + service.nakedDomain = service.otherNakedDomain; + service.isHttps = service.otherIsHttps; + service.isWWW = service.otherIsWWW; + service.port = 9000; + configureMiddleware(service, traefik); + } + + if (scriptName) { + traefik.http.middlewares[`${id}-redir`] = { + replacepathregex: { + regex: `/js/${scriptName}`, + replacement: '/js/plausible.js' + } + }; + } + } + for (const coolify of data.coolify) { + configureMiddleware(coolify, traefik); + } + if (Object.keys(traefik.http.routers).length === 0) { + traefik.http.routers = null; + } + if (Object.keys(traefik.http.services).length === 0) { + traefik.http.services = null; + } + return { + status: 200, + body: { + ...traefik + } + }; +}; diff --git a/src/routes/webhooks/traefik/other.json.ts b/src/routes/webhooks/traefik/other.json.ts new file mode 100644 index 000000000..3dc4d0e59 --- /dev/null +++ b/src/routes/webhooks/traefik/other.json.ts @@ -0,0 +1,137 @@ +import { dev } from '$app/env'; +import { getDomain } from '$lib/common'; +import * as db from '$lib/database'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const get: RequestHandler = async (event) => { + const id = event.url.searchParams.get('id'); + if (id) { + const privatePort = event.url.searchParams.get('privatePort'); + const publicPort = event.url.searchParams.get('publicPort'); + const type = event.url.searchParams.get('type'); + const address = event.url.searchParams.get('address') || id; + let traefik = {}; + if (publicPort && type && privatePort) { + if (type === 'tcp') { + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `HostSNI(\`*\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [{ address: `${address}:${privatePort}` }] + } + } + } + } + }; + } else if (type === 'http') { + const service = await db.prisma.service.findFirst({ + where: { id }, + include: { minio: true } + }); + if (service) { + if (service.type === 'minio') { + if (service?.minio?.apiFqdn) { + const { + minio: { apiFqdn } + } = service; + const domain = getDomain(apiFqdn); + const isHttps = apiFqdn.startsWith('https://'); + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `Host(\`${domain}\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [{ url: `http://${id}:${privatePort}` }] + } + } + } + } + }; + if (isHttps) { + if (dev) { + traefik[type].routers[id].tls = { + domains: { + main: `${domain}` + } + }; + } else { + traefik[type].routers[id].tls = { + certresolver: 'letsencrypt' + }; + } + } + } + } else { + if (service?.fqdn) { + const domain = getDomain(service.fqdn); + const isHttps = service.fqdn.startsWith('https://'); + traefik = { + [type]: { + routers: { + [id]: { + entrypoints: [type], + rule: `Host(\`${domain}:${privatePort}\`)`, + service: id + } + }, + services: { + [id]: { + loadbalancer: { + servers: [{ url: `http://${id}:${privatePort}` }] + } + } + } + } + }; + if (isHttps) { + if (dev) { + traefik[type].routers[id].tls = { + domains: { + main: `${domain}` + } + }; + } else { + traefik[type].routers[id].tls = { + certresolver: 'letsencrypt' + }; + } + } + } + } + } else { + return { + status: 500 + }; + } + } + } else { + return { + status: 500 + }; + } + return { + status: 200, + body: { + ...traefik + } + }; + } + return { + status: 500 + }; +}; diff --git a/src/tailwind.css b/src/tailwind.css index 8996a07c8..b8f1e647b 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -49,6 +49,9 @@ #svelte .custom-select-wrapper .selectContainer { @apply h-12 w-96 rounded border-none 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 .custom-select-wrapper .spinner { + @apply text-coollabs-100; +} #svelte .listContainer { @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-green-500 scrollbar-track-coolgray-200; } @@ -66,26 +69,6 @@ #svelte .item.active { 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; } -.svelte-select { - --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 w-64 text-xs tracking-tight md:text-sm; diff --git a/svelte.config.js b/svelte.config.js index 352c3e330..8fa7ba43e 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -1,6 +1,5 @@ import preprocess from 'svelte-preprocess'; import adapter from '@sveltejs/adapter-node'; - const config = { preprocess: preprocess(), kit: { @@ -10,6 +9,12 @@ const config = { }, floc: true, vite: { + proxy: { + '/api/v2': { + target: 'http://localhost:3001/api/v2', + changeOrigin: true + } + }, optimizeDeps: { exclude: ['svelte-kit-cookie-session'] },