This commit is contained in:
Andras Bacsai 2022-12-12 16:04:41 +01:00
parent 2007ba0c3b
commit 4ad7e1f8e6
21 changed files with 520 additions and 198 deletions

View File

@ -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",

View File

@ -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';

View File

@ -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 @@
</svg>
IAM {#if $appSession.pendingInvitations.length > 0}
<span class="indicator-item rounded-full badge badge-primary"
>{pendingInvitations.length}</span
>{$appSession.pendingInvitations.length}</span
>
{/if}
</a>

View File

@ -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) {

View File

@ -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/*"]
}
}
}
}

View File

@ -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",

View File

@ -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');

View File

@ -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<void> {
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 } });
}

View File

@ -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
};
}
}
export async function removeContainer({
id,
dockerId
}: {
id: string;
dockerId: string;
}): Promise<void> {
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<boolean> {
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;
}
}

View File

@ -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 {

View File

@ -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<any> => {
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<any> => {
if (buildId === 'undefined' || buildId === 'null' || !buildId) return;
if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return;
const { default: got } = await import('got');

View File

@ -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') {

View File

@ -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 {

View File

@ -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;

View File

@ -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 {};
})
});

View File

@ -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 {};
})
});

View File

@ -3,3 +3,4 @@ export * from './dashboard';
export * from './settings';
export * from './applications';
export * from './services';
export * from './databases';

View File

@ -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 {};
})
});

8
pnpm-lock.yaml generated
View File

@ -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: