From 4ad7e1f8e661c92af57e2b4a00e3765d8873332a Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Mon, 12 Dec 2022 16:04:41 +0100 Subject: [PATCH] wip --- apps/client/package.json | 1 + apps/client/src/lib/store.ts | 4 +- apps/client/src/routes/+layout.svelte | 4 +- apps/client/src/routes/+page.svelte | 40 ++-- apps/client/tsconfig.json | 11 +- apps/server/package.json | 1 + apps/server/src/env.js | 2 +- apps/server/src/lib/common.ts | 29 ++- apps/server/src/lib/docker.ts | 101 ++++++++-- apps/server/src/lib/executeCommand.ts | 25 +-- apps/server/src/lib/logging.ts | 16 +- apps/server/src/prisma.ts | 2 +- apps/server/{ => src}/tags.json | 0 apps/server/{ => src}/templates.json | 0 apps/server/src/trpc/context.ts | 4 +- apps/server/src/trpc/index.ts | 6 +- apps/server/src/trpc/routers/applications.ts | 196 +++++++++++++------ apps/server/src/trpc/routers/databases.ts | 84 ++++++++ apps/server/src/trpc/routers/index.ts | 1 + apps/server/src/trpc/routers/services.ts | 183 +++++++++++------ pnpm-lock.yaml | 8 + 21 files changed, 520 insertions(+), 198 deletions(-) rename apps/server/{ => src}/tags.json (100%) rename apps/server/{ => src}/templates.json (100%) create mode 100644 apps/server/src/trpc/routers/databases.ts diff --git a/apps/client/package.json b/apps/client/package.json index 2f067f30a..b9fda399a 100644 --- a/apps/client/package.json +++ b/apps/client/package.json @@ -17,6 +17,7 @@ "@playwright/test": "1.28.1", "@sveltejs/adapter-static": "1.0.0-next.48", "@sveltejs/kit": "1.0.0-next.572", + "@types/js-cookie": "3.0.2", "@typescript-eslint/eslint-plugin": "5.44.0", "@typescript-eslint/parser": "5.44.0", "autoprefixer": "10.4.13", diff --git a/apps/client/src/lib/store.ts b/apps/client/src/lib/store.ts index 0022e6c93..0b08d624e 100644 --- a/apps/client/src/lib/store.ts +++ b/apps/client/src/lib/store.ts @@ -1,6 +1,6 @@ -import { writable, readable, type Writable, type Readable } from 'svelte/store'; +import { writable, readable, type Writable } from 'svelte/store'; import superjson from 'superjson'; -import type { AppRouter, PrismaPermission } from 'server/src/trpc'; +import type { AppRouter } from 'server/src/trpc'; import { createTRPCProxyClient, httpBatchLink } from '@trpc/client'; import { browser, dev } from '$app/environment'; import Cookies from 'js-cookie'; diff --git a/apps/client/src/routes/+layout.svelte b/apps/client/src/routes/+layout.svelte index 0664797ca..d219cd63a 100644 --- a/apps/client/src/routes/+layout.svelte +++ b/apps/client/src/routes/+layout.svelte @@ -6,7 +6,7 @@ import { appSession } from '$lib/store'; import Tooltip from '$lib/components/Tooltip.svelte'; import { page } from '$app/stores'; - import UpdateAvailable from '$lib/components/UpdateAvailable.svelte'; + // import UpdateAvailable from '$lib/components/UpdateAvailable.svelte'; import Cookies from 'js-cookie'; import { errorNotification } from '$lib/common'; import Toasts from '$lib/components/Toasts.svelte'; @@ -346,7 +346,7 @@ IAM {#if $appSession.pendingInvitations.length > 0} {pendingInvitations.length}{$appSession.pendingInvitations.length} {/if} diff --git a/apps/client/src/routes/+page.svelte b/apps/client/src/routes/+page.svelte index b3e1c2879..71163b87e 100644 --- a/apps/client/src/routes/+page.svelte +++ b/apps/client/src/routes/+page.svelte @@ -2,6 +2,18 @@ import type { PageData } from './$types'; export let data: PageData; + import { dev } from '$app/environment'; + import { onMount } from 'svelte'; + + import { asyncSleep, errorNotification, getRndInteger } from '$lib/common'; + import { appSession, search, t } from '$lib/store'; + + import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte'; + import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte'; + import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte'; + import NewResource from '$lib/components/NewResource.svelte'; + import DeleteIcon from '$lib/components/DeleteIcon.svelte'; + const { applications, foundUnconfiguredApplication, @@ -14,17 +26,6 @@ settings } = data; let filtered: any = setInitials(); - import { asyncSleep, errorNotification, getRndInteger } from '$lib/common'; - import { appSession, search, t } from '$lib/store'; - - import ApplicationsIcons from '$lib/components/svg/applications/ApplicationIcons.svelte'; - import DatabaseIcons from '$lib/components/svg/databases/DatabaseIcons.svelte'; - import ServiceIcons from '$lib/components/svg/services/ServiceIcons.svelte'; - import { dev } from '$app/environment'; - import NewResource from '$lib/components/NewResource.svelte'; - import { onMount } from 'svelte'; - import DeleteIcon from '$lib/components/DeleteIcon.svelte'; - let numberOfGetStatus = 0; let status: any = {}; let noInitialStatus: any = { @@ -155,7 +156,7 @@ let isRunning = false; let isDegraded = false; if (buildPack || simpleDockerfile) { - const response = await t.applications.status.query({ id }) + const response = await t.applications.status.query({ id }); if (response.length === 0) { isRunning = false; } else if (response.length === 1) { @@ -177,7 +178,7 @@ } } } else if (typeof dualCerts !== 'undefined') { - const response = await t.services.status.query({ id }) + const response = await t.services.status.query({ id }); if (Object.keys(response).length === 0) { isRunning = false; } else { @@ -197,7 +198,7 @@ } } } else { - const response = await get(`/databases/${id}/status`); + const response = await t.databases.status.query({ id }); isRunning = response.isRunning; } @@ -381,7 +382,7 @@ 'Are you sure? This will delete all UNCONFIGURED applications and their data.' ); if (sure) { - // await post(`/applications/cleanup/unconfigured`, {}); + await t.applications.cleanup.query(); return window.location.reload(); } } catch (error) { @@ -394,7 +395,7 @@ 'Are you sure? This will delete all UNCONFIGURED services and their data.' ); if (sure) { - // await post(`/services/cleanup/unconfigured`, {}); + await t.services.cleanup.query(); return window.location.reload(); } } catch (error) { @@ -407,7 +408,7 @@ 'Are you sure? This will delete all UNCONFIGURED databases and their data.' ); if (sure) { - // await post(`/databases/cleanup/unconfigured`, {}); + await t.databases.cleanup.query(); return window.location.reload(); } } catch (error) { @@ -418,7 +419,7 @@ try { const sure = confirm('Are you sure? This will delete this application!'); if (sure) { - // await del(`/applications/${id}`, { force: true }); + await t.applications.delete.mutate({ id, force: true }); return window.location.reload(); } } catch (error) { @@ -429,6 +430,7 @@ try { const sure = confirm('Are you sure? This will delete this service!'); if (sure) { + await t.services.delete.mutate({ id }); // await del(`/services/${id}`, {}); return window.location.reload(); } @@ -440,7 +442,7 @@ try { const sure = confirm('Are you sure? This will delete this database!'); if (sure) { - // await del(`/databases/${id}`, { force: true }); + await t.databases.delete.mutate({ id, force: true }); return window.location.reload(); } } catch (error) { diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index b959b7dc5..dc270e7f0 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -1,5 +1,6 @@ { "extends": "./.svelte-kit/tsconfig.json", + "exclude": ["node_modules/*", ".svelte-kit/*", "public/*"], "compilerOptions": { "allowJs": true, "checkJs": true, @@ -10,12 +11,8 @@ "sourceMap": true, "strict": false, "paths": { - "$lib": [ - "src/lib" - ], - "$lib/*": [ - "src/lib/*" - ], + "$lib": ["src/lib"], + "$lib/*": ["src/lib/*"] } } -} \ No newline at end of file +} diff --git a/apps/server/package.json b/apps/server/package.json index 3ce4e8afa..2ae9cbb50 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -51,6 +51,7 @@ "@types/node": "18.11.9", "@types/node-fetch": "2.6.2", "@types/shell-quote": "^1.7.1", + "@types/bcryptjs": "^2.4.2", "@types/ws": "8.5.3", "npm-run-all": "4.1.5", "rimraf": "3.0.2", diff --git a/apps/server/src/env.js b/apps/server/src/env.js index f69754110..fece0be52 100644 --- a/apps/server/src/env.js +++ b/apps/server/src/env.js @@ -1,5 +1,5 @@ const dotenv = require('dotenv'); -const isDev = process.env.NODE_ENV === 'development'; +// const isDev = process.env.NODE_ENV === 'development'; // dotenv.config({ path: isDev ? '../../.env' : '.env' }); dotenv.config(); const { z } = require('zod'); diff --git a/apps/server/src/lib/common.ts b/apps/server/src/lib/common.ts index 77b73b284..f6eb14199 100644 --- a/apps/server/src/lib/common.ts +++ b/apps/server/src/lib/common.ts @@ -68,8 +68,8 @@ export const decrypt = (hashString: string) => { return false; }; -export function generateRangeArray(start, end) { - return Array.from({ length: end - start }, (v, k) => k + start); +export function generateRangeArray(start: number, end: number) { + return Array.from({ length: end - start }, (_v, k) => k + start); } export function generateTimestamp(): string { return `${day().format('HH:mm:ss.SSS')}`; @@ -94,7 +94,7 @@ export async function getTemplates() { let data = await open.readFile({ encoding: 'utf-8' }); let jsonData = JSON.parse(data); if (isARM(process.arch)) { - jsonData = jsonData.filter((d) => d.arch !== 'amd64'); + jsonData = jsonData.filter((d: { arch: string }) => d.arch !== 'amd64'); } return jsonData; } catch (error) { @@ -109,3 +109,26 @@ export function isARM(arch: string) { } return false; } + +export async function removeService({ id }: { id: string }): Promise { + await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); + await prisma.serviceSetting.deleteMany({ where: { serviceId: id } }); + await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); + await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); + await prisma.fider.deleteMany({ where: { serviceId: id } }); + await prisma.ghost.deleteMany({ where: { serviceId: id } }); + await prisma.umami.deleteMany({ where: { serviceId: id } }); + await prisma.hasura.deleteMany({ where: { serviceId: id } }); + await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); + await prisma.minio.deleteMany({ where: { serviceId: id } }); + await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); + await prisma.wordpress.deleteMany({ where: { serviceId: id } }); + await prisma.glitchTip.deleteMany({ where: { serviceId: id } }); + await prisma.moodle.deleteMany({ where: { serviceId: id } }); + await prisma.appwrite.deleteMany({ where: { serviceId: id } }); + await prisma.searxng.deleteMany({ where: { serviceId: id } }); + await prisma.weblate.deleteMany({ where: { serviceId: id } }); + await prisma.taiga.deleteMany({ where: { serviceId: id } }); + + await prisma.service.delete({ where: { id } }); +} diff --git a/apps/server/src/lib/docker.ts b/apps/server/src/lib/docker.ts index 27bf380a3..85eb78e30 100644 --- a/apps/server/src/lib/docker.ts +++ b/apps/server/src/lib/docker.ts @@ -1,31 +1,39 @@ -import { executeCommand } from "./executeCommand"; +import { executeCommand } from './executeCommand'; -export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> { +export async function checkContainer({ + dockerId, + container, + remove = false +}: { + dockerId: string; + container: string; + remove?: boolean; +}): Promise<{ + found: boolean; + status?: { isExited: boolean; isRunning: boolean; isRestarting: boolean }; +}> { let containerFound = false; try { const { stdout } = await executeCommand({ dockerId, - command: - `docker inspect --format '{{json .State}}' ${container}` + command: `docker inspect --format '{{json .State}}' ${container}` }); - containerFound = true + containerFound = true; const parsedStdout = JSON.parse(stdout); const status = parsedStdout.Status; const isRunning = status === 'running'; - const isRestarting = status === 'restarting' - const isExited = status === 'exited' + const isRestarting = status === 'restarting'; + const isExited = status === 'exited'; if (status === 'created') { await executeCommand({ dockerId, - command: - `docker rm ${container}` + command: `docker rm ${container}` }); } if (remove && status === 'exited') { await executeCommand({ dockerId, - command: - `docker rm ${container}` + command: `docker rm ${container}` }); } @@ -43,5 +51,74 @@ export async function checkContainer({ dockerId, container, remove = false }: { return { found: false }; +} -} \ No newline at end of file +export async function removeContainer({ + id, + dockerId +}: { + id: string; + dockerId: string; +}): Promise { + try { + const { stdout } = await executeCommand({ + dockerId, + command: `docker inspect --format '{{json .State}}' ${id}` + }); + if (JSON.parse(stdout).Running) { + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }); + await executeCommand({ dockerId, command: `docker rm ${id}` }); + } + if (JSON.parse(stdout).Status === 'exited') { + await executeCommand({ dockerId, command: `docker rm ${id}` }); + } + } catch (error) { + throw error; + } +} + +export async function stopDatabaseContainer(database: any): Promise { + let everStarted = false; + const { + id, + destinationDockerId, + destinationDocker: { engine, id: dockerId } + } = database; + if (destinationDockerId) { + try { + const { stdout } = await executeCommand({ + dockerId, + command: `docker inspect --format '{{json .State}}' ${id}` + }); + + if (stdout) { + everStarted = true; + await removeContainer({ id, dockerId }); + } + } catch (error) { + // + } + } + return everStarted; +} +export async function stopTcpHttpProxy( + id: string, + destinationDocker: any, + publicPort: number, + forceName: string | null = null +): Promise<{ stdout: string; stderr: string } | Error | unknown> { + const { id: dockerId } = destinationDocker; + let container = `${id}-${publicPort}`; + if (forceName) container = forceName; + const { found } = await checkContainer({ dockerId, container }); + try { + if (!found) return true; + return await executeCommand({ + dockerId, + command: `docker stop -t 0 ${container} && docker rm ${container}`, + shell: true + }); + } catch (error) { + return error; + } +} diff --git a/apps/server/src/lib/executeCommand.ts b/apps/server/src/lib/executeCommand.ts index 198c4046c..8a3bf4afa 100644 --- a/apps/server/src/lib/executeCommand.ts +++ b/apps/server/src/lib/executeCommand.ts @@ -6,7 +6,7 @@ import sshConfig from 'ssh-config'; import { getFreeSSHLocalPort } from './ssh'; import { env } from '../env'; -import { saveBuildLog } from './logging'; +import { BuildLog, saveBuildLog } from './logging'; import { decrypt } from './common'; export async function executeCommand({ @@ -31,23 +31,26 @@ export async function executeCommand({ const { execa, execaCommand } = await import('execa'); const { parse } = await import('shell-quote'); const parsedCommand = parse(command); - const dockerCommand = parsedCommand[0]; - const dockerArgs = parsedCommand.slice(1); + const dockerCommand = parsedCommand[0]?.toString(); + const dockerArgs = parsedCommand.slice(1).toString(); - if (dockerId) { + if (dockerId && dockerCommand && dockerArgs) { const destinationDocker = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }); if (!destinationDocker) { throw new Error('Destination docker not found'); } - let { remoteEngine, remoteIpAddress, engine } = destinationDocker; + let { + remoteEngine, + remoteIpAddress, + engine = 'unix:///var/run/docker.sock' + } = destinationDocker; if (remoteEngine) { await createRemoteEngineConfiguration(dockerId); engine = `ssh://${remoteIpAddress}-remote`; - } else { - engine = 'unix:///var/run/docker.sock'; } + if (env.CODESANDBOX_HOST) { if (command.startsWith('docker compose')) { command = command.replace(/docker compose/gi, 'docker-compose'); @@ -73,12 +76,12 @@ export async function executeCommand({ } const logs: any[] = []; if (subprocess && subprocess.stdout && subprocess.stderr) { - subprocess.stdout.on('data', async (data) => { + subprocess.stdout.on('data', async (data: string) => { const stdout = data.toString(); const array = stdout.split('\n'); for (const line of array) { if (line !== '\n' && line !== '') { - const log = { + const log: BuildLog = { line: `${line.replace('\n', '')}`, buildId, applicationId @@ -90,7 +93,7 @@ export async function executeCommand({ } } }); - subprocess.stderr.on('data', async (data) => { + subprocess.stderr.on('data', async (data: string) => { const stderr = data.toString(); const array = stderr.split('\n'); for (const line of array) { @@ -107,7 +110,7 @@ export async function executeCommand({ } } }); - subprocess.on('exit', async (code) => { + subprocess.on('exit', async (code: number) => { if (code === 0) { resolve('success'); } else { diff --git a/apps/server/src/lib/logging.ts b/apps/server/src/lib/logging.ts index c8f460402..bb4dde7a0 100644 --- a/apps/server/src/lib/logging.ts +++ b/apps/server/src/lib/logging.ts @@ -2,15 +2,13 @@ import { prisma } from '../prisma'; import { encrypt, generateTimestamp, isDev } from './common'; import { day } from './dayjs'; -export const saveBuildLog = async ({ - line, - buildId, - applicationId -}: { - line: string; - buildId: string; - applicationId: string; -}): Promise => { +export type Line = string | { shortMessage: string; stderr: string }; +export type BuildLog = { + line: Line; + buildId?: string; + applicationId?: string; +}; +export const saveBuildLog = async ({ line, buildId, applicationId }: BuildLog): Promise => { if (buildId === 'undefined' || buildId === 'null' || !buildId) return; if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return; const { default: got } = await import('got'); diff --git a/apps/server/src/prisma.ts b/apps/server/src/prisma.ts index e315eae42..7c818b869 100644 --- a/apps/server/src/prisma.ts +++ b/apps/server/src/prisma.ts @@ -12,7 +12,7 @@ const prismaGlobal = global as typeof global & { export const prisma: PrismaClient = prismaGlobal.prisma || new PrismaClient({ - log: env.NODE_ENV === 'developments' ? ['query', 'error', 'warn'] : ['error'] + log: env.NODE_ENV === 'development' ? ['query', 'error', 'warn'] : ['error'] }); if (env.NODE_ENV !== 'production') { diff --git a/apps/server/tags.json b/apps/server/src/tags.json similarity index 100% rename from apps/server/tags.json rename to apps/server/src/tags.json diff --git a/apps/server/templates.json b/apps/server/src/templates.json similarity index 100% rename from apps/server/templates.json rename to apps/server/src/templates.json diff --git a/apps/server/src/trpc/context.ts b/apps/server/src/trpc/context.ts index 8c7631f8d..e1e64c29f 100644 --- a/apps/server/src/trpc/context.ts +++ b/apps/server/src/trpc/context.ts @@ -1,5 +1,5 @@ -import { inferAsyncReturnType } from '@trpc/server'; -import { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; +import type { inferAsyncReturnType } from '@trpc/server'; +import type { CreateFastifyContextOptions } from '@trpc/server/adapters/fastify'; import jwt from 'jsonwebtoken'; import { env } from '../env'; export interface User { diff --git a/apps/server/src/trpc/index.ts b/apps/server/src/trpc/index.ts index 4eddbb785..a5bc3e969 100644 --- a/apps/server/src/trpc/index.ts +++ b/apps/server/src/trpc/index.ts @@ -6,7 +6,8 @@ import { authRouter, dashboardRouter, applicationsRouter, - servicesRouter + servicesRouter, + databasesRouter } from './routers'; export const appRouter = router({ @@ -14,7 +15,8 @@ export const appRouter = router({ auth: authRouter, dashboard: dashboardRouter, applications: applicationsRouter, - services: servicesRouter + services: servicesRouter, + databases: databasesRouter }); export type AppRouter = typeof appRouter; diff --git a/apps/server/src/trpc/routers/applications.ts b/apps/server/src/trpc/routers/applications.ts index 3a37d70e8..3277b43cc 100644 --- a/apps/server/src/trpc/routers/applications.ts +++ b/apps/server/src/trpc/routers/applications.ts @@ -1,72 +1,44 @@ import { z } from 'zod'; import { privateProcedure, router } from '../trpc'; -import { decrypt, isARM, listSettings } from '../../lib/common'; +import { decrypt, isARM } from '../../lib/common'; import { prisma } from '../../prisma'; import { executeCommand } from '../../lib/executeCommand'; -import { checkContainer } from '../../lib/docker'; +import { checkContainer, removeContainer } from '../../lib/docker'; export const applicationsRouter = router({ - status: privateProcedure - .input( - z.object({ - id: z.string() - }) - ) - .query(async ({ ctx, input }) => { - const id = input.id; - const teamId = ctx.user?.teamId; - if (!teamId) { - throw { status: 400, message: 'Team not found.' }; - } - let payload = []; - const application: any = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - if (application.buildPack === 'compose') { - const { stdout: containers } = await executeCommand({ - dockerId: application.destinationDocker.id, - command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` - }); - const containersArray = containers.trim().split('\n'); - if (containersArray.length > 0 && containersArray[0] !== '') { - for (const container of containersArray) { - let isRunning = false; - let isExited = false; - let isRestarting = false; - const containerObj = JSON.parse(container); - const status = containerObj.State; - if (status === 'running') { - isRunning = true; - } - if (status === 'exited') { - isExited = true; - } - if (status === 'restarting') { - isRestarting = true; - } - payload.push({ - name: containerObj.Names, - status: { - isRunning, - isExited, - isRestarting - } - }); + status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => { + const id: string = input.id; + const teamId = ctx.user?.teamId; + if (!teamId) { + throw { status: 400, message: 'Team not found.' }; + } + let payload = []; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + if (application.buildPack === 'compose') { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` + }); + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + for (const container of containersArray) { + let isRunning = false; + let isExited = false; + let isRestarting = false; + const containerObj = JSON.parse(container); + const status = containerObj.State; + if (status === 'running') { + isRunning = true; + } + if (status === 'exited') { + isExited = true; + } + if (status === 'restarting') { + isRestarting = true; } - } - } else { - let isRunning = false; - let isExited = false; - let isRestarting = false; - const status = await checkContainer({ - dockerId: application.destinationDocker.id, - container: id - }); - if (status?.found) { - isRunning = status.status.isRunning; - isExited = status.status.isExited; - isRestarting = status.status.isRestarting; payload.push({ - name: id, + name: containerObj.Names, status: { isRunning, isExited, @@ -75,8 +47,108 @@ export const applicationsRouter = router({ }); } } + } else { + let isRunning = false; + let isExited = false; + let isRestarting = false; + const status = await checkContainer({ + dockerId: application.destinationDocker.id, + container: id + }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting; + payload.push({ + name: id, + status: { + isRunning, + isExited, + isRestarting + } + }); + } } - return payload; + } + return payload; + }), + cleanup: privateProcedure.query(async ({ ctx }) => { + const teamId = ctx.user?.teamId; + let applications = await prisma.application.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true } + }); + for (const application of applications) { + if ( + !application.buildPack || + !application.destinationDockerId || + !application.branch || + (!application.settings?.isBot && !application?.fqdn) + ) { + if (application?.destinationDockerId && application.destinationDocker?.network) { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${application.id} --format '{{json .}}'` + }); + if (containers) { + const containersArray = containers.trim().split('\n'); + for (const container of containersArray) { + const containerObj = JSON.parse(container); + const id = containerObj.ID; + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } + } + } + await prisma.applicationSettings.deleteMany({ where: { applicationId: application.id } }); + await prisma.buildLog.deleteMany({ where: { applicationId: application.id } }); + await prisma.build.deleteMany({ where: { applicationId: application.id } }); + await prisma.secret.deleteMany({ where: { applicationId: application.id } }); + await prisma.applicationPersistentStorage.deleteMany({ + where: { applicationId: application.id } + }); + await prisma.applicationConnectedDatabase.deleteMany({ + where: { applicationId: application.id } + }); + await prisma.application.deleteMany({ where: { id: application.id } }); + } + } + return {}; + }), + delete: privateProcedure + .input(z.object({ force: z.boolean(), id: z.string() })) + .mutation(async ({ ctx, input }) => { + const { id, force } = input; + const teamId = ctx.user?.teamId; + const application = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + if (!force && application?.destinationDockerId && application.destinationDocker?.network) { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` + }); + if (containers) { + const containersArray = containers.trim().split('\n'); + for (const container of containersArray) { + const containerObj = JSON.parse(container); + const id = containerObj.ID; + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } + } + } + await prisma.applicationSettings.deleteMany({ where: { application: { id } } }); + await prisma.buildLog.deleteMany({ where: { applicationId: id } }); + await prisma.build.deleteMany({ where: { applicationId: id } }); + await prisma.secret.deleteMany({ where: { applicationId: id } }); + await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); + await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } }); + if (teamId === '0') { + await prisma.application.deleteMany({ where: { id } }); + } else { + await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); + } + return {}; }) }); diff --git a/apps/server/src/trpc/routers/databases.ts b/apps/server/src/trpc/routers/databases.ts new file mode 100644 index 000000000..8d4d8a0ee --- /dev/null +++ b/apps/server/src/trpc/routers/databases.ts @@ -0,0 +1,84 @@ +import { z } from 'zod'; +import { privateProcedure, router } from '../trpc'; +import { decrypt } from '../../lib/common'; +import { prisma } from '../../prisma'; +import { executeCommand } from '../../lib/executeCommand'; +import { stopDatabaseContainer, stopTcpHttpProxy } from '../../lib/docker'; + +export const databasesRouter = router({ + status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => { + const id = input.id; + const teamId = ctx.user?.teamId; + + let isRunning = false; + const database = await prisma.database.findFirst({ + where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { destinationDocker: true, settings: true } + }); + if (database) { + const { destinationDockerId, destinationDocker } = database; + if (destinationDockerId) { + try { + const { stdout } = await executeCommand({ + dockerId: destinationDocker.id, + command: `docker inspect --format '{{json .State}}' ${id}` + }); + + if (JSON.parse(stdout).Running) { + isRunning = true; + } + } catch (error) { + // + } + } + } + return { + isRunning + }; + }), + cleanup: privateProcedure.query(async ({ ctx }) => { + const teamId = ctx.user?.teamId; + let databases = await prisma.database.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { settings: true, destinationDocker: true, teams: true } + }); + for (const database of databases) { + if (!database?.version) { + const { id } = database; + if (database.destinationDockerId) { + const everStarted = await stopDatabaseContainer(database); + if (everStarted) + await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); + } + await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); + await prisma.databaseSecret.deleteMany({ where: { databaseId: id } }); + await prisma.database.delete({ where: { id } }); + } + } + return {}; + }), + delete: privateProcedure + .input(z.object({ id: z.string(), force: z.boolean() })) + .mutation(async ({ ctx, input }) => { + const { id, force } = input; + const teamId = ctx.user?.teamId; + const database = await prisma.database.findFirst({ + where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { destinationDocker: true, settings: true } + }); + if (!force) { + if (database.dbUserPassword) database.dbUserPassword = decrypt(database.dbUserPassword); + if (database.rootUserPassword) + database.rootUserPassword = decrypt(database.rootUserPassword); + if (database.destinationDockerId) { + const everStarted = await stopDatabaseContainer(database); + if (everStarted) + await stopTcpHttpProxy(id, database.destinationDocker, database.publicPort); + } + } + await prisma.databaseSettings.deleteMany({ where: { databaseId: id } }); + await prisma.databaseSecret.deleteMany({ where: { databaseId: id } }); + await prisma.database.delete({ where: { id } }); + return {}; + }) +}); diff --git a/apps/server/src/trpc/routers/index.ts b/apps/server/src/trpc/routers/index.ts index 3dea86e15..0e16e9c41 100644 --- a/apps/server/src/trpc/routers/index.ts +++ b/apps/server/src/trpc/routers/index.ts @@ -3,3 +3,4 @@ export * from './dashboard'; export * from './settings'; export * from './applications'; export * from './services'; +export * from './databases'; diff --git a/apps/server/src/trpc/routers/services.ts b/apps/server/src/trpc/routers/services.ts index 1e5a805ed..6368d60f1 100644 --- a/apps/server/src/trpc/routers/services.ts +++ b/apps/server/src/trpc/routers/services.ts @@ -1,82 +1,135 @@ import { z } from 'zod'; import { privateProcedure, router } from '../trpc'; -import { decrypt, getTemplates, listSettings } from '../../lib/common'; +import { decrypt, getTemplates, removeService } from '../../lib/common'; import { prisma } from '../../prisma'; import { executeCommand } from '../../lib/executeCommand'; -import { checkContainer } from '../../lib/docker'; export const servicesRouter = router({ - status: privateProcedure - .input( - z.object({ - id: z.string() - }) - ) - .query(async ({ ctx, input }) => { - const id = input.id; - const teamId = ctx.user?.teamId; - if (!teamId) { - throw { status: 400, message: 'Team not found.' }; - } - const service = await getServiceFromDB({ id, teamId }); - const { destinationDockerId } = service; - let payload = {}; - if (destinationDockerId) { - const { stdout: containers } = await executeCommand({ - dockerId: service.destinationDocker.id, - command: `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'` - }); - if (containers) { - const containersArray = containers.trim().split('\n'); - if (containersArray.length > 0 && containersArray[0] !== '') { - const templates = await getTemplates(); - let template = templates.find((t) => t.type === service.type); - const templateStr = JSON.stringify(template); - if (templateStr) { - template = JSON.parse(templateStr.replaceAll('$$id', service.id)); - } - for (const container of containersArray) { - let isRunning = false; - let isExited = false; - let isRestarting = false; - let isExcluded = false; - const containerObj = JSON.parse(container); - const exclude = template?.services[containerObj.Names]?.exclude; - if (exclude) { - payload[containerObj.Names] = { - status: { - isExcluded: true, - isRunning: false, - isExited: false, - isRestarting: false - } - }; - continue; - } - - const status = containerObj.State; - if (status === 'running') { - isRunning = true; - } - if (status === 'exited') { - isExited = true; - } - if (status === 'restarting') { - isRestarting = true; - } + status: privateProcedure.input(z.object({ id: z.string() })).query(async ({ ctx, input }) => { + const id = input.id; + const teamId = ctx.user?.teamId; + if (!teamId) { + throw { status: 400, message: 'Team not found.' }; + } + const service = await getServiceFromDB({ id, teamId }); + const { destinationDockerId } = service; + let payload = {}; + if (destinationDockerId) { + const { stdout: containers } = await executeCommand({ + dockerId: service.destinationDocker.id, + command: `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'` + }); + if (containers) { + const containersArray = containers.trim().split('\n'); + if (containersArray.length > 0 && containersArray[0] !== '') { + const templates = await getTemplates(); + let template = templates.find((t: { type: string }) => t.type === service.type); + const templateStr = JSON.stringify(template); + if (templateStr) { + template = JSON.parse(templateStr.replaceAll('$$id', service.id)); + } + for (const container of containersArray) { + let isRunning = false; + let isExited = false; + let isRestarting = false; + let isExcluded = false; + const containerObj = JSON.parse(container); + const exclude = template?.services[containerObj.Names]?.exclude; + if (exclude) { payload[containerObj.Names] = { status: { - isExcluded, - isRunning, - isExited, - isRestarting + isExcluded: true, + isRunning: false, + isExited: false, + isRestarting: false } }; + continue; } + + const status = containerObj.State; + if (status === 'running') { + isRunning = true; + } + if (status === 'exited') { + isExited = true; + } + if (status === 'restarting') { + isRestarting = true; + } + payload[containerObj.Names] = { + status: { + isExcluded, + isRunning, + isExited, + isRestarting + } + }; } } } - return payload; + } + return payload; + }), + cleanup: privateProcedure.query(async ({ ctx }) => { + const teamId = ctx.user?.teamId; + let services = await prisma.service.findMany({ + where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, + include: { destinationDocker: true, teams: true } + }); + for (const service of services) { + if (!service.fqdn) { + if (service.destinationDockerId) { + const { stdout: containers } = await executeCommand({ + dockerId: service.destinationDockerId, + command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}` + }); + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ + dockerId: service.destinationDockerId, + command: `docker stop -t 0 ${container}` + }); + await executeCommand({ + dockerId: service.destinationDockerId, + command: `docker rm --force ${container}` + }); + } + } + } + } + await removeService({ id: service.id }); + } + } + }), + delete: privateProcedure + .input(z.object({ force: z.boolean(), id: z.string() })) + .mutation(async ({ input }) => { + // todo: check if user is allowed to delete service + const { id } = input; + await prisma.serviceSecret.deleteMany({ where: { serviceId: id } }); + await prisma.serviceSetting.deleteMany({ where: { serviceId: id } }); + await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } }); + await prisma.meiliSearch.deleteMany({ where: { serviceId: id } }); + await prisma.fider.deleteMany({ where: { serviceId: id } }); + await prisma.ghost.deleteMany({ where: { serviceId: id } }); + await prisma.umami.deleteMany({ where: { serviceId: id } }); + await prisma.hasura.deleteMany({ where: { serviceId: id } }); + await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } }); + await prisma.minio.deleteMany({ where: { serviceId: id } }); + await prisma.vscodeserver.deleteMany({ where: { serviceId: id } }); + await prisma.wordpress.deleteMany({ where: { serviceId: id } }); + await prisma.glitchTip.deleteMany({ where: { serviceId: id } }); + await prisma.moodle.deleteMany({ where: { serviceId: id } }); + await prisma.appwrite.deleteMany({ where: { serviceId: id } }); + await prisma.searxng.deleteMany({ where: { serviceId: id } }); + await prisma.weblate.deleteMany({ where: { serviceId: id } }); + await prisma.taiga.deleteMany({ where: { serviceId: id } }); + + await prisma.service.delete({ where: { id } }); + return {}; }) }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4482f8b63..a62d53050 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -160,6 +160,7 @@ importers: '@sveltejs/kit': 1.0.0-next.572 '@trpc/client': 10.1.0 '@trpc/server': 10.1.0 + '@types/js-cookie': 3.0.2 '@typescript-eslint/eslint-plugin': 5.44.0 '@typescript-eslint/parser': 5.44.0 autoprefixer: 10.4.13 @@ -196,6 +197,7 @@ importers: '@playwright/test': 1.28.1 '@sveltejs/adapter-static': 1.0.0-next.48 '@sveltejs/kit': 1.0.0-next.572_svelte@3.53.1+vite@3.2.4 + '@types/js-cookie': 3.0.2 '@typescript-eslint/eslint-plugin': 5.44.0_fnsv2sbzcckq65bwfk7a5xwslu '@typescript-eslint/parser': 5.44.0_hsf322ms6xhhd4b5ne6lb74y4a autoprefixer: 10.4.13_postcss@8.4.19 @@ -237,6 +239,7 @@ importers: '@prisma/client': 4.6.1 '@trpc/client': 10.1.0 '@trpc/server': 10.1.0 + '@types/bcryptjs': ^2.4.2 '@types/jsonwebtoken': ^8.5.9 '@types/node': 18.11.9 '@types/node-fetch': 2.6.2 @@ -299,6 +302,7 @@ importers: ws: 8.11.0 zod: 3.19.1 devDependencies: + '@types/bcryptjs': 2.4.2 '@types/jsonwebtoken': 8.5.9 '@types/node': 18.11.9 '@types/node-fetch': 2.6.2 @@ -2026,6 +2030,10 @@ packages: resolution: {integrity: sha512-eZxlbI8GZscaGS7kkc/trHTT5xgrjH3/1n2JDwusC9iahPKWMRvRjJSAN5mCXviuTGQ/lHnhvv8Q1YTpnfz9gA==} dev: false + /@types/bcryptjs/2.4.2: + resolution: {integrity: sha512-LiMQ6EOPob/4yUL66SZzu6Yh77cbzJFYll+ZfaPiPPFswtIlA/Fs1MzdKYA7JApHU49zQTbJGX3PDmCpIdDBRQ==} + dev: true + /@types/cacheable-request/6.0.2: resolution: {integrity: sha512-B3xVo+dlKM6nnKTcmm5ZtY/OL8bOAOd2Olee9M1zft65ox50OzjEHW91sDiU9j6cvW8Ejg1/Qkf4xd2kugApUA==} dependencies: