From 6aeafda60419bd454ce1e670be2c05462aa9cdb2 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sat, 23 Apr 2022 16:12:16 +0200 Subject: [PATCH 01/13] fix: Reactivate posgtres password --- src/lib/database/common.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index 47a8c3f9a..968208f55 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -154,6 +154,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D ulimits: Record; privatePort: number; environmentVariables: { + POSTGRESQL_POSTGRES_PASSWORD: string; POSTGRESQL_USERNAME: string; POSTGRESQL_PASSWORD: string; POSTGRESQL_DATABASE: string; @@ -220,6 +221,7 @@ export function generateDatabaseConfiguration(database: Database & { settings: D return { privatePort: 5432, environmentVariables: { + POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword, POSTGRESQL_PASSWORD: dbUserPassword, POSTGRESQL_USERNAME: dbUser, POSTGRESQL_DATABASE: defaultDatabase From cbabf7fc51d3ca52b5ac630e49812a0a22b6a6a3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sat, 23 Apr 2022 18:46:00 +0200 Subject: [PATCH 02/13] chore: version++ --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b73d42a8e..9f38331ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.5.1", + "version": "2.5.2", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && cross-env NODE_ENV=development & svelte-kit dev --host 0.0.0.0", From 06fe3f33c0ca23d31235c7c7e5bb5c74c8abdbf7 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sun, 24 Apr 2022 00:23:35 +0200 Subject: [PATCH 03/13] fix: Contribution guide --- CONTRIBUTING.md | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a4df5233d..648a02826 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -87,6 +87,15 @@ I use MinIO as an example. You need to add a new folder to [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. It should have three files with the following properties: +1. If you need to store passwords or any persistent data for the service, do the followings: + +- Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma). Add a new model with details about the required fields. +- If you finished with the Prism schema, update the database schema with `pnpm db:push` command. It will also generate the Prisma Typescript types for you. + - Tip: If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode. +- Include the new service to `listServicesWithIncludes` function in `src/lib/database/services.ts` + +**Important**: You need to take care of encryption / decryption of the data (where applicable). + 1. `index.json.ts`: A POST endpoint that updates Coolify's database about the service. Basic services only require updating the URL(fqdn) and the name of the service. @@ -164,19 +173,3 @@ If your language doesn't appear in the [locales folder list](src/lib/locales/), 1. In `src/lib/locales/`, Copy paste `en.json` and rename it with your language (eg: `cz.json`). 2. In the [lang.json](src/lib/lang.json) file, add a line after the first bracket (`{`) with `"ISO of your language": "Language",` (eg: `"cz": "Czech",`). 3. Have fun translating! - -### Additionnal pull requests steps - -Please add the emoji 🌐 to your pull request title to indicate that it is a translation. - -## 📄 Help sorting out the issues - -ToDo - -## 🎯 Test Pull Requests - -ToDo - -## ✒️ Help with the documentation - -ToDo From 72b650b0869228400209c38b537e422f8db62ec3 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sun, 24 Apr 2022 00:24:08 +0200 Subject: [PATCH 04/13] fix: Simplify list services --- src/lib/database/services.ts | 14 ++++++++++++++ src/lib/haproxy/configuration.ts | 13 ++----------- src/lib/letsencrypt/index.ts | 14 ++------------ 3 files changed, 18 insertions(+), 23 deletions(-) diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 314cf4548..6e78ea944 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -4,6 +4,20 @@ import cuid from 'cuid'; import { generatePassword } from '.'; import { prisma } from './common'; +export async function listServicesWithIncludes() { + return await prisma.service.findMany({ + include: { + destinationDocker: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true + }, + orderBy: { createdAt: 'desc' } + }); +} export async function listServices(teamId: string): Promise { if (teamId === '0') { return await prisma.service.findMany({ include: { teams: true } }); diff --git a/src/lib/haproxy/configuration.ts b/src/lib/haproxy/configuration.ts index 36dded536..f2f9f500b 100644 --- a/src/lib/haproxy/configuration.ts +++ b/src/lib/haproxy/configuration.ts @@ -6,6 +6,7 @@ import crypto from 'crypto'; import { checkContainer, checkHAProxy } from '.'; import { asyncExecShell, getDomain, getEngine } from '$lib/common'; import { supportedServiceTypesAndVersions } from '$lib/components/common'; +import { listServicesWithIncludes } from '$lib/database'; const url = dev ? 'http://localhost:5555' : 'http://coolify-haproxy:5555'; @@ -208,17 +209,7 @@ export async function configureHAProxy(): Promise { } } } - const services = await db.prisma.service.findMany({ - include: { - destinationDocker: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true, - meiliSearch: true - } - }); + const services = await listServicesWithIncludes(); for (const service of services) { const { fqdn, id, type, destinationDocker, destinationDockerId, updatedAt } = service; diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index da923c3d5..7682ecb27 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -7,6 +7,7 @@ import fs from 'fs/promises'; import getPort, { portNumbers } from 'get-port'; import { supportedServiceTypesAndVersions } from '$lib/components/common'; import { promises as dns } from 'dns'; +import { listServicesWithIncludes } from '$lib/database'; export async function letsEncrypt(domain: string, id?: string, isCoolify = false): Promise { try { @@ -145,18 +146,7 @@ export async function generateSSLCerts(): Promise { console.log(`Error during generateSSLCerts with ${application.fqdn}: ${error}`); } } - const services = await db.prisma.service.findMany({ - include: { - destinationDocker: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true, - meiliSearch: true - }, - orderBy: { createdAt: 'desc' } - }); + const services = await listServicesWithIncludes(); for (const service of services) { try { From 41adc02801eb20efb579d9d760bcbd920307f61b Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sun, 24 Apr 2022 00:25:38 +0200 Subject: [PATCH 05/13] fix: Contribution --- CONTRIBUTING.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 648a02826..78b5104d1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -96,11 +96,11 @@ You need to add a new folder to [src/routes/services/[id]](src/routes/services/[ **Important**: You need to take care of encryption / decryption of the data (where applicable). -1. `index.json.ts`: A POST endpoint that updates Coolify's database about the service. +2. `index.json.ts`: A POST endpoint that updates Coolify's database about the service. Basic services only require updating the URL(fqdn) and the name of the service. -2. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service. +3. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service. - To start a service, you need to know Coolify supported images and tags of the service. For that you need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). @@ -131,11 +131,11 @@ You need to add a new folder to [src/routes/services/[id]](src/routes/services/[ - 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)) -3. `stop.json.ts` A stop endpoint that stops the service. +4. `stop.json.ts` A stop endpoint that stops the service. It needs to stop all the services by their container name and proxies (if applicable). -4. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function. +5. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function. ## Frontend From df5e23c7c26d36eba627f3dc316d623725a36acf Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Sun, 24 Apr 2022 00:27:27 +0200 Subject: [PATCH 06/13] fix: Contribution guide --- CONTRIBUTING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 78b5104d1..ea2c3d0f8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -92,9 +92,9 @@ You need to add a new folder to [src/routes/services/[id]](src/routes/services/[ - Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma). Add a new model with details about the required fields. - If you finished with the Prism schema, update the database schema with `pnpm db:push` command. It will also generate the Prisma Typescript types for you. - Tip: If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode. -- Include the new service to `listServicesWithIncludes` function in `src/lib/database/services.ts` +- Include the new service to `listServicesWithIncludes` function in [src/lib/database/services.ts](src/lib/database/services.ts) -**Important**: You need to take care of encryption / decryption of the data (where applicable). + **Important**: You need to take care of encryption / decryption of the data (where applicable). 2. `index.json.ts`: A POST endpoint that updates Coolify's database about the service. From 07708155ac807caba05a79a83d3b41643d001d5f Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 00:00:06 +0200 Subject: [PATCH 07/13] WIP: Umami service --- CONTRIBUTING.md | 164 ++++++++++++--- prisma/schema.prisma | 19 +- src/lib/common.ts | 6 +- src/lib/components/common.ts | 11 + src/lib/database/services.ts | 93 ++++---- src/routes/services/[id]/umami/index.json.ts | 21 ++ src/routes/services/[id]/umami/start.json.ts | 210 +++++++++++++++++++ src/routes/services/[id]/umami/stop.json.ts | 42 ++++ 8 files changed, 489 insertions(+), 77 deletions(-) create mode 100644 src/routes/services/[id]/umami/index.json.ts create mode 100644 src/routes/services/[id]/umami/start.json.ts create mode 100644 src/routes/services/[id]/umami/stop.json.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ea2c3d0f8..628ed952f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,9 +12,6 @@ This is a little list of what you can do to help the project: - [🧑‍💻 Develop your own ideas](#developer-contribution) - [🌐 Translate the project](#translation) -- [📄 Help sorting out the issues](#help-sorting-out-the-issues) -- [🎯 Test Pull Requests](#test-pull-requests) -- [✒️ Help with the documentation](#help-with-the-documentation) ## 👋 Introduction @@ -60,6 +57,7 @@ You need to have [Docker Engine](https://docs.docker.com/engine/install/) instal - **Languages**: Node.js / Javascript / Typescript - **Framework JS/TS**: Svelte / SvelteKit - **Database ORM**: Prisma.io +- **Docker Engine** ### Database migrations @@ -83,59 +81,159 @@ You can add any open-source and self-hostable software (service/application) to ## Backend -I use MinIO as an example. +There are 5 steps you should make on the backend side. -You need to add a new folder to [src/routes/services/[id]](src/routes/services/[id]) with the low-capital name of the service. It should have three files with the following properties: +1. Create Prisma / database schema for the new service. +2. Add supported versions of the service. +3. Update global functions. +4. Create API endpoints. +5. Define automatically generated variables. -1. If you need to store passwords or any persistent data for the service, do the followings: +> I will use [Umami](https://umami.is/) as an example service. -- Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma). Add a new model with details about the required fields. -- If you finished with the Prism schema, update the database schema with `pnpm db:push` command. It will also generate the Prisma Typescript types for you. - - Tip: If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode. -- Include the new service to `listServicesWithIncludes` function in [src/lib/database/services.ts](src/lib/database/services.ts) +### Create Prisma / database schema for the new service. - **Important**: You need to take care of encryption / decryption of the data (where applicable). +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. -2. `index.json.ts`: A POST endpoint that updates Coolify's database about the service. +Update Prisma schema in [prisma/schema.prisma](prisma/schema.prisma). - Basic services only require updating the URL(fqdn) and the name of the service. +- Add new model with the new service name. +- Make a relationshup 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. -3. `start.json.ts`: A start endpoint that setups the docker-compose file (for Local Docker Engines) and starts the service. +If you are finished with the Prisma schema, you should update the database schema with `pnpm db:push` command. - - To start a service, you need to know Coolify supported images and tags of the service. For that you need to update `supportedServiceTypesAndVersions` function at [src/lib/components/common.ts](src/lib/components/common.ts). +> You must restart the running development environment to be able to use the new model - Example JSON: +> If you use VSCode, you probably need to restart the `Typescript Language Server` to get the new types loaded in the running VSCode. - ```js +### 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: + +```js { - // Name used to identify the service in Coolify - name: 'minio', + // Name used to identify the service internally + name: 'umami', // Fancier name to show to the user - fancyName: 'MinIO', + fancyName: 'Umami', // Docker base image for the service - baseImage: 'minio/minio', + baseImage: 'ghcr.io/mikecao/umami', + // Optional: If there is any dependent image, you should list it here + images: [], // Usable tags - versions: ['latest'], + versions: ['postgresql-latest'], // Which tag is the recommended - recommendedVersion: 'latest', - // Application's default port, MinIO listens on 9001 (and 9000, more details later on) + recommendedVersion: 'postgresql-latest', + // Application's default port, Umami listens on 3000 ports: { - main: 9001 + main: 3000 } - }, - ``` + } +``` - - You need to define a compose file as `const composeFile: ComposeFile` found in [src/routes/services/[id]/minio/start.json.ts](src/routes/services/[id]/minio/start.json.ts) +### Update global functions - **IMPORTANT:** It should contain `all the default environment variables` that are required for the service to function correctly and `all the volumes to persist data` in restarts. +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. - - 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)) +```js +const include: Prisma.ServiceInclude = { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true, + umami: true // This line! +}; +``` -4. `stop.json.ts` A stop endpoint that stops the service. +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). - It needs to stop all the services by their container name and proxies (if applicable). +```js +[...] +else if (type === 'umami') { + const postgresqlUser = cuid(); + const postgresqlPassword = encrypt(generatePassword()); + const postgresqlDatabase = 'umami'; + const hashSalt = encrypt(generatePassword(64)); + await prisma.service.update({ + where: { id }, + data: { + type, + umami: { + create: { + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + hashSalt, + } + } + } + }); + } +``` -5. You need to add the automatically generated variables (passwords, users, etc.) for the new service at [src/lib/database/services.ts](src/lib/database/services.ts), `configureServiceType` function. +3. Add decryption process for configurations and passwords to `getService` function in [src/lib/database/services.ts](src/lib/database/services.ts) + +```js +if (body.umami?.postgresqlPassword) + body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword); + +if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt); +``` + +4. Add service deletion query to `removeService` function in [src/lib/database/services.ts](src/lib/database/services.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. + +#### `index.json.ts`: + +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 diff --git a/prisma/schema.prisma b/prisma/schema.prisma index abbfe24b2..493cdd3fe 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -91,9 +91,9 @@ model Application { pythonWSGI String? pythonModule String? pythonVariable String? - dockerFileLocation String? + dockerFileLocation String? denoMainFile String? - denoOptions String? + denoOptions String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt settings ApplicationSettings? @@ -301,6 +301,7 @@ model Service { serviceSecret ServiceSecret[] meiliSearch MeiliSearch? persistentStorage ServicePersistentStorage[] + umami Umami? } model PlausibleAnalytics { @@ -385,3 +386,17 @@ model MeiliSearch { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Umami { + id String @id @default(cuid()) + serviceId String @unique + postgresqlUser String + postgresqlPassword String + postgresqlDatabase String + postgresqlPublicPort Int? + umamiAdminPassword String + hashSalt String + service Service @relation(fields: [serviceId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/lib/common.ts b/src/lib/common.ts index 8bcc48b99..acbe6c88c 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -26,7 +26,7 @@ try { initialScope: { tags: { appId: process.env['COOLIFY_APP_ID'], - 'os.arch': os.arch(), + 'os.arch': getOsArch(), 'os.platform': os.platform(), 'os.release': os.release() } @@ -175,3 +175,7 @@ export function generateTimestamp(): string { export function getDomain(domain: string): string { return domain?.replace('https://', '').replace('http://', ''); } + +export function getOsArch() { + return os.arch(); +} diff --git a/src/lib/components/common.ts b/src/lib/components/common.ts index 6bc60202d..760653cfa 100644 --- a/src/lib/components/common.ts +++ b/src/lib/components/common.ts @@ -180,5 +180,16 @@ export const supportedServiceTypesAndVersions = [ 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 + } } ]; diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index 6e78ea944..4cb2e8b22 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -1,20 +1,24 @@ import { decrypt, encrypt } from '$lib/crypto'; -import type { Minio, Service } from '@prisma/client'; +import type { Minio, Prisma, Service } from '@prisma/client'; import cuid from 'cuid'; import { generatePassword } from '.'; import { prisma } from './common'; +const include: Prisma.ServiceInclude = { + destinationDocker: true, + persistentStorage: true, + serviceSecret: true, + minio: true, + plausibleAnalytics: true, + vscodeserver: true, + wordpress: true, + ghost: true, + meiliSearch: true, + umami: true +}; export async function listServicesWithIncludes() { return await prisma.service.findMany({ - include: { - destinationDocker: true, - minio: true, - plausibleAnalytics: true, - vscodeserver: true, - wordpress: true, - ghost: true, - meiliSearch: true - }, + include, orderBy: { createdAt: 'desc' } }); } @@ -44,35 +48,21 @@ export async function getService({ id, teamId }: { id: string; teamId: string }) if (teamId === '0') { body = await prisma.service.findFirst({ where: { id }, - include: { - destinationDocker: true, - plausibleAnalytics: true, - minio: true, - vscodeserver: true, - wordpress: true, - ghost: true, - serviceSecret: true, - meiliSearch: true, - persistentStorage: true - } + include }); } else { body = await prisma.service.findFirst({ where: { id, teams: { some: { id: teamId } } }, - include: { - destinationDocker: true, - plausibleAnalytics: true, - minio: true, - vscodeserver: true, - wordpress: true, - ghost: true, - serviceSecret: true, - meiliSearch: true, - persistentStorage: true - } + include }); } + if (body?.serviceSecret.length > 0) { + body.serviceSecret = body.serviceSecret.map((s) => { + s.value = decrypt(s.value); + return s; + }); + } if (body.plausibleAnalytics?.postgresqlPassword) body.plausibleAnalytics.postgresqlPassword = decrypt( body.plausibleAnalytics.postgresqlPassword @@ -99,15 +89,14 @@ export async function getService({ id, teamId }: { id: string; teamId: string }) if (body.meiliSearch?.masterKey) body.meiliSearch.masterKey = decrypt(body.meiliSearch.masterKey); - if (body?.serviceSecret.length > 0) { - body.serviceSecret = body.serviceSecret.map((s) => { - s.value = decrypt(s.value); - return s; - }); - } - if (body.wordpress?.ftpPassword) { - body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword); - } + if (body.wordpress?.ftpPassword) body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword); + + if (body.umami?.postgresqlPassword) + body.umami.postgresqlPassword = decrypt(body.umami.postgresqlPassword); + if (body.umami?.umamiAdminPassword) + body.umami.umamiAdminPassword = decrypt(body.umami.umamiAdminPassword); + if (body.umami?.hashSalt) body.umami.hashSalt = decrypt(body.umami.hashSalt); + const settings = await prisma.setting.findFirst(); return { ...body, settings }; @@ -233,6 +222,27 @@ export async function configureServiceType({ 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(64)); + await prisma.service.update({ + where: { id }, + data: { + type, + umami: { + create: { + umamiAdminPassword, + postgresqlDatabase, + postgresqlPassword, + postgresqlUser, + hashSalt + } + } + } + }); } } @@ -389,6 +399,7 @@ export async function removeService({ id }: { id: string }): Promise { await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); await prisma.ghost.deleteMany({ where: { serviceId: id } }); + await prisma.umami.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 } }); diff --git a/src/routes/services/[id]/umami/index.json.ts b/src/routes/services/[id]/umami/index.json.ts new file mode 100644 index 000000000..d717502c5 --- /dev/null +++ b/src/routes/services/[id]/umami/index.json.ts @@ -0,0 +1,21 @@ +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); + } +}; diff --git a/src/routes/services/[id]/umami/start.json.ts b/src/routes/services/[id]/umami/start.json.ts new file mode 100644 index 000000000..eb29e65a8 --- /dev/null +++ b/src/routes/services/[id]/umami/start.json.ts @@ -0,0 +1,210 @@ +import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; +import type { ComposeFile } from '$lib/types/composeFile'; +import type { Service, DestinationDocker, ServiceSecret, Prisma } from '@prisma/client'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service: Service & Prisma.ServiceInclude & { destinationDocker: DestinationDocker } = + await db.getService({ id, teamId }); + const { + type, + version, + destinationDockerId, + destinationDocker, + serviceSecret, + umami: { + umamiAdminPassword, + postgresqlUser, + postgresqlPassword, + postgresqlDatabase, + hashSalt + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const host = getEngine(destinationDocker.engine); + + 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; + }); + } + console.log(umamiAdminPassword); + 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', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', 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 composeFile: ComposeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.umami.image, + environment: config.umami.environmentVariables, + networks: [network], + volumes: [], + restart: 'always', + labels: makeLabelForServices('umami'), + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + }, + depends_on: [`${id}-postgresql`] + }, + [`${id}-postgresql`]: { + build: workdir, + container_name: `${id}-postgresql`, + environment: config.postgresql.environmentVariables, + networks: [network], + volumes: [config.postgresql.volume], + restart: 'always', + deploy: { + restart_policy: { + condition: 'on-failure', + delay: '5s', + max_attempts: 3, + window: '120s' + } + } + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.postgresql.volume.split(':')[0]]: { + name: config.postgresql.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + console.log(error); + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/umami/stop.json.ts b/src/routes/services/[id]/umami/stop.json.ts new file mode 100644 index 000000000..67dd96d04 --- /dev/null +++ b/src/routes/services/[id]/umami/stop.json.ts @@ -0,0 +1,42 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer, stopTcpHttpProxy } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + const found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + } catch (error) { + console.error(error); + } + try { + const found = await checkContainer(engine, `${id}-postgresql`); + if (found) { + await removeDestinationDocker({ id: `${id}-postgresql`, engine }); + } + } catch (error) { + console.error(error); + } + } + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; From 046f738b7dd9327216b1ea4e52c40a75bfd15046 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 08:54:53 +0200 Subject: [PATCH 08/13] feat: Umami service --- src/lib/components/ServiceLinks.svelte | 5 ++ src/lib/components/svg/services/Umami.svelte | 85 +++++++++++++++++++ src/routes/applications/[id]/__layout.svelte | 4 +- .../services/[id]/_Services/_Services.svelte | 3 + .../services/[id]/_Services/_Umami.svelte | 29 +++++++ .../services/[id]/configuration/type.svelte | 3 + src/routes/services/[id]/umami/start.json.ts | 12 ++- src/routes/services/index.svelte | 5 ++ 8 files changed, 140 insertions(+), 6 deletions(-) create mode 100644 src/lib/components/svg/services/Umami.svelte create mode 100644 src/routes/services/[id]/_Services/_Umami.svelte diff --git a/src/lib/components/ServiceLinks.svelte b/src/lib/components/ServiceLinks.svelte index a3b4ce2cd..980505119 100644 --- a/src/lib/components/ServiceLinks.svelte +++ b/src/lib/components/ServiceLinks.svelte @@ -6,6 +6,7 @@ import N8n from './svg/services/N8n.svelte'; import NocoDb from './svg/services/NocoDB.svelte'; import PlausibleAnalytics from './svg/services/PlausibleAnalytics.svelte'; + import Umami from './svg/services/Umami.svelte'; import UptimeKuma from './svg/services/UptimeKuma.svelte'; import VaultWarden from './svg/services/VaultWarden.svelte'; import VsCodeServer from './svg/services/VSCodeServer.svelte'; @@ -52,4 +53,8 @@ +{:else if service.type === 'umami'} + + + {/if} diff --git a/src/lib/components/svg/services/Umami.svelte b/src/lib/components/svg/services/Umami.svelte new file mode 100644 index 000000000..ac0df85af --- /dev/null +++ b/src/lib/components/svg/services/Umami.svelte @@ -0,0 +1,85 @@ + + + + Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + diff --git a/src/routes/applications/[id]/__layout.svelte b/src/routes/applications/[id]/__layout.svelte index 2a09bfd1d..bf634edde 100644 --- a/src/routes/applications/[id]/__layout.svelte +++ b/src/routes/applications/[id]/__layout.svelte @@ -401,10 +401,10 @@ class:bg-coolgray-500={$page.url.pathname === `/applications/${id}/logs`} > diff --git a/src/routes/services/[id]/umami/start.json.ts b/src/routes/services/[id]/umami/start.json.ts index eb29e65a8..6a51e5cde 100644 --- a/src/routes/services/[id]/umami/start.json.ts +++ b/src/routes/services/[id]/umami/start.json.ts @@ -3,10 +3,11 @@ import * as db from '$lib/database'; import { promises as fs } from 'fs'; import yaml from 'js-yaml'; import type { RequestHandler } from '@sveltejs/kit'; -import { ErrorHandler, getFreePort, getServiceImage } from '$lib/database'; +import { ErrorHandler, getServiceImage } from '$lib/database'; import { makeLabelForServices } from '$lib/buildPacks/common'; import type { ComposeFile } from '$lib/types/composeFile'; -import type { Service, DestinationDocker, ServiceSecret, Prisma } from '@prisma/client'; +import type { Service, DestinationDocker, Prisma } from '@prisma/client'; +import bcrypt from 'bcryptjs'; export const post: RequestHandler = async (event) => { const { teamId, status, body } = await getUserDetails(event); @@ -61,7 +62,7 @@ export const post: RequestHandler = async (event) => { config.umami.environmentVariables[secret.name] = secret.value; }); } - console.log(umamiAdminPassword); + const initDbSQL = ` drop table if exists event; drop table if exists pageview; @@ -136,7 +137,10 @@ export const post: RequestHandler = async (event) => { 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', '$2b$10$BUli0c.muyCW1ErNJc3jL.vFRFtFJWrT8/GcR4A.sUdCznaXiqFXa', true);`; + 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} diff --git a/src/routes/services/index.svelte b/src/routes/services/index.svelte index c5f6dea5f..fd784d049 100644 --- a/src/routes/services/index.svelte +++ b/src/routes/services/index.svelte @@ -15,6 +15,7 @@ import MeiliSearch from '$lib/components/svg/services/MeiliSearch.svelte'; import { session } from '$app/stores'; import { getDomain } from '$lib/components/common'; + import Umami from '$lib/components/svg/services/Umami.svelte'; export let services; async function newService() { @@ -86,6 +87,8 @@ {:else if service.type === 'meilisearch'} + {:else if service.type === 'umami'} + {/if}
{service.name} @@ -133,6 +136,8 @@ {:else if service.type === 'meilisearch'} + {:else if service.type === 'umami'} + {/if}
{service.name} From 08332c8321f28afae8ca50d24cd16cdf444bb736 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 08:55:04 +0200 Subject: [PATCH 09/13] fix: Contribution guide --- CONTRIBUTING.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 628ed952f..2be3c4ff5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -241,7 +241,11 @@ It has a POST endpoint that stops the service and all dependent (TCP/HTTP proxie 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 [src/routes/services/index.svelte](src/routes/services/index.svelte) with `isAbsolute` and [src/lib/components/ServiceLinks.svelte](src/lib/components/ServiceLinks.svelte) with a link to the docs/main site of the service. +2. 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`. 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). From 8290ee856fb04beacafbba9c1cf263d41b8ad349 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 09:11:49 +0200 Subject: [PATCH 10/13] migration for umami --- .../20220425071132_umami/migration.sql | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 prisma/migrations/20220425071132_umami/migration.sql diff --git a/prisma/migrations/20220425071132_umami/migration.sql b/prisma/migrations/20220425071132_umami/migration.sql new file mode 100644 index 000000000..259fb76cd --- /dev/null +++ b/prisma/migrations/20220425071132_umami/migration.sql @@ -0,0 +1,17 @@ +-- CreateTable +CREATE TABLE "Umami" ( + "id" TEXT NOT NULL PRIMARY KEY, + "serviceId" TEXT NOT NULL, + "postgresqlUser" TEXT NOT NULL, + "postgresqlPassword" TEXT NOT NULL, + "postgresqlDatabase" TEXT NOT NULL, + "postgresqlPublicPort" INTEGER, + "umamiAdminPassword" TEXT NOT NULL, + "hashSalt" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Umami_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Umami_serviceId_key" ON "Umami"("serviceId"); From 11d74c0c1f363964725538891d2237903a2d75c8 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 09:54:28 +0200 Subject: [PATCH 11/13] feat: Coolify auto-updater --- .env.template | 6 ++- .../migration.sql | 22 +++++++++++ prisma/schema.prisma | 1 + prisma/seed.cjs | 14 +++++++ src/lib/locales/en.json | 4 +- src/lib/queues/autoUpdater.ts | 39 +++++++++++++++++++ src/lib/queues/index.ts | 17 +++++++- src/routes/settings/index.json.ts | 8 +++- src/routes/settings/index.svelte | 16 ++++++-- 9 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 prisma/migrations/20220425075326_auto_update_coolify/migration.sql create mode 100644 src/lib/queues/autoUpdater.ts diff --git a/.env.template b/.env.template index 0fa7427f3..3237d9b2c 100644 --- a/.env.template +++ b/.env.template @@ -2,5 +2,7 @@ COOLIFY_APP_ID= COOLIFY_SECRET_KEY=12341234123412341234123412341234 COOLIFY_DATABASE_URL=file:../db/dev.db COOLIFY_SENTRY_DSN= -COOLIFY_IS_ON="docker" -COOLIFY_WHITE_LABELED="false" \ No newline at end of file +COOLIFY_IS_ON=docker +COOLIFY_WHITE_LABELED=false +COOLIFY_WHITE_LABELED_ICON= +COOLIFY_AUTO_UPDATE=false \ No newline at end of file diff --git a/prisma/migrations/20220425075326_auto_update_coolify/migration.sql b/prisma/migrations/20220425075326_auto_update_coolify/migration.sql new file mode 100644 index 000000000..a102973ee --- /dev/null +++ b/prisma/migrations/20220425075326_auto_update_coolify/migration.sql @@ -0,0 +1,22 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "id" TEXT NOT NULL PRIMARY KEY, + "fqdn" TEXT, + "isRegistrationEnabled" BOOLEAN NOT NULL DEFAULT false, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "minPort" INTEGER NOT NULL DEFAULT 9000, + "maxPort" INTEGER NOT NULL DEFAULT 9100, + "proxyPassword" TEXT NOT NULL, + "proxyUser" TEXT NOT NULL, + "proxyHash" TEXT, + "isAutoUpdateEnabled" BOOLEAN NOT NULL DEFAULT false, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt") SELECT "createdAt", "dualCerts", "fqdn", "id", "isRegistrationEnabled", "maxPort", "minPort", "proxyHash", "proxyPassword", "proxyUser", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +CREATE UNIQUE INDEX "Setting_fqdn_key" ON "Setting"("fqdn"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 493cdd3fe..82ba9baa8 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -18,6 +18,7 @@ model Setting { proxyPassword String proxyUser String proxyHash String? + isAutoUpdateEnabled Boolean @default(false) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/prisma/seed.cjs b/prisma/seed.cjs index 61f3928f6..1e6b160c6 100644 --- a/prisma/seed.cjs +++ b/prisma/seed.cjs @@ -50,6 +50,20 @@ async function main() { } }); } + + // Set auto-update based on env variable + const isAutoUpdateEnabled = process.env['COOLIFY_AUTO_UPDATE'] === 'true'; + const settings = await prisma.setting.findFirst({}); + if (settings) { + await prisma.setting.update({ + where: { + id: settings.id + }, + data: { + isAutoUpdateEnabled + } + }); + } } main() .catch((e) => { diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index dc98ab2c3..573ca00e8 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -302,7 +302,9 @@ "registration_allowed": "Registration allowed?", "registration_allowed_explainer": "Allow further registrations to the application.
It's turned off after the first registration.", "coolify_proxy_settings": "Coolify Proxy Settings", - "credential_stat_explainer": "Credentials for stats page." + "credential_stat_explainer": "Credentials for stats page.", + "auto_update_enabled": "Auto update enabled?", + "auto_update_enabled_explainer": "Enable automatic updates for Coolify." }, "team": { "pending_invitations": "Pending invitations", diff --git a/src/lib/queues/autoUpdater.ts b/src/lib/queues/autoUpdater.ts new file mode 100644 index 000000000..835010177 --- /dev/null +++ b/src/lib/queues/autoUpdater.ts @@ -0,0 +1,39 @@ +import { prisma } from '$lib/database'; +import { buildQueue } from '.'; +import got from 'got'; +import { asyncExecShell, version } from '$lib/common'; +import compare from 'compare-versions'; +import { dev } from '$app/env'; + +export default async function (): Promise { + const currentVersion = version; + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); + if (isAutoUpdateEnabled) { + const versions = await got + .get( + `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` + ) + .json(); + const latestVersion = versions['coolify'].main.version; + const isUpdateAvailable = compare(latestVersion, currentVersion); + if (isUpdateAvailable === 1) { + const activeCount = await buildQueue.getActiveCount(); + if (activeCount === 0) { + if (!dev) { + console.log('Updating...'); + await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); + await asyncExecShell(`env | grep COOLIFY > .env`); + await asyncExecShell( + `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"` + ); + } else { + console.log('Updating (not really in dev mode).'); + } + } + } else { + console.log('No update available.'); + } + } else { + console.log('Auto update is disabled.'); + } +} diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index 84d5f7cbc..5f8ebfb11 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -10,6 +10,7 @@ import proxy from './proxy'; import proxyTcpHttp from './proxyTcpHttp'; import ssl from './ssl'; import sslrenewal from './sslrenewal'; +import autoUpdater from './autoUpdater'; import { asyncExecShell, saveBuildLog } from '$lib/common'; @@ -34,19 +35,22 @@ const cron = async (): Promise => { new QueueScheduler('cleanup', connectionOptions); new QueueScheduler('ssl', connectionOptions); new QueueScheduler('sslRenew', connectionOptions); + new QueueScheduler('autoUpdater', connectionOptions); const queue = { proxy: new Queue('proxy', { ...connectionOptions }), proxyTcpHttp: new Queue('proxyTcpHttp', { ...connectionOptions }), cleanup: new Queue('cleanup', { ...connectionOptions }), ssl: new Queue('ssl', { ...connectionOptions }), - sslRenew: new Queue('sslRenew', { ...connectionOptions }) + sslRenew: new Queue('sslRenew', { ...connectionOptions }), + autoUpdater: new Queue('autoUpdater', { ...connectionOptions }) }; await queue.proxy.drain(); await queue.proxyTcpHttp.drain(); await queue.cleanup.drain(); await queue.ssl.drain(); await queue.sslRenew.drain(); + await queue.autoUpdater.drain(); new Worker( 'proxy', @@ -98,11 +102,22 @@ const cron = async (): Promise => { } ); + new Worker( + 'autoUpdater', + async () => { + await autoUpdater(); + }, + { + ...connectionOptions + } + ); + await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } }); await queue.proxyTcpHttp.add('proxyTcpHttp', {}, { repeat: { every: 10000 } }); await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } }); if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } }); await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } }); + await queue.autoUpdater.add('autoUpdater', {}, { repeat: { every: 60000 } }); }; cron().catch((error) => { console.log('cron failed to start'); diff --git a/src/routes/settings/index.json.ts b/src/routes/settings/index.json.ts index 2f15c5066..210f12782 100644 --- a/src/routes/settings/index.json.ts +++ b/src/routes/settings/index.json.ts @@ -64,10 +64,14 @@ export const post: RequestHandler = async (event) => { }; if (status === 401) return { status, body }; - const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort } = await event.request.json(); + const { fqdn, isRegistrationEnabled, dualCerts, minPort, maxPort, isAutoUpdateEnabled } = + await event.request.json(); try { const { id } = await db.listSettings(); - await db.prisma.setting.update({ where: { id }, data: { isRegistrationEnabled, dualCerts } }); + await db.prisma.setting.update({ + where: { id }, + data: { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled } + }); if (fqdn) { await db.prisma.setting.update({ where: { id }, data: { fqdn } }); } diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index f52cab13f..5040089ba 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -40,10 +40,9 @@ import { toast } from '@zerodevx/svelte-toast'; import { t } from '$lib/translations'; - import Language from './_Language.svelte'; - let isRegistrationEnabled = settings.isRegistrationEnabled; let dualCerts = settings.dualCerts; + let isAutoUpdateEnabled = settings.isAutoUpdateEnabled; let minPort = settings.minPort; let maxPort = settings.maxPort; @@ -76,7 +75,10 @@ if (name === 'dualCerts') { dualCerts = !dualCerts; } - await post(`/settings.json`, { isRegistrationEnabled, dualCerts }); + if (name === 'isAutoUpdateEnabled') { + isAutoUpdateEnabled = !isAutoUpdateEnabled; + } + await post(`/settings.json`, { isRegistrationEnabled, dualCerts, isAutoUpdateEnabled }); return toast.push(t.get('application.settings_saved')); } catch ({ error }) { return errorNotification(error); @@ -192,6 +194,14 @@ on:click={() => changeSettings('isRegistrationEnabled')} />
+
+ changeSettings('isAutoUpdateEnabled')} + /> +
From c1a48dcf1ea7a47243d2fa4ad86c41308afd6518 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 15:51:43 +0200 Subject: [PATCH 12/13] feat: Autoupdater --- src/lib/locales/en.json | 2 +- src/lib/queues/autoUpdater.ts | 55 +++++++++++++++++--------------- src/lib/queues/index.ts | 5 +-- src/routes/settings/index.svelte | 18 ++++++----- 4 files changed, 43 insertions(+), 37 deletions(-) diff --git a/src/lib/locales/en.json b/src/lib/locales/en.json index 573ca00e8..d77d2a1c1 100644 --- a/src/lib/locales/en.json +++ b/src/lib/locales/en.json @@ -304,7 +304,7 @@ "coolify_proxy_settings": "Coolify Proxy Settings", "credential_stat_explainer": "Credentials for stats page.", "auto_update_enabled": "Auto update enabled?", - "auto_update_enabled_explainer": "Enable automatic updates for Coolify." + "auto_update_enabled_explainer": "Enable automatic updates for Coolify. It will be done automatically behind the scenes, if there is no build process running." }, "team": { "pending_invitations": "Pending invitations", diff --git a/src/lib/queues/autoUpdater.ts b/src/lib/queues/autoUpdater.ts index 835010177..4bfb995c7 100644 --- a/src/lib/queues/autoUpdater.ts +++ b/src/lib/queues/autoUpdater.ts @@ -6,34 +6,37 @@ import compare from 'compare-versions'; import { dev } from '$app/env'; export default async function (): Promise { - const currentVersion = version; - const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); - if (isAutoUpdateEnabled) { - const versions = await got - .get( - `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` - ) - .json(); - const latestVersion = versions['coolify'].main.version; - const isUpdateAvailable = compare(latestVersion, currentVersion); - if (isUpdateAvailable === 1) { - const activeCount = await buildQueue.getActiveCount(); - if (activeCount === 0) { - if (!dev) { - console.log('Updating...'); - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"` - ); - } else { - console.log('Updating (not really in dev mode).'); + try { + const currentVersion = version; + const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); + if (isAutoUpdateEnabled) { + const versions = await got + .get( + `https://get.coollabs.io/versions.json?appId=${process.env['COOLIFY_APP_ID']}&version=${currentVersion}` + ) + .json(); + const latestVersion = versions['coolify'].main.version; + const isUpdateAvailable = compare(latestVersion, currentVersion); + if (isUpdateAvailable === 1) { + const activeCount = await buildQueue.getActiveCount(); + if (activeCount === 0) { + if (!dev) { + await buildQueue.pause(); + console.log(`Updating Coolify to ${latestVersion}.`); + await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); + await asyncExecShell(`env | grep COOLIFY > .env`); + await asyncExecShell( + `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-redis && docker rm coolify coolify-redis && docker compose up -d --force-recreate"` + ); + } else { + await buildQueue.pause(); + console.log('Updating (not really in dev mode).'); + } } } - } else { - console.log('No update available.'); } - } else { - console.log('Auto update is disabled.'); + } catch (error) { + await buildQueue.resume(); + console.log(error); } } diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index 5f8ebfb11..60097680d 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -130,6 +130,9 @@ const buildWorker = new Worker(buildQueueName, async (job) => await builder(job) concurrency: 1, ...connectionOptions }); +buildQueue.resume().catch((err) => { + console.log('Build queue failed to resume!', err); +}); buildWorker.on('completed', async (job: Bullmq.Job) => { try { @@ -138,7 +141,6 @@ buildWorker.on('completed', async (job: Bullmq.Job) => { setTimeout(async () => { await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } }); }, 1234); - console.log(error); } finally { const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`; if (!dev) await asyncExecShell(`rm -fr ${workdir}`); @@ -154,7 +156,6 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => { setTimeout(async () => { await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'failed' } }); }, 1234); - console.log(error); } finally { const workdir = `/tmp/build-sources/${job.data.repository}`; if (!dev) await asyncExecShell(`rm -fr ${workdir}`); diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index 5040089ba..500fc1943 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -194,14 +194,16 @@ on:click={() => changeSettings('isRegistrationEnabled')} />
-
- changeSettings('isAutoUpdateEnabled')} - /> -
+ {#if browser && window.location.hostname === 'staging.coolify.io'} +
+ changeSettings('isAutoUpdateEnabled')} + /> +
+ {/if}
From 8ccb1bd34ca2e70ef48c1be3fc4069f00a1c0185 Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 25 Apr 2022 17:48:25 +0200 Subject: [PATCH 13/13] show autoupdate in localhost --- src/routes/settings/index.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/settings/index.svelte b/src/routes/settings/index.svelte index 500fc1943..dce1362d7 100644 --- a/src/routes/settings/index.svelte +++ b/src/routes/settings/index.svelte @@ -194,7 +194,7 @@ on:click={() => changeSettings('isRegistrationEnabled')} />
- {#if browser && window.location.hostname === 'staging.coolify.io'} + {#if browser && (window.location.hostname === 'staging.coolify.io' || window.location.hostname === 'localhost')}