diff --git a/.github/ISSUE_TEMPLATE/--bug-report.yaml b/.github/ISSUE_TEMPLATE/--bug-report.yaml new file mode 100644 index 000000000..53853d157 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--bug-report.yaml @@ -0,0 +1,47 @@ +name: 🐞 Bug report +description: Create a bug report to help us improve coolify +title: "[Bug]: " +labels: [Bug] +assignees: +- andrasbacsai +- vasani-arpit +body: +- type: markdown + attributes: + value: | + Thanks for taking the time to fill out this bug report! Please fill the form in English +- type: checkboxes + attributes: + label: Is there an existing issue for this? + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: Description + description: A concise description of what you're experiencing and what you expect. + placeholder: | + When I do , happens and I see the error message attached below: + ```...``` + What I expect is + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Add steps to reproduce this behaviour, include console / network logs & videos + placeholder: | + 1. Go to '...' + 2. Click on '....' + 3. Scroll down to '....' + 4. See error + validations: + required: true +- type: input + id: version + attributes: + label: Version + description: "The version of your coolify Instance" + placeholder: "2.5.2" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/--feature-request.yaml b/.github/ISSUE_TEMPLATE/--feature-request.yaml new file mode 100644 index 000000000..9e217ff23 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--feature-request.yaml @@ -0,0 +1,31 @@ +name: 🛠️ Feature request +description: Suggest an idea to improve coolify +title: '[Feature]: ' +labels: [Enhancement] +assignees: + - andrasbacsai + - vasani-arpit +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to request a feature for coolify! Please also add your request here to get feedback from the community: https://feedback.coolify.io/! + - type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue related to this feature request already exists. + options: + - label: I have searched the existing issues + required: true + - type: textarea + attributes: + label: Summary + description: One paragraph description of the feature. + validations: + required: true + - type: textarea + attributes: + label: Why should this be worked on? + description: A concise description of the problems or use cases for this feature request. + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/--task.yaml b/.github/ISSUE_TEMPLATE/--task.yaml new file mode 100644 index 000000000..a1ff080c5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/--task.yaml @@ -0,0 +1,20 @@ +name: 📝 Task +description: Create a task for the team to work on +title: "[Task]: " +labels: [Task] +body: +- type: checkboxes + attributes: + label: Is there an existing issue for this? + description: Please search to see if an issue related to this already exists. + options: + - label: I have searched the existing issues + required: true +- type: textarea + attributes: + label: SubTasks + placeholder: | + - Sub Task 1 + - Sub Task 2 + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..49618c8e7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: true +contact_links: + - name: 🤔 Questions and Help + url: https://discord.com/invite/6rDM4fkymF + about: Reach out to us on discord or our github discussions page. diff --git a/README.md b/README.md index 5660915df..e735b6c30 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ These are the predefined build packs, but with the Docker build pack, you can ho - NuxtJS - NextJS - React/Preact -- NextJS - Gatsby - Svelte - PHP @@ -68,6 +67,7 @@ These are the predefined build packs, but with the Docker build pack, you can ho One-click database is ready to be used internally or shared over the internet: - MongoDB +- MariaDB - MySQL - PostgreSQL - CouchDB diff --git a/package.json b/package.json index ece20c8eb..5c93290e9 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.6.3", + "version": "2.7.0", "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", @@ -77,6 +77,7 @@ "generate-password": "1.7.0", "get-port": "6.1.2", "got": "12.0.3", + "is-ip": "^4.0.0", "js-cookie": "3.0.1", "js-yaml": "4.1.0", "jsonwebtoken": "8.5.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8af565eeb..10f21d2f8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: 5.3 +lockfileVersion: 5.4 specifiers: '@iarna/toml': 2.2.5 @@ -31,6 +31,7 @@ specifiers: get-port: 6.1.2 got: 12.0.3 husky: 7.0.4 + is-ip: ^4.0.0 js-cookie: 3.0.1 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 @@ -71,6 +72,7 @@ dependencies: generate-password: 1.7.0 get-port: 6.1.2 got: 12.0.3 + is-ip: 4.0.0 js-cookie: 3.0.1 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 @@ -88,29 +90,29 @@ devDependencies: '@types/js-yaml': 4.0.5 '@types/node': 17.0.25 '@types/node-forge': 1.0.1 - '@typescript-eslint/eslint-plugin': 4.31.1_8ede7edd7694646e12d33c52460f622c - '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3 + '@typescript-eslint/eslint-plugin': 4.31.1_r3ph5xlwsrsg4ewthrjemd3cfq + '@typescript-eslint/parser': 4.31.1_hrkuebk64jiu2ut2d2sm4oylnu '@zerodevx/svelte-toast': 0.7.1 autoprefixer: 10.4.4_postcss@8.4.12 cross-env: 7.0.3 cross-var: 1.1.0 eslint: 7.32.0 eslint-config-prettier: 8.5.0_eslint@7.32.0 - eslint-plugin-svelte3: 3.4.1_eslint@7.32.0+svelte@3.47.0 + eslint-plugin-svelte3: 3.4.1_4oxeyilw5mxcaksmcxtpjddhfe husky: 7.0.4 lint-staged: 12.4.0 postcss: 8.4.12 prettier: 2.6.2 - prettier-plugin-svelte: 2.7.0_prettier@2.6.2+svelte@3.47.0 + prettier-plugin-svelte: 2.7.0_sqtt6dzjlskmywoml5ykunxlce prettier-plugin-tailwindcss: 0.1.10_prettier@2.6.2 prisma: 3.11.1 svelte: 3.47.0 - svelte-check: 2.7.0_postcss@8.4.12+svelte@3.47.0 - svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38 + svelte-check: 2.7.0_cp6olp7pwsfaq5mjijwt65d6uy + svelte-preprocess: 4.10.6_igaqrb5onrwvsmrrc32h4m72ha svelte-select: 4.4.7 sveltekit-i18n: 2.1.2_svelte@3.47.0 tailwindcss: 3.0.24_ts-node@10.7.0 - ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3 + ts-node: 10.7.0_3z6inmgn4ud4moqealnfxgbl2m tslib: 2.3.1 typescript: 4.6.3 @@ -571,7 +573,7 @@ packages: '@types/node': 17.0.25 dev: true - /@typescript-eslint/eslint-plugin/4.31.1_8ede7edd7694646e12d33c52460f622c: + /@typescript-eslint/eslint-plugin/4.31.1_r3ph5xlwsrsg4ewthrjemd3cfq: resolution: { integrity: sha512-UDqhWmd5i0TvPLmbK5xY3UZB0zEGseF+DHPghZ37Sb83Qd3p8ujhvAtkU4OF46Ka5Pm5kWvFIx0cCTBFKo0alA== @@ -585,8 +587,8 @@ packages: typescript: optional: true dependencies: - '@typescript-eslint/experimental-utils': 4.31.1_eslint@7.32.0+typescript@4.6.3 - '@typescript-eslint/parser': 4.31.1_eslint@7.32.0+typescript@4.6.3 + '@typescript-eslint/experimental-utils': 4.31.1_hrkuebk64jiu2ut2d2sm4oylnu + '@typescript-eslint/parser': 4.31.1_hrkuebk64jiu2ut2d2sm4oylnu '@typescript-eslint/scope-manager': 4.31.1 debug: 4.3.3 eslint: 7.32.0 @@ -599,7 +601,7 @@ packages: - supports-color dev: true - /@typescript-eslint/experimental-utils/4.31.1_eslint@7.32.0+typescript@4.6.3: + /@typescript-eslint/experimental-utils/4.31.1_hrkuebk64jiu2ut2d2sm4oylnu: resolution: { integrity: sha512-NtoPsqmcSsWty0mcL5nTZXMf7Ei0Xr2MT8jWjXMVgRK0/1qeQ2jZzLFUh4QtyJ4+/lPUyMw5cSfeeME+Zrtp9Q== @@ -620,7 +622,7 @@ packages: - typescript dev: true - /@typescript-eslint/parser/4.31.1_eslint@7.32.0+typescript@4.6.3: + /@typescript-eslint/parser/4.31.1_hrkuebk64jiu2ut2d2sm4oylnu: resolution: { integrity: sha512-dnVZDB6FhpIby6yVbHkwTKkn2ypjVIfAR9nh+kYsA/ZL0JlTsd22BiDjouotisY3Irmd3OW1qlk9EI5R8GrvRQ== @@ -2613,7 +2615,7 @@ packages: eslint: 7.32.0 dev: true - /eslint-plugin-svelte3/3.4.1_eslint@7.32.0+svelte@3.47.0: + /eslint-plugin-svelte3/3.4.1_4oxeyilw5mxcaksmcxtpjddhfe: resolution: { integrity: sha512-7p59WG8qV8L6wLdl4d/c3mdjkgVglQCdv5XOTk/iNPBKXuuV+Q0eFP5Wa6iJd/G2M1qR3BkLPEzaANOqKAZczw== @@ -3283,6 +3285,14 @@ packages: - supports-color dev: false + /ip-regex/5.0.0: + resolution: + { + integrity: sha512-fOCG6lhoKKakwv+C6KdsOnGvgXnmgfmp0myi3bcNwj3qfwPAxRKWEuFhvEFF7ceYIz6+1jRZ+yguLFAmUNPEfw== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dev: false + /is-binary-path/2.1.0: resolution: { @@ -3341,6 +3351,16 @@ packages: is-extglob: 2.1.1 dev: true + /is-ip/4.0.0: + resolution: + { + integrity: sha512-4B4XA2HEIm/PY+OSpeMBXr8pGWBYbXuHgjMAqrwbLO3CPTCAd9ArEJzBUKGZtk9viY6+aSfadGnWyjY3ydYZkw== + } + engines: { node: ^12.20.0 || ^14.13.1 || >=16.0.0 } + dependencies: + ip-regex: 5.0.0 + dev: false + /is-number/7.0.0: resolution: { @@ -4103,7 +4123,7 @@ packages: postcss: 8.4.12 dev: true - /postcss-load-config/3.1.4_postcss@8.4.12+ts-node@10.7.0: + /postcss-load-config/3.1.4_ysmyu6g5dtd6yanj6zrab4uqoy: resolution: { integrity: sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg== @@ -4120,7 +4140,7 @@ packages: dependencies: lilconfig: 2.0.5 postcss: 8.4.12 - ts-node: 10.7.0_de7c86b0cde507c63a0402da5b982bd3 + ts-node: 10.7.0_3z6inmgn4ud4moqealnfxgbl2m yaml: 1.10.2 dev: true @@ -4175,7 +4195,7 @@ packages: engines: { node: '>= 0.8.0' } dev: true - /prettier-plugin-svelte/2.7.0_prettier@2.6.2+svelte@3.47.0: + /prettier-plugin-svelte/2.7.0_sqtt6dzjlskmywoml5ykunxlce: resolution: { integrity: sha512-fQhhZICprZot2IqEyoiUYLTRdumULGRvw0o4dzl5jt0jfzVWdGqeYW27QTWAeXhoupEZJULmNoH3ueJwUWFLIA== @@ -4858,7 +4878,7 @@ packages: engines: { node: '>= 0.4' } dev: true - /svelte-check/2.7.0_postcss@8.4.12+svelte@3.47.0: + /svelte-check/2.7.0_cp6olp7pwsfaq5mjijwt65d6uy: resolution: { integrity: sha512-GrvG24j0+i8AOm0k0KyJ6Dqc+TAR2yzB7rtS4nljHStunVxCTr/1KYlv4EsOeoqtHLzeWMOd5D2O6nDdP/yw4A== @@ -4874,7 +4894,7 @@ packages: sade: 1.7.4 source-map: 0.7.3 svelte: 3.47.0 - svelte-preprocess: 4.10.6_41810887ae6c6d59323116f47e33fa38 + svelte-preprocess: 4.10.6_igaqrb5onrwvsmrrc32h4m72ha typescript: 4.6.3 transitivePeerDependencies: - '@babel/core' @@ -4907,7 +4927,7 @@ packages: } dev: false - /svelte-preprocess/4.10.6_41810887ae6c6d59323116f47e33fa38: + /svelte-preprocess/4.10.6_igaqrb5onrwvsmrrc32h4m72ha: resolution: { integrity: sha512-I2SV1w/AveMvgIQlUF/ZOO3PYVnhxfcpNyGt8pxpUVhPfyfL/CZBkkw/KPfuFix5FJ9TnnNYMhACK3DtSaYVVQ== @@ -5039,7 +5059,7 @@ packages: picocolors: 1.0.0 postcss: 8.4.12 postcss-js: 4.0.0_postcss@8.4.12 - postcss-load-config: 3.1.4_postcss@8.4.12+ts-node@10.7.0 + postcss-load-config: 3.1.4_ysmyu6g5dtd6yanj6zrab4uqoy postcss-nested: 5.0.6_postcss@8.4.12 postcss-selector-parser: 6.0.10 postcss-value-parser: 4.2.0 @@ -5113,7 +5133,7 @@ packages: engines: { node: '>=0.10.0' } dev: true - /ts-node/10.7.0_de7c86b0cde507c63a0402da5b982bd3: + /ts-node/10.7.0_3z6inmgn4ud4moqealnfxgbl2m: resolution: { integrity: sha512-TbIGS4xgJoX2i3do417KSaep1uRAW/Lu+WAL2doDHC0D6ummjirVOXU5/7aiZotbQ5p1Zp9tP7U6cYhA0O7M8A== diff --git a/prisma/migrations/20220408070805_added_expose_port/migration.sql b/prisma/migrations/20220408070805_added_expose_port/migration.sql new file mode 100644 index 000000000..a23afd64a --- /dev/null +++ b/prisma/migrations/20220408070805_added_expose_port/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Application" ADD COLUMN "exposePort" INTEGER; diff --git a/prisma/migrations/20220430124553_expose_port_for_services/migration.sql b/prisma/migrations/20220430124553_expose_port_for_services/migration.sql new file mode 100644 index 000000000..fdbab5713 --- /dev/null +++ b/prisma/migrations/20220430124553_expose_port_for_services/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Service" ADD COLUMN "exposePort" INTEGER; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 58e4fd8a2..db5b89cba 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -84,6 +84,7 @@ model Application { buildPack String? projectId Int? port Int? + exposePort Int? installCommand String? buildCommand String? startCommand String? @@ -289,6 +290,7 @@ model Service { id String @id @default(cuid()) name String fqdn String? + exposePort Int? dualCerts Boolean @default(false) type String? version String? diff --git a/src/lib/buildPacks/gatsby.ts b/src/lib/buildPacks/gatsby.ts index cdf95f1dd..a10f84c43 100644 --- a/src/lib/buildPacks/gatsby.ts +++ b/src/lib/buildPacks/gatsby.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, imageforBuild): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageforBuild}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, imageforBuild): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/laravel.ts b/src/lib/buildPacks/laravel.ts index a83363dc0..4912a772f 100644 --- a/src/lib/buildPacks/laravel.ts +++ b/src/lib/buildPacks/laravel.ts @@ -2,7 +2,7 @@ import { buildCacheImageForLaravel, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { workdir, applicationId, tag, buildId } = data; + const { workdir, applicationId, tag, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -24,7 +24,7 @@ const createDockerfile = async (data, image): Promise => { `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json` ); Dockerfile.push(`COPY --chown=application:application . ./`); - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/php.ts b/src/lib/buildPacks/php.ts index b3c9651ce..d04e86712 100644 --- a/src/lib/buildPacks/php.ts +++ b/src/lib/buildPacks/php.ts @@ -2,7 +2,7 @@ import { buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image, htaccessFound): Promise => { - const { workdir, baseDirectory, buildId } = data; + const { workdir, baseDirectory, buildId, port } = data; const Dockerfile: Array = []; let composerFound = false; try { @@ -22,7 +22,7 @@ const createDockerfile = async (data, image, htaccessFound): Promise => { } Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/react.ts b/src/lib/buildPacks/react.ts index 3b55bfa23..5217e8f96 100644 --- a/src/lib/buildPacks/react.ts +++ b/src/lib/buildPacks/react.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/static.ts b/src/lib/buildPacks/static.ts index 8f3c2c7d4..79647ae93 100644 --- a/src/lib/buildPacks/static.ts +++ b/src/lib/buildPacks/static.ts @@ -12,7 +12,8 @@ const createDockerfile = async (data, image): Promise => { secrets, pullmergeRequestId, baseImage, - buildId + buildId, + port } = data; const Dockerfile: Array = []; @@ -42,7 +43,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/svelte.ts b/src/lib/buildPacks/svelte.ts index 5604e7ed6..bede0e806 100644 --- a/src/lib/buildPacks/svelte.ts +++ b/src/lib/buildPacks/svelte.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/buildPacks/vuejs.ts b/src/lib/buildPacks/vuejs.ts index 5604e7ed6..bede0e806 100644 --- a/src/lib/buildPacks/vuejs.ts +++ b/src/lib/buildPacks/vuejs.ts @@ -2,7 +2,7 @@ import { buildCacheImageWithNode, buildImage } from '$lib/docker'; import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { - const { applicationId, tag, workdir, publishDirectory, baseImage, buildId } = data; + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; const Dockerfile: Array = []; Dockerfile.push(`FROM ${image}`); @@ -12,7 +12,7 @@ const createDockerfile = async (data, image): Promise => { if (baseImage.includes('nginx')) { Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); } - Dockerfile.push(`EXPOSE 80`); + Dockerfile.push(`EXPOSE ${port}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); }; diff --git a/src/lib/common.ts b/src/lib/common.ts index acbe6c88c..f9c77f3d3 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -4,6 +4,8 @@ import { dev } from '$app/env'; import * as Sentry from '@sentry/node'; import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; import type { Config } from 'unique-names-generator'; +import { promises as dns } from 'dns'; +import { isIP } from 'is-ip'; import * as db from '$lib/database'; import { buildLogQueue } from './queues'; @@ -14,6 +16,7 @@ import Cookie from 'cookie'; import os from 'os'; import type { RequestEvent } from '@sveltejs/kit/types/internal'; import type { Job } from 'bullmq'; +import { t } from './translations'; try { if (!dev) { @@ -179,3 +182,97 @@ export function getDomain(domain: string): string { export function getOsArch() { return os.arch(); } + +export async function isDNSValid(event: any, domain: string): Promise { + let resolves = []; + try { + if (isIP(event.url.hostname)) { + resolves = [event.url.hostname]; + } else { + resolves = await dns.resolve4(event.url.hostname); + } + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + + try { + let ipDomainFound = false; + dns.setServers(['1.1.1.1', '8.8.8.8']); + const dnsResolve = await dns.resolve4(domain); + if (dnsResolve.length > 0) { + for (const ip of dnsResolve) { + if (resolves.includes(ip)) { + ipDomainFound = true; + } + } + } + if (!ipDomainFound) throw false; + } catch (error) { + throw { + message: t.get('application.domain_not_valid') + }; + } +} + +export async function checkDomainsIsValidInDNS({ event, fqdn, dualCerts }): Promise { + const domain = getDomain(fqdn); + const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`; + dns.setServers(['1.1.1.1', '8.8.8.8']); + let resolves = []; + try { + if (isIP(event.url.hostname)) { + resolves = [event.url.hostname]; + } else { + resolves = await dns.resolve4(event.url.hostname); + } + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + + if (dualCerts) { + try { + const ipDomain = await dns.resolve4(domain); + const ipDomainDualCert = await dns.resolve4(domainDualCert); + + let ipDomainFound = false; + let ipDomainDualCertFound = false; + + for (const ip of ipDomain) { + if (resolves.includes(ip)) { + ipDomainFound = true; + } + } + for (const ip of ipDomainDualCert) { + if (resolves.includes(ip)) { + ipDomainDualCertFound = true; + } + } + if (ipDomainFound && ipDomainDualCertFound) return { status: 200 }; + throw false; + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + } else { + try { + const ipDomain = await dns.resolve4(domain); + let ipDomainFound = false; + for (const ip of ipDomain) { + if (resolves.includes(ip)) { + ipDomainFound = true; + } + } + if (ipDomainFound) return { status: 200 }; + throw false; + } catch (error) { + throw { + message: t.get('application.dns_not_set_error', { domain }) + }; + } + } +} diff --git a/src/lib/components/DatabaseLinks.svelte b/src/lib/components/DatabaseLinks.svelte index 9ef11a238..5927c405c 100644 --- a/src/lib/components/DatabaseLinks.svelte +++ b/src/lib/components/DatabaseLinks.svelte @@ -3,6 +3,7 @@ import Clickhouse from './svg/databases/Clickhouse.svelte'; import CouchDb from './svg/databases/CouchDB.svelte'; import MongoDb from './svg/databases/MongoDB.svelte'; + import MariaDb from './svg/databases/MariaDB.svelte'; import MySql from './svg/databases/MySQL.svelte'; import PostgreSql from './svg/databases/PostgreSQL.svelte'; import Redis from './svg/databases/Redis.svelte'; @@ -17,6 +18,8 @@ {:else if database.type === 'mysql'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'postgresql'} {:else if database.type === 'redis'} diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index d6ff2f8ea..d42fcb726 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -52,6 +52,12 @@ export const supportedDatabaseTypesAndVersions = [ versions: ['5.0', '4.4', '4.2'] }, { name: 'mysql', fancyName: 'MySQL', baseImage: 'bitnami/mysql', versions: ['8.0', '5.7'] }, + { + name: 'mariadb', + fancyName: 'MariaDB', + baseImage: 'bitnami/mariadb', + versions: ['10.7', '10.6', '10.5', '10.4', '10.3', '10.2'] + }, { name: 'postgresql', fancyName: 'PostgreSQL', @@ -215,3 +221,11 @@ export const supportedServiceTypesAndVersions = [ } } ]; + +export const getServiceMainPort = (service: string) => { + const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service); + if (serviceType) { + return serviceType.ports.main; + } + return null; +}; diff --git a/src/lib/components/svg/databases/MariaDB.svelte b/src/lib/components/svg/databases/MariaDB.svelte new file mode 100644 index 000000000..1de83ef15 --- /dev/null +++ b/src/lib/components/svg/databases/MariaDB.svelte @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/src/lib/components/svg/databases/MongoDB.svelte b/src/lib/components/svg/databases/MongoDB.svelte index b99ffe4da..ea0d2859d 100644 --- a/src/lib/components/svg/databases/MongoDB.svelte +++ b/src/lib/components/svg/databases/MongoDB.svelte @@ -3,31 +3,13 @@ - + d="M203.77731,148.85754c-10.8147-12.762-20.13269-25.8139-22.02478-28.49224a.426.426,0,0,0-.70032.00006c-1.89172,2.6784-11.20758,15.73022-22.02191,28.49218-92.69141,118.085,14.62982,197.75507,14.62982,197.75507l.87.60461c.8136,12.32624,2.83508,30.041,2.83508,30.041H185.442s2.01282-17.63849,2.83-29.96106l.87549-.68451S296.46774,266.94257,203.77731,148.85754ZM181.404,344.88123h-.001s-4.811-4.10383-6.10962-6.16l-.01172-.22131,5.81946-128.56055a.30281.30281,0,0,1,.605,0l5.81946,128.56036-.01135.22065C186.21652,340.77625,181.404,344.88123,181.404,344.88123Z" + fill="#00684a" + /> diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index caceef396..87d6f90d8 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -278,6 +278,7 @@ export async function configureApplication({ name, fqdn, port, + exposePort, installCommand, buildCommand, startCommand, @@ -297,6 +298,7 @@ export async function configureApplication({ name: string; fqdn: string; port: number; + exposePort: number; installCommand: string; buildCommand: string; startCommand: string; @@ -318,6 +320,7 @@ export async function configureApplication({ buildPack, fqdn, port, + exposePort, installCommand, buildCommand, startCommand, diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index 314f77b08..e83308878 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -127,60 +127,73 @@ export function getServiceImages(type: string): string[] { export function generateDatabaseConfiguration(database: Database & { settings: DatabaseSettings }): | { - volume: string; - image: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MYSQL_DATABASE: string; - MYSQL_PASSWORD: string; - MYSQL_ROOT_USER: string; - MYSQL_USER: string; - MYSQL_ROOT_PASSWORD: string; - }; - } + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MYSQL_DATABASE: string; + MYSQL_PASSWORD: string; + MYSQL_ROOT_USER: string; + MYSQL_USER: string; + MYSQL_ROOT_PASSWORD: string; + }; + } | { - volume: string; - image: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - MONGODB_ROOT_USER: string; - MONGODB_ROOT_PASSWORD: string; - }; - } + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MONGODB_ROOT_USER: string; + MONGODB_ROOT_PASSWORD: string; + }; + } | { - volume: string; - image: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - POSTGRESQL_POSTGRES_PASSWORD: string; - POSTGRESQL_USERNAME: string; - POSTGRESQL_PASSWORD: string; - POSTGRESQL_DATABASE: string; - }; - } + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + MARIADB_ROOT_USER: string; + MARIADB_ROOT_PASSWORD: string; + MARIADB_USER: string; + MARIADB_PASSWORD: string; + MARIADB_DATABASE: string; + }; + } | { - volume: string; - image: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - REDIS_AOF_ENABLED: string; - REDIS_PASSWORD: string; - }; - } + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + POSTGRESQL_POSTGRES_PASSWORD: string; + POSTGRESQL_USERNAME: string; + POSTGRESQL_PASSWORD: string; + POSTGRESQL_DATABASE: string; + }; + } | { - volume: string; - image: string; - ulimits: Record; - privatePort: number; - environmentVariables: { - COUCHDB_PASSWORD: string; - COUCHDB_USER: string; - }; - } { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + REDIS_AOF_ENABLED: string; + REDIS_PASSWORD: string; + }; + } + | { + volume: string; + image: string; + ulimits: Record; + privatePort: number; + environmentVariables: { + COUCHDB_PASSWORD: string; + COUCHDB_USER: string; + }; + } { const { id, dbUser, @@ -207,6 +220,20 @@ export function generateDatabaseConfiguration(database: Database & { settings: D volume: `${id}-${type}-data:/bitnami/mysql/data`, ulimits: {} }; + } else if (type === 'mariadb') { + return { + privatePort: 3306, + environmentVariables: { + MARIADB_ROOT_USER: rootUser, + MARIADB_ROOT_PASSWORD: rootUserPassword, + MARIADB_USER: dbUser, + MARIADB_PASSWORD: dbUserPassword, + MARIADB_DATABASE: defaultDatabase + }, + image: `${baseImage}:${version}`, + volume: `${id}-${type}-data:/bitnami/mariadb`, + ulimits: {} + }; } else if (type === 'mongodb') { return { privatePort: 27017, diff --git a/src/lib/database/databases.ts b/src/lib/database/databases.ts index 388047ac3..edc15c729 100644 --- a/src/lib/database/databases.ts +++ b/src/lib/database/databases.ts @@ -184,6 +184,10 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { await asyncExecShell( `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"` ); + } else if (type === 'mariadb') { + await asyncExecShell( + `DOCKER_HOST=${host} docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"` + ); } else if (type === 'postgresql') { if (isRoot) { await asyncExecShell( diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index df760cf76..99bbf224c 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -327,35 +327,40 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, + exposePort, username, name }: { id: string; fqdn: string; + exposePort?: number; name: string; email: string; username: string; }): Promise { await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); - await prisma.service.update({ where: { id }, data: { name, fqdn } }); + await prisma.service.update({ where: { id }, data: { name, fqdn, exposePort } }); } export async function updateService({ id, fqdn, + exposePort, name }: { id: string; fqdn: string; + exposePort?: number; name: string; }): Promise { - return await prisma.service.update({ where: { id }, data: { fqdn, name } }); + return await prisma.service.update({ where: { id }, data: { fqdn, name, exposePort } }); } export async function updateFiderService({ id, fqdn, name, + exposePort, emailNoreply, emailMailgunApiKey, emailMailgunDomain, @@ -368,6 +373,7 @@ export async function updateFiderService({ }: { id: string; fqdn: string; + exposePort?: number; name: string; emailNoreply: string; emailMailgunApiKey: string; @@ -384,6 +390,7 @@ export async function updateFiderService({ data: { fqdn, name, + exposePort, fider: { update: { emailNoreply, @@ -405,18 +412,20 @@ export async function updateWordpress({ id, fqdn, name, + exposePort, mysqlDatabase, extraConfig }: { id: string; fqdn: string; name: string; + exposePort?: number; mysqlDatabase: string; extraConfig: string; }): Promise { return await prisma.service.update({ where: { id }, - data: { fqdn, name, wordpress: { update: { mysqlDatabase, extraConfig } } } + data: { fqdn, name, exposePort, wordpress: { update: { mysqlDatabase, extraConfig } } } }); } @@ -434,16 +443,18 @@ export async function updateGhostService({ id, fqdn, name, + exposePort, mariadbDatabase }: { id: string; fqdn: string; name: string; + exposePort?: number; mariadbDatabase: string; }): Promise { return await prisma.service.update({ where: { id }, - data: { fqdn, name, ghost: { update: { mariadbDatabase } } } + data: { fqdn, name, exposePort, ghost: { update: { mariadbDatabase } } } }); } diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index 7b64b8a9e..0f2a2b1d6 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -292,26 +292,28 @@ export async function generateSSLCerts(): Promise { } export async function renewSSLCerts(): Promise { - const host = 'unix:///var/run/docker.sock'; - await asyncExecShell(`docker pull alpine:latest`); - const certbotImage = - process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest'; + if (!dev) { + const host = 'unix:///var/run/docker.sock'; + await asyncExecShell(`docker pull alpine:latest`); + const certbotImage = + process.arch === 'x64' ? 'certbot/certbot' : 'certbot/certbot:arm64v8-latest'; - const { stdout: certificates } = await asyncExecShell( - `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /etc/letsencrypt/live/ | grep -v README"` - ); + const { stdout: certificates } = await asyncExecShell( + `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "ls -1 /etc/letsencrypt/live/ | grep -v README"` + ); - for (const certificate of certificates.trim().split('\n')) { - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker run --rm --name certbot-renewal -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --cert-name ${certificate} --logs-dir /etc/letsencrypt/logs renew --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080` - ); - await asyncExecShell( - `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${certificate}/ && cat /etc/letsencrypt/live/${certificate}/fullchain.pem /etc/letsencrypt/live/${certificate}/privkey.pem > /app/ssl/${certificate}.pem"` - ); - } catch (error) { - console.log(error); + for (const certificate of certificates.trim().split('\n')) { + try { + await asyncExecShell( + `DOCKER_HOST=${host} docker run --rm --name certbot-renewal -p 9080:9080 -v "coolify-letsencrypt:/etc/letsencrypt" ${certbotImage} --cert-name ${certificate} --logs-dir /etc/letsencrypt/logs renew --standalone --preferred-challenges http --http-01-address 0.0.0.0 --http-01-port 9080` + ); + await asyncExecShell( + `DOCKER_HOST=${host} docker run --rm -v "coolify-letsencrypt:/etc/letsencrypt" -v "coolify-ssl-certs:/app/ssl" alpine:latest sh -c "test -d /etc/letsencrypt/live/${certificate}/ && cat /etc/letsencrypt/live/${certificate}/fullchain.pem /etc/letsencrypt/live/${certificate}/privkey.pem > /app/ssl/${certificate}.pem"` + ); + } catch (error) { + console.log(error); + } } + await reloadHaproxy('unix:///var/run/docker.sock'); } - await reloadHaproxy('unix:///var/run/docker.sock'); } diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 0a535db35..c455523af 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -178,9 +178,11 @@ "delete_application": "Delete application", "permission_denied_delete_application": "You do not have permission to delete this application", "domain_already_in_use": "Domain {{domain}} is already used.", - "dns_not_set_error": "DNS not set or propogated for {{domain}}.

Please check your DNS settings.", + "dns_not_set_error": "DNS not set correctly or propogated for {{domain}}.

Please check your DNS settings.", + "domain_required": "Domain is required.", "settings_saved": "Settings saved.", "dns_not_set_partial_error": "DNS not set", + "domain_not_valid": "Could not resolve domain or it's not pointing to the server IP address.

Please check your DNS configuration and try again.", "git_source": "Git Source", "git_repository": "Git Repository", "build_pack": "Build Pack", @@ -204,6 +206,7 @@ "enable_automatic_deployment": "Enable Automatic Deployment", "enable_auto_deploy_webhooks": "Enable automatic deployment through webhooks.", "enable_mr_pr_previews": "Enable MR/PR Previews", + "expose_a_port": "Expose a port", "enable_preview_deploy_mr_pr_requests": "Enable preview deployments from pull or merge requests.", "debug_logs": "Debug Logs", "enable_debug_log_during_build": "Enable debug logs during build phase.
Sensitive information could be visible and saved in logs.", @@ -311,7 +314,7 @@ "credential_stat_explainer": "Credentials for stats page.", "auto_update_enabled": "Auto update enabled?", "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running.", - "generate_www_non_www_ssl": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.

Service needs to be restarted.", + "generate_www_non_www_ssl": "It will generate certificates for both www and non-www.
You need to have both DNS entries set in advance.", "is_dns_check_enabled": "DNS check enabled?", "is_dns_check_enabled_explainer": "You can disable DNS check before creating SSL certificates.

Turning it off is useful when Coolify is behind a reverse proxy or tunnel." }, diff --git a/src/lib/locales/fr.json b/src/lib/locales/fr.json index 36f10dfb4..242ac6fce 100644 --- a/src/lib/locales/fr.json +++ b/src/lib/locales/fr.json @@ -61,6 +61,7 @@ "enable_debug_log_during_build": "Activez les journaux de débogage pendant la phase de build.
Les informations sensibles peuvent être visibles et enregistrées dans les journaux.", "enable_mr_pr_previews": "Activer les aperçus MR/PR", "enable_preview_deploy_mr_pr_requests": "Activez les déploiements de prévisualisation à partir de demandes d'extraction ou de fusion.", + "expose_a_port": "Exposer un port", "features": "Caractéristiques", "git_repository": "Dépôt Git", "git_source": "Source Git", diff --git a/src/lib/queues/builder.ts b/src/lib/queues/builder.ts index 3a74320ef..7c7740945 100644 --- a/src/lib/queues/builder.ts +++ b/src/lib/queues/builder.ts @@ -48,6 +48,7 @@ export default async function (job: Job): Promise): Promise): Promise): Promise): Promise { } catch (error) { console.log(error); } - console.log(`Is LowDiskSpace detected? ${lowDiskSpace}`); if (lowDiskSpace) { // Cleanup old coolify images try { diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index cc340b883..b35a058d7 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -117,7 +117,7 @@ const cron = async (): Promise => { await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); if (!dev) await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); - await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); + if (!dev) await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); }; cron().catch((error) => { console.log('cron failed to start'); diff --git a/src/lib/types/builderJob.ts b/src/lib/types/builderJob.ts index 145f7cc46..968e2c897 100644 --- a/src/lib/types/builderJob.ts +++ b/src/lib/types/builderJob.ts @@ -12,6 +12,7 @@ export type BuilderJob = { buildPack: BuildPackName; projectId: number; port: number; + exposePort?: number; installCommand: string; buildCommand?: string; startCommand?: string; diff --git a/src/lib/types/composeFile.ts b/src/lib/types/composeFile.ts index 33dfbdd43..724a53b69 100644 --- a/src/lib/types/composeFile.ts +++ b/src/lib/types/composeFile.ts @@ -18,6 +18,7 @@ export type ComposeFileService = { restart: ComposeFileRestartOption; depends_on?: string[]; command?: string; + ports?: string[]; build?: { context: string; dockerfile: string; diff --git a/src/routes/applications/[id]/cancel.json.ts b/src/routes/applications/[id]/cancel.json.ts index 11c83ae9c..edaa22c95 100644 --- a/src/routes/applications/[id]/cancel.json.ts +++ b/src/routes/applications/[id]/cancel.json.ts @@ -17,21 +17,25 @@ export const post: RequestHandler = async (event) => { let count = 0; await new Promise(async (resolve, reject) => { const job = await buildQueue.getJob(buildId); + if (!job) { + return resolve(); + } const { destinationDocker: { engine } - } = job.data; + } = job?.data; const host = getEngine(engine); let interval = setInterval(async () => { - const { status } = await db.prisma.build.findUnique({ where: { id: buildId } }); - if (status === 'failed') { - clearInterval(interval); - return resolve(); - } - if (count > 1200) { - clearInterval(interval); - reject(new Error('Could not cancel build.')); - } try { + const data = await db.prisma.build.findUnique({ where: { id: buildId } }); + if (data?.status === 'failed') { + clearInterval(interval); + return resolve(); + } + if (count > 60) { + clearInterval(interval); + reject(new Error('Could not cancel build.')); + } + const { stdout: buildContainers } = await asyncExecShell( `DOCKER_HOST=${host} docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` ); @@ -53,11 +57,14 @@ export const post: RequestHandler = async (event) => { } count++; } catch (error) {} - }, 100); + }, 1000); resolve(); }); - + const data = await db.prisma.build.findUnique({ where: { id: buildId } }); + if (data?.status === 'queued' || data?.status === 'running') { + await db.prisma.build.update({ where: { id: buildId }, data: { status: 'failed' } }); + } return { status: 200, body: { diff --git a/src/routes/applications/[id]/check.json.ts b/src/routes/applications/[id]/check.json.ts index c15fd8bab..021cb5975 100644 --- a/src/routes/applications/[id]/check.json.ts +++ b/src/routes/applications/[id]/check.json.ts @@ -1,21 +1,44 @@ import { dev } from '$app/env'; -import { getDomain, getUserDetails } from '$lib/common'; +import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } from '$lib/common'; import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; import type { RequestHandler } from '@sveltejs/kit'; import { promises as dns } from 'dns'; +import getPort from 'get-port'; import { t } from '$lib/translations'; +export const get: RequestHandler = async (event) => { + const { status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + const domain = event.url.searchParams.get('domain'); + if (!domain) { + return { + status: 500, + body: { + message: t.get('application.domain_required') + } + }; + } + try { + await isDNSValid(event, domain); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; + export const post: RequestHandler = async (event) => { const { status, body } = await getUserDetails(event); if (status === 401) return { status, body }; const { id } = event.params; - let { fqdn, forceSave } = await event.request.json(); + let { exposePort, fqdn, forceSave, dualCerts } = await event.request.json(); fqdn = fqdn.toLowerCase(); try { - const domain = getDomain(fqdn); + const { isDNSCheckEnabled } = await db.prisma.setting.findFirst({}); const found = await db.isDomainConfigured({ id, fqdn }); if (found) { throw { @@ -24,25 +47,22 @@ export const post: RequestHandler = async (event) => { }) }; } - if (!dev && !forceSave) { - let ip = []; - let localIp = []; - dns.setServers(['1.1.1.1', '8.8.8.8']); - try { - localIp = await dns.resolve4(event.url.hostname); - } catch (error) {} - try { - ip = await dns.resolve4(domain); - } catch (error) {} + if (exposePort) { + exposePort = Number(exposePort); - if (localIp?.length > 0) { - if (ip?.length === 0 || !ip.includes(localIp[0])) { - throw { - message: t.get('application.dns_not_set_error', { domain: domain }) - }; - } + if (exposePort < 1024 || exposePort > 65535) { + throw { message: `Expose 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.` }; + } + } + + if (isDNSCheckEnabled && !dev && !forceSave) { + return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts }); } return { diff --git a/src/routes/applications/[id]/deploy.json.ts b/src/routes/applications/[id]/deploy.json.ts index 92311ae1f..c22d6a21a 100644 --- a/src/routes/applications/[id]/deploy.json.ts +++ b/src/routes/applications/[id]/deploy.json.ts @@ -22,6 +22,7 @@ export const post: RequestHandler = async (event) => { JSON.stringify({ buildPack: applicationFound.buildPack, port: applicationFound.port, + exposePort: applicationFound.exposePort, installCommand: applicationFound.installCommand, buildCommand: applicationFound.buildCommand, startCommand: applicationFound.startCommand diff --git a/src/routes/applications/[id]/index.json.ts b/src/routes/applications/[id]/index.json.ts index 115564476..02082e656 100644 --- a/src/routes/applications/[id]/index.json.ts +++ b/src/routes/applications/[id]/index.json.ts @@ -3,8 +3,6 @@ import * as db from '$lib/database'; import { ErrorHandler } from '$lib/database'; import { checkContainer, isContainerExited } from '$lib/haproxy'; import type { RequestHandler } from '@sveltejs/kit'; -import jsonwebtoken from 'jsonwebtoken'; -import { get as getRequest } from '$lib/api'; import { setDefaultConfiguration } from '$lib/buildPacks/common'; export const get: RequestHandler = async (event) => { @@ -52,6 +50,7 @@ export const post: RequestHandler = async (event) => { buildPack, fqdn, port, + exposePort, installCommand, buildCommand, startCommand, @@ -67,6 +66,9 @@ export const post: RequestHandler = async (event) => { baseBuildImage } = await event.request.json(); if (port) port = Number(port); + if (exposePort) { + exposePort = Number(exposePort); + } if (denoOptions) denoOptions = denoOptions.trim(); try { @@ -87,6 +89,7 @@ export const post: RequestHandler = async (event) => { name, fqdn, port, + exposePort, installCommand, buildCommand, startCommand, diff --git a/src/routes/applications/[id]/index.svelte b/src/routes/applications/[id]/index.svelte index 719b95b40..6d8807123 100644 --- a/src/routes/applications/[id]/index.svelte +++ b/src/routes/applications/[id]/index.svelte @@ -45,9 +45,9 @@ import Explainer from '$lib/components/Explainer.svelte'; import Setting from '$lib/components/Setting.svelte'; import type Prisma from '@prisma/client'; - import { notNodeDeployments, staticDeployments } from '$lib/components/common'; + import { getDomain, notNodeDeployments, staticDeployments } from '$lib/components/common'; import { toast } from '@zerodevx/svelte-toast'; - import { post } from '$lib/api'; + import { get, post } from '$lib/api'; import cuid from 'cuid'; import { browser } from '$app/env'; import { disabledButton } from '$lib/store'; @@ -63,6 +63,10 @@ let dualCerts = application.settings.dualCerts; let autodeploy = application.settings.autodeploy; + let nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); + let isNonWWWDomainOK = false; + let isWWWDomainOK = false; + let wsgis = [ { value: 'None', @@ -127,13 +131,31 @@ async function handleSubmit() { loading = true; try { - await post(`/applications/${id}/check.json`, { fqdn: application.fqdn, forceSave }); + nonWWWDomain = application.fqdn && getDomain(application.fqdn).replace(/^www\./, ''); + await post(`/applications/${id}/check.json`, { + fqdn: application.fqdn, + forceSave, + dualCerts, + exposePort: application.exposePort + }); await post(`/applications/${id}.json`, { ...application }); $disabledButton = false; + forceSave = false; return toast.push('Configurations saved.'); } catch ({ error }) { if (error?.startsWith($t('application.dns_not_set_partial_error'))) { forceSave = true; + if (dualCerts) { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + const isWWW = getDomain(application.fqdn).includes('www.'); + if (isWWW) { + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + } + } } return errorNotification(error); } finally { @@ -151,6 +173,19 @@ application.baseBuildImage = event.detail.value; await handleSubmit(); } + + async function isDNSValid(domain, isWWW) { + try { + await get(`/applications/${id}/check.json?domain=${domain}`); + toast.push('DNS configuration is valid.'); + isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true); + return true; + } catch ({ error }) { + errorNotification(error); + isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false); + return false; + } + }
@@ -383,17 +418,52 @@ {/if}
- +
+ + {#if forceSave} +
+ {#if isNonWWWDomainOK} + + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if} +
{/if} - - {#if !notNodeDeployments.includes(application.buildPack)} + {#if application.buildPack !== 'docker'}
+ + +
Useful if you would like to use your own reverse proxy or tunnel and also in development mode. Otherwise leave empty.'} + /> +
+ {/if} + {#if !notNodeDeployments.includes(application.buildPack)} +
@@ -491,7 +575,7 @@
{/if} {#if application.buildPack === 'docker'} -
+
diff --git a/src/routes/databases/[id]/_Databases/_Databases.svelte b/src/routes/databases/[id]/_Databases/_Databases.svelte index 1034a3543..b40cfb345 100644 --- a/src/routes/databases/[id]/_Databases/_Databases.svelte +++ b/src/routes/databases/[id]/_Databases/_Databases.svelte @@ -11,6 +11,7 @@ import MySql from './_MySQL.svelte'; import MongoDb from './_MongoDB.svelte'; + import MariaDb from './_MariaDB.svelte'; import PostgreSql from './_PostgreSQL.svelte'; import Redis from './_Redis.svelte'; import CouchDb from './_CouchDb.svelte'; @@ -190,6 +191,8 @@ {:else if database.type === 'mongodb'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'redis'} {:else if database.type === 'couchdb'} diff --git a/src/routes/databases/[id]/_Databases/_MariaDB.svelte b/src/routes/databases/[id]/_Databases/_MariaDB.svelte new file mode 100644 index 000000000..3bb135421 --- /dev/null +++ b/src/routes/databases/[id]/_Databases/_MariaDB.svelte @@ -0,0 +1,79 @@ + + +
+
MariaDB
+
+
+
+ + +
+
+ + +
+
+ + + +
+
+ + +
+
+ + + +
+
diff --git a/src/routes/databases/[id]/configuration/type.svelte b/src/routes/databases/[id]/configuration/type.svelte index a73163058..fadba1365 100644 --- a/src/routes/databases/[id]/configuration/type.svelte +++ b/src/routes/databases/[id]/configuration/type.svelte @@ -37,6 +37,7 @@ import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; import MongoDB from '$lib/components/svg/databases/MongoDB.svelte'; + import MariaDB from '$lib/components/svg/databases/MariaDB.svelte'; import MySQL from '$lib/components/svg/databases/MySQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte'; @@ -68,6 +69,8 @@ {:else if type.name === 'mongodb'} + {:else if type.name === 'mariadb'} + {:else if type.name === 'mysql'} {:else if type.name === 'postgresql'} diff --git a/src/routes/databases/index.svelte b/src/routes/databases/index.svelte index 6717ac88a..ec6b86291 100644 --- a/src/routes/databases/index.svelte +++ b/src/routes/databases/index.svelte @@ -3,6 +3,7 @@ import Clickhouse from '$lib/components/svg/databases/Clickhouse.svelte'; import CouchDB from '$lib/components/svg/databases/CouchDB.svelte'; import MongoDB from '$lib/components/svg/databases/MongoDB.svelte'; + import MariaDB from '$lib/components/svg/databases/MariaDB.svelte'; import MySQL from '$lib/components/svg/databases/MySQL.svelte'; import PostgreSQL from '$lib/components/svg/databases/PostgreSQL.svelte'; import Redis from '$lib/components/svg/databases/Redis.svelte'; @@ -66,6 +67,8 @@ {:else if database.type === 'mysql'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'postgresql'} {:else if database.type === 'redis'} @@ -98,6 +101,8 @@ {:else if database.type === 'mongodb'} + {:else if database.type === 'mariadb'} + {:else if database.type === 'mysql'} {:else if database.type === 'postgresql'} diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte index a2a2effac..a34f140f0 100644 --- a/src/routes/services/[id]/_Services/_Services.svelte +++ b/src/routes/services/[id]/_Services/_Services.svelte @@ -27,6 +27,7 @@ let loading = false; let loadingVerification = false; let dualCerts = service.dualCerts; + let showExposePort = service.exposePort !== null; async function handleSubmit() { loading = true; @@ -160,6 +161,32 @@ on:click={() => !isRunning && changeSettings('dualCerts')} />
+
+ { + showExposePort = !showExposePort; + service.exposePort = undefined; + }} + title={$t('application.expose_a_port')} + description="Expose a port to the host system" + /> +
+ + {#if showExposePort} +
+ + +
+ {/if} + {#if service.type === 'plausibleanalytics'} {:else if service.type === 'minio'} diff --git a/src/routes/services/[id]/ghost/index.json.ts b/src/routes/services/[id]/ghost/index.json.ts index 81ca8ade1..1b1225f59 100644 --- a/src/routes/services/[id]/ghost/index.json.ts +++ b/src/routes/services/[id]/ghost/index.json.ts @@ -11,11 +11,12 @@ export const post: RequestHandler = async (event) => { let { name, fqdn, + exposePort, ghost: { mariadbDatabase } } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); try { - await db.updateGhostService({ id, fqdn, name, mariadbDatabase }); + await db.updateGhostService({ id, fqdn, name, exposePort, mariadbDatabase }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/ghost/start.json.ts b/src/routes/services/[id]/ghost/start.json.ts index 45c6936fe..67e9364b9 100644 --- a/src/routes/services/[id]/ghost/start.json.ts +++ b/src/routes/services/[id]/ghost/start.json.ts @@ -12,6 +12,7 @@ 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); @@ -19,6 +20,8 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; + const port = getServiceMainPort('ghost'); + try { const service = await db.getService({ id, teamId }); const { @@ -27,6 +30,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, fqdn, ghost: { defaultEmail, @@ -89,6 +93,7 @@ export const post: RequestHandler = async (event) => { volumes: [config.ghost.volume], environment: config.ghost.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('ghost'), depends_on: [`${id}-mariadb`], deploy: { diff --git a/src/routes/services/[id]/languagetool/index.json.ts b/src/routes/services/[id]/languagetool/index.json.ts index d717502c5..dcba0b6f8 100644 --- a/src/routes/services/[id]/languagetool/index.json.ts +++ b/src/routes/services/[id]/languagetool/index.json.ts @@ -9,13 +9,15 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { + console.log(error); return ErrorHandler(error); } }; diff --git a/src/routes/services/[id]/languagetool/start.json.ts b/src/routes/services/[id]/languagetool/start.json.ts index e11263161..0818ac6f7 100644 --- a/src/routes/services/[id]/languagetool/start.json.ts +++ b/src/routes/services/[id]/languagetool/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('languagetool'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -42,6 +45,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes: [config.volume], labels: makeLabelForServices('languagetool'), deploy: { diff --git a/src/routes/services/[id]/meilisearch/index.json.ts b/src/routes/services/[id]/meilisearch/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/meilisearch/index.json.ts +++ b/src/routes/services/[id]/meilisearch/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/meilisearch/start.json.ts b/src/routes/services/[id]/meilisearch/start.json.ts index 1f11054a8..b019d69b9 100644 --- a/src/routes/services/[id]/meilisearch/start.json.ts +++ b/src/routes/services/[id]/meilisearch/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -18,9 +19,11 @@ export const post: RequestHandler = async (event) => { const { meiliSearch: { masterKey } } = service; - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('meilisearch'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -47,6 +50,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), volumes: [config.volume], labels: makeLabelForServices('meilisearch'), deploy: { diff --git a/src/routes/services/[id]/minio/index.json.ts b/src/routes/services/[id]/minio/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/minio/index.json.ts +++ b/src/routes/services/[id]/minio/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, 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 c744eae80..3782bf467 100644 --- a/src/routes/services/[id]/minio/start.json.ts +++ b/src/routes/services/[id]/minio/start.json.ts @@ -7,6 +7,7 @@ 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'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -22,12 +23,14 @@ export const post: RequestHandler = async (event) => { fqdn, destinationDockerId, destinationDocker, + exposePort, minio: { rootUser, rootUserPassword }, serviceSecret } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('minio'); const publicPort = await getFreePort(); @@ -62,6 +65,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [config.volume], restart: 'always', + ...(exposePort && { ports: [`${port}:${port}`] }), labels: makeLabelForServices('minio'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/n8n/index.json.ts b/src/routes/services/[id]/n8n/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/n8n/index.json.ts +++ b/src/routes/services/[id]/n8n/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/n8n/start.json.ts b/src/routes/services/[id]/n8n/start.json.ts index 7386a4fd9..cca55b61e 100644 --- a/src/routes/services/[id]/n8n/start.json.ts +++ b/src/routes/services/[id]/n8n/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + 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); diff --git a/src/routes/services/[id]/nocodb/index.json.ts b/src/routes/services/[id]/nocodb/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/nocodb/index.json.ts +++ b/src/routes/services/[id]/nocodb/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/nocodb/start.json.ts b/src/routes/services/[id]/nocodb/start.json.ts index 4933a5347..bf3a702df 100644 --- a/src/routes/services/[id]/nocodb/start.json.ts +++ b/src/routes/services/[id]/nocodb/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('nocodb'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -40,6 +43,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('nocodb'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/plausibleanalytics/index.json.ts b/src/routes/services/[id]/plausibleanalytics/index.json.ts index 519b582a2..0a5d375e7 100644 --- a/src/routes/services/[id]/plausibleanalytics/index.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/index.json.ts @@ -11,14 +11,16 @@ export const post: RequestHandler = async (event) => { let { name, fqdn, + exposePort, plausibleAnalytics: { email, username } } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); if (email) email = email.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updatePlausibleAnalyticsService({ id, fqdn, name, email, username }); + await db.updatePlausibleAnalyticsService({ id, fqdn, name, email, username, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/plausibleanalytics/start.json.ts b/src/routes/services/[id]/plausibleanalytics/start.json.ts index 8b61e1ff8..8adce773a 100644 --- a/src/routes/services/[id]/plausibleanalytics/start.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -22,6 +23,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, plausibleAnalytics: { id: plausibleDbId, username, @@ -78,6 +80,7 @@ export const post: RequestHandler = async (event) => { } const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('plausibleanalytics'); const { workdir } = await createDirectories({ repository: type, buildId: id }); @@ -132,6 +135,7 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; networks: [network], environment: config.plausibleAnalytics.environmentVariables, restart: 'always', + ...(exposePort && { ports: [`${port}:${exposePort}`] }), depends_on: [`${id}-postgresql`, `${id}-clickhouse`], labels: makeLabelForServices('plausibleAnalytics'), deploy: { diff --git a/src/routes/services/[id]/umami/index.json.ts b/src/routes/services/[id]/umami/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/umami/index.json.ts +++ b/src/routes/services/[id]/umami/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/umami/start.json.ts b/src/routes/services/[id]/umami/start.json.ts index 6a51e5cde..c2cd78fe4 100644 --- a/src/routes/services/[id]/umami/start.json.ts +++ b/src/routes/services/[id]/umami/start.json.ts @@ -8,6 +8,7 @@ import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; import type { Service, DestinationDocker, Prisma } from '@prisma/client'; import bcrypt from 'bcryptjs'; +import { getServiceMainPort } from '$lib/components/common'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -24,6 +25,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, destinationDocker, serviceSecret, + exposePort, umami: { umamiAdminPassword, postgresqlUser, @@ -34,6 +36,7 @@ export const post: RequestHandler = async (event) => { } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('umami'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -156,6 +159,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [], restart: 'always', + ...(exposePort ? { ports: [`${port}:${port}`] } : {}), labels: makeLabelForServices('umami'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/uptimekuma/index.json.ts b/src/routes/services/[id]/uptimekuma/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/uptimekuma/index.json.ts +++ b/src/routes/services/[id]/uptimekuma/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/uptimekuma/start.json.ts b/src/routes/services/[id]/uptimekuma/start.json.ts index 6327db0ec..9032ce469 100644 --- a/src/routes/services/[id]/uptimekuma/start.json.ts +++ b/src/routes/services/[id]/uptimekuma/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -15,9 +16,11 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('uptimekuma'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -42,6 +45,7 @@ export const post: RequestHandler = async (event) => { volumes: [config.volume], environment: config.environmentVariables, restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('uptimekuma'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/vaultwarden/index.json.ts b/src/routes/services/[id]/vaultwarden/index.json.ts index 5ec3fa69a..e269e8fe7 100644 --- a/src/routes/services/[id]/vaultwarden/index.json.ts +++ b/src/routes/services/[id]/vaultwarden/index.json.ts @@ -8,11 +8,12 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/vaultwarden/start.json.ts b/src/routes/services/[id]/vaultwarden/start.json.ts index 511b040a3..41b790266 100644 --- a/src/routes/services/[id]/vaultwarden/start.json.ts +++ b/src/routes/services/[id]/vaultwarden/start.json.ts @@ -6,6 +6,7 @@ import type { RequestHandler } from '@sveltejs/kit'; import { getServiceImage, ErrorHandler } 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); @@ -15,10 +16,12 @@ export const post: RequestHandler = async (event) => { try { const service = await db.getService({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = + service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('vaultwarden'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -43,6 +46,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [config.volume], restart: 'always', + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), labels: makeLabelForServices('vaultWarden'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/vscodeserver/index.json.ts b/src/routes/services/[id]/vscodeserver/index.json.ts index d717502c5..ff98ede6d 100644 --- a/src/routes/services/[id]/vscodeserver/index.json.ts +++ b/src/routes/services/[id]/vscodeserver/index.json.ts @@ -9,11 +9,12 @@ export const post: RequestHandler = async (event) => { const { id } = event.params; - let { name, fqdn } = await event.request.json(); + 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 }); + await db.updateService({ id, fqdn, name, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts index 4ca29c215..1501a43c6 100644 --- a/src/routes/services/[id]/vscodeserver/start.json.ts +++ b/src/routes/services/[id]/vscodeserver/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -22,11 +23,13 @@ export const post: RequestHandler = async (event) => { destinationDocker, serviceSecret, persistentStorage, + exposePort, vscodeserver: { password } } = service; const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); + const port = getServiceMainPort('vscodeserver'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const image = getServiceImage(type); @@ -75,6 +78,7 @@ export const post: RequestHandler = async (event) => { networks: [network], volumes: [config.volume, ...volumes], restart: 'always', + ...(exposePort ? { ports: [`${port}:${exposePort}`] } : {}), labels: makeLabelForServices('vscodeServer'), deploy: { restart_policy: { diff --git a/src/routes/services/[id]/wordpress/index.json.ts b/src/routes/services/[id]/wordpress/index.json.ts index 413bd1f75..c76cd1a92 100644 --- a/src/routes/services/[id]/wordpress/index.json.ts +++ b/src/routes/services/[id]/wordpress/index.json.ts @@ -11,12 +11,14 @@ export const post: RequestHandler = async (event) => { let { name, fqdn, + exposePort, wordpress: { extraConfig, mysqlDatabase } } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); + if (exposePort) exposePort = Number(exposePort); try { - await db.updateWordpress({ id, fqdn, name, extraConfig, mysqlDatabase }); + await db.updateWordpress({ id, fqdn, name, extraConfig, mysqlDatabase, exposePort }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/wordpress/settings.json.ts b/src/routes/services/[id]/wordpress/settings.json.ts index 4f9c1f9b2..cee85bc41 100644 --- a/src/routes/services/[id]/wordpress/settings.json.ts +++ b/src/routes/services/[id]/wordpress/settings.json.ts @@ -8,7 +8,6 @@ import type { ComposeFile } from '$lib/types/composeFile'; import type { RequestHandler } from '@sveltejs/kit'; import cuid from 'cuid'; import fs from 'fs/promises'; -import getPort, { portNumbers } from 'get-port'; import yaml from 'js-yaml'; export const post: RequestHandler = async (event) => { diff --git a/src/routes/services/[id]/wordpress/start.json.ts b/src/routes/services/[id]/wordpress/start.json.ts index 0572be971..43bab2e9d 100644 --- a/src/routes/services/[id]/wordpress/start.json.ts +++ b/src/routes/services/[id]/wordpress/start.json.ts @@ -6,6 +6,7 @@ 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); @@ -22,6 +23,7 @@ export const post: RequestHandler = async (event) => { destinationDockerId, serviceSecret, destinationDocker, + exposePort, wordpress: { mysqlDatabase, mysqlUser, @@ -35,6 +37,7 @@ export const post: RequestHandler = async (event) => { const network = destinationDockerId && destinationDocker.network; const host = getEngine(destinationDocker.engine); const image = getServiceImage(type); + const port = getServiceMainPort('wordpress'); const { workdir } = await createDirectories({ repository: type, buildId: id }); const config = { @@ -76,6 +79,7 @@ export const post: RequestHandler = async (event) => { volumes: [config.wordpress.volume], networks: [network], restart: 'always', + ...(exposePort ? { ports: [`${port}:${port}`] } : {}), depends_on: [`${id}-mysql`], labels: makeLabelForServices('wordpress'), deploy: { diff --git a/src/routes/settings/check.json.ts b/src/routes/settings/check.json.ts index bec577001..88eaa0293 100644 --- a/src/routes/settings/check.json.ts +++ b/src/routes/settings/check.json.ts @@ -1,25 +1,54 @@ -import { asyncExecShell, getEngine, getUserDetails } from '$lib/common'; +import { dev } from '$app/env'; +import { checkDomainsIsValidInDNS, getDomain, getUserDetails, isDNSValid } 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'; +export const get: RequestHandler = async (event) => { + const { status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + const domain = event.url.searchParams.get('domain'); + if (!domain) { + return { + status: 500, + body: { + message: t.get('application.domain_required') + } + }; + } + try { + await isDNSValid(event, domain); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; + 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, forceSave, dualCerts, isDNSCheckEnabled } = await event.request.json(); if (fqdn) fqdn = fqdn.toLowerCase(); - try { const found = await db.isDomainConfigured({ id, fqdn }); + console.log(found); + if (found) { + throw { + message: t.get('application.domain_already_in_use', { + domain: getDomain(fqdn).replace('www.', '') + }) + }; + } + if (isDNSCheckEnabled && !forceSave) { + return await checkDomainsIsValidInDNS({ event, fqdn, dualCerts }); + } return { - status: found ? 500 : 200, - body: { - error: - found && t.get('application.domain_already_in_use', { domain: fqdn.replace('www.', '') }) - } + status: 200 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/settings/index.json.ts b/src/routes/settings/index.json.ts index 5546d782f..21f4907f2 100644 --- a/src/routes/settings/index.json.ts +++ b/src/routes/settings/index.json.ts @@ -1,3 +1,4 @@ +import { dev } from '$app/env'; import { getUserDetails } from '$lib/common'; import * as db from '$lib/database'; import { listSettings, ErrorHandler } from '$lib/database'; @@ -71,7 +72,8 @@ export const post: RequestHandler = async (event) => { minPort, maxPort, isAutoUpdateEnabled, - isDNSCheckEnabled + isDNSCheckEnabled, + forceSave } = 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 20e1c788d..c601db4ed 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -31,7 +31,7 @@ import Setting from '$lib/components/Setting.svelte'; import Explainer from '$lib/components/Explainer.svelte'; import { errorNotification } from '$lib/form'; - import { del, post } from '$lib/api'; + import { del, get, post } from '$lib/api'; import CopyPasswordField from '$lib/components/CopyPasswordField.svelte'; import { browser } from '$app/env'; import { getDomain } from '$lib/components/common'; @@ -47,7 +47,11 @@ let minPort = settings.minPort; let maxPort = settings.maxPort; + let forceSave = false; let fqdn = settings.fqdn; + let nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, ''); + let isNonWWWDomainOK = false; + let isWWWDomainOK = false; let isFqdnSet = !!settings.fqdn; let loading = { save: false, @@ -69,6 +73,7 @@ } async function changeSettings(name) { try { + resetView(); if (name === 'isRegistrationEnabled') { isRegistrationEnabled = !isRegistrationEnabled; } @@ -95,8 +100,10 @@ async function handleSubmit() { try { loading.save = true; + nonWWWDomain = fqdn && getDomain(fqdn).replace(/^www\./, ''); + if (fqdn !== settings.fqdn) { - await post(`/settings/check.json`, { fqdn }); + await post(`/settings/check.json`, { fqdn, forceSave, dualCerts, isDNSCheckEnabled }); await post(`/settings.json`, { fqdn }); return window.location.reload(); } @@ -105,7 +112,22 @@ settings.minPort = minPort; settings.maxPort = maxPort; } + forceSave = false; } catch ({ error }) { + if (error?.startsWith($t('application.dns_not_set_partial_error'))) { + forceSave = true; + if (dualCerts) { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + const isWWW = getDomain(settings.fqdn).includes('www.'); + if (isWWW) { + isWWWDomainOK = await isDNSValid(getDomain(`www.${nonWWWDomain}`), true); + } else { + isNonWWWDomainOK = await isDNSValid(getDomain(nonWWWDomain), false); + } + } + } return errorNotification(error); } finally { loading.save = false; @@ -119,6 +141,21 @@ return errorNotification(error); } } + async function isDNSValid(domain, isWWW) { + try { + await get(`/settings/check.json?domain=${domain}`); + toast.push('DNS configuration is valid.'); + isWWW ? (isWWWDomainOK = true) : (isNonWWWDomainOK = true); + return true; + } catch ({ error }) { + errorNotification(error); + isWWW ? (isWWWDomainOK = false) : (isNonWWWDomainOK = false); + return false; + } + } + function resetView() { + forceSave = false; + }
@@ -131,11 +168,18 @@
{$t('index.global_settings')}
{loading.save + ? $t('forms.saving') + : forceSave + ? $t('forms.confirm_continue') + : $t('forms.save')} + {#if isFqdnSet} + {:else} + + {/if} + {#if dualCerts} + {#if isWWWDomainOK} + + {:else} + + {/if} + {/if} +
+ {/if}
diff --git a/src/routes/webhooks/github/events.ts b/src/routes/webhooks/github/events.ts index 74214a7ba..cc777a6c5 100644 --- a/src/routes/webhooks/github/events.ts +++ b/src/routes/webhooks/github/events.ts @@ -73,6 +73,7 @@ export const post: RequestHandler = async (event) => { JSON.stringify({ buildPack: applicationFound.buildPack, port: applicationFound.port, + exposePort: applicationFound.exposePort, installCommand: applicationFound.installCommand, buildCommand: applicationFound.buildCommand, startCommand: applicationFound.startCommand diff --git a/src/routes/webhooks/gitlab/events.ts b/src/routes/webhooks/gitlab/events.ts index d84646088..6d2cdb65c 100644 --- a/src/routes/webhooks/gitlab/events.ts +++ b/src/routes/webhooks/gitlab/events.ts @@ -46,6 +46,7 @@ export const post: RequestHandler = async (event) => { JSON.stringify({ buildPack: applicationFound.buildPack, port: applicationFound.port, + exposePort: applicationFound.exposePort, installCommand: applicationFound.installCommand, buildCommand: applicationFound.buildCommand, startCommand: applicationFound.startCommand