From ccd550bbc46c92d3de715e55a408c92c92f7fb6c Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 29 Aug 2022 15:29:00 +0200 Subject: [PATCH] Contribution guide + code refactor + package updates --- .devcontainer/devcontainer.json | 6 +- CONTRIBUTING.md | 153 +- apps/api/.env.example | 7 +- apps/api/package.json | 14 +- apps/api/src/index.ts | 6 +- apps/api/src/jobs/infrastructure.ts | 4 +- apps/api/src/lib/common.ts | 524 +--- apps/api/src/lib/services/common.ts | 344 +++ apps/api/src/lib/services/handlers.ts | 2226 ++++++++++++++++ .../src/lib/{ => services}/serviceFields.ts | 0 .../api/src/lib/services/supportedVersions.ts | 193 ++ apps/api/src/routes/api/v1/handlers.ts | 3 +- .../src/routes/api/v1/services/handlers.ts | 2352 +---------------- apps/api/src/routes/api/v1/services/index.ts | 3 +- .../src/routes/webhooks/traefik/handlers.ts | 6 +- apps/ui/src/lib/common.ts | 203 -- apps/ui/src/lib/store.ts | 6 +- apps/ui/src/routes/__layout.svelte | 2 + .../[id]/configuration/version.svelte | 5 +- pnpm-lock.yaml | 289 +- 20 files changed, 3103 insertions(+), 3243 deletions(-) create mode 100644 apps/api/src/lib/services/common.ts create mode 100644 apps/api/src/lib/services/handlers.ts rename apps/api/src/lib/{ => services}/serviceFields.ts (100%) create mode 100644 apps/api/src/lib/services/supportedVersions.ts diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 4c0e03d40..64c664a9e 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -8,7 +8,7 @@ // Append -bullseye or -buster to pin to an OS version. // Use -bullseye variants on local arm64/Apple Silicon. "args": { - "VARIANT": "16-bullseye" + "VARIANT": "18-bullseye" } }, // Set *default* container specific settings.json values on container create. @@ -23,9 +23,9 @@ "bradlc.vscode-tailwindcss" ], // Use 'forwardPorts' to make a list of ports inside the container available locally. - "forwardPorts": [3000], + "forwardPorts": [3000, 3001], // Use 'postCreateCommand' to run commands after the container is created. - "postCreateCommand": "cp .env.template .env && pnpm install && pnpm db:push && pnpm db:seed", + "postCreateCommand": "cp apps/api/.env.example pps/api/.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": { diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 99c5433bf..b64c7afba 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,6 @@ First of all, thank you for considering contributing to my project! It means a lot 💜. -Contribution guide is for v2, not applicable for v3 ## 🙋 Want to help? @@ -17,13 +16,17 @@ This is a little list of what you can do to help the project: ## 👋 Introduction -### Setup with github codespaces +### 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 with Gitpod + +If you have a [Gitpod](https://gitpod.io), you can just create a workspace from this repository, run `pnpm install && pnpm db:push && pnpm db:seed` and then `pnpm dev`. 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 +> 🔴 At the moment, Coolify **doesn't support Windows**. You must use Linux or MacOS. Consider using Gitpod or Github Codespaces. #### Recommended Pull Request Guideline @@ -31,21 +34,38 @@ If you have github codespaces enabled then you can just create a codespace and r - Clone your fork repo to local - Create a new branch - Push to your fork repo -- Create a pull request: https://github.com/coollabsio/compare +- Create a pull request: https://github.com/coollabsio/coolify/compare - Write a proper description - Open the pull request to review against `next` branch --- -# How to start after you set up your local fork? +# 🧑‍💻 Developer contribution +## Technical skills required -Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient! +- **Languages**: Node.js / Javascript / Typescript +- **Framework JS/TS**: [SvelteKit](https://kit.svelte.dev/) & [Fastify](https://www.fastify.io/) +- **Database ORM**: [Prisma.io](https://www.prisma.io/) +- **Docker Engine API** -You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. +--- -#### Steps for local setup +## How to start after you set up your local fork? -1. Copy `.env.template` to `.env` and set the `COOLIFY_APP_ID` environment variable to something cool. +### Prerequisites +1. Due to the lock file, this repository is best with [pnpm](https://pnpm.io). I recommend you try and use `pnpm` because it is cool and efficient! + +2. You need to have [Docker Engine](https://docs.docker.com/engine/install/) installed locally. +3. You need to have [Docker Compose Plugin](https://docs.docker.com/compose/install/compose-plugin/) installed locally. +4. You need to have [GIT LFS Support](https://git-lfs.github.com/) installed locally. + +Optional: + +4. To test Heroku buildpacks, you need [pack](https://github.com/buildpacks/pack) binary installed locally. + +### Steps for local setup + +1. Copy `apps/api/.env.template` to `apps/api/.env.template` 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`. @@ -54,28 +74,17 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal 4. Seed the database with base entities with `pnpm db:seed` 5. You can start coding after starting `pnpm dev`. -## 🧑‍💻 Developer contribution +--- -### Technical skills required - -- **Languages**: Node.js / Javascript / Typescript -- **Framework JS/TS**: Svelte / SvelteKit -- **Database ORM**: Prisma.io -- **Docker Engine** - -### Database migrations +## Database migrations During development, if you change the database layout, you need to run `pnpm db:push` to migrate the database and create types for Prisma. You also need to restart the development process. If the schema is finalized, you need to create a migration file with `pnpm db:migrate ` where `nameOfMigration` is given by you. Make it sense. :) -### Tricky parts - -- BullMQ, the queue system Coolify uses, cannot be hot reloaded. So if you change anything in the files related to it, you need to restart the development process. I'm actively looking for a different queue/scheduler library. I'm open to discussion! - --- -# How to add new services +## How to add new services You can add any open-source and self-hostable software (service/application) to Coolify if the following statements are true: @@ -95,14 +104,14 @@ There are 5 steps you should make on the backend side. > I will use [Umami](https://umami.is/) as an example service. -### Create Prisma / database schema for the new service. +### Create Prisma / Database schema for the new service. You only need to do this if you store passwords or any persistent configuration. Mostly it is required by all services, but there are some exceptions, like NocoDB. Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma). - Add new model with the new service name. -- Make a relationshup with `Service` model. +- Make a relationship with `Service` model. - In the `Service` model, the name of the new field should be with low-capital. - If the service needs a database, define a `publicPort` field to be able to make it's database public, example field name in case of PostgreSQL: `postgresqlPublicPort`. It should be a optional field. @@ -110,13 +119,13 @@ If you are finished with the Prisma schema, you should update the database schem > You must restart the running development environment to be able to use the new model -> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode. +> If you use VSCode/TLS, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running environment. ### Add supported versions Supported versions are hardcoded into Coolify (for now). -You need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). Example JSON: +You need to update `supportedServiceTypesAndVersions` function at [src/apps/api/src/lib/supportedVersions.ts](src/apps/api/src/lib/supportedVersions.ts). Example JSON: ```js { @@ -139,12 +148,12 @@ You need to update `supportedServiceTypesAndVersions` function at [src/lib/compo } ``` -### Update global functions +### Add required functions/properties -1. Add the new service to the `include` variable in [src/lib/database/services.ts](src/lib/database/services.ts), so it will be included in all places in the database queries where it is required. +1. Add the new service to the `includeServices` variable in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts), so it will be included in all places in the database queries where it is required. ```js -const include: Prisma.ServiceInclude = { +const include: any = { destinationDocker: true, persistentStorage: true, serviceSecret: true, @@ -158,7 +167,7 @@ const include: Prisma.ServiceInclude = { }; ``` -2. Update the database update query with the new service type to `configureServiceType` function in [src/lib/database/services.ts](src/lib/database/services.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable). +2. Update the database update query with the new service type to `configureServiceType` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts). This function defines the automatically generated variables (passwords, users, etc.) and it's encryption process (if applicable). ```js [...] @@ -184,80 +193,46 @@ else if (type === 'umami') { } ``` -3. Add decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts) +3. Add field details to [apps/api/src/lib/services/serviceFields.ts](apps/api/src/lib/services/serviceFields.ts), so every component will know what to do with the values (decrypt/show it by default/readonly) ```js -if (body.umami?.postgresqlPassword) - body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword); - -if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt); +export const umami = [{ + name: 'postgresqlUser', + isEditable: false, + isLowerCase: false, + isNumber: false, + isBoolean: false, + isEncrypted: false +}] ``` -4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.ts) +4. Add service deletion query to `removeService` function in [apps/api/src/lib/services/common.ts](apps/api/src/lib/services/common.ts) -### Create API endpoints. -You need to add a new folder under [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. You need 3 default files in that folder. +5. You need to add start process for the new service in [apps/api/src/routes/api/v1/services/handlers.ts](apps/api/src/routes/api/v1/services/handlers.ts) -#### `index.json.ts`: +> See startUmamiService() function as example. -It has a POST endpoint that updates the service details in Coolify's database, such as name, url, other configurations, like passwords. It should look something like this: -```js -import { getUserDetails } from '$lib/common'; -import * as db from '$lib/database'; -import { ErrorHandler } from '$lib/database'; -import type { RequestHandler } from '@sveltejs/kit'; - -export const post: RequestHandler = async (event) => { - const { status, body } = await getUserDetails(event); - if (status === 401) return { status, body }; - - const { id } = event.params; - - let { name, fqdn } = await event.request.json(); - if (fqdn) fqdn = fqdn.toLowerCase(); - - try { - await db.updateService({ id, fqdn, name }); - return { status: 201 }; - } catch (error) { - return ErrorHandler(error); - } -}; -``` - -If it's necessary, you can create your own database update function, specifically for the new service. - -#### `start.json.ts` - -It has a POST endpoint that sets all the required secrets, persistent volumes, `docker-compose.yaml` file and sends a request to the specified docker engine. - -You could also define an `HTTP` or `TCP` proxy for every other port that should be proxied to your server. (See `startHttpProxy` and `startTcpProxy` functions in [src/lib/haproxy/index.ts](src/lib/haproxy/index.ts)) - -#### `stop.json.ts` - -It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxies) containers. If publicPort is specified it also needs to cleanup it from the database. - -## Frontend - -1. You need to add a custom logo at [src/lib/components/svg/services/](src/lib/components/svg/services/) as a svelte component. +6. You need to add a custom logo at [apps/ui/src/lib/components/svg/services](apps/ui/src/lib/components/svg/services) as a svelte component and export it in [apps/ui/src/lib/components/svg/services/index.ts](apps/ui/src/lib/components/svg/services/index.ts) SVG is recommended, but you can use PNG as well. It should have the `isAbsolute` variable with the suitable CSS classes, primarily for sizing and positioning. -2. You need to include it the logo at +7. You need to include it the logo at: -- [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` in two places, -- [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with `isAbsolute` and a link to the docs/main site of the service -- [src/routes/services/[id]/configuration/type.svelte](src/routes/services/[id]/configuration/type.svelte) with `isAbsolute`. +- [apps/ui/src/lib/components/svg/services/ServiceIcons.svelte](apps/ui/src/lib/components/svg/services/ServiceIcons.svelte) with `isAbsolute`. +- [apps/ui/src/routes/services/[id]/_ServiceLinks.svelte](apps/ui/src/routes/services/[id]/_ServiceLinks.svelte) with the link to the docs/main site of the service -3. By default the URL and the name frontend forms are included in [src/routes/services/[id]/\_Services/\_Services.svelte](src/routes/services/[id]/_Services/_Services.svelte). +8. By default the URL and the name frontend forms are included in [apps/ui/src/routes/services/[id]/_Services/_Services.svelte](apps/ui/src/routes/services/[id]/_Services/_Services.svelte). - If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [src/routes/services/[id]/\_Services](src/routes/services/[id]/_Services) with an underscore. For example, see other files in that folder. + If you need to show more details on the frontend, such as users/passwords, you need to add Svelte component to [apps/ui/src/routes/services/[id]/_Services](apps/ui/src/routes/services/[id]/_Services) with an underscore. + + > For example, see other [here](apps/ui/src/routes/services/[id]/_Services/_Umami.svelte). - You also need to add the new inputs to the `index.json.ts` file of the specific service, like for MinIO here: [src/routes/services/[id]/minio/index.json.ts](src/routes/services/[id]/minio/index.json.ts) -## 🌐 Translate the project +Good job! 👏 + + diff --git a/apps/api/.env.example b/apps/api/.env.example index 3d630cd57..800c40929 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -1,6 +1,7 @@ -COOLIFY_APP_ID= -COOLIFY_SECRET_KEY="12341234123412341234123412341234 - 32 long" -COOLIFY_DATABASE_URL= +COOLIFY_APP_ID=local-dev +# 32 bits long secret key +COOLIFY_SECRET_KEY=12341234123412341234123412341234 +COOLIFY_DATABASE_URL=file:../db/dev.db COOLIFY_SENTRY_DSN= COOLIFY_IS_ON=docker diff --git a/apps/api/package.json b/apps/api/package.json index 6b48e4b78..2b6cf28e4 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -16,26 +16,26 @@ "dependencies": { "@breejs/ts-worker": "2.0.0", "@fastify/autoload": "5.2.0", - "@fastify/cookie": "8.0.0", + "@fastify/cookie": "8.1.0", "@fastify/cors": "8.1.0", "@fastify/env": "4.1.0", "@fastify/jwt": "6.3.2", "@fastify/static": "6.5.0", "@iarna/toml": "2.2.5", "@ladjs/graceful": "3.0.2", - "@prisma/client": "3.15.2", + "@prisma/client": "4.2.1", "axios": "0.27.2", "bcryptjs": "2.4.3", "bree": "9.1.2", "cabin": "9.1.2", - "compare-versions": "4.1.4", + "compare-versions": "5.0.1", "cuid": "2.1.8", "dayjs": "1.11.5", "dockerode": "3.3.4", "dotenv-extended": "2.9.0", "execa": "6.1.0", - "fastify": "4.5.2", - "fastify-plugin": "4.2.0", + "fastify": "4.5.3", + "fastify-plugin": "4.2.1", "generate-password": "1.7.0", "got": "12.3.1", "is-ip": "5.0.0", @@ -57,12 +57,12 @@ "@typescript-eslint/eslint-plugin": "5.35.1", "@typescript-eslint/parser": "5.35.1", "esbuild": "0.15.5", - "eslint": "8.22.0", + "eslint": "8.23.0", "eslint-config-prettier": "8.5.0", "eslint-plugin-prettier": "4.2.1", "nodemon": "2.0.19", "prettier": "2.7.1", - "prisma": "3.15.2", + "prisma": "4.2.1", "rimraf": "3.0.2", "tsconfig-paths": "4.1.0", "typescript": "4.7.4" diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index 00865adca..8c81b0350 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -5,9 +5,9 @@ import env from '@fastify/env'; import cookie from '@fastify/cookie'; import path, { join } from 'path'; import autoLoad from '@fastify/autoload'; -import { asyncExecShell, asyncSleep, isDev, listSettings, prisma, version } from './lib/common'; +import { asyncExecShell, isDev, listSettings, prisma, version } from './lib/common'; import { scheduler } from './lib/scheduler'; -import compareVersions from 'compare-versions'; +import { compareVersions } from 'compare-versions'; import Graceful from '@ladjs/graceful' declare module 'fastify' { interface FastifyInstance { @@ -106,7 +106,7 @@ fastify.listen({ port, host }, async (err: any, address: any) => { const graceful = new Graceful({ brees: [scheduler] }); graceful.listen(); - + setInterval(async () => { if (!scheduler.workers.has('deployApplication')) { scheduler.run('deployApplication'); diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts index d19163686..19ab713aa 100644 --- a/apps/api/src/jobs/infrastructure.ts +++ b/apps/api/src/jobs/infrastructure.ts @@ -1,6 +1,6 @@ import { parentPort } from 'node:worker_threads'; import axios from 'axios'; -import compareVersions from 'compare-versions'; +import { compareVersions } from 'compare-versions'; import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version } from '../lib/common'; async function disconnect() { @@ -103,7 +103,7 @@ async function checkProxies() { } } } catch (error) { - + } } async function cleanupPrismaEngines() { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index c04e9ceac..4df1fe424 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -15,9 +15,11 @@ import sshConfig from 'ssh-config' import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; -import * as serviceFields from './serviceFields' +import * as serviceFields from './services/serviceFields' import { saveBuildLog } from './buildPacks/common'; import { scheduler } from './scheduler'; +import { supportedServiceTypesAndVersions } from './services/supportedVersions'; +import { includeServices } from './services/common'; export const version = '3.8.5'; export const isDev = process.env.NODE_ENV === 'development'; @@ -67,25 +69,6 @@ const otherTraefikEndpoint = isDev : 'http://coolify:3000/webhooks/traefik/other.json'; -export const include: any = { - destinationDocker: true, - persistentStorage: true, - serviceSecret: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true, - meiliSearch: true, - umami: true, - hasura: true, - fider: true, - moodle: true, - appwrite: true, - glitchTip: true, - searxng: true -}; - export const uniqueName = (): string => uniqueNamesGenerator(customConfig); export const asyncExecShell = util.promisify(exec); export const asyncExecShellStream = async ({ debug, buildId, applicationId, command, engine }: { debug: boolean, buildId: string, applicationId: string, command: string, engine: string }) => { @@ -200,199 +183,7 @@ export const encrypt = (text: string) => { } }; -export const supportedServiceTypesAndVersions = [ - { - name: 'plausibleanalytics', - fancyName: 'Plausible Analytics', - baseImage: 'plausible/analytics', - images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'], - versions: ['latest', 'stable'], - recommendedVersion: 'stable', - ports: { - main: 8000 - } - }, - { - name: 'nocodb', - fancyName: 'NocoDB', - baseImage: 'nocodb/nocodb', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, - { - name: 'minio', - fancyName: 'MinIO', - baseImage: 'minio/minio', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 9001 - } - }, - { - name: 'vscodeserver', - fancyName: 'VSCode Server', - baseImage: 'codercom/code-server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, - { - name: 'wordpress', - fancyName: 'Wordpress', - baseImage: 'wordpress', - images: ['bitnami/mysql:5.7'], - versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], - recommendedVersion: 'latest', - ports: { - main: 80 - } - }, - { - name: 'vaultwarden', - fancyName: 'Vaultwarden', - baseImage: 'vaultwarden/server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 80 - } - }, - { - name: 'languagetool', - fancyName: 'LanguageTool', - baseImage: 'silviof/docker-languagetool', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8010 - } - }, - { - name: 'n8n', - fancyName: 'n8n', - baseImage: 'n8nio/n8n', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 5678 - } - }, - { - name: 'uptimekuma', - fancyName: 'Uptime Kuma', - baseImage: 'louislam/uptime-kuma', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 3001 - } - }, - { - name: 'ghost', - fancyName: 'Ghost', - baseImage: 'bitnami/ghost', - images: ['bitnami/mariadb'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 2368 - } - }, - { - name: 'meilisearch', - fancyName: 'Meilisearch', - baseImage: 'getmeili/meilisearch', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 7700 - } - }, - { - name: 'umami', - fancyName: 'Umami', - baseImage: 'ghcr.io/mikecao/umami', - images: ['postgres:12-alpine'], - versions: ['postgresql-latest'], - recommendedVersion: 'postgresql-latest', - ports: { - main: 3000 - } - }, - { - name: 'hasura', - fancyName: 'Hasura', - baseImage: 'hasura/graphql-engine', - images: ['postgres:12-alpine'], - versions: ['latest', 'v2.10.0', 'v2.5.1'], - recommendedVersion: 'v2.10.0', - ports: { - main: 8080 - } - }, - { - name: 'fider', - fancyName: 'Fider', - baseImage: 'getfider/fider', - images: ['postgres:12-alpine'], - versions: ['stable'], - recommendedVersion: 'stable', - ports: { - main: 3000 - } - }, - { - name: 'appwrite', - fancyName: 'Appwrite', - baseImage: 'appwrite/appwrite', - images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], - versions: ['latest', '0.15.3'], - recommendedVersion: '0.15.3', - ports: { - main: 80 - } - }, - // { - // name: 'moodle', - // fancyName: 'Moodle', - // baseImage: 'bitnami/moodle', - // images: [], - // versions: ['latest', 'v4.0.2'], - // recommendedVersion: 'latest', - // ports: { - // main: 8080 - // } - // } - { - name: 'glitchTip', - fancyName: 'GlitchTip', - baseImage: 'glitchtip/glitchtip', - images: ['postgres:14-alpine', 'redis:7-alpine'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8000 - } - }, - { - name: 'searxng', - fancyName: 'SearXNG', - baseImage: 'searxng/searxng', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, -]; + export async function checkDoubleBranch(branch: string, projectId: number): Promise { const applications = await prisma.application.findMany({ where: { branch, projectId } }); @@ -570,7 +361,7 @@ export function generateTimestamp(): string { export async function listServicesWithIncludes(): Promise { return await prisma.service.findMany({ - include, + include: includeServices, orderBy: { createdAt: 'desc' } }); } @@ -1499,311 +1290,6 @@ export function getServiceImages(type: string): string[] { return []; } -export async function configureServiceType({ - id, - type -}: { - id: string; - type: string; -}): Promise { - if (type === 'plausibleanalytics') { - const password = encrypt(generatePassword({})); - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'plausibleanalytics'; - const secretKeyBase = encrypt(generatePassword({ length: 64 })); - - await prisma.service.update({ - where: { id }, - data: { - type, - plausibleAnalytics: { - create: { - postgresqlDatabase, - postgresqlUser, - postgresqlPassword, - password, - secretKeyBase - } - } - } - }); - } else if (type === 'nocodb') { - await prisma.service.update({ - where: { id }, - data: { type } - }); - } else if (type === 'minio') { - const rootUser = cuid(); - const rootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { type, minio: { create: { rootUser, rootUserPassword } } } - }); - } else if (type === 'vscodeserver') { - const password = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { type, vscodeserver: { create: { password } } } - }); - } else if (type === 'wordpress') { - const mysqlUser = cuid(); - const mysqlPassword = encrypt(generatePassword({})); - const mysqlRootUser = cuid(); - const mysqlRootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - wordpress: { create: { mysqlPassword, mysqlRootUserPassword, mysqlRootUser, mysqlUser } } - } - }); - } else if (type === 'vaultwarden') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'languagetool') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'n8n') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'uptimekuma') { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } else if (type === 'ghost') { - const defaultEmail = `${cuid()}@example.com`; - const defaultPassword = encrypt(generatePassword({})); - const mariadbUser = cuid(); - const mariadbPassword = encrypt(generatePassword({})); - const mariadbRootUser = cuid(); - const mariadbRootUserPassword = encrypt(generatePassword({})); - - await prisma.service.update({ - where: { id }, - data: { - type, - ghost: { - create: { - defaultEmail, - defaultPassword, - mariadbUser, - mariadbPassword, - mariadbRootUser, - mariadbRootUserPassword - } - } - } - }); - } else if (type === 'meilisearch') { - const masterKey = encrypt(generatePassword({ length: 32 })); - await prisma.service.update({ - where: { id }, - data: { - type, - meiliSearch: { create: { masterKey } } - } - }); - } else if (type === 'umami') { - const umamiAdminPassword = encrypt(generatePassword({})); - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'umami'; - const hashSalt = encrypt(generatePassword({ length: 64 })); - await prisma.service.update({ - where: { id }, - data: { - type, - umami: { - create: { - umamiAdminPassword, - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - hashSalt - } - } - } - }); - } else if (type === 'hasura') { - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'hasura'; - const graphQLAdminPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - hasura: { - create: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - graphQLAdminPassword - } - } - } - }); - } else if (type === 'fider') { - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'fider'; - const jwtSecret = encrypt(generatePassword({ length: 64, symbols: true })); - await prisma.service.update({ - where: { id }, - data: { - type, - fider: { - create: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - jwtSecret - } - } - } - }); - } else if (type === 'moodle') { - const defaultUsername = cuid(); - const defaultPassword = encrypt(generatePassword({})); - const defaultEmail = `${cuid()} @example.com`; - const mariadbUser = cuid(); - const mariadbPassword = encrypt(generatePassword({})); - const mariadbDatabase = 'moodle_db'; - const mariadbRootUser = cuid(); - const mariadbRootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - moodle: { - create: { - defaultUsername, - defaultPassword, - defaultEmail, - mariadbUser, - mariadbPassword, - mariadbDatabase, - mariadbRootUser, - mariadbRootUserPassword - } - } - } - }); - } else if (type === 'appwrite') { - const opensslKeyV1 = encrypt(generatePassword({})); - const executorSecret = encrypt(generatePassword({})); - const redisPassword = encrypt(generatePassword({})); - const mariadbHost = `${id}-mariadb` - const mariadbUser = cuid(); - const mariadbPassword = encrypt(generatePassword({})); - const mariadbDatabase = 'appwrite'; - const mariadbRootUser = cuid(); - const mariadbRootUserPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - appwrite: { - create: { - opensslKeyV1, - executorSecret, - redisPassword, - mariadbHost, - mariadbUser, - mariadbPassword, - mariadbDatabase, - mariadbRootUser, - mariadbRootUserPassword - } - } - } - }); - } else if (type === 'glitchTip') { - const defaultUsername = cuid(); - const defaultEmail = `${defaultUsername}@example.com`; - const defaultPassword = encrypt(generatePassword({})); - const postgresqlUser = cuid(); - const postgresqlPassword = encrypt(generatePassword({})); - const postgresqlDatabase = 'glitchTip'; - const secretKeyBase = encrypt(generatePassword({ length: 64 })); - - await prisma.service.update({ - where: { id }, - data: { - type, - glitchTip: { - create: { - postgresqlDatabase, - postgresqlUser, - postgresqlPassword, - secretKeyBase, - defaultEmail, - defaultUsername, - defaultPassword, - } - } - } - }); - } else if (type === 'searxng') { - const secretKey = encrypt(generatePassword({ length: 32, isHex: true })) - const redisPassword = encrypt(generatePassword({})); - await prisma.service.update({ - where: { id }, - data: { - type, - searxng: { - create: { - secretKey, - redisPassword, - } - } - } - }); - } else { - await prisma.service.update({ - where: { id }, - data: { - type - } - }); - } -} - -export async function removeService({ id }: { id: string }): Promise { - await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); - await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); - await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); - await prisma.fider.deleteMany({ where: { serviceId: id } }); - await prisma.ghost.deleteMany({ where: { serviceId: id } }); - await prisma.umami.deleteMany({ where: { serviceId: id } }); - await prisma.hasura.deleteMany({ where: { serviceId: id } }); - await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); - await prisma.minio.deleteMany({ where: { serviceId: id } }); - await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); - await prisma.wordpress.deleteMany({ where: { serviceId: id } }); - await prisma.glitchTip.deleteMany({ where: { serviceId: id } }); - await prisma.moodle.deleteMany({ where: { serviceId: id } }); - await prisma.appwrite.deleteMany({ where: { serviceId: id } }); - await prisma.searxng.deleteMany({ where: { serviceId: id } }); - await prisma.service.delete({ where: { id } }); -} - export function saveUpdateableFields(type: string, data: any) { const update = {}; if (type && serviceFields[type]) { diff --git a/apps/api/src/lib/services/common.ts b/apps/api/src/lib/services/common.ts new file mode 100644 index 000000000..44caeabe9 --- /dev/null +++ b/apps/api/src/lib/services/common.ts @@ -0,0 +1,344 @@ +import { exec } from 'node:child_process' +import util from 'util'; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; +import forge from 'node-forge'; +import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator'; +import type { Config } from 'unique-names-generator'; +import generator from 'generate-password'; +import crypto from 'crypto'; +import { promises as dns } from 'dns'; +import { PrismaClient } from '@prisma/client'; +import cuid from 'cuid'; +import os from 'os'; +import sshConfig from 'ssh-config' +import { encrypt, generatePassword, prisma } from '../common'; + + +export const version = '3.8.2'; +export const isDev = process.env.NODE_ENV === 'development'; + +export const includeServices: any = { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true, + umami: true, + hasura: true, + fider: true, + moodle: true, + appwrite: true, + glitchTip: true, + searxng: true +}; +export async function configureServiceType({ + id, + type +}: { + id: string; + type: string; +}): Promise { + if (type === 'plausibleanalytics') { + const password = encrypt(generatePassword({})); + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'plausibleanalytics'; + const secretKeyBase = encrypt(generatePassword({ length: 64 })); + + await prisma.service.update({ + where: { id }, + data: { + type, + plausibleAnalytics: { + create: { + postgresqlDatabase, + postgresqlUser, + postgresqlPassword, + password, + secretKeyBase + } + } + } + }); + } else if (type === 'nocodb') { + await prisma.service.update({ + where: { id }, + data: { type } + }); + } else if (type === 'minio') { + const rootUser = cuid(); + const rootUserPassword = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { type, minio: { create: { rootUser, rootUserPassword } } } + }); + } else if (type === 'vscodeserver') { + const password = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { type, vscodeserver: { create: { password } } } + }); + } else if (type === 'wordpress') { + const mysqlUser = cuid(); + const mysqlPassword = encrypt(generatePassword({})); + const mysqlRootUser = cuid(); + const mysqlRootUserPassword = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { + type, + wordpress: { create: { mysqlPassword, mysqlRootUserPassword, mysqlRootUser, mysqlUser } } + } + }); + } else if (type === 'vaultwarden') { + await prisma.service.update({ + where: { id }, + data: { + type + } + }); + } else if (type === 'languagetool') { + await prisma.service.update({ + where: { id }, + data: { + type + } + }); + } else if (type === 'n8n') { + await prisma.service.update({ + where: { id }, + data: { + type + } + }); + } else if (type === 'uptimekuma') { + await prisma.service.update({ + where: { id }, + data: { + type + } + }); + } else if (type === 'ghost') { + const defaultEmail = `${cuid()}@example.com`; + const defaultPassword = encrypt(generatePassword({})); + const mariadbUser = cuid(); + const mariadbPassword = encrypt(generatePassword({})); + const mariadbRootUser = cuid(); + const mariadbRootUserPassword = encrypt(generatePassword({})); + + await prisma.service.update({ + where: { id }, + data: { + type, + ghost: { + create: { + defaultEmail, + defaultPassword, + mariadbUser, + mariadbPassword, + mariadbRootUser, + mariadbRootUserPassword + } + } + } + }); + } else if (type === 'meilisearch') { + const masterKey = encrypt(generatePassword({ length: 32 })); + await prisma.service.update({ + where: { id }, + data: { + type, + meiliSearch: { create: { masterKey } } + } + }); + } else if (type === 'umami') { + const umamiAdminPassword = encrypt(generatePassword({})); + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'umami'; + const hashSalt = encrypt(generatePassword({ length: 64 })); + await prisma.service.update({ + where: { id }, + data: { + type, + umami: { + create: { + umamiAdminPassword, + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + hashSalt + } + } + } + }); + } else if (type === 'hasura') { + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'hasura'; + const graphQLAdminPassword = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { + type, + hasura: { + create: { + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + graphQLAdminPassword + } + } + } + }); + } else if (type === 'fider') { + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'fider'; + const jwtSecret = encrypt(generatePassword({ length: 64, symbols: true })); + await prisma.service.update({ + where: { id }, + data: { + type, + fider: { + create: { + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + jwtSecret + } + } + } + }); + } else if (type === 'moodle') { + const defaultUsername = cuid(); + const defaultPassword = encrypt(generatePassword({})); + const defaultEmail = `${cuid()} @example.com`; + const mariadbUser = cuid(); + const mariadbPassword = encrypt(generatePassword({})); + const mariadbDatabase = 'moodle_db'; + const mariadbRootUser = cuid(); + const mariadbRootUserPassword = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { + type, + moodle: { + create: { + defaultUsername, + defaultPassword, + defaultEmail, + mariadbUser, + mariadbPassword, + mariadbDatabase, + mariadbRootUser, + mariadbRootUserPassword + } + } + } + }); + } else if (type === 'appwrite') { + const opensslKeyV1 = encrypt(generatePassword({})); + const executorSecret = encrypt(generatePassword({})); + const redisPassword = encrypt(generatePassword({})); + const mariadbHost = `${id}-mariadb` + const mariadbUser = cuid(); + const mariadbPassword = encrypt(generatePassword({})); + const mariadbDatabase = 'appwrite'; + const mariadbRootUser = cuid(); + const mariadbRootUserPassword = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { + type, + appwrite: { + create: { + opensslKeyV1, + executorSecret, + redisPassword, + mariadbHost, + mariadbUser, + mariadbPassword, + mariadbDatabase, + mariadbRootUser, + mariadbRootUserPassword + } + } + } + }); + } else if (type === 'glitchTip') { + const defaultUsername = cuid(); + const defaultEmail = `${defaultUsername}@example.com`; + const defaultPassword = encrypt(generatePassword({})); + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword({})); + const postgresqlDatabase = 'glitchTip'; + const secretKeyBase = encrypt(generatePassword({ length: 64 })); + + await prisma.service.update({ + where: { id }, + data: { + type, + glitchTip: { + create: { + postgresqlDatabase, + postgresqlUser, + postgresqlPassword, + secretKeyBase, + defaultEmail, + defaultUsername, + defaultPassword, + } + } + } + }); + } else if (type === 'searxng') { + const secretKey = encrypt(generatePassword({ length: 32, isHex: true })) + const redisPassword = encrypt(generatePassword({})); + await prisma.service.update({ + where: { id }, + data: { + type, + searxng: { + create: { + secretKey, + redisPassword, + } + } + } + }); + } else { + await prisma.service.update({ + where: { id }, + data: { + type + } + }); + } +} + +export async function removeService({ id }: { id: string }): Promise { + await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); + await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); + await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); + await prisma.fider.deleteMany({ where: { serviceId: id } }); + await prisma.ghost.deleteMany({ where: { serviceId: id } }); + await prisma.umami.deleteMany({ where: { serviceId: id } }); + await prisma.hasura.deleteMany({ where: { serviceId: id } }); + await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); + await prisma.minio.deleteMany({ where: { serviceId: id } }); + await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); + await prisma.wordpress.deleteMany({ where: { serviceId: id } }); + await prisma.glitchTip.deleteMany({ where: { serviceId: id } }); + await prisma.moodle.deleteMany({ where: { serviceId: id } }); + await prisma.appwrite.deleteMany({ where: { serviceId: id } }); + await prisma.searxng.deleteMany({ where: { serviceId: id } }); + + + await prisma.service.delete({ where: { id } }); +} \ No newline at end of file diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts new file mode 100644 index 000000000..cbefd7a94 --- /dev/null +++ b/apps/api/src/lib/services/handlers.ts @@ -0,0 +1,2226 @@ +import type { FastifyReply, FastifyRequest } from 'fastify'; +import fs from 'fs/promises'; +import yaml from 'js-yaml'; +import bcrypt from 'bcryptjs'; +import { ServiceStartStop } from '../../routes/api/v1/services/types'; +import { asyncSleep, ComposeFile, createDirectories, defaultComposeConfiguration, errorHandler, executeDockerCmd, getDomain, getFreePublicPort, getServiceFromDB, getServiceImage, getServiceMainPort, isARM, makeLabelForServices, persistentVolumes, prisma } from '../common'; +import { defaultServiceConfigurations } from '../services'; + +export async function startService(request: FastifyRequest) { + try { + const { type } = request.params + if (type === 'plausibleanalytics') { + return await startPlausibleAnalyticsService(request) + } + if (type === 'nocodb') { + return await startNocodbService(request) + } + if (type === 'minio') { + return await startMinioService(request) + } + if (type === 'vscodeserver') { + return await startVscodeService(request) + } + if (type === 'wordpress') { + return await startWordpressService(request) + } + if (type === 'vaultwarden') { + return await startVaultwardenService(request) + } + if (type === 'languagetool') { + return await startLanguageToolService(request) + } + if (type === 'n8n') { + return await startN8nService(request) + } + if (type === 'uptimekuma') { + return await startUptimekumaService(request) + } + if (type === 'ghost') { + return await startGhostService(request) + } + if (type === 'meilisearch') { + return await startMeilisearchService(request) + } + if (type === 'umami') { + return await startUmamiService(request) + } + if (type === 'hasura') { + return await startHasuraService(request) + } + if (type === 'fider') { + return await startFiderService(request) + } + if (type === 'moodle') { + return await startMoodleService(request) + } + if (type === 'appwrite') { + return await startAppWriteService(request) + } + if (type === 'glitchTip') { + return await startGlitchTipService(request) + } + if (type === 'searxng') { + return await startSearXNGService(request) + } + throw `Service type ${type} not supported.` + } catch (error) { + throw { status: 500, message: error?.message || error } + } +} +export async function stopService(request: FastifyRequest) { + try { + return await stopServiceContainers(request) + } catch (error) { + throw { status: 500, message: error?.message || error } + } +} + +async function startPlausibleAnalyticsService(request: FastifyRequest) { + try { + const { id } = request.params + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + fqdn, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + plausibleAnalytics: { + id: plausibleDbId, + username, + email, + password, + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + secretKeyBase + } + } = service; + const image = getServiceImage(type); + + const config = { + plausibleAnalytics: { + image: `${image}:${version}`, + environmentVariables: { + ADMIN_USER_EMAIL: email, + ADMIN_USER_NAME: username, + ADMIN_USER_PWD: password, + BASE_URL: fqdn, + SECRET_KEY_BASE: secretKeyBase, + DISABLE_AUTH: 'false', + DISABLE_REGISTRATION: 'true', + DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, + CLICKHOUSE_DATABASE_URL: `http://${id}-clickhouse:8123/plausible` + } + }, + postgresql: { + volume: `${plausibleDbId}-postgresql-data:/bitnami/postgresql/`, + image: 'bitnami/postgresql:13.2.0', + environmentVariables: { + POSTGRESQL_PASSWORD: postgresqlPassword, + POSTGRESQL_USERNAME: postgresqlUser, + POSTGRESQL_DATABASE: postgresqlDatabase + } + }, + clickhouse: { + volume: `${plausibleDbId}-clickhouse-data:/var/lib/clickhouse`, + image: 'yandex/clickhouse-server:21.3.2.5', + environmentVariables: {}, + ulimits: { + nofile: { + soft: 262144, + hard: 262144 + } + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.plausibleAnalytics.environmentVariables[secret.name] = secret.value; + }); + } + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('plausibleanalytics'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + + const clickhouseConfigXml = ` + + + warning + true + + + + + + + + + + + + `; + const clickhouseUserConfigXml = ` + + + + 0 + 0 + + + `; + + const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'; + const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'; + await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml); + await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml); + await fs.writeFile(`${workdir}/init.query`, initQuery); + await fs.writeFile(`${workdir}/init-db.sh`, initScript); + + const Dockerfile = ` +FROM ${config.clickhouse.image} +COPY ./clickhouse-config.xml /etc/clickhouse-server/users.d/logging.xml +COPY ./clickhouse-user-config.xml /etc/clickhouse-server/config.d/logging.xml +COPY ./init.query /docker-entrypoint-initdb.d/init.query +COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; + + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); + + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.plausibleAnalytics) + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.plausibleAnalytics.image, + volumes, + command: + 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', + environment: config.plausibleAnalytics.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + depends_on: [`${id}-postgresql`, `${id}-clickhouse`], + labels: makeLabelForServices('plausibleAnalytics'), + ...defaultComposeConfiguration(network), + }, + [`${id}-postgresql`]: { + container_name: `${id}-postgresql`, + image: config.postgresql.image, + environment: config.postgresql.environmentVariables, + volumes: [config.postgresql.volume], + ...defaultComposeConfiguration(network), + }, + [`${id}-clickhouse`]: { + build: workdir, + container_name: `${id}-clickhouse`, + environment: config.clickhouse.environmentVariables, + volumes: [config.clickhouse.volume], + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + }, + [config.clickhouse.volume.split(':')[0]]: { + name: config.clickhouse.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startNocodbService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('nocodb'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-nc:/usr/app/data`, + environmentVariables: {} + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + volumes, + environment: config.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('nocodb'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startMinioService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + fqdn, + destinationDockerId, + destinationDocker, + persistentStorage, + exposePort, + minio: { rootUser, rootUserPassword }, + serviceSecret + } = service; + + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('minio'); + + const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) + const publicPort = await getFreePublicPort(id, dockerId); + + const consolePort = 9001; + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-minio-data:/data`, + environmentVariables: { + MINIO_ROOT_USER: rootUser, + MINIO_ROOT_PASSWORD: rootUserPassword, + MINIO_BROWSER_REDIRECT_URL: fqdn + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + command: `server /data --console-address ":${consolePort}"`, + environment: config.environmentVariables, + volumes, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('minio'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await startServiceContainers(destinationDocker.id, composeFileDestination) + await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startVscodeService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + vscodeserver: { password } + } = service; + + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('vscodeserver'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-vscodeserver-data:/home/coder`, + environmentVariables: { + PASSWORD: password + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + environment: config.environmentVariables, + volumes, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('vscodeServer'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + const changePermissionOn = persistentStorage.map((p) => p.path); + if (changePermissionOn.length > 0) { + await executeDockerCmd({ + dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( + ' ' + )}` + }) + } + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startWordpressService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + arch, + type, + version, + destinationDockerId, + serviceSecret, + destinationDocker, + persistentStorage, + exposePort, + wordpress: { + mysqlDatabase, + mysqlHost, + mysqlPort, + mysqlUser, + mysqlPassword, + extraConfig, + mysqlRootUser, + mysqlRootUserPassword, + ownMysql + } + } = service; + + const network = destinationDockerId && destinationDocker.network; + const image = getServiceImage(type); + const port = getServiceMainPort('wordpress'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const config = { + wordpress: { + image: `${image}:${version}`, + volume: `${id}-wordpress-data:/var/www/html`, + environmentVariables: { + WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`, + WORDPRESS_DB_USER: mysqlUser, + WORDPRESS_DB_PASSWORD: mysqlPassword, + WORDPRESS_DB_NAME: mysqlDatabase, + WORDPRESS_CONFIG_EXTRA: extraConfig + } + }, + mysql: { + image: `bitnami/mysql:5.7`, + volume: `${id}-mysql-data:/bitnami/mysql/data`, + environmentVariables: { + MYSQL_ROOT_PASSWORD: mysqlRootUserPassword, + MYSQL_ROOT_USER: mysqlRootUser, + MYSQL_USER: mysqlUser, + MYSQL_PASSWORD: mysqlPassword, + MYSQL_DATABASE: mysqlDatabase + } + } + }; + if (isARM(arch)) { + config.mysql.image = 'mysql:5.7' + config.mysql.volume = `${id}-mysql-data:/var/lib/mysql` + } + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.wordpress.environmentVariables[secret.name] = secret.value; + }); + } + + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress) + + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.wordpress.image, + environment: config.wordpress.environmentVariables, + volumes, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('wordpress'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + if (!ownMysql) { + composeFile.services[id].depends_on = [`${id}-mysql`]; + composeFile.services[`${id}-mysql`] = { + container_name: `${id}-mysql`, + image: config.mysql.image, + volumes: [config.mysql.volume], + environment: config.mysql.environmentVariables, + ...defaultComposeConfiguration(network), + }; + + composeFile.volumes[config.mysql.volume.split(':')[0]] = { + name: config.mysql.volume.split(':')[0] + }; + } + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startVaultwardenService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = + service; + + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('vaultwarden'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-vaultwarden-data:/data/`, + environmentVariables: {} + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + environment: config.environmentVariables, + volumes, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('vaultWarden'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startLanguageToolService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('languagetool'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-ngrams:/ngrams`, + environmentVariables: {} + }; + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + environment: config.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes, + labels: makeLabelForServices('languagetool'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startN8nService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('n8n'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-n8n:/root/.n8n`, + environmentVariables: { + WEBHOOK_URL: `${service.fqdn}` + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + volumes, + environment: config.environmentVariables, + labels: makeLabelForServices('n8n'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startUptimekumaService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('uptimekuma'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-uptimekuma:/app/data`, + environmentVariables: {} + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + volumes, + environment: config.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('uptimekuma'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startGhostService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + fqdn, + ghost: { + defaultEmail, + defaultPassword, + mariadbRootUser, + mariadbRootUserPassword, + mariadbDatabase, + mariadbPassword, + mariadbUser + } + } = service; + const network = destinationDockerId && destinationDocker.network; + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + const domain = getDomain(fqdn); + const port = getServiceMainPort('ghost'); + const isHttps = fqdn.startsWith('https://'); + const config = { + ghost: { + image: `${image}:${version}`, + volume: `${id}-ghost:/bitnami/ghost`, + environmentVariables: { + url: fqdn, + GHOST_HOST: domain, + GHOST_ENABLE_HTTPS: isHttps ? 'yes' : 'no', + GHOST_EMAIL: defaultEmail, + GHOST_PASSWORD: defaultPassword, + GHOST_DATABASE_HOST: `${id}-mariadb`, + GHOST_DATABASE_USER: mariadbUser, + GHOST_DATABASE_PASSWORD: mariadbPassword, + GHOST_DATABASE_NAME: mariadbDatabase, + GHOST_DATABASE_PORT_NUMBER: 3306 + } + }, + mariadb: { + image: `bitnami/mariadb:latest`, + volume: `${id}-mariadb:/bitnami/mariadb`, + environmentVariables: { + MARIADB_USER: mariadbUser, + MARIADB_PASSWORD: mariadbPassword, + MARIADB_DATABASE: mariadbDatabase, + MARIADB_ROOT_USER: mariadbRootUser, + MARIADB_ROOT_PASSWORD: mariadbRootUserPassword + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.ghost.environmentVariables[secret.name] = secret.value; + }); + } + + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.ghost.image, + volumes, + environment: config.ghost.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('ghost'), + depends_on: [`${id}-mariadb`], + ...defaultComposeConfiguration(network), + }, + [`${id}-mariadb`]: { + container_name: `${id}-mariadb`, + image: config.mariadb.image, + volumes: [config.mariadb.volume], + environment: config.mariadb.environmentVariables, + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.mariadb.volume.split(':')[0]]: { + name: config.mariadb.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startMeilisearchService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + meiliSearch: { masterKey } + } = service; + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('meilisearch'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + image: `${image}:${version}`, + volume: `${id}-datams:/data.ms`, + environmentVariables: { + MEILI_MASTER_KEY: masterKey + } + }; + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + environment: config.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + volumes, + labels: makeLabelForServices('meilisearch'), + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startUmamiService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + umami: { + umamiAdminPassword, + postgresqlUser, + postgresqlPassword, + postgresqlDatabase, + hashSalt + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('umami'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + umami: { + image: `${image}:${version}`, + environmentVariables: { + DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, + DATABASE_TYPE: 'postgresql', + HASH_SALT: hashSalt + } + }, + postgresql: { + image: 'postgres:12-alpine', + volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + environmentVariables: { + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_DB: postgresqlDatabase + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.umami.environmentVariables[secret.name] = secret.value; + }); + } + + const initDbSQL = ` + drop table if exists event; + drop table if exists pageview; + drop table if exists session; + drop table if exists website; + drop table if exists account; + + create table account ( + user_id serial primary key, + username varchar(255) unique not null, + password varchar(60) not null, + is_admin bool not null default false, + created_at timestamp with time zone default current_timestamp, + updated_at timestamp with time zone default current_timestamp + ); + + create table website ( + website_id serial primary key, + website_uuid uuid unique not null, + user_id int not null references account(user_id) on delete cascade, + name varchar(100) not null, + domain varchar(500), + share_id varchar(64) unique, + created_at timestamp with time zone default current_timestamp + ); + + create table session ( + session_id serial primary key, + session_uuid uuid unique not null, + website_id int not null references website(website_id) on delete cascade, + created_at timestamp with time zone default current_timestamp, + hostname varchar(100), + browser varchar(20), + os varchar(20), + device varchar(20), + screen varchar(11), + language varchar(35), + country char(2) + ); + + create table pageview ( + view_id serial primary key, + website_id int not null references website(website_id) on delete cascade, + session_id int not null references session(session_id) on delete cascade, + created_at timestamp with time zone default current_timestamp, + url varchar(500) not null, + referrer varchar(500) + ); + + create table event ( + event_id serial primary key, + website_id int not null references website(website_id) on delete cascade, + session_id int not null references session(session_id) on delete cascade, + created_at timestamp with time zone default current_timestamp, + url varchar(500) not null, + event_type varchar(50) not null, + event_value varchar(50) not null + ); + + create index website_user_id_idx on website(user_id); + + create index session_created_at_idx on session(created_at); + create index session_website_id_idx on session(website_id); + + create index pageview_created_at_idx on pageview(created_at); + create index pageview_website_id_idx on pageview(website_id); + create index pageview_session_id_idx on pageview(session_id); + create index pageview_website_id_created_at_idx on pageview(website_id, created_at); + create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at); + + create index event_created_at_idx on event(created_at); + create index event_website_id_idx on event(website_id); + create index event_session_id_idx on event(session_id); + + insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync( + umamiAdminPassword, + 10 + )}', true);`; + await fs.writeFile(`${workdir}/schema.postgresql.sql`, initDbSQL); + const Dockerfile = ` + FROM ${config.postgresql.image} + COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.umami.image, + environment: config.umami.environmentVariables, + volumes, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('umami'), + depends_on: [`${id}-postgresql`], + ...defaultComposeConfiguration(network), + }, + [`${id}-postgresql`]: { + build: workdir, + container_name: `${id}-postgresql`, + environment: config.postgresql.environmentVariables, + volumes: [config.postgresql.volume], + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startHasuraService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + destinationDockerId, + destinationDocker, + persistentStorage, + serviceSecret, + exposePort, + hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase } + } = service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('hasura'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + hasura: { + image: `${image}:${version}`, + environmentVariables: { + HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}` + } + }, + postgresql: { + image: 'postgres:12-alpine', + volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + environmentVariables: { + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_DB: postgresqlDatabase + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.hasura.environmentVariables[secret.name] = secret.value; + }); + } + + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.hasura.image, + environment: config.hasura.environmentVariables, + volumes, + labels: makeLabelForServices('hasura'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + depends_on: [`${id}-postgresql`], + ...defaultComposeConfiguration(network), + }, + [`${id}-postgresql`]: { + image: config.postgresql.image, + container_name: `${id}-postgresql`, + environment: config.postgresql.environmentVariables, + volumes: [config.postgresql.volume], + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startFiderService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + fqdn, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + fider: { + postgresqlUser, + postgresqlPassword, + postgresqlDatabase, + jwtSecret, + emailNoreply, + emailMailgunApiKey, + emailMailgunDomain, + emailMailgunRegion, + emailSmtpHost, + emailSmtpPort, + emailSmtpUser, + emailSmtpPassword, + emailSmtpEnableStartTls + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('fider'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + const config = { + fider: { + image: `${image}:${version}`, + environmentVariables: { + BASE_URL: fqdn, + DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`, + JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`, + EMAIL_NOREPLY: emailNoreply, + EMAIL_MAILGUN_API: emailMailgunApiKey, + EMAIL_MAILGUN_REGION: emailMailgunRegion, + EMAIL_MAILGUN_DOMAIN: emailMailgunDomain, + EMAIL_SMTP_HOST: emailSmtpHost, + EMAIL_SMTP_PORT: emailSmtpPort, + EMAIL_SMTP_USER: emailSmtpUser, + EMAIL_SMTP_PASSWORD: emailSmtpPassword, + EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls + } + }, + postgresql: { + image: 'postgres:12-alpine', + volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + environmentVariables: { + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_DB: postgresqlDatabase + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.fider.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.fider.image, + environment: config.fider.environmentVariables, + volumes, + labels: makeLabelForServices('fider'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + depends_on: [`${id}-postgresql`], + ...defaultComposeConfiguration(network), + }, + [`${id}-postgresql`]: { + image: config.postgresql.image, + container_name: `${id}-postgresql`, + environment: config.postgresql.environmentVariables, + volumes: [config.postgresql.volume], + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startAppWriteService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId }) + + let isStatsEnabled = false + if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) { + isStatsEnabled = true + } + const { + opensslKeyV1, + executorSecret, + mariadbHost, + mariadbPort, + mariadbUser, + mariadbPassword, + mariadbRootUser, + mariadbRootUserPassword, + mariadbDatabase + } = appwrite; + + const dockerCompose = { + [id]: { + image: `${image}:${version}`, + container_name: id, + labels: makeLabelForServices('appwrite'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + "volumes": [ + `${id}-uploads:/storage/uploads:rw`, + `${id}-cache:/storage/cache:rw`, + `${id}-config:/storage/config:rw`, + `${id}-certificates:/storage/certificates:rw`, + `${id}-functions:/storage/functions:rw` + ], + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + "_APP_LOCALE=en", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DOMAIN=${fqdn}`, + `_APP_DOMAIN_TARGET=${fqdn}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + `_APP_STATSD_HOST=${id}-telegraf`, + "_APP_STATSD_PORT=8125", + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-realtime`]: { + image: `${image}:${version}`, + container_name: `${id}-realtime`, + entrypoint: "realtime", + labels: makeLabelForServices('appwrite'), + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-audits`]: { + + image: `${image}:${version}`, + container_name: `${id}-worker-audits`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-audits", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-webhooks`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-webhooks`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-webhooks", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-deletes`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-deletes`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-deletes", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "volumes": [ + `${id}-uploads:/storage/uploads:rw`, + `${id}-cache:/storage/cache:rw`, + `${id}-config:/storage/config:rw`, + `${id}-certificates:/storage/certificates:rw`, + `${id}-functions:/storage/functions:rw`, + `${id}-builds:/storage/builds:rw`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-databases`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-databases`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-databases", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-builds`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-builds`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-builds", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-certificates`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-certificates`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-certificates", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + ], + "volumes": [ + `${id}-config:/storage/config:rw`, + `${id}-certificates:/storage/certificates:rw`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DOMAIN=${fqdn}`, + `_APP_DOMAIN_TARGET=${fqdn}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-functions`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-functions`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-functions", + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + `${id}-executor` + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_EXECUTOR_SECRET=${executorSecret}`, + `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-executor`]: { + image: `${image}:${version}`, + container_name: `${id}-executor`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "executor", + "stop_signal": "SIGINT", + "volumes": [ + `${id}-functions:/storage/functions:rw`, + `${id}-builds:/storage/builds:rw`, + "/var/run/docker.sock:/var/run/docker.sock", + "/tmp:/tmp:rw" + ], + "depends_on": [ + `${id}-mariadb`, + `${id}-redis`, + `${id}` + ], + "environment": [ + "_APP_ENV=production", + `_APP_EXECUTOR_SECRET=${executorSecret}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-mails`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-mails`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-mails", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker-messaging`]: { + image: `${image}:${version}`, + container_name: `${id}-worker-messaging`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "worker-messaging", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-maintenance`]: { + image: `${image}:${version}`, + container_name: `${id}-maintenance`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "maintenance", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DOMAIN=${fqdn}`, + `_APP_DOMAIN_TARGET=${fqdn}`, + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-schedule`]: { + image: `${image}:${version}`, + container_name: `${id}-schedule`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "schedule", + "depends_on": [ + `${id}-redis`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultComposeConfiguration(network), + }, + [`${id}-mariadb`]: { + "image": "mariadb:10.7", + container_name: `${id}-mariadb`, + labels: makeLabelForServices('appwrite'), + "volumes": [ + `${id}-mariadb:/var/lib/mysql:rw` + ], + "environment": [ + `MYSQL_ROOT_USER=${mariadbRootUser}`, + `MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`, + `MYSQL_USER=${mariadbUser}`, + `MYSQL_PASSWORD=${mariadbPassword}`, + `MYSQL_DATABASE=${mariadbDatabase}` + ], + "command": "mysqld --innodb-flush-method=fsync", + ...defaultComposeConfiguration(network), + }, + [`${id}-redis`]: { + "image": "redis:6.2-alpine", + container_name: `${id}-redis`, + "command": `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`, + "volumes": [ + `${id}-redis:/data:rw` + ], + ...defaultComposeConfiguration(network), + }, + + }; + if (isStatsEnabled) { + dockerCompose[id].depends_on.push(`${id}-influxdb`); + dockerCompose[`${id}-usage`] = { + image: `${image}:${version}`, + container_name: `${id}-usage`, + labels: makeLabelForServices('appwrite'), + "entrypoint": "usage", + "depends_on": [ + `${id}-mariadb`, + `${id}-influxdb`, + ], + "environment": [ + "_APP_ENV=production", + `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, + `_APP_DB_HOST=${mariadbHost}`, + `_APP_DB_PORT=${mariadbPort}`, + `_APP_DB_SCHEMA=${mariadbDatabase}`, + `_APP_DB_USER=${mariadbUser}`, + `_APP_DB_PASS=${mariadbPassword}`, + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + `_APP_REDIS_HOST=${id}-redis`, + "_APP_REDIS_PORT=6379", + ...secrets + ], + ...defaultComposeConfiguration(network), + } + dockerCompose[`${id}-influxdb`] = { + "image": "appwrite/influxdb:1.5.0", + container_name: `${id}-influxdb`, + "volumes": [ + `${id}-influxdb:/var/lib/influxdb:rw` + ], + ...defaultComposeConfiguration(network), + } + dockerCompose[`${id}-telegraf`] = { + "image": "appwrite/telegraf:1.4.0", + container_name: `${id}-telegraf`, + "environment": [ + `_APP_INFLUXDB_HOST=${id}-influxdb`, + "_APP_INFLUXDB_PORT=8086", + ], + ...defaultComposeConfiguration(network), + } + } + + const composeFile: any = { + version: '3.8', + services: dockerCompose, + networks: { + [network]: { + external: true + } + }, + volumes: { + [`${id}-uploads`]: { + name: `${id}-uploads` + }, + [`${id}-cache`]: { + name: `${id}-cache` + }, + [`${id}-config`]: { + name: `${id}-config` + }, + [`${id}-certificates`]: { + name: `${id}-certificates` + }, + [`${id}-functions`]: { + name: `${id}-functions` + }, + [`${id}-builds`]: { + name: `${id}-builds` + }, + [`${id}-mariadb`]: { + name: `${id}-mariadb` + }, + [`${id}-redis`]: { + name: `${id}-redis` + }, + [`${id}-influxdb`]: { + name: `${id}-influxdb` + } + } + + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +async function startServiceContainers(dockerId, composeFileDestination) { + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) + await asyncSleep(1000); + await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) +} +async function stopServiceContainers(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const { destinationDockerId } = await getServiceFromDB({ id, teamId }); + if (destinationDockerId) { + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker stop -t 0` + }) + await executeDockerCmd({ + dockerId: destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker rm --force` + }) + return {} + } + throw { status: 500, message: 'Could not stop containers.' } + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} +async function startMoodleService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + fqdn, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + moodle: { + defaultUsername, + defaultPassword, + defaultEmail, + mariadbRootUser, + mariadbRootUserPassword, + mariadbDatabase, + mariadbPassword, + mariadbUser + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('moodle'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + const config = { + moodle: { + image: `${image}:${version}`, + volume: `${id}-data:/bitnami/moodle`, + environmentVariables: { + MOODLE_USERNAME: defaultUsername, + MOODLE_PASSWORD: defaultPassword, + MOODLE_EMAIL: defaultEmail, + MOODLE_DATABASE_HOST: `${id}-mariadb`, + MOODLE_DATABASE_USER: mariadbUser, + MOODLE_DATABASE_PASSWORD: mariadbPassword, + MOODLE_DATABASE_NAME: mariadbDatabase, + MOODLE_REVERSEPROXY: 'yes' + } + }, + mariadb: { + image: 'bitnami/mariadb:latest', + volume: `${id}-mariadb-data:/bitnami/mariadb`, + environmentVariables: { + MARIADB_USER: mariadbUser, + MARIADB_PASSWORD: mariadbPassword, + MARIADB_DATABASE: mariadbDatabase, + MARIADB_ROOT_USER: mariadbRootUser, + MARIADB_ROOT_PASSWORD: mariadbRootUserPassword + } + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.moodle.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.moodle.image, + environment: config.moodle.environmentVariables, + networks: [network], + volumes, + restart: 'always', + labels: makeLabelForServices('moodle'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + }, + depends_on: [`${id}-mariadb`] + }, + [`${id}-mariadb`]: { + container_name: `${id}-mariadb`, + image: config.mariadb.image, + environment: config.mariadb.environmentVariables, + networks: [network], + volumes: [], + restart: 'always', + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + }, + depends_on: [] + } + + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.mariadb.volume.split(':')[0]]: { + name: config.mariadb.volume.split(':')[0] + } + } + + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startGlitchTipService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { + type, + version, + fqdn, + destinationDockerId, + destinationDocker, + serviceSecret, + persistentStorage, + exposePort, + glitchTip: { + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + secretKeyBase, + defaultEmail, + defaultUsername, + defaultPassword, + defaultFromEmail, + emailSmtpHost, + emailSmtpPort, + emailSmtpUser, + emailSmtpPassword, + emailSmtpUseTls, + emailSmtpUseSsl, + emailBackend, + mailgunApiKey, + sendgridApiKey, + enableOpenUserRegistration, + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('glitchTip'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + glitchTip: { + image: `${image}:${version}`, + environmentVariables: { + PORT: port, + GLITCHTIP_DOMAIN: fqdn, + SECRET_KEY: secretKeyBase, + DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, + REDIS_URL: `redis://${id}-redis:6379/0`, + DEFAULT_FROM_EMAIL: defaultFromEmail, + EMAIL_HOST: emailSmtpHost, + EMAIL_PORT: emailSmtpPort, + EMAIL_HOST_USER: emailSmtpUser, + EMAIL_HOST_PASSWORD: emailSmtpPassword, + EMAIL_USE_TLS: emailSmtpUseTls, + EMAIL_USE_SSL: emailSmtpUseSsl, + EMAIL_BACKEND: emailBackend, + MAILGUN_API_KEY: mailgunApiKey, + SENDGRID_API_KEY: sendgridApiKey, + ENABLE_OPEN_USER_REGISTRATION: enableOpenUserRegistration, + DJANGO_SUPERUSER_EMAIL: defaultEmail, + DJANGO_SUPERUSER_USERNAME: defaultUsername, + DJANGO_SUPERUSER_PASSWORD: defaultPassword, + } + }, + postgresql: { + image: 'postgres:14-alpine', + volume: `${id}-postgresql-data:/var/lib/postgresql/data`, + environmentVariables: { + POSTGRES_USER: postgresqlUser, + POSTGRES_PASSWORD: postgresqlPassword, + POSTGRES_DB: postgresqlDatabase + } + }, + redis: { + image: 'redis:7-alpine', + volume: `${id}-redis-data:/data`, + } + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.glitchTip.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.glitchTip.image, + environment: config.glitchTip.environmentVariables, + volumes, + labels: makeLabelForServices('glitchTip'), + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + depends_on: [`${id}-postgresql`, `${id}-redis`], + ...defaultComposeConfiguration(network), + }, + [`${id}-worker`]: { + container_name: `${id}-worker`, + image: config.glitchTip.image, + command: './bin/run-celery-with-beat.sh', + environment: config.glitchTip.environmentVariables, + depends_on: [`${id}-postgresql`, `${id}-redis`], + ...defaultComposeConfiguration(network), + }, + [`${id}-setup`]: { + container_name: `${id}-setup`, + image: config.glitchTip.image, + command: 'sh -c "(./manage.py migrate || true) && (./manage.py createsuperuser --noinput || true)"', + environment: config.glitchTip.environmentVariables, + networks: [network], + restart: "no", + depends_on: [`${id}-postgresql`, `${id}-redis`] + }, + [`${id}-postgresql`]: { + image: config.postgresql.image, + container_name: `${id}-postgresql`, + environment: config.postgresql.environmentVariables, + volumes: [config.postgresql.volume], + ...defaultComposeConfiguration(network), + }, + [`${id}-redis`]: { + image: config.redis.image, + container_name: `${id}-redis`, + volumes: [config.redis.volume], + ...defaultComposeConfiguration(network), + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + ...volumeMounts, + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + }, + [config.redis.volume.split(':')[0]]: { + name: config.redis.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) + await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) + + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} + +async function startSearXNGService(request: FastifyRequest) { + try { + const { id } = request.params; + const teamId = request.user.teamId; + const service = await getServiceFromDB({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn, searxng: { secretKey, redisPassword } } = + service; + const network = destinationDockerId && destinationDocker.network; + const port = getServiceMainPort('searxng'); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + + const config = { + searxng: { + image: `${image}:${version}`, + volume: `${id}-searxng:/etc/searxng`, + environmentVariables: { + SEARXNG_BASE_URL: `${fqdn}` + }, + }, + redis: { + image: 'redis:7-alpine', + } + }; + + const settingsYml = ` + # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings + use_default_settings: true + server: + secret_key: ${secretKey} + limiter: true + image_proxy: true + ui: + static_use_hash: true + redis: + url: redis://:${redisPassword}@${id}-redis:6379/0` + + const Dockerfile = ` + FROM ${config.searxng.image} + COPY ./settings.yml /etc/searxng/settings.yml`; + + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.searxng.environmentVariables[secret.name] = secret.value; + }); + } + const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) + const composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + build: workdir, + container_name: id, + volumes, + environment: config.searxng.environmentVariables, + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + labels: makeLabelForServices('searxng'), + cap_drop: ['ALL'], + cap_add: ['CHOWN', 'SETGID', 'SETUID', 'DAC_OVERRIDE'], + depends_on: [`${id}-redis`], + ...defaultComposeConfiguration(network), + }, + [`${id}-redis`]: { + container_name: `${id}-redis`, + image: config.redis.image, + command: `redis-server --requirepass ${redisPassword} --save "" --appendonly "no"`, + labels: makeLabelForServices('searxng'), + cap_drop: ['ALL'], + cap_add: ['SETGID', 'SETUID', 'DAC_OVERRIDE'], + ...defaultComposeConfiguration(network), + }, + }, + networks: { + [network]: { + external: true + } + }, + volumes: volumeMounts + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); + await fs.writeFile(`${workdir}/settings.yml`, settingsYml); + + await startServiceContainers(destinationDocker.id, composeFileDestination) + return {} + } catch ({ status, message }) { + return errorHandler({ status, message }) + } +} diff --git a/apps/api/src/lib/serviceFields.ts b/apps/api/src/lib/services/serviceFields.ts similarity index 100% rename from apps/api/src/lib/serviceFields.ts rename to apps/api/src/lib/services/serviceFields.ts diff --git a/apps/api/src/lib/services/supportedVersions.ts b/apps/api/src/lib/services/supportedVersions.ts new file mode 100644 index 000000000..b6f039695 --- /dev/null +++ b/apps/api/src/lib/services/supportedVersions.ts @@ -0,0 +1,193 @@ +export const supportedServiceTypesAndVersions = [ + { + name: 'plausibleanalytics', + fancyName: 'Plausible Analytics', + baseImage: 'plausible/analytics', + images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'], + versions: ['latest', 'stable'], + recommendedVersion: 'stable', + ports: { + main: 8000 + } + }, + { + name: 'nocodb', + fancyName: 'NocoDB', + baseImage: 'nocodb/nocodb', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8080 + } + }, + { + name: 'minio', + fancyName: 'MinIO', + baseImage: 'minio/minio', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 9001 + } + }, + { + name: 'vscodeserver', + fancyName: 'VSCode Server', + baseImage: 'codercom/code-server', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8080 + } + }, + { + name: 'wordpress', + fancyName: 'Wordpress', + baseImage: 'wordpress', + images: ['bitnami/mysql:5.7'], + versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], + recommendedVersion: 'latest', + ports: { + main: 80 + } + }, + { + name: 'vaultwarden', + fancyName: 'Vaultwarden', + baseImage: 'vaultwarden/server', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 80 + } + }, + { + name: 'languagetool', + fancyName: 'LanguageTool', + baseImage: 'silviof/docker-languagetool', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8010 + } + }, + { + name: 'n8n', + fancyName: 'n8n', + baseImage: 'n8nio/n8n', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 5678 + } + }, + { + name: 'uptimekuma', + fancyName: 'Uptime Kuma', + baseImage: 'louislam/uptime-kuma', + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 3001 + } + }, + { + name: 'ghost', + fancyName: 'Ghost', + baseImage: 'bitnami/ghost', + images: ['bitnami/mariadb'], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 2368 + } + }, + { + name: 'meilisearch', + fancyName: 'Meilisearch', + baseImage: 'getmeili/meilisearch', + images: [], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 7700 + } + }, + { + name: 'umami', + fancyName: 'Umami', + baseImage: 'ghcr.io/mikecao/umami', + images: ['postgres:12-alpine'], + versions: ['postgresql-latest'], + recommendedVersion: 'postgresql-latest', + ports: { + main: 3000 + } + }, + { + name: 'hasura', + fancyName: 'Hasura', + baseImage: 'hasura/graphql-engine', + images: ['postgres:12-alpine'], + versions: ['latest', 'v2.10.0', 'v2.5.1'], + recommendedVersion: 'v2.10.0', + ports: { + main: 8080 + } + }, + { + name: 'fider', + fancyName: 'Fider', + baseImage: 'getfider/fider', + images: ['postgres:12-alpine'], + versions: ['stable'], + recommendedVersion: 'stable', + ports: { + main: 3000 + } + }, + { + name: 'appwrite', + fancyName: 'Appwrite', + baseImage: 'appwrite/appwrite', + images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], + versions: ['latest', '0.15.3'], + recommendedVersion: '0.15.3', + ports: { + main: 80 + } + }, + // { + // name: 'moodle', + // fancyName: 'Moodle', + // baseImage: 'bitnami/moodle', + // images: [], + // versions: ['latest', 'v4.0.2'], + // recommendedVersion: 'latest', + // ports: { + // main: 8080 + // } + // } + { + name: 'glitchTip', + fancyName: 'GlitchTip', + baseImage: 'glitchtip/glitchtip', + images: ['postgres:14-alpine', 'redis:7-alpine'], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8000 + } + }, + { + name: 'searxng', + fancyName: 'SearXNG', + baseImage: 'searxng/searxng', + images: [], + versions: ['latest'], + recommendedVersion: 'latest', + ports: { + main: 8080 + } + }, +]; \ No newline at end of file diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index f5946c447..f4a908b86 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -5,7 +5,7 @@ import compare from 'compare-versions'; import cuid from 'cuid'; import bcrypt from 'bcryptjs'; import { asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, isDev, listSettings, prisma, uniqueName, version } from '../../../lib/common'; - +import { supportedServiceTypesAndVersions } from '../../../lib/services/supportedVersions'; import type { FastifyReply, FastifyRequest } from 'fastify'; import type { Login, Update } from '.'; import type { GetCurrentUser } from './types'; @@ -300,6 +300,7 @@ export async function getCurrentUser(request: FastifyRequest, fa } return { settings: await prisma.setting.findFirst(), + supportedServiceTypesAndVersions, token, ...request.user } diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index c2a23e5de..8691f2b41 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; -import { prisma, uniqueName, asyncExecShell, getServiceImage, configureServiceType, getServiceFromDB, getContainerUsage, removeService, isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, supportedServiceTypesAndVersions, executeDockerCmd, listSettings, getFreeExposedPort, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common'; +import { prisma, uniqueName, asyncExecShell, getServiceImage, getServiceFromDB, getContainerUsage,isDomainConfigured, saveUpdateableFields, fixType, decrypt, encrypt, getServiceMainPort, createDirectories, ComposeFile, makeLabelForServices, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, persistentVolumes, asyncSleep, isARM, defaultComposeConfiguration, checkExposedPort } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, isContainerExited, removeContainer } from '../../../../lib/docker'; import cuid from 'cuid'; @@ -10,137 +10,8 @@ import cuid from 'cuid'; import type { OnlyId } from '../../../../types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; import { defaultServiceConfigurations } from '../../../../lib/services'; - -// async function startServiceNew(request: FastifyRequest) { -// try { -// const { id } = request.params; -// const teamId = request.user.teamId; -// const service = await getServiceFromDB({ id, teamId }); -// const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort } = -// service; -// const network = destinationDockerId && destinationDocker.network; -// const host = getEngine(destinationDocker.engine); -// const port = getServiceMainPort(type); - -// const { workdir } = await createDirectories({ repository: type, buildId: id }); -// const image = getServiceImage(type); -// const config = (await getAvailableServices()).find((name) => name.name === type).compose -// const environmentVariables = {} -// if (serviceSecret.length > 0) { -// serviceSecret.forEach((secret) => { -// environmentVariables[secret.name] = secret.value; -// }); -// } -// config.newVolumes = {} -// for (const service of Object.entries(config.services)) { -// const name = service[0] -// const details: any = service[1] -// config.services[`${id}-${name}`] = JSON.parse(JSON.stringify(details)) -// config.services[`${id}-${name}`].container_name = `${id}-${name}` -// config.services[`${id}-${name}`].restart = "always" -// config.services[`${id}-${name}`].networks = [network] -// config.services[`${id}-${name}`].labels = makeLabelForServices(type) -// if (name === config.name) { -// config.services[`${id}-${name}`].image = `${details.image.split(':')[0]}:${version}` -// config.services[`${id}-${name}`].ports = (exposePort ? [`${exposePort}:${port}`] : []) -// config.services[`${id}-${name}`].environment = environmentVariables -// } -// config.services[`${id}-${name}`].deploy = { -// restart_policy: { -// condition: 'on-failure', -// delay: '5s', -// max_attempts: 3, -// window: '120s' -// } -// } -// if (config.services[`${id}-${name}`]?.volumes?.length > 0) { -// config.services[`${id}-${name}`].volumes.forEach((volume, index) => { -// let oldVolumeName = volume.split(':')[0] -// const path = volume.split(':')[1] -// // if (config?.volumes[oldVolumeName]) delete config?.volumes[oldVolumeName] -// const newName = convertTolOldVolumeNames(type) -// if (newName) oldVolumeName = newName - -// const volumeName = `${id}-${oldVolumeName}` -// config.newVolumes[volumeName] = { -// name: volumeName -// } -// config.services[`${id}-${name}`].volumes[index] = `${volumeName}:${path}` -// }) -// config.services[`${id}-${config.name}`] = { -// ...config.services[`${id}-${config.name}`], -// environment: environmentVariables -// } -// } -// config.networks = { -// [network]: { -// external: true -// } -// } - -// config.volumes = config.newVolumes - -// // config.services[`${id}-${name}`]?.volumes?.length > 0 && config.services[`${id}-${name}`].volumes.forEach((volume, index) => { -// // let oldVolumeName = volume.split(':')[0] -// // const path = volume.split(':')[1] -// // oldVolumeName = convertTolOldVolumeNames(type) -// // const volumeName = `${id}-${oldVolumeName}` -// // config.volumes[volumeName] = { -// // name: volumeName -// // } -// // config.services[`${id}-${name}`].volumes[index] = `${volumeName}:${path}` -// // }) -// // config.services[`${id}-${config.name}`] = { -// // ...config.services[`${id}-${config.name}`], -// // environment: environmentVariables -// // } -// delete config.services[name] - -// } -// console.log(config.services) -// console.log(config.volumes) - -// // config.services[id] = JSON.parse(JSON.stringify(config.services[type])) -// // config.services[id].container_name = id -// // config.services[id].image = `${image}:${version}` -// // config.services[id].ports = (exposePort ? [`${exposePort}:${port}`] : []), -// // config.services[id].restart = "always" -// // config.services[id].networks = [network] -// // config.services[id].labels = makeLabelForServices(type) -// // config.services[id].deploy = { -// // restart_policy: { -// // condition: 'on-failure', -// // delay: '5s', -// // max_attempts: 3, -// // window: '120s' -// // } -// // } -// // config.networks = { -// // [network]: { -// // external: true -// // } -// // } -// // config.volumes = {} -// // config.services[id].volumes.forEach((volume, index) => { -// // let oldVolumeName = volume.split(':')[0] -// // const path = volume.split(':')[1] -// // oldVolumeName = convertTolOldVolumeNames(type) -// // const volumeName = `${id}-${oldVolumeName}` -// // config.volumes[volumeName] = { -// // name: volumeName -// // } -// // config.services[id].volumes[index] = `${volumeName}:${path}` -// // }) -// // delete config.services[type] -// // config.services[id].environment = environmentVariables -// const composeFileDestination = `${workdir}/docker-compose.yaml`; -// // await fs.writeFile(composeFileDestination, yaml.dump(config)); -// await startServiceContainers(destinationDocker.id, composeFileDestination) -// return {} -// } catch ({ status, message }) { -// return errorHandler({ status, message }) -// } -// } +import { supportedServiceTypesAndVersions } from '../../../../lib/services/supportedVersions'; +import { configureServiceType, removeService } from '../../../../lib/services/common'; export async function listServices(request: FastifyRequest) { try { @@ -529,75 +400,6 @@ export async function deleteServiceStorage(request: FastifyRequest) { - try { - const { type } = request.params - if (type === 'plausibleanalytics') { - return await startPlausibleAnalyticsService(request) - } - if (type === 'nocodb') { - return await startNocodbService(request) - } - if (type === 'minio') { - return await startMinioService(request) - } - if (type === 'vscodeserver') { - return await startVscodeService(request) - } - if (type === 'wordpress') { - return await startWordpressService(request) - } - if (type === 'vaultwarden') { - return await startVaultwardenService(request) - } - if (type === 'languagetool') { - return await startLanguageToolService(request) - } - if (type === 'n8n') { - return await startN8nService(request) - } - if (type === 'uptimekuma') { - return await startUptimekumaService(request) - } - if (type === 'ghost') { - return await startGhostService(request) - } - if (type === 'meilisearch') { - return await startMeilisearchService(request) - } - if (type === 'umami') { - return await startUmamiService(request) - } - if (type === 'hasura') { - return await startHasuraService(request) - } - if (type === 'fider') { - return await startFiderService(request) - } - if (type === 'moodle') { - return await startMoodleService(request) - } - if (type === 'appwrite') { - return await startAppWriteService(request) - } - if (type === 'glitchTip') { - return await startGlitchTipService(request) - } - if (type === 'searxng') { - return await startSearXNGService(request) - } - throw `Service type ${type} not supported.` - } catch (error) { - throw { status: 500, message: error?.message || error } - } -} -export async function stopService(request: FastifyRequest) { - try { - return await stopServiceContainers(request) - } catch (error) { - throw { status: 500, message: error?.message || error } - } -} export async function setSettingsService(request: FastifyRequest, reply: FastifyReply) { try { const { type } = request.params @@ -623,2154 +425,6 @@ async function setWordpressSettings(request: FastifyRequest) { - try { - const { id } = request.params - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - plausibleAnalytics: { - id: plausibleDbId, - username, - email, - password, - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - secretKeyBase - } - } = service; - const image = getServiceImage(type); - - const config = { - plausibleAnalytics: { - image: `${image}:${version}`, - environmentVariables: { - ADMIN_USER_EMAIL: email, - ADMIN_USER_NAME: username, - ADMIN_USER_PWD: password, - BASE_URL: fqdn, - SECRET_KEY_BASE: secretKeyBase, - DISABLE_AUTH: 'false', - DISABLE_REGISTRATION: 'true', - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, - CLICKHOUSE_DATABASE_URL: `http://${id}-clickhouse:8123/plausible` - } - }, - postgresql: { - volume: `${plausibleDbId}-postgresql-data:/bitnami/postgresql/`, - image: 'bitnami/postgresql:13.2.0', - environmentVariables: { - POSTGRESQL_PASSWORD: postgresqlPassword, - POSTGRESQL_USERNAME: postgresqlUser, - POSTGRESQL_DATABASE: postgresqlDatabase - } - }, - clickhouse: { - volume: `${plausibleDbId}-clickhouse-data:/var/lib/clickhouse`, - image: 'yandex/clickhouse-server:21.3.2.5', - environmentVariables: {}, - ulimits: { - nofile: { - soft: 262144, - hard: 262144 - } - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.plausibleAnalytics.environmentVariables[secret.name] = secret.value; - }); - } - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('plausibleanalytics'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - - const clickhouseConfigXml = ` - - - warning - true - - - - - - - - - - - - `; - const clickhouseUserConfigXml = ` - - - - 0 - 0 - - - `; - - const initQuery = 'CREATE DATABASE IF NOT EXISTS plausible;'; - const initScript = 'clickhouse client --queries-file /docker-entrypoint-initdb.d/init.query'; - await fs.writeFile(`${workdir}/clickhouse-config.xml`, clickhouseConfigXml); - await fs.writeFile(`${workdir}/clickhouse-user-config.xml`, clickhouseUserConfigXml); - await fs.writeFile(`${workdir}/init.query`, initQuery); - await fs.writeFile(`${workdir}/init-db.sh`, initScript); - - const Dockerfile = ` -FROM ${config.clickhouse.image} -COPY ./clickhouse-config.xml /etc/clickhouse-server/users.d/logging.xml -COPY ./clickhouse-user-config.xml /etc/clickhouse-server/config.d/logging.xml -COPY ./init.query /docker-entrypoint-initdb.d/init.query -COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; - - await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.plausibleAnalytics) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.plausibleAnalytics.image, - volumes, - command: - 'sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh db init-admin && /entrypoint.sh run"', - environment: config.plausibleAnalytics.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`, `${id}-clickhouse`], - labels: makeLabelForServices('plausibleAnalytics'), - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - container_name: `${id}-postgresql`, - image: config.postgresql.image, - environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], - ...defaultComposeConfiguration(network), - }, - [`${id}-clickhouse`]: { - build: workdir, - container_name: `${id}-clickhouse`, - environment: config.clickhouse.environmentVariables, - volumes: [config.clickhouse.volume], - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - }, - [config.clickhouse.volume.split(':')[0]]: { - name: config.clickhouse.volume.split(':')[0] - } - } - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startNocodbService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('nocodb'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-nc:/usr/app/data`, - environmentVariables: {} - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - volumes, - environment: config.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('nocodb'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startMinioService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - persistentStorage, - exposePort, - minio: { rootUser, rootUserPassword }, - serviceSecret - } = service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('minio'); - - const { service: { destinationDocker: { id: dockerId } } } = await prisma.minio.findUnique({ where: { serviceId: id }, include: { service: { include: { destinationDocker: true } } } }) - const publicPort = await getFreePublicPort(id, dockerId); - - const consolePort = 9001; - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-minio-data:/data`, - environmentVariables: { - MINIO_ROOT_USER: rootUser, - MINIO_ROOT_PASSWORD: rootUserPassword, - MINIO_BROWSER_REDIRECT_URL: fqdn - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - command: `server /data --console-address ":${consolePort}"`, - environment: config.environmentVariables, - volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('minio'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startVscodeService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - vscodeserver: { password } - } = service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('vscodeserver'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-vscodeserver-data:/home/coder`, - environmentVariables: { - PASSWORD: password - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - environment: config.environmentVariables, - volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('vscodeServer'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - const changePermissionOn = persistentStorage.map((p) => p.path); - if (changePermissionOn.length > 0) { - await executeDockerCmd({ - dockerId: destinationDocker.id, command: `docker exec -u root ${id} chown -R 1000:1000 ${changePermissionOn.join( - ' ' - )}` - }) - } - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startWordpressService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - arch, - type, - version, - destinationDockerId, - serviceSecret, - destinationDocker, - persistentStorage, - exposePort, - wordpress: { - mysqlDatabase, - mysqlHost, - mysqlPort, - mysqlUser, - mysqlPassword, - extraConfig, - mysqlRootUser, - mysqlRootUserPassword, - ownMysql - } - } = service; - - const network = destinationDockerId && destinationDocker.network; - const image = getServiceImage(type); - const port = getServiceMainPort('wordpress'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const config = { - wordpress: { - image: `${image}:${version}`, - volume: `${id}-wordpress-data:/var/www/html`, - environmentVariables: { - WORDPRESS_DB_HOST: ownMysql ? `${mysqlHost}:${mysqlPort}` : `${id}-mysql`, - WORDPRESS_DB_USER: mysqlUser, - WORDPRESS_DB_PASSWORD: mysqlPassword, - WORDPRESS_DB_NAME: mysqlDatabase, - WORDPRESS_CONFIG_EXTRA: extraConfig - } - }, - mysql: { - image: `bitnami/mysql:5.7`, - volume: `${id}-mysql-data:/bitnami/mysql/data`, - environmentVariables: { - MYSQL_ROOT_PASSWORD: mysqlRootUserPassword, - MYSQL_ROOT_USER: mysqlRootUser, - MYSQL_USER: mysqlUser, - MYSQL_PASSWORD: mysqlPassword, - MYSQL_DATABASE: mysqlDatabase - } - } - }; - if (isARM(arch)) { - config.mysql.image = 'mysql:5.7' - config.mysql.volume = `${id}-mysql-data:/var/lib/mysql` - } - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.wordpress.environmentVariables[secret.name] = secret.value; - }); - } - - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.wordpress) - - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.wordpress.image, - environment: config.wordpress.environmentVariables, - volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('wordpress'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - if (!ownMysql) { - composeFile.services[id].depends_on = [`${id}-mysql`]; - composeFile.services[`${id}-mysql`] = { - container_name: `${id}-mysql`, - image: config.mysql.image, - volumes: [config.mysql.volume], - environment: config.mysql.environmentVariables, - ...defaultComposeConfiguration(network), - }; - - composeFile.volumes[config.mysql.volume.split(':')[0]] = { - name: config.mysql.volume.split(':')[0] - }; - } - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startVaultwardenService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('vaultwarden'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-vaultwarden-data:/data/`, - environmentVariables: {} - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - environment: config.environmentVariables, - volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('vaultWarden'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startLanguageToolService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('languagetool'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-ngrams:/ngrams`, - environmentVariables: {} - }; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - environment: config.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes, - labels: makeLabelForServices('languagetool'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startN8nService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('n8n'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-n8n:/root/.n8n`, - environmentVariables: { - WEBHOOK_URL: `${service.fqdn}` - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - volumes, - environment: config.environmentVariables, - labels: makeLabelForServices('n8n'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startUptimekumaService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('uptimekuma'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-uptimekuma:/app/data`, - environmentVariables: {} - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - volumes, - environment: config.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('uptimekuma'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startGhostService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - fqdn, - ghost: { - defaultEmail, - defaultPassword, - mariadbRootUser, - mariadbRootUserPassword, - mariadbDatabase, - mariadbPassword, - mariadbUser - } - } = service; - const network = destinationDockerId && destinationDocker.network; - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - const domain = getDomain(fqdn); - const port = getServiceMainPort('ghost'); - const isHttps = fqdn.startsWith('https://'); - const config = { - ghost: { - image: `${image}:${version}`, - volume: `${id}-ghost:/bitnami/ghost`, - environmentVariables: { - url: fqdn, - GHOST_HOST: domain, - GHOST_ENABLE_HTTPS: isHttps ? 'yes' : 'no', - GHOST_EMAIL: defaultEmail, - GHOST_PASSWORD: defaultPassword, - GHOST_DATABASE_HOST: `${id}-mariadb`, - GHOST_DATABASE_USER: mariadbUser, - GHOST_DATABASE_PASSWORD: mariadbPassword, - GHOST_DATABASE_NAME: mariadbDatabase, - GHOST_DATABASE_PORT_NUMBER: 3306 - } - }, - mariadb: { - image: `bitnami/mariadb:latest`, - volume: `${id}-mariadb:/bitnami/mariadb`, - environmentVariables: { - MARIADB_USER: mariadbUser, - MARIADB_PASSWORD: mariadbPassword, - MARIADB_DATABASE: mariadbDatabase, - MARIADB_ROOT_USER: mariadbRootUser, - MARIADB_ROOT_PASSWORD: mariadbRootUserPassword - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.ghost.environmentVariables[secret.name] = secret.value; - }); - } - - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.ghost) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.ghost.image, - volumes, - environment: config.ghost.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('ghost'), - depends_on: [`${id}-mariadb`], - ...defaultComposeConfiguration(network), - }, - [`${id}-mariadb`]: { - container_name: `${id}-mariadb`, - image: config.mariadb.image, - volumes: [config.mariadb.volume], - environment: config.mariadb.environmentVariables, - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.mariadb.volume.split(':')[0]]: { - name: config.mariadb.volume.split(':')[0] - } - } - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startMeilisearchService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - meiliSearch: { masterKey } - } = service; - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('meilisearch'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - image: `${image}:${version}`, - volume: `${id}-datams:/data.ms`, - environmentVariables: { - MEILI_MASTER_KEY: masterKey - } - }; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.image, - environment: config.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - volumes, - labels: makeLabelForServices('meilisearch'), - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startUmamiService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - umami: { - umamiAdminPassword, - postgresqlUser, - postgresqlPassword, - postgresqlDatabase, - hashSalt - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('umami'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - umami: { - image: `${image}:${version}`, - environmentVariables: { - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, - DATABASE_TYPE: 'postgresql', - HASH_SALT: hashSalt - } - }, - postgresql: { - image: 'postgres:12-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.umami.environmentVariables[secret.name] = secret.value; - }); - } - - const initDbSQL = ` - drop table if exists event; - drop table if exists pageview; - drop table if exists session; - drop table if exists website; - drop table if exists account; - - create table account ( - user_id serial primary key, - username varchar(255) unique not null, - password varchar(60) not null, - is_admin bool not null default false, - created_at timestamp with time zone default current_timestamp, - updated_at timestamp with time zone default current_timestamp - ); - - create table website ( - website_id serial primary key, - website_uuid uuid unique not null, - user_id int not null references account(user_id) on delete cascade, - name varchar(100) not null, - domain varchar(500), - share_id varchar(64) unique, - created_at timestamp with time zone default current_timestamp - ); - - create table session ( - session_id serial primary key, - session_uuid uuid unique not null, - website_id int not null references website(website_id) on delete cascade, - created_at timestamp with time zone default current_timestamp, - hostname varchar(100), - browser varchar(20), - os varchar(20), - device varchar(20), - screen varchar(11), - language varchar(35), - country char(2) - ); - - create table pageview ( - view_id serial primary key, - website_id int not null references website(website_id) on delete cascade, - session_id int not null references session(session_id) on delete cascade, - created_at timestamp with time zone default current_timestamp, - url varchar(500) not null, - referrer varchar(500) - ); - - create table event ( - event_id serial primary key, - website_id int not null references website(website_id) on delete cascade, - session_id int not null references session(session_id) on delete cascade, - created_at timestamp with time zone default current_timestamp, - url varchar(500) not null, - event_type varchar(50) not null, - event_value varchar(50) not null - ); - - create index website_user_id_idx on website(user_id); - - create index session_created_at_idx on session(created_at); - create index session_website_id_idx on session(website_id); - - create index pageview_created_at_idx on pageview(created_at); - create index pageview_website_id_idx on pageview(website_id); - create index pageview_session_id_idx on pageview(session_id); - create index pageview_website_id_created_at_idx on pageview(website_id, created_at); - create index pageview_website_id_session_id_created_at_idx on pageview(website_id, session_id, created_at); - - create index event_created_at_idx on event(created_at); - create index event_website_id_idx on event(website_id); - create index event_session_id_idx on event(session_id); - - insert into account (username, password, is_admin) values ('admin', '${bcrypt.hashSync( - umamiAdminPassword, - 10 - )}', true);`; - await fs.writeFile(`${workdir}/schema.postgresql.sql`, initDbSQL); - const Dockerfile = ` - FROM ${config.postgresql.image} - COPY ./schema.postgresql.sql /docker-entrypoint-initdb.d/schema.postgresql.sql`; - await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.umami) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.umami.image, - environment: config.umami.environmentVariables, - volumes, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('umami'), - depends_on: [`${id}-postgresql`], - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - build: workdir, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - } - } - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startHasuraService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - destinationDockerId, - destinationDocker, - persistentStorage, - serviceSecret, - exposePort, - hasura: { postgresqlUser, postgresqlPassword, postgresqlDatabase } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('hasura'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - hasura: { - image: `${image}:${version}`, - environmentVariables: { - HASURA_GRAPHQL_METADATA_DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}` - } - }, - postgresql: { - image: 'postgres:12-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.hasura.environmentVariables[secret.name] = secret.value; - }); - } - - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.hasura) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.hasura.image, - environment: config.hasura.environmentVariables, - volumes, - labels: makeLabelForServices('hasura'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`], - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - image: config.postgresql.image, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - } - } - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startFiderService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - fider: { - postgresqlUser, - postgresqlPassword, - postgresqlDatabase, - jwtSecret, - emailNoreply, - emailMailgunApiKey, - emailMailgunDomain, - emailMailgunRegion, - emailSmtpHost, - emailSmtpPort, - emailSmtpUser, - emailSmtpPassword, - emailSmtpEnableStartTls - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('fider'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - const config = { - fider: { - image: `${image}:${version}`, - environmentVariables: { - BASE_URL: fqdn, - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}?sslmode=disable`, - JWT_SECRET: `${jwtSecret.replace(/\$/g, '$$$')}`, - EMAIL_NOREPLY: emailNoreply, - EMAIL_MAILGUN_API: emailMailgunApiKey, - EMAIL_MAILGUN_REGION: emailMailgunRegion, - EMAIL_MAILGUN_DOMAIN: emailMailgunDomain, - EMAIL_SMTP_HOST: emailSmtpHost, - EMAIL_SMTP_PORT: emailSmtpPort, - EMAIL_SMTP_USER: emailSmtpUser, - EMAIL_SMTP_PASSWORD: emailSmtpPassword, - EMAIL_SMTP_ENABLE_STARTTLS: emailSmtpEnableStartTls - } - }, - postgresql: { - image: 'postgres:12-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.fider.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.fider) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.fider.image, - environment: config.fider.environmentVariables, - volumes, - labels: makeLabelForServices('fider'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`], - ...defaultComposeConfiguration(network), - }, - [`${id}-postgresql`]: { - image: config.postgresql.image, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - } - } - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startAppWriteService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const { version, fqdn, destinationDocker, secrets, exposePort, network, port, workdir, image, appwrite } = await defaultServiceConfigurations({ id, teamId }) - - let isStatsEnabled = false - if (secrets.find(s => s === '_APP_USAGE_STATS=enabled')) { - isStatsEnabled = true - } - const { - opensslKeyV1, - executorSecret, - mariadbHost, - mariadbPort, - mariadbUser, - mariadbPassword, - mariadbRootUser, - mariadbRootUserPassword, - mariadbDatabase - } = appwrite; - - const dockerCompose = { - [id]: { - image: `${image}:${version}`, - container_name: id, - labels: makeLabelForServices('appwrite'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - "volumes": [ - `${id}-uploads:/storage/uploads:rw`, - `${id}-cache:/storage/cache:rw`, - `${id}-config:/storage/config:rw`, - `${id}-certificates:/storage/certificates:rw`, - `${id}-functions:/storage/functions:rw` - ], - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - "_APP_LOCALE=en", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DOMAIN=${fqdn}`, - `_APP_DOMAIN_TARGET=${fqdn}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - `_APP_STATSD_HOST=${id}-telegraf`, - "_APP_STATSD_PORT=8125", - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-realtime`]: { - image: `${image}:${version}`, - container_name: `${id}-realtime`, - entrypoint: "realtime", - labels: makeLabelForServices('appwrite'), - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-audits`]: { - - image: `${image}:${version}`, - container_name: `${id}-worker-audits`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-audits", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-webhooks`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-webhooks`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-webhooks", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-deletes`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-deletes`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-deletes", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "volumes": [ - `${id}-uploads:/storage/uploads:rw`, - `${id}-cache:/storage/cache:rw`, - `${id}-config:/storage/config:rw`, - `${id}-certificates:/storage/certificates:rw`, - `${id}-functions:/storage/functions:rw`, - `${id}-builds:/storage/builds:rw`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-databases`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-databases`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-databases", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-builds`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-builds`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-builds", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-certificates`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-certificates`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-certificates", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - ], - "volumes": [ - `${id}-config:/storage/config:rw`, - `${id}-certificates:/storage/certificates:rw`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DOMAIN=${fqdn}`, - `_APP_DOMAIN_TARGET=${fqdn}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-functions`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-functions`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-functions", - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - `${id}-executor` - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_EXECUTOR_SECRET=${executorSecret}`, - `_APP_EXECUTOR_HOST=http://${id}-executor/v1`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-executor`]: { - image: `${image}:${version}`, - container_name: `${id}-executor`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "executor", - "stop_signal": "SIGINT", - "volumes": [ - `${id}-functions:/storage/functions:rw`, - `${id}-builds:/storage/builds:rw`, - "/var/run/docker.sock:/var/run/docker.sock", - "/tmp:/tmp:rw" - ], - "depends_on": [ - `${id}-mariadb`, - `${id}-redis`, - `${id}` - ], - "environment": [ - "_APP_ENV=production", - `_APP_EXECUTOR_SECRET=${executorSecret}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-mails`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-mails`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-mails", - "depends_on": [ - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker-messaging`]: { - image: `${image}:${version}`, - container_name: `${id}-worker-messaging`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "worker-messaging", - "depends_on": [ - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-maintenance`]: { - image: `${image}:${version}`, - container_name: `${id}-maintenance`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "maintenance", - "depends_on": [ - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DOMAIN=${fqdn}`, - `_APP_DOMAIN_TARGET=${fqdn}`, - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-schedule`]: { - image: `${image}:${version}`, - container_name: `${id}-schedule`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "schedule", - "depends_on": [ - `${id}-redis`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - ...secrets - ], - ...defaultComposeConfiguration(network), - }, - [`${id}-mariadb`]: { - "image": "mariadb:10.7", - container_name: `${id}-mariadb`, - labels: makeLabelForServices('appwrite'), - "volumes": [ - `${id}-mariadb:/var/lib/mysql:rw` - ], - "environment": [ - `MYSQL_ROOT_USER=${mariadbRootUser}`, - `MYSQL_ROOT_PASSWORD=${mariadbRootUserPassword}`, - `MYSQL_USER=${mariadbUser}`, - `MYSQL_PASSWORD=${mariadbPassword}`, - `MYSQL_DATABASE=${mariadbDatabase}` - ], - "command": "mysqld --innodb-flush-method=fsync", - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - "image": "redis:6.2-alpine", - container_name: `${id}-redis`, - "command": `redis-server --maxmemory 512mb --maxmemory-policy allkeys-lru --maxmemory-samples 5\n`, - "volumes": [ - `${id}-redis:/data:rw` - ], - ...defaultComposeConfiguration(network), - }, - - }; - if (isStatsEnabled) { - dockerCompose[id].depends_on.push(`${id}-influxdb`); - dockerCompose[`${id}-usage`] = { - image: `${image}:${version}`, - container_name: `${id}-usage`, - labels: makeLabelForServices('appwrite'), - "entrypoint": "usage", - "depends_on": [ - `${id}-mariadb`, - `${id}-influxdb`, - ], - "environment": [ - "_APP_ENV=production", - `_APP_OPENSSL_KEY_V1=${opensslKeyV1}`, - `_APP_DB_HOST=${mariadbHost}`, - `_APP_DB_PORT=${mariadbPort}`, - `_APP_DB_SCHEMA=${mariadbDatabase}`, - `_APP_DB_USER=${mariadbUser}`, - `_APP_DB_PASS=${mariadbPassword}`, - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - `_APP_REDIS_HOST=${id}-redis`, - "_APP_REDIS_PORT=6379", - ...secrets - ], - ...defaultComposeConfiguration(network), - } - dockerCompose[`${id}-influxdb`] = { - "image": "appwrite/influxdb:1.5.0", - container_name: `${id}-influxdb`, - "volumes": [ - `${id}-influxdb:/var/lib/influxdb:rw` - ], - ...defaultComposeConfiguration(network), - } - dockerCompose[`${id}-telegraf`] = { - "image": "appwrite/telegraf:1.4.0", - container_name: `${id}-telegraf`, - "environment": [ - `_APP_INFLUXDB_HOST=${id}-influxdb`, - "_APP_INFLUXDB_PORT=8086", - ], - ...defaultComposeConfiguration(network), - } - } - - const composeFile: any = { - version: '3.8', - services: dockerCompose, - networks: { - [network]: { - external: true - } - }, - volumes: { - [`${id}-uploads`]: { - name: `${id}-uploads` - }, - [`${id}-cache`]: { - name: `${id}-cache` - }, - [`${id}-config`]: { - name: `${id}-config` - }, - [`${id}-certificates`]: { - name: `${id}-certificates` - }, - [`${id}-functions`]: { - name: `${id}-functions` - }, - [`${id}-builds`]: { - name: `${id}-builds` - }, - [`${id}-mariadb`]: { - name: `${id}-mariadb` - }, - [`${id}-redis`]: { - name: `${id}-redis` - }, - [`${id}-influxdb`]: { - name: `${id}-influxdb` - } - } - - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function startServiceContainers(dockerId, composeFileDestination) { - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) - await asyncSleep(1000); - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) -} -async function stopServiceContainers(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const { destinationDockerId } = await getServiceFromDB({ id, teamId }); - if (destinationDockerId) { - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -n 1 docker rm --force` - }) - return {} - } - throw { status: 500, message: 'Could not stop containers.' } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} -async function startMoodleService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - moodle: { - defaultUsername, - defaultPassword, - defaultEmail, - mariadbRootUser, - mariadbRootUserPassword, - mariadbDatabase, - mariadbPassword, - mariadbUser - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('moodle'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - const config = { - moodle: { - image: `${image}:${version}`, - volume: `${id}-data:/bitnami/moodle`, - environmentVariables: { - MOODLE_USERNAME: defaultUsername, - MOODLE_PASSWORD: defaultPassword, - MOODLE_EMAIL: defaultEmail, - MOODLE_DATABASE_HOST: `${id}-mariadb`, - MOODLE_DATABASE_USER: mariadbUser, - MOODLE_DATABASE_PASSWORD: mariadbPassword, - MOODLE_DATABASE_NAME: mariadbDatabase, - MOODLE_REVERSEPROXY: 'yes' - } - }, - mariadb: { - image: 'bitnami/mariadb:latest', - volume: `${id}-mariadb-data:/bitnami/mariadb`, - environmentVariables: { - MARIADB_USER: mariadbUser, - MARIADB_PASSWORD: mariadbPassword, - MARIADB_DATABASE: mariadbDatabase, - MARIADB_ROOT_USER: mariadbRootUser, - MARIADB_ROOT_PASSWORD: mariadbRootUserPassword - } - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.moodle.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.moodle) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.moodle.image, - environment: config.moodle.environmentVariables, - networks: [network], - volumes, - restart: 'always', - labels: makeLabelForServices('moodle'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, - depends_on: [`${id}-mariadb`] - }, - [`${id}-mariadb`]: { - container_name: `${id}-mariadb`, - image: config.mariadb.image, - environment: config.mariadb.environmentVariables, - networks: [network], - volumes: [], - restart: 'always', - deploy: { - restart_policy: { - condition: 'on-failure', - delay: '5s', - max_attempts: 3, - window: '120s' - } - }, - depends_on: [] - } - - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.mariadb.volume.split(':')[0]]: { - name: config.mariadb.volume.split(':')[0] - } - } - - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startGlitchTipService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { - type, - version, - fqdn, - destinationDockerId, - destinationDocker, - serviceSecret, - persistentStorage, - exposePort, - glitchTip: { - postgresqlDatabase, - postgresqlPassword, - postgresqlUser, - secretKeyBase, - defaultEmail, - defaultUsername, - defaultPassword, - defaultFromEmail, - emailSmtpHost, - emailSmtpPort, - emailSmtpUser, - emailSmtpPassword, - emailSmtpUseTls, - emailSmtpUseSsl, - emailBackend, - mailgunApiKey, - sendgridApiKey, - enableOpenUserRegistration, - } - } = service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('glitchTip'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - glitchTip: { - image: `${image}:${version}`, - environmentVariables: { - PORT: port, - GLITCHTIP_DOMAIN: fqdn, - SECRET_KEY: secretKeyBase, - DATABASE_URL: `postgresql://${postgresqlUser}:${postgresqlPassword}@${id}-postgresql:5432/${postgresqlDatabase}`, - REDIS_URL: `redis://${id}-redis:6379/0`, - DEFAULT_FROM_EMAIL: defaultFromEmail, - EMAIL_HOST: emailSmtpHost, - EMAIL_PORT: emailSmtpPort, - EMAIL_HOST_USER: emailSmtpUser, - EMAIL_HOST_PASSWORD: emailSmtpPassword, - EMAIL_USE_TLS: emailSmtpUseTls, - EMAIL_USE_SSL: emailSmtpUseSsl, - EMAIL_BACKEND: emailBackend, - MAILGUN_API_KEY: mailgunApiKey, - SENDGRID_API_KEY: sendgridApiKey, - ENABLE_OPEN_USER_REGISTRATION: enableOpenUserRegistration, - DJANGO_SUPERUSER_EMAIL: defaultEmail, - DJANGO_SUPERUSER_USERNAME: defaultUsername, - DJANGO_SUPERUSER_PASSWORD: defaultPassword, - } - }, - postgresql: { - image: 'postgres:14-alpine', - volume: `${id}-postgresql-data:/var/lib/postgresql/data`, - environmentVariables: { - POSTGRES_USER: postgresqlUser, - POSTGRES_PASSWORD: postgresqlPassword, - POSTGRES_DB: postgresqlDatabase - } - }, - redis: { - image: 'redis:7-alpine', - volume: `${id}-redis-data:/data`, - } - }; - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.glitchTip.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config.glitchTip) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - container_name: id, - image: config.glitchTip.image, - environment: config.glitchTip.environmentVariables, - volumes, - labels: makeLabelForServices('glitchTip'), - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - depends_on: [`${id}-postgresql`, `${id}-redis`], - ...defaultComposeConfiguration(network), - }, - [`${id}-worker`]: { - container_name: `${id}-worker`, - image: config.glitchTip.image, - command: './bin/run-celery-with-beat.sh', - environment: config.glitchTip.environmentVariables, - depends_on: [`${id}-postgresql`, `${id}-redis`], - ...defaultComposeConfiguration(network), - }, - [`${id}-setup`]: { - container_name: `${id}-setup`, - image: config.glitchTip.image, - command: 'sh -c "(./manage.py migrate || true) && (./manage.py createsuperuser --noinput || true)"', - environment: config.glitchTip.environmentVariables, - networks: [network], - restart: "no", - depends_on: [`${id}-postgresql`, `${id}-redis`] - }, - [`${id}-postgresql`]: { - image: config.postgresql.image, - container_name: `${id}-postgresql`, - environment: config.postgresql.environmentVariables, - volumes: [config.postgresql.volume], - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - image: config.redis.image, - container_name: `${id}-redis`, - volumes: [config.redis.volume], - ...defaultComposeConfiguration(network), - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: { - ...volumeMounts, - [config.postgresql.volume.split(':')[0]]: { - name: config.postgresql.volume.split(':')[0] - }, - [config.redis.volume.split(':')[0]]: { - name: config.redis.volume.split(':')[0] - } - } - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} pull` }) - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up --build -d` }) - - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} - -async function startSearXNGService(request: FastifyRequest) { - try { - const { id } = request.params; - const teamId = request.user.teamId; - const service = await getServiceFromDB({ id, teamId }); - const { type, version, destinationDockerId, destinationDocker, serviceSecret, exposePort, persistentStorage, fqdn, searxng: { secretKey, redisPassword } } = - service; - const network = destinationDockerId && destinationDocker.network; - const port = getServiceMainPort('searxng'); - - const { workdir } = await createDirectories({ repository: type, buildId: id }); - const image = getServiceImage(type); - - const config = { - searxng: { - image: `${image}:${version}`, - volume: `${id}-searxng:/etc/searxng`, - environmentVariables: { - SEARXNG_BASE_URL: `${fqdn}` - }, - }, - redis: { - image: 'redis:7-alpine', - } - }; - - const settingsYml = ` - # see https://docs.searxng.org/admin/engines/settings.html#use-default-settings - use_default_settings: true - server: - secret_key: ${secretKey} - limiter: true - image_proxy: true - ui: - static_use_hash: true - redis: - url: redis://:${redisPassword}@${id}-redis:6379/0` - - const Dockerfile = ` - FROM ${config.searxng.image} - COPY ./settings.yml /etc/searxng/settings.yml`; - - if (serviceSecret.length > 0) { - serviceSecret.forEach((secret) => { - config.searxng.environmentVariables[secret.name] = secret.value; - }); - } - const { volumes, volumeMounts } = persistentVolumes(id, persistentStorage, config) - const composeFile: ComposeFile = { - version: '3.8', - services: { - [id]: { - build: workdir, - container_name: id, - volumes, - environment: config.searxng.environmentVariables, - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - labels: makeLabelForServices('searxng'), - cap_drop: ['ALL'], - cap_add: ['CHOWN', 'SETGID', 'SETUID', 'DAC_OVERRIDE'], - depends_on: [`${id}-redis`], - ...defaultComposeConfiguration(network), - }, - [`${id}-redis`]: { - container_name: `${id}-redis`, - image: config.redis.image, - command: `redis-server --requirepass ${redisPassword} --save "" --appendonly "no"`, - labels: makeLabelForServices('searxng'), - cap_drop: ['ALL'], - cap_add: ['SETGID', 'SETUID', 'DAC_OVERRIDE'], - ...defaultComposeConfiguration(network), - }, - }, - networks: { - [network]: { - external: true - } - }, - volumes: volumeMounts - }; - const composeFileDestination = `${workdir}/docker-compose.yaml`; - await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile); - await fs.writeFile(`${workdir}/settings.yml`, settingsYml); - - await startServiceContainers(destinationDocker.id, composeFileDestination) - return {} - } catch ({ status, message }) { - return errorHandler({ status, message }) - } -} export async function activatePlausibleUsers(request: FastifyRequest, reply: FastifyReply) { try { diff --git a/apps/api/src/routes/api/v1/services/index.ts b/apps/api/src/routes/api/v1/services/index.ts index c43966f78..fd1ab061b 100644 --- a/apps/api/src/routes/api/v1/services/index.ts +++ b/apps/api/src/routes/api/v1/services/index.ts @@ -26,12 +26,11 @@ import { saveServiceType, saveServiceVersion, setSettingsService, - startService, - stopService } from './handlers'; import type { OnlyId } from '../../../../types'; import type { ActivateWordpressFtp, CheckService, CheckServiceDomain, DeleteServiceSecret, DeleteServiceStorage, GetServiceLogs, SaveService, SaveServiceDestination, SaveServiceSecret, SaveServiceSettings, SaveServiceStorage, SaveServiceType, SaveServiceVersion, ServiceStartStop, SetWordpressSettings } from './types'; +import { startService, stopService } from '../../../../lib/services/handlers'; const root: FastifyPluginAsync = async (fastify): Promise => { fastify.addHook('onRequest', async (request) => { diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index c4012bc9d..118354e06 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -1,5 +1,7 @@ import { FastifyRequest } from "fastify"; -import { errorHandler, getDomain, isDev, prisma, supportedServiceTypesAndVersions, include, executeDockerCmd } from "../../../lib/common"; +import { errorHandler, getDomain, isDev, prisma, executeDockerCmd } from "../../../lib/common"; +import { supportedServiceTypesAndVersions } from "../../../lib/services/supportedVersions"; +import { includeServices } from "../../../lib/services/common"; import { TraefikOtherConfiguration } from "./types"; function configureMiddleware( @@ -234,7 +236,7 @@ export async function traefikConfiguration(request, reply) { } const services: any = await prisma.service.findMany({ where: { destinationDocker: { remoteEngine: false } }, - include, + include: includeServices, orderBy: { createdAt: 'desc' }, }); diff --git a/apps/ui/src/lib/common.ts b/apps/ui/src/lib/common.ts index ab346bb54..4af26c100 100644 --- a/apps/ui/src/lib/common.ts +++ b/apps/ui/src/lib/common.ts @@ -1,199 +1,5 @@ import { addToast } from '$lib/store'; -export const supportedServiceTypesAndVersions = [ - { - name: 'plausibleanalytics', - fancyName: 'Plausible Analytics', - baseImage: 'plausible/analytics', - images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'], - versions: ['latest', 'stable'], - recommendedVersion: 'stable', - ports: { - main: 8000 - } - }, - { - name: 'nocodb', - fancyName: 'NocoDB', - baseImage: 'nocodb/nocodb', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, - { - name: 'minio', - fancyName: 'MinIO', - baseImage: 'minio/minio', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 9001 - } - }, - { - name: 'vscodeserver', - fancyName: 'VSCode Server', - baseImage: 'codercom/code-server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, - { - name: 'wordpress', - fancyName: 'Wordpress', - baseImage: 'wordpress', - images: ['bitnami/mysql:5.7'], - versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], - recommendedVersion: 'latest', - ports: { - main: 80 - } - }, - { - name: 'vaultwarden', - fancyName: 'Vaultwarden', - baseImage: 'vaultwarden/server', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 80 - } - }, - { - name: 'languagetool', - fancyName: 'LanguageTool', - baseImage: 'silviof/docker-languagetool', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8010 - } - }, - { - name: 'n8n', - fancyName: 'n8n', - baseImage: 'n8nio/n8n', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 5678 - } - }, - { - name: 'uptimekuma', - fancyName: 'Uptime Kuma', - baseImage: 'louislam/uptime-kuma', - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 3001 - } - }, - { - name: 'ghost', - fancyName: 'Ghost', - baseImage: 'bitnami/ghost', - images: ['bitnami/mariadb'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 2368 - } - }, - { - name: 'meilisearch', - fancyName: 'Meilisearch', - baseImage: 'getmeili/meilisearch', - images: [], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 7700 - } - }, - { - name: 'umami', - fancyName: 'Umami', - baseImage: 'ghcr.io/mikecao/umami', - images: ['postgres:12-alpine'], - versions: ['postgresql-latest'], - recommendedVersion: 'postgresql-latest', - ports: { - main: 3000 - } - }, - { - name: 'hasura', - fancyName: 'Hasura', - baseImage: 'hasura/graphql-engine', - images: ['postgres:12-alpine'], - versions: ['latest', 'v2.10.0', 'v2.5.1'], - recommendedVersion: 'v2.10.0', - ports: { - main: 8080 - } - }, - { - name: 'fider', - fancyName: 'Fider', - baseImage: 'getfider/fider', - images: ['postgres:12-alpine'], - versions: ['stable'], - recommendedVersion: 'stable', - ports: { - main: 3000 - } - }, - { - name: 'appwrite', - fancyName: 'Appwrite', - baseImage: 'appwrite/appwrite', - images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'], - versions: ['latest', '0.15.3'], - recommendedVersion: '0.15.3', - ports: { - main: 80 - } - }, - // { - // name: 'moodle', - // fancyName: 'Moodle', - // baseImage: 'bitnami/moodle', - // images: [], - // versions: ['latest', 'v4.0.2'], - // recommendedVersion: 'latest', - // ports: { - // main: 8080 - // } - // } - { - name: 'glitchTip', - fancyName: 'GlitchTip', - baseImage: 'glitchtip/glitchtip', - images: ['postgres:14-alpine', 'redis:7-alpine'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8000 - } - }, - { - name: 'searxng', - fancyName: 'SearXNG', - baseImage: 'searxng/searxng', - images: ['redis:6.2-alpine'], - versions: ['latest'], - recommendedVersion: 'latest', - ports: { - main: 8080 - } - }, -]; - export const asyncSleep = (delay: number) => new Promise((resolve) => setTimeout(resolve, delay)); @@ -265,15 +71,6 @@ export function changeQueryParams(buildId: string) { return history.pushState(null, null, '?' + queryParams.toString()); } -export const getServiceMainPort = (service: string) => { - const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service); - if (serviceType) { - return serviceType.ports.main; - } - return null; -}; - - export function handlerNotFoundLoad(error: any, url: URL) { if (error?.status === 404) { diff --git a/apps/ui/src/lib/store.ts b/apps/ui/src/lib/store.ts index f09872030..cbe906d51 100644 --- a/apps/ui/src/lib/store.ts +++ b/apps/ui/src/lib/store.ts @@ -17,7 +17,8 @@ interface AppSession { tokens: { github: string | null, gitlab: string | null, - } + }, + supportedServiceTypesAndVersions: Array } interface AddToast { type?: "info" | "success" | "error", @@ -40,7 +41,8 @@ export const appSession: Writable = writable({ tokens: { github: null, gitlab: null - } + }, + supportedServiceTypesAndVersions: [] }); export const disabledButton: Writable = writable(false); export const status: Writable = writable({ diff --git a/apps/ui/src/routes/__layout.svelte b/apps/ui/src/routes/__layout.svelte index 0be33466c..95f06ae2d 100644 --- a/apps/ui/src/routes/__layout.svelte +++ b/apps/ui/src/routes/__layout.svelte @@ -65,11 +65,13 @@