diff --git a/package.json b/package.json index 820aaca49..d1bb03ba4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.1.1", + "version": "2.2.0", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev", diff --git a/prisma/migrations/20220327180323_ghost/migration.sql b/prisma/migrations/20220327180323_ghost/migration.sql new file mode 100644 index 000000000..3c1cec36f --- /dev/null +++ b/prisma/migrations/20220327180323_ghost/migration.sql @@ -0,0 +1,19 @@ +-- CreateTable +CREATE TABLE "Ghost" ( + "id" TEXT NOT NULL PRIMARY KEY, + "defaultEmail" TEXT NOT NULL, + "defaultPassword" TEXT NOT NULL, + "mariadbUser" TEXT NOT NULL, + "mariadbPassword" TEXT NOT NULL, + "mariadbRootUser" TEXT NOT NULL, + "mariadbRootUserPassword" TEXT NOT NULL, + "mariadbDatabase" TEXT, + "mariadbPublicPort" INTEGER, + "serviceId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Ghost_serviceId_fkey" FOREIGN KEY ("serviceId") REFERENCES "Service" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); + +-- CreateIndex +CREATE UNIQUE INDEX "Ghost_serviceId_key" ON "Ghost"("serviceId"); diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 95310d2aa..a849ae36f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -278,6 +278,7 @@ model Service { minio Minio? vscodeserver Vscodeserver? wordpress Wordpress? + ghost Ghost? serviceSecret ServiceSecret[] } @@ -332,3 +333,19 @@ model Wordpress { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } + +model Ghost { + id String @id @default(cuid()) + defaultEmail String + defaultPassword String + mariadbUser String + mariadbPassword String + mariadbRootUser String + mariadbRootUserPassword String + mariadbDatabase String? + mariadbPublicPort Int? + serviceId String @unique + service Service @relation(fields: [serviceId], references: [id]) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} diff --git a/src/app.d.ts b/src/app.d.ts index d69042fe8..2ebfc021b 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -8,9 +8,11 @@ declare namespace App { interface Platform {} interface Session extends SessionData {} interface Stuff { + service: any; application: any; isRunning: boolean; appId: string; + readOnly: boolean; } } diff --git a/src/lib/components/svg/services/Ghost.svelte b/src/lib/components/svg/services/Ghost.svelte new file mode 100644 index 000000000..567ab69bc --- /dev/null +++ b/src/lib/components/svg/services/Ghost.svelte @@ -0,0 +1,9 @@ + + +ghost logo diff --git a/src/lib/components/svg/services/N8n.svelte b/src/lib/components/svg/services/N8n.svelte new file mode 100644 index 000000000..04b2e8216 --- /dev/null +++ b/src/lib/components/svg/services/N8n.svelte @@ -0,0 +1,24 @@ + + + + + + + diff --git a/src/lib/components/svg/services/UptimeKuma.svelte b/src/lib/components/svg/services/UptimeKuma.svelte new file mode 100644 index 000000000..e043ea54b --- /dev/null +++ b/src/lib/components/svg/services/UptimeKuma.svelte @@ -0,0 +1,159 @@ + + + diff --git a/src/lib/database/common.ts b/src/lib/database/common.ts index 4638e483e..e89105f05 100644 --- a/src/lib/database/common.ts +++ b/src/lib/database/common.ts @@ -107,6 +107,7 @@ 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'], ports: { main: 8000 @@ -143,6 +144,7 @@ export const supportedServiceTypesAndVersions = [ name: 'wordpress', fancyName: 'Wordpress', baseImage: 'wordpress', + images: ['bitnami/mysql:5.7'], versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'], ports: { main: 80 @@ -165,6 +167,34 @@ export const supportedServiceTypesAndVersions = [ ports: { main: 8010 } + }, + { + name: 'n8n', + fancyName: 'n8n', + baseImage: 'n8nio/n8n', + versions: ['latest'], + ports: { + main: 5678 + } + }, + { + name: 'uptimekuma', + fancyName: 'Uptime Kuma', + baseImage: 'louislam/uptime-kuma', + versions: ['latest'], + ports: { + main: 3001 + } + }, + { + name: 'ghost', + fancyName: 'Ghost', + baseImage: 'bitnami/ghost', + images: ['bitnami/mariadb'], + versions: ['latest'], + ports: { + main: 2368 + } } ]; @@ -189,6 +219,13 @@ export function getServiceImage(type) { } return ''; } +export function getServiceImages(type) { + const found = supportedServiceTypesAndVersions.find((t) => t.name === type); + if (found) { + return found.images; + } + return []; +} export function generateDatabaseConfiguration(database) { const { id, diff --git a/src/lib/database/services.ts b/src/lib/database/services.ts index ea0be0a83..a5c2ee9ea 100644 --- a/src/lib/database/services.ts +++ b/src/lib/database/services.ts @@ -1,3 +1,4 @@ +import { asyncExecShell, getEngine } from '$lib/common'; import { decrypt, encrypt } from '$lib/crypto'; import cuid from 'cuid'; import { generatePassword } from '.'; @@ -20,6 +21,7 @@ export async function getService({ id, teamId }) { minio: true, vscodeserver: true, wordpress: true, + ghost: true, serviceSecret: true } }); @@ -43,12 +45,18 @@ export async function getService({ id, teamId }) { if (body.wordpress?.mysqlRootUserPassword) body.wordpress.mysqlRootUserPassword = decrypt(body.wordpress.mysqlRootUserPassword); + if (body.ghost?.mariadbPassword) body.ghost.mariadbPassword = decrypt(body.ghost.mariadbPassword); + if (body.ghost?.mariadbRootUserPassword) + body.ghost.mariadbRootUserPassword = decrypt(body.ghost.mariadbRootUserPassword); + if (body.ghost?.defaultPassword) body.ghost.defaultPassword = decrypt(body.ghost.defaultPassword); + if (body?.serviceSecret.length > 0) { body.serviceSecret = body.serviceSecret.map((s) => { s.value = decrypt(s.value); return s; }); } + return { ...body }; } @@ -119,6 +127,44 @@ export async function configureServiceType({ id, type }) { 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()}@coolify.io`; + 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 + } + } + } + }); } } export async function setServiceVersion({ id, version }) { @@ -139,7 +185,7 @@ export async function updatePlausibleAnalyticsService({ id, fqdn, email, usernam await prisma.plausibleAnalytics.update({ where: { serviceId: id }, data: { email, username } }); await prisma.service.update({ where: { id }, data: { name, fqdn } }); } -export async function updateNocoDbOrMinioService({ id, fqdn, name }) { +export async function updateService({ id, fqdn, name }) { return await prisma.service.update({ where: { id }, data: { fqdn, name } }); } export async function updateLanguageToolService({ id, fqdn, name }) { @@ -160,8 +206,15 @@ export async function updateWordpress({ id, fqdn, name, mysqlDatabase, extraConf export async function updateMinioService({ id, publicPort }) { return await prisma.minio.update({ where: { serviceId: id }, data: { publicPort } }); } +export async function updateGhostService({ id, fqdn, name, mariadbDatabase }) { + return await prisma.service.update({ + where: { id }, + data: { fqdn, name, ghost: { update: { mariadbDatabase } } } + }); +} export async function removeService({ id }) { + await prisma.ghost.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/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index e85f539de..eb74aa252 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -104,32 +104,34 @@ export async function generateSSLCerts() { }); for (const application of applications) { try { - const { - fqdn, - id, - destinationDocker: { engine, network }, - settings: { previews } - } = application; - const isRunning = await checkContainer(engine, id); - const domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - if (isRunning) { - if (isHttps) ssls.push({ domain, id, isCoolify: false }); - } - if (previews) { - const host = getEngine(engine); - const { stdout } = await asyncExecShell( - `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` - ); - const containers = stdout - .trim() - .split('\n') - .filter((a) => a) - .map((c) => c.replace(/"/g, '')); - if (containers.length > 0) { - for (const container of containers) { - let previewDomain = `${container.split('-')[1]}.${domain}`; - if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); + if (application.fqdn && application.destinationDockerId) { + const { + fqdn, + id, + destinationDocker: { engine, network }, + settings: { previews } + } = application; + const isRunning = await checkContainer(engine, id); + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + if (isRunning) { + if (isHttps) ssls.push({ domain, id, isCoolify: false }); + } + if (previews) { + const host = getEngine(engine); + const { stdout } = await asyncExecShell( + `DOCKER_HOST=${host} docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` + ); + const containers = stdout + .trim() + .split('\n') + .filter((a) => a) + .map((c) => c.replace(/"/g, '')); + if (containers.length > 0) { + for (const container of containers) { + let previewDomain = `${container.split('-')[1]}.${domain}`; + if (isHttps) ssls.push({ domain: previewDomain, id, isCoolify: false }); + } } } } @@ -143,26 +145,29 @@ export async function generateSSLCerts() { minio: true, plausibleAnalytics: true, vscodeserver: true, - wordpress: true + wordpress: true, + ghost: true }, orderBy: { createdAt: 'desc' } }); for (const service of services) { try { - const { - fqdn, - id, - type, - destinationDocker: { engine } - } = service; - const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); - if (found) { - const domain = getDomain(fqdn); - const isHttps = fqdn.startsWith('https://'); - const isRunning = await checkContainer(engine, id); - if (isRunning) { - if (isHttps) ssls.push({ domain, id, isCoolify: false }); + if (service.fqdn && service.destinationDockerId) { + const { + fqdn, + id, + type, + destinationDocker: { engine } + } = service; + const found = db.supportedServiceTypesAndVersions.find((a) => a.name === type); + if (found) { + const domain = getDomain(fqdn); + const isHttps = fqdn.startsWith('https://'); + const isRunning = await checkContainer(engine, id); + if (isRunning) { + if (isHttps) ssls.push({ domain, id, isCoolify: false }); + } } } } catch (error) { diff --git a/src/routes/applications/[id]/configuration/_GithubRepositories.svelte b/src/routes/applications/[id]/configuration/_GithubRepositories.svelte index 4afbea65c..06e58be58 100644 --- a/src/routes/applications/[id]/configuration/_GithubRepositories.svelte +++ b/src/routes/applications/[id]/configuration/_GithubRepositories.svelte @@ -1,6 +1,6 @@ + +
+
Ghost
+
+
+ + +
+
+ + +
+
+
MariaDB
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/src/routes/services/[id]/_Services/_Services.svelte b/src/routes/services/[id]/_Services/_Services.svelte index 896953cc4..6a54d9b98 100644 --- a/src/routes/services/[id]/_Services/_Services.svelte +++ b/src/routes/services/[id]/_Services/_Services.svelte @@ -10,6 +10,7 @@ import Setting from '$lib/components/Setting.svelte'; import { errorNotification } from '$lib/form'; import { toast } from '@zerodevx/svelte-toast'; + import Ghost from './_Ghost.svelte'; import MinIo from './_MinIO.svelte'; import PlausibleAnalytics from './_PlausibleAnalytics.svelte'; import VsCodeServer from './_VSCodeServer.svelte'; @@ -142,6 +143,8 @@ {:else if service.type === 'wordpress'} + {:else if service.type === 'ghost'} + {/if} diff --git a/src/routes/services/[id]/__layout.svelte b/src/routes/services/[id]/__layout.svelte index fffce0007..362f89786 100644 --- a/src/routes/services/[id]/__layout.svelte +++ b/src/routes/services/[id]/__layout.svelte @@ -35,6 +35,7 @@ } if (service.plausibleAnalytics?.email && service.plausibleAnalytics.username) readOnly = true; if (service.wordpress?.mysqlDatabase) readOnly = true; + if (service.ghost?.mariadbDatabase && service.ghost.mariadbDatabase) readOnly = true; return { props: { diff --git a/src/routes/services/[id]/configuration/type.svelte b/src/routes/services/[id]/configuration/type.svelte index 0dec45c5f..bf8575065 100644 --- a/src/routes/services/[id]/configuration/type.svelte +++ b/src/routes/services/[id]/configuration/type.svelte @@ -38,6 +38,9 @@ import { post } from '$lib/api'; import VaultWarden from '$lib/components/svg/services/VaultWarden.svelte'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; + import N8n from '$lib/components/svg/services/N8n.svelte'; + import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; + import Ghost from '$lib/components/svg/services/Ghost.svelte'; const { id } = $page.params; const from = $page.url.searchParams.get('from'); @@ -77,6 +80,12 @@ {:else if type.name === 'languagetool'} + {:else if type.name === 'n8n'} + + {:else if type.name === 'uptimekuma'} + + {:else if type.name === 'ghost'} + {/if}{type.fancyName} diff --git a/src/routes/services/[id]/ghost/index.json.ts b/src/routes/services/[id]/ghost/index.json.ts new file mode 100644 index 000000000..81ca8ade1 --- /dev/null +++ b/src/routes/services/[id]/ghost/index.json.ts @@ -0,0 +1,23 @@ +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, + ghost: { mariadbDatabase } + } = await event.request.json(); + if (fqdn) fqdn = fqdn.toLowerCase(); + try { + await db.updateGhostService({ id, fqdn, name, mariadbDatabase }); + return { status: 201 }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/ghost/start.json.ts b/src/routes/services/[id]/ghost/start.json.ts new file mode 100644 index 000000000..4fd9e7fa4 --- /dev/null +++ b/src/routes/services/[id]/ghost/start.json.ts @@ -0,0 +1,133 @@ +import { + asyncExecShell, + createDirectories, + getDomain, + getEngine, + getUserDetails +} from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { + type, + version, + destinationDockerId, + destinationDocker, + serviceSecret, + fqdn, + ghost: { + defaultEmail, + defaultPassword, + mariadbRootUser, + mariadbRootUserPassword, + mariadbDatabase, + mariadbPassword, + mariadbUser + } + } = service; + const network = destinationDockerId && destinationDocker.network; + const host = getEngine(destinationDocker.engine); + + const { workdir } = await createDirectories({ repository: type, buildId: id }); + const image = getServiceImage(type); + const domain = getDomain(fqdn); + const config = { + ghost: { + image: `${image}:${version}`, + volume: `${id}-ghost:/bitnami/ghost`, + environmentVariables: { + GHOST_HOST: domain, + 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 composeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.ghost.image, + networks: [network], + volumes: [config.ghost.volume], + environment: config.ghost.environmentVariables, + restart: 'always', + labels: makeLabelForServices('ghost'), + depends_on: [`${id}-mariadb`] + }, + [`${id}-mariadb`]: { + container_name: `${id}-mariadb`, + image: config.mariadb.image, + networks: [network], + volumes: [config.mariadb.volume], + environment: config.mariadb.environmentVariables, + restart: 'always' + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.ghost.volume.split(':')[0]]: { + name: config.ghost.volume.split(':')[0] + }, + [config.mariadb.volume.split(':')[0]]: { + name: config.mariadb.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/ghost/stop.json.ts b/src/routes/services/[id]/ghost/stop.json.ts new file mode 100644 index 000000000..478f5b946 --- /dev/null +++ b/src/routes/services/[id]/ghost/stop.json.ts @@ -0,0 +1,39 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker, fqdn } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + let found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + found = await checkContainer(engine, `${id}-mariadb`); + if (found) { + await removeDestinationDocker({ id: `${id}-mariadb`, engine }); + } + } catch (error) { + console.error(error); + } + } + + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/index.json.ts b/src/routes/services/[id]/index.json.ts index d712c1c72..676ff6405 100644 --- a/src/routes/services/[id]/index.json.ts +++ b/src/routes/services/[id]/index.json.ts @@ -4,7 +4,8 @@ import { generateDatabaseConfiguration, getServiceImage, getVersions, - ErrorHandler + ErrorHandler, + getServiceImages } from '$lib/database'; import { dockerInstance } from '$lib/docker'; import type { RequestHandler } from '@sveltejs/kit'; @@ -23,7 +24,13 @@ export const get: RequestHandler = async (event) => { const host = getEngine(destinationDocker.engine); const docker = dockerInstance({ destinationDocker }); const baseImage = getServiceImage(type); + const images = getServiceImages(type); docker.engine.pull(`${baseImage}:${version}`); + if (images?.length > 0) { + for (const image of images) { + docker.engine.pull(`${image}:latest`); + } + } try { const { stdout } = await asyncExecShell( `DOCKER_HOST=${host} docker inspect --format '{{json .State}}' ${id}` diff --git a/src/routes/services/[id]/index.svelte b/src/routes/services/[id]/index.svelte index 573ec26ac..ada5c0d9e 100644 --- a/src/routes/services/[id]/index.svelte +++ b/src/routes/services/[id]/index.svelte @@ -39,6 +39,9 @@ import cuid from 'cuid'; import { browser } from '$app/env'; import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; + import N8n from '$lib/components/svg/services/N8n.svelte'; + import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; + import Ghost from '$lib/components/svg/services/Ghost.svelte'; export let service; export let isRunning; @@ -109,6 +112,18 @@ + {:else if service.type === 'n8n'} + + + + {:else if service.type === 'uptimekuma'} + + + + {:else if service.type === 'ghost'} + + + {/if} diff --git a/src/routes/services/[id]/languagetool/start.json.ts b/src/routes/services/[id]/languagetool/start.json.ts index 4666fea79..a6a81d475 100644 --- a/src/routes/services/[id]/languagetool/start.json.ts +++ b/src/routes/services/[id]/languagetool/start.json.ts @@ -41,7 +41,7 @@ export const post: RequestHandler = async (event) => { networks: [network], environment: config.environmentVariables, restart: 'always', - volumes: [`${id}-ngrams:/ngrams`], + volumes: [config.volume], labels: makeLabelForServices('languagetool') } }, @@ -51,20 +51,20 @@ export const post: RequestHandler = async (event) => { } }, volumes: { - [`${id}-ngrams`]: { - external: true + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - try { - await asyncExecShell(`DOCKER_HOST=${host} docker volume create ${id}-ngrams`); - } catch (error) { - console.log(error); - } try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200 diff --git a/src/routes/services/[id]/minio/index.json.ts b/src/routes/services/[id]/minio/index.json.ts index be5c0525f..d717502c5 100644 --- a/src/routes/services/[id]/minio/index.json.ts +++ b/src/routes/services/[id]/minio/index.json.ts @@ -13,7 +13,7 @@ export const post: RequestHandler = async (event) => { if (fqdn) fqdn = fqdn.toLowerCase(); try { - await db.updateNocoDbOrMinioService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/minio/start.json.ts b/src/routes/services/[id]/minio/start.json.ts index 99665dfca..bcffd9e8f 100644 --- a/src/routes/services/[id]/minio/start.json.ts +++ b/src/routes/services/[id]/minio/start.json.ts @@ -76,19 +76,13 @@ export const post: RequestHandler = async (event) => { }, volumes: { [config.volume.split(':')[0]]: { - external: true + name: config.volume.split(':')[0] } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}` - ); - } catch (error) { - console.log(error); - } + try { await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); await db.updateMinioService({ id, publicPort }); diff --git a/src/routes/services/[id]/n8n/index.json.ts b/src/routes/services/[id]/n8n/index.json.ts new file mode 100644 index 000000000..5ec3fa69a --- /dev/null +++ b/src/routes/services/[id]/n8n/index.json.ts @@ -0,0 +1,20 @@ +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]/n8n/start.json.ts b/src/routes/services/[id]/n8n/start.json.ts new file mode 100644 index 000000000..4a04917c7 --- /dev/null +++ b/src/routes/services/[id]/n8n/start.json.ts @@ -0,0 +1,77 @@ +import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret } = 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 = { + image: `${image}:${version}`, + volume: `${id}-n8n:/root/.n8n`, + environmentVariables: {} + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const composeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + networks: [network], + volumes: [config.volume], + environment: config.environmentVariables, + restart: 'always', + labels: makeLabelForServices('n8n') + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/n8n/stop.json.ts b/src/routes/services/[id]/n8n/stop.json.ts new file mode 100644 index 000000000..c604e1cc3 --- /dev/null +++ b/src/routes/services/[id]/n8n/stop.json.ts @@ -0,0 +1,35 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker, fqdn } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + const found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + } catch (error) { + console.error(error); + } + } + + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/nocodb/index.json.ts b/src/routes/services/[id]/nocodb/index.json.ts index bc9f1a0d1..5ec3fa69a 100644 --- a/src/routes/services/[id]/nocodb/index.json.ts +++ b/src/routes/services/[id]/nocodb/index.json.ts @@ -12,7 +12,7 @@ export const post: RequestHandler = async (event) => { if (fqdn) fqdn = fqdn.toLowerCase(); try { - await db.updateNocoDbOrMinioService({ id, fqdn, name }); + await db.updateService({ id, fqdn, name }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/services/[id]/nocodb/start.json.ts b/src/routes/services/[id]/nocodb/start.json.ts index 1088bf817..ebc59ce97 100644 --- a/src/routes/services/[id]/nocodb/start.json.ts +++ b/src/routes/services/[id]/nocodb/start.json.ts @@ -52,6 +52,11 @@ export const post: RequestHandler = async (event) => { await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200 diff --git a/src/routes/services/[id]/plausibleanalytics/start.json.ts b/src/routes/services/[id]/plausibleanalytics/start.json.ts index 4b635264b..836bd0178 100644 --- a/src/routes/services/[id]/plausibleanalytics/start.json.ts +++ b/src/routes/services/[id]/plausibleanalytics/start.json.ts @@ -158,29 +158,21 @@ COPY ./init-db.sh /docker-entrypoint-initdb.d/init-db.sh`; }, volumes: { [config.postgresql.volume.split(':')[0]]: { - external: true + name: config.postgresql.volume.split(':')[0] }, [config.clickhouse.volume.split(':')[0]]: { - external: true + name: config.clickhouse.volume.split(':')[0] } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.postgresql.volume.split(':')[0]}` - ); - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.clickhouse.volume.split(':')[0]}` - ); - } catch (error) { - console.log(error); + if (version === 'latest') { + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); } await asyncExecShell( `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up --build -d` ); - return { status: 200 }; diff --git a/src/routes/services/[id]/uptimekuma/index.json.ts b/src/routes/services/[id]/uptimekuma/index.json.ts new file mode 100644 index 000000000..5ec3fa69a --- /dev/null +++ b/src/routes/services/[id]/uptimekuma/index.json.ts @@ -0,0 +1,20 @@ +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]/uptimekuma/start.json.ts b/src/routes/services/[id]/uptimekuma/start.json.ts new file mode 100644 index 000000000..845873bb0 --- /dev/null +++ b/src/routes/services/[id]/uptimekuma/start.json.ts @@ -0,0 +1,77 @@ +import { asyncExecShell, createDirectories, getEngine, getUserDetails } from '$lib/common'; +import * as db from '$lib/database'; +import { promises as fs } from 'fs'; +import yaml from 'js-yaml'; +import type { RequestHandler } from '@sveltejs/kit'; +import { ErrorHandler, getServiceImage } from '$lib/database'; +import { makeLabelForServices } from '$lib/buildPacks/common'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { type, version, destinationDockerId, destinationDocker, serviceSecret } = 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 = { + image: `${image}:${version}`, + volume: `${id}-uptimekuma:/app/data`, + environmentVariables: {} + }; + if (serviceSecret.length > 0) { + serviceSecret.forEach((secret) => { + config.environmentVariables[secret.name] = secret.value; + }); + } + const composeFile = { + version: '3.8', + services: { + [id]: { + container_name: id, + image: config.image, + networks: [network], + volumes: [config.volume], + environment: config.environmentVariables, + restart: 'always', + labels: makeLabelForServices('uptimekuma') + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: { + [config.volume.split(':')[0]]: { + name: config.volume.split(':')[0] + } + } + }; + const composeFileDestination = `${workdir}/docker-compose.yaml`; + await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); + + try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/uptimekuma/stop.json.ts b/src/routes/services/[id]/uptimekuma/stop.json.ts new file mode 100644 index 000000000..c604e1cc3 --- /dev/null +++ b/src/routes/services/[id]/uptimekuma/stop.json.ts @@ -0,0 +1,35 @@ +import { getUserDetails, removeDestinationDocker } from '$lib/common'; +import * as db from '$lib/database'; +import { ErrorHandler } from '$lib/database'; +import { checkContainer } from '$lib/haproxy'; +import type { RequestHandler } from '@sveltejs/kit'; + +export const post: RequestHandler = async (event) => { + const { teamId, status, body } = await getUserDetails(event); + if (status === 401) return { status, body }; + + const { id } = event.params; + + try { + const service = await db.getService({ id, teamId }); + const { destinationDockerId, destinationDocker, fqdn } = service; + if (destinationDockerId) { + const engine = destinationDocker.engine; + + try { + const found = await checkContainer(engine, id); + if (found) { + await removeDestinationDocker({ id, engine }); + } + } catch (error) { + console.error(error); + } + } + + return { + status: 200 + }; + } catch (error) { + return ErrorHandler(error); + } +}; diff --git a/src/routes/services/[id]/vaultwarden/start.json.ts b/src/routes/services/[id]/vaultwarden/start.json.ts index 703a71473..f977adda3 100644 --- a/src/routes/services/[id]/vaultwarden/start.json.ts +++ b/src/routes/services/[id]/vaultwarden/start.json.ts @@ -52,20 +52,18 @@ export const post: RequestHandler = async (event) => { }, volumes: { [config.volume.split(':')[0]]: { - external: true + name: config.volume.split(':')[0] } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); try { - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}` - ); - } catch (error) { - console.log(error); - } - try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200 diff --git a/src/routes/services/[id]/vscodeserver/start.json.ts b/src/routes/services/[id]/vscodeserver/start.json.ts index be43cb2a7..6f51fcf2a 100644 --- a/src/routes/services/[id]/vscodeserver/start.json.ts +++ b/src/routes/services/[id]/vscodeserver/start.json.ts @@ -61,29 +61,20 @@ export const post: RequestHandler = async (event) => { }, volumes: { [config.volume.split(':')[0]]: { - external: true + name: config.volume.split(':')[0] } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.volume.split(':')[0]}` - ); - } catch (error) { - console.log(error); - } - - try { - await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); - return { - status: 200 - }; - } catch (error) { - return ErrorHandler(error); + if (version === 'latest') { + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull`); } + await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); + return { + status: 200 + }; } catch (error) { return ErrorHandler(error); } diff --git a/src/routes/services/[id]/wordpress/start.json.ts b/src/routes/services/[id]/wordpress/start.json.ts index d1685d8b1..98e7d6aab 100644 --- a/src/routes/services/[id]/wordpress/start.json.ts +++ b/src/routes/services/[id]/wordpress/start.json.ts @@ -72,6 +72,7 @@ export const post: RequestHandler = async (event) => { container_name: id, image: config.wordpress.image, environment: config.wordpress.environmentVariables, + volumes: [config.wordpress.volume], networks: [network], restart: 'always', depends_on: [`${id}-mysql`], @@ -80,6 +81,7 @@ export const post: RequestHandler = async (event) => { [`${id}-mysql`]: { container_name: `${id}-mysql`, image: config.mysql.image, + volumes: [config.mysql.volume], environment: config.mysql.environmentVariables, networks: [network], restart: 'always' @@ -91,29 +93,22 @@ export const post: RequestHandler = async (event) => { } }, volumes: { - [config.mysql.volume.split(':')[0]]: { - external: true - }, [config.wordpress.volume.split(':')[0]]: { - external: true + name: config.wordpress.volume.split(':')[0] + }, + [config.mysql.volume.split(':')[0]]: { + name: config.mysql.volume.split(':')[0] } } }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - - try { - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.mysql.volume.split(':')[0]}` - ); - await asyncExecShell( - `DOCKER_HOST=${host} docker volume create ${config.wordpress.volume.split(':')[0]}` - ); - } catch (error) { - console.log(error); - } - try { + if (version === 'latest') { + await asyncExecShell( + `DOCKER_HOST=${host} docker compose -f ${composeFileDestination} pull` + ); + } await asyncExecShell(`DOCKER_HOST=${host} docker compose -f ${composeFileDestination} up -d`); return { status: 200 diff --git a/src/routes/services/index.svelte b/src/routes/services/index.svelte index a4d34cd5f..2b5584270 100644 --- a/src/routes/services/index.svelte +++ b/src/routes/services/index.svelte @@ -8,6 +8,9 @@ import LanguageTool from '$lib/components/svg/services/LanguageTool.svelte'; import { post } from '$lib/api'; import { goto } from '$app/navigation'; + import N8n from '$lib/components/svg/services/N8n.svelte'; + import UptimeKuma from '$lib/components/svg/services/UptimeKuma.svelte'; + import Ghost from '$lib/components/svg/services/Ghost.svelte'; export let services; async function newService() { @@ -58,6 +61,12 @@ {:else if service.type === 'languagetool'} + {:else if service.type === 'n8n'} + + {:else if service.type === 'uptimekuma'} + + {:else if service.type === 'ghost'} + {/if}
{service.name} diff --git a/src/tailwind.css b/src/tailwind.css index 1a30a3ac0..1f357acfb 100644 --- a/src/tailwind.css +++ b/src/tailwind.css @@ -37,6 +37,29 @@ textarea { @apply min-w-[24rem] rounded border border-transparent bg-transparent bg-coolgray-200 p-2 text-xs tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:border disabled:border-dashed disabled:border-coolgray-300 disabled:bg-transparent md:text-sm; } +#svelte .custom-select-wrapper .selectContainer.disabled input { + @apply placeholder:text-stone-600; +} + +#svelte .custom-select-wrapper .selectContainer input { + @apply text-white; +} + +#svelte .custom-select-wrapper .selectContainer { + @apply h-12 w-96 rounded border-none bg-coolgray-200 p-2 text-xs font-bold tracking-tight outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 md:text-sm; +} + +#svelte .listContainer { + @apply bg-coolgray-400 text-white scrollbar-w-2 scrollbar-thumb-coollabs scrollbar-track-coolgray-200; +} + +#svelte .item.hover { + @apply bg-coolgray-100 text-white; +} +#svelte .item.active { + @apply bg-coollabs text-white; +} + select { @apply h-12 w-96 rounded bg-coolgray-200 p-2 text-xs font-bold tracking-tight text-white placeholder-stone-600 outline-none transition duration-150 hover:bg-coolgray-500 focus:bg-coolgray-500 disabled:text-stone-600 md:text-sm; } diff --git a/static/ghost.png b/static/ghost.png new file mode 100644 index 000000000..f4e300654 Binary files /dev/null and b/static/ghost.png differ