mirror of
https://github.com/ershisan99/coolify.git
synced 2025-12-30 20:59:22 +00:00
913 lines
28 KiB
TypeScript
913 lines
28 KiB
TypeScript
import { z } from 'zod';
|
|
import yaml from 'js-yaml';
|
|
import fs from 'fs/promises';
|
|
import path from 'path';
|
|
import { privateProcedure, router } from '../../trpc';
|
|
import {
|
|
createDirectories,
|
|
decrypt,
|
|
encrypt,
|
|
fixType,
|
|
getTags,
|
|
getTemplates,
|
|
isARM,
|
|
isDev,
|
|
listSettings,
|
|
makeLabelForServices,
|
|
removeService
|
|
} from '../../../lib/common';
|
|
import { prisma } from '../../../prisma';
|
|
import { executeCommand } from '../../../lib/executeCommand';
|
|
import {
|
|
generatePassword,
|
|
getFreePublicPort,
|
|
parseAndFindServiceTemplates,
|
|
persistentVolumes,
|
|
startServiceContainers,
|
|
verifyAndDecryptServiceSecrets
|
|
} from './lib';
|
|
import { checkContainer, defaultComposeConfiguration, stopTcpHttpProxy } from '../../../lib/docker';
|
|
import cuid from 'cuid';
|
|
import { day } from '../../../lib/dayjs';
|
|
|
|
export const servicesRouter = router({
|
|
getLogs: privateProcedure
|
|
.input(
|
|
z.object({
|
|
id: z.string(),
|
|
containerId: z.string(),
|
|
since: z.number().optional().default(0)
|
|
})
|
|
)
|
|
.query(async ({ input, ctx }) => {
|
|
let { id, containerId, since } = input;
|
|
if (since !== 0) {
|
|
since = day(since).unix();
|
|
}
|
|
const {
|
|
destinationDockerId,
|
|
destinationDocker: { id: dockerId }
|
|
} = await prisma.service.findUnique({
|
|
where: { id },
|
|
include: { destinationDocker: true }
|
|
});
|
|
if (destinationDockerId) {
|
|
try {
|
|
const { default: ansi } = await import('strip-ansi');
|
|
const { stdout, stderr } = await executeCommand({
|
|
dockerId,
|
|
command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}`
|
|
});
|
|
const stripLogsStdout = stdout
|
|
.toString()
|
|
.split('\n')
|
|
.map((l) => ansi(l))
|
|
.filter((a) => a);
|
|
const stripLogsStderr = stderr
|
|
.toString()
|
|
.split('\n')
|
|
.map((l) => ansi(l))
|
|
.filter((a) => a);
|
|
return {
|
|
data: {
|
|
logs: stripLogsStderr.concat(stripLogsStdout)
|
|
}
|
|
};
|
|
} catch (error) {
|
|
const { statusCode, stderr } = error;
|
|
if (stderr.startsWith('Error: No such container')) {
|
|
return {
|
|
data: {
|
|
logs: [],
|
|
noContainer: true
|
|
}
|
|
};
|
|
}
|
|
if (statusCode === 404) {
|
|
return {
|
|
data: {
|
|
logs: []
|
|
}
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
message: 'No logs found.'
|
|
};
|
|
}),
|
|
deleteStorage: privateProcedure
|
|
.input(
|
|
z.object({
|
|
storageId: z.string()
|
|
})
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { storageId } = input;
|
|
await prisma.servicePersistentStorage.deleteMany({ where: { id: storageId } });
|
|
}),
|
|
saveStorage: privateProcedure
|
|
.input(
|
|
z.object({
|
|
id: z.string(),
|
|
path: z.string(),
|
|
isNewStorage: z.boolean(),
|
|
storageId: z.string().optional().nullable(),
|
|
containerId: z.string().optional()
|
|
})
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id, path, isNewStorage, storageId, containerId } = input;
|
|
|
|
if (isNewStorage) {
|
|
const volumeName = `${id}-custom${path.replace(/\//gi, '-')}`;
|
|
const found = await prisma.servicePersistentStorage.findFirst({
|
|
where: { path, containerId }
|
|
});
|
|
if (found) {
|
|
throw {
|
|
status: 500,
|
|
message: 'Persistent storage already exists for this container and path.'
|
|
};
|
|
}
|
|
await prisma.servicePersistentStorage.create({
|
|
data: { path, volumeName, containerId, service: { connect: { id } } }
|
|
});
|
|
} else {
|
|
await prisma.servicePersistentStorage.update({
|
|
where: { id: storageId },
|
|
data: { path, containerId }
|
|
});
|
|
}
|
|
}),
|
|
getStorages: privateProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
const persistentStorages = await prisma.servicePersistentStorage.findMany({
|
|
where: { serviceId: id }
|
|
});
|
|
return {
|
|
success: true,
|
|
data: {
|
|
persistentStorages
|
|
}
|
|
};
|
|
}),
|
|
deleteSecret: privateProcedure
|
|
.input(z.object({ id: z.string(), name: z.string() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id, name } = input;
|
|
await prisma.serviceSecret.deleteMany({ where: { serviceId: id, name } });
|
|
}),
|
|
saveService: privateProcedure
|
|
.input(
|
|
z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
fqdn: z.string().optional(),
|
|
exposePort: z.string().optional(),
|
|
type: z.string(),
|
|
serviceSetting: z.any(),
|
|
version: z.string().optional()
|
|
})
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const teamId = ctx.user?.teamId;
|
|
let { id, name, fqdn, exposePort, type, serviceSetting, version } = input;
|
|
if (fqdn) fqdn = fqdn.toLowerCase();
|
|
if (exposePort) exposePort = Number(exposePort);
|
|
type = fixType(type);
|
|
|
|
const data = {
|
|
fqdn,
|
|
name,
|
|
exposePort,
|
|
version
|
|
};
|
|
const templates = await getTemplates();
|
|
const service = await prisma.service.findUnique({ where: { id } });
|
|
const foundTemplate = templates.find((t) => fixType(t.type) === fixType(service.type));
|
|
for (const setting of serviceSetting) {
|
|
let { id: settingId, name, value, changed = false, isNew = false, variableName } = setting;
|
|
if (value) {
|
|
if (changed) {
|
|
await prisma.serviceSetting.update({ where: { id: settingId }, data: { value } });
|
|
}
|
|
if (isNew) {
|
|
if (!variableName) {
|
|
variableName = foundTemplate?.variables.find((v) => v.name === name).id;
|
|
}
|
|
await prisma.serviceSetting.create({
|
|
data: { name, value, variableName, service: { connect: { id } } }
|
|
});
|
|
}
|
|
}
|
|
}
|
|
await prisma.service.update({
|
|
where: { id },
|
|
data
|
|
});
|
|
}),
|
|
createSecret: privateProcedure
|
|
.input(
|
|
z.object({
|
|
id: z.string(),
|
|
name: z.string(),
|
|
value: z.string(),
|
|
isBuildSecret: z.boolean().optional(),
|
|
isPRMRSecret: z.boolean().optional(),
|
|
isNew: z.boolean().optional()
|
|
})
|
|
)
|
|
.mutation(async ({ input }) => {
|
|
let { id, name, value, isNew } = input;
|
|
if (isNew) {
|
|
const found = await prisma.serviceSecret.findFirst({ where: { name, serviceId: id } });
|
|
if (found) {
|
|
throw `Secret ${name} already exists.`;
|
|
} else {
|
|
value = encrypt(value.trim());
|
|
await prisma.serviceSecret.create({
|
|
data: { name, value, service: { connect: { id } } }
|
|
});
|
|
}
|
|
} else {
|
|
value = encrypt(value.trim());
|
|
const found = await prisma.serviceSecret.findFirst({ where: { serviceId: id, name } });
|
|
|
|
if (found) {
|
|
await prisma.serviceSecret.updateMany({
|
|
where: { serviceId: id, name },
|
|
data: { value }
|
|
});
|
|
} else {
|
|
await prisma.serviceSecret.create({
|
|
data: { name, value, service: { connect: { id } } }
|
|
});
|
|
}
|
|
}
|
|
}),
|
|
getSecrets: privateProcedure.input(z.object({ id: z.string() })).query(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
const teamId = ctx.user?.teamId;
|
|
const service = await getServiceFromDB({ id, teamId });
|
|
let secrets = await prisma.serviceSecret.findMany({
|
|
where: { serviceId: id },
|
|
orderBy: { createdAt: 'desc' }
|
|
});
|
|
const templates = await getTemplates();
|
|
if (!templates) throw new Error('No templates found. Please contact support.');
|
|
const foundTemplate = templates.find((t) => fixType(t.type) === service.type);
|
|
secrets = secrets.map((secret) => {
|
|
const foundVariable = foundTemplate?.variables?.find((v) => v.name === secret.name) || null;
|
|
if (foundVariable) {
|
|
secret.readOnly = foundVariable.readOnly;
|
|
}
|
|
secret.value = decrypt(secret.value);
|
|
return secret;
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
data: {
|
|
secrets
|
|
}
|
|
};
|
|
}),
|
|
wordpress: privateProcedure
|
|
.input(z.object({ id: z.string(), ftpEnabled: z.boolean() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
const teamId = ctx.user?.teamId;
|
|
const {
|
|
service: {
|
|
destinationDocker: { engine, remoteEngine, remoteIpAddress }
|
|
}
|
|
} = await prisma.wordpress.findUnique({
|
|
where: { serviceId: id },
|
|
include: { service: { include: { destinationDocker: true } } }
|
|
});
|
|
|
|
const publicPort = await getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress });
|
|
|
|
let ftpUser = cuid();
|
|
let ftpPassword = generatePassword({});
|
|
|
|
const hostkeyDir = isDev ? '/tmp/hostkeys' : '/app/ssl/hostkeys';
|
|
try {
|
|
const data = await prisma.wordpress.update({
|
|
where: { serviceId: id },
|
|
data: { ftpEnabled },
|
|
include: { service: { include: { destinationDocker: true } } }
|
|
});
|
|
const {
|
|
service: { destinationDockerId, destinationDocker },
|
|
ftpPublicPort,
|
|
ftpUser: user,
|
|
ftpPassword: savedPassword,
|
|
ftpHostKey,
|
|
ftpHostKeyPrivate
|
|
} = data;
|
|
const { network, engine } = destinationDocker;
|
|
if (ftpEnabled) {
|
|
if (user) ftpUser = user;
|
|
if (savedPassword) ftpPassword = decrypt(savedPassword);
|
|
|
|
// TODO: rewrite these to usable without shell
|
|
const { stdout: password } = await executeCommand({
|
|
command: `echo ${ftpPassword} | openssl passwd -1 -stdin`,
|
|
shell: true
|
|
});
|
|
if (destinationDockerId) {
|
|
try {
|
|
await fs.stat(hostkeyDir);
|
|
} catch (error) {
|
|
await executeCommand({ command: `mkdir -p ${hostkeyDir}` });
|
|
}
|
|
if (!ftpHostKey) {
|
|
await executeCommand({
|
|
command: `ssh-keygen -t ed25519 -f ssh_host_ed25519_key -N "" -q -f ${hostkeyDir}/${id}.ed25519`
|
|
});
|
|
const { stdout: ftpHostKey } = await executeCommand({
|
|
command: `cat ${hostkeyDir}/${id}.ed25519`
|
|
});
|
|
await prisma.wordpress.update({
|
|
where: { serviceId: id },
|
|
data: { ftpHostKey: encrypt(ftpHostKey) }
|
|
});
|
|
} else {
|
|
await executeCommand({
|
|
command: `echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`,
|
|
shell: true
|
|
});
|
|
}
|
|
if (!ftpHostKeyPrivate) {
|
|
await executeCommand({
|
|
command: `ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`
|
|
});
|
|
const { stdout: ftpHostKeyPrivate } = await executeCommand({
|
|
command: `cat ${hostkeyDir}/${id}.rsa`
|
|
});
|
|
await prisma.wordpress.update({
|
|
where: { serviceId: id },
|
|
data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) }
|
|
});
|
|
} else {
|
|
await executeCommand({
|
|
command: `echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`,
|
|
shell: true
|
|
});
|
|
}
|
|
|
|
await prisma.wordpress.update({
|
|
where: { serviceId: id },
|
|
data: {
|
|
ftpPublicPort: publicPort,
|
|
ftpUser: user ? undefined : ftpUser,
|
|
ftpPassword: savedPassword ? undefined : encrypt(ftpPassword)
|
|
}
|
|
});
|
|
|
|
try {
|
|
const { found: isRunning } = await checkContainer({
|
|
dockerId: destinationDocker.id,
|
|
container: `${id}-ftp`
|
|
});
|
|
if (isRunning) {
|
|
await executeCommand({
|
|
dockerId: destinationDocker.id,
|
|
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`,
|
|
shell: true
|
|
});
|
|
}
|
|
} catch (error) {}
|
|
const volumes = [
|
|
`${id}-wordpress-data:/home/${ftpUser}/wordpress`,
|
|
`${
|
|
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
|
}/${id}.ed25519:/etc/ssh/ssh_host_ed25519_key`,
|
|
`${
|
|
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
|
}/${id}.rsa:/etc/ssh/ssh_host_rsa_key`,
|
|
`${
|
|
isDev ? hostkeyDir : '/var/lib/docker/volumes/coolify-ssl-certs/_data/hostkeys'
|
|
}/${id}.sh:/etc/sftp.d/chmod.sh`
|
|
];
|
|
|
|
const compose = {
|
|
version: '3.8',
|
|
services: {
|
|
[`${id}-ftp`]: {
|
|
image: `atmoz/sftp:alpine`,
|
|
command: `'${ftpUser}:${password.replace('\n', '').replace(/\$/g, '$$$')}:e:33'`,
|
|
extra_hosts: ['host.docker.internal:host-gateway'],
|
|
container_name: `${id}-ftp`,
|
|
volumes,
|
|
networks: [network],
|
|
depends_on: [],
|
|
restart: 'always'
|
|
}
|
|
},
|
|
networks: {
|
|
[network]: {
|
|
external: true
|
|
}
|
|
},
|
|
volumes: {
|
|
[`${id}-wordpress-data`]: {
|
|
external: true,
|
|
name: `${id}-wordpress-data`
|
|
}
|
|
}
|
|
};
|
|
await fs.writeFile(
|
|
`${hostkeyDir}/${id}.sh`,
|
|
`#!/bin/bash\nchmod 600 /etc/ssh/ssh_host_ed25519_key /etc/ssh/ssh_host_rsa_key\nuserdel -f xfs\nchown -R 33:33 /home/${ftpUser}/wordpress/`
|
|
);
|
|
await executeCommand({ command: `chmod +x ${hostkeyDir}/${id}.sh` });
|
|
await fs.writeFile(`${hostkeyDir}/${id}-docker-compose.yml`, yaml.dump(compose));
|
|
await executeCommand({
|
|
dockerId: destinationDocker.id,
|
|
command: `docker compose -f ${hostkeyDir}/${id}-docker-compose.yml up -d`
|
|
});
|
|
}
|
|
return {
|
|
publicPort,
|
|
ftpUser,
|
|
ftpPassword
|
|
};
|
|
} else {
|
|
await prisma.wordpress.update({
|
|
where: { serviceId: id },
|
|
data: { ftpPublicPort: null }
|
|
});
|
|
try {
|
|
await executeCommand({
|
|
dockerId: destinationDocker.id,
|
|
command: `docker stop -t 0 ${id}-ftp && docker rm ${id}-ftp`,
|
|
shell: true
|
|
});
|
|
} catch (error) {
|
|
//
|
|
}
|
|
await stopTcpHttpProxy(id, destinationDocker, ftpPublicPort);
|
|
}
|
|
} catch ({ status, message }) {
|
|
throw message;
|
|
} finally {
|
|
try {
|
|
await executeCommand({
|
|
command: `rm -fr ${hostkeyDir}/${id}-docker-compose.yml ${hostkeyDir}/${id}.ed25519 ${hostkeyDir}/${id}.ed25519.pub ${hostkeyDir}/${id}.rsa ${hostkeyDir}/${id}.rsa.pub ${hostkeyDir}/${id}.sh`
|
|
});
|
|
} catch (error) {}
|
|
}
|
|
}),
|
|
start: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
const teamId = ctx.user?.teamId;
|
|
const service = await getServiceFromDB({ id, teamId });
|
|
const arm = isARM(service.arch);
|
|
const { type, destinationDockerId, destinationDocker, persistentStorage, exposePort } = service;
|
|
|
|
const { workdir } = await createDirectories({ repository: type, buildId: id });
|
|
const template: any = await parseAndFindServiceTemplates(service, workdir, true);
|
|
const network = destinationDockerId && destinationDocker.network;
|
|
const config = {};
|
|
for (const s in template.services) {
|
|
let newEnvironments = [];
|
|
if (arm) {
|
|
if (template.services[s]?.environmentArm?.length > 0) {
|
|
for (const environment of template.services[s].environmentArm) {
|
|
let [env, ...value] = environment.split('=');
|
|
value = value.join('=');
|
|
if (!value.startsWith('$$secret') && value !== '') {
|
|
newEnvironments.push(`${env}=${value}`);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if (template.services[s]?.environment?.length > 0) {
|
|
for (const environment of template.services[s].environment) {
|
|
let [env, ...value] = environment.split('=');
|
|
value = value.join('=');
|
|
if (!value.startsWith('$$secret') && value !== '') {
|
|
newEnvironments.push(`${env}=${value}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const secrets = await verifyAndDecryptServiceSecrets(id);
|
|
for (const secret of secrets) {
|
|
const { name, value } = secret;
|
|
if (value) {
|
|
const foundEnv = !!template.services[s].environment?.find((env) =>
|
|
env.startsWith(`${name}=`)
|
|
);
|
|
const foundNewEnv = !!newEnvironments?.find((env) => env.startsWith(`${name}=`));
|
|
if (foundEnv && !foundNewEnv) {
|
|
newEnvironments.push(`${name}=${value}`);
|
|
}
|
|
if (!foundEnv && !foundNewEnv && s === id) {
|
|
newEnvironments.push(`${name}=${value}`);
|
|
}
|
|
}
|
|
}
|
|
const customVolumes = await prisma.servicePersistentStorage.findMany({
|
|
where: { serviceId: id }
|
|
});
|
|
let volumes = new Set();
|
|
if (arm) {
|
|
template.services[s]?.volumesArm &&
|
|
template.services[s].volumesArm.length > 0 &&
|
|
template.services[s].volumesArm.forEach((v) => volumes.add(v));
|
|
} else {
|
|
template.services[s]?.volumes &&
|
|
template.services[s].volumes.length > 0 &&
|
|
template.services[s].volumes.forEach((v) => volumes.add(v));
|
|
}
|
|
|
|
// Workaround: old plausible analytics service wrong volume id name
|
|
if (service.type === 'plausibleanalytics' && service.plausibleAnalytics?.id) {
|
|
let temp = Array.from(volumes);
|
|
temp.forEach((a) => {
|
|
const t = a.replace(service.id, service.plausibleAnalytics.id);
|
|
volumes.delete(a);
|
|
volumes.add(t);
|
|
});
|
|
}
|
|
|
|
if (customVolumes.length > 0) {
|
|
for (const customVolume of customVolumes) {
|
|
const { volumeName, path, containerId } = customVolume;
|
|
if (
|
|
volumes &&
|
|
volumes.size > 0 &&
|
|
!volumes.has(`${volumeName}:${path}`) &&
|
|
containerId === service
|
|
) {
|
|
volumes.add(`${volumeName}:${path}`);
|
|
}
|
|
}
|
|
}
|
|
let ports = [];
|
|
if (template.services[s].proxy?.length > 0) {
|
|
for (const proxy of template.services[s].proxy) {
|
|
if (proxy.hostPort) {
|
|
ports.push(`${proxy.hostPort}:${proxy.port}`);
|
|
}
|
|
}
|
|
} else {
|
|
if (template.services[s].ports?.length === 1) {
|
|
for (const port of template.services[s].ports) {
|
|
if (exposePort) {
|
|
ports.push(`${exposePort}:${port}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let image = template.services[s].image;
|
|
if (arm && template.services[s].imageArm) {
|
|
image = template.services[s].imageArm;
|
|
}
|
|
config[s] = {
|
|
container_name: s,
|
|
build: template.services[s].build || undefined,
|
|
command: template.services[s].command,
|
|
entrypoint: template.services[s]?.entrypoint,
|
|
image,
|
|
expose: template.services[s].ports,
|
|
ports: ports.length > 0 ? ports : undefined,
|
|
volumes: Array.from(volumes),
|
|
environment: newEnvironments,
|
|
depends_on: template.services[s]?.depends_on,
|
|
ulimits: template.services[s]?.ulimits,
|
|
cap_drop: template.services[s]?.cap_drop,
|
|
cap_add: template.services[s]?.cap_add,
|
|
labels: makeLabelForServices(type),
|
|
...defaultComposeConfiguration(network)
|
|
};
|
|
// Generate files for builds
|
|
if (template.services[s]?.files?.length > 0) {
|
|
if (!config[s].build) {
|
|
config[s].build = {
|
|
context: workdir,
|
|
dockerfile: `Dockerfile.${s}`
|
|
};
|
|
}
|
|
let Dockerfile = `
|
|
FROM ${template.services[s].image}`;
|
|
for (const file of template.services[s].files) {
|
|
const { location, content } = file;
|
|
const source = path.join(workdir, location);
|
|
await fs.mkdir(path.dirname(source), { recursive: true });
|
|
await fs.writeFile(source, content);
|
|
Dockerfile += `
|
|
COPY .${location} ${location}`;
|
|
}
|
|
await fs.writeFile(`${workdir}/Dockerfile.${s}`, Dockerfile);
|
|
}
|
|
}
|
|
const { volumeMounts } = persistentVolumes(id, persistentStorage, config);
|
|
const composeFile = {
|
|
version: '3.8',
|
|
services: config,
|
|
networks: {
|
|
[network]: {
|
|
external: true
|
|
}
|
|
},
|
|
volumes: volumeMounts
|
|
};
|
|
const composeFileDestination = `${workdir}/docker-compose.yaml`;
|
|
await fs.writeFile(composeFileDestination, yaml.dump(composeFile));
|
|
// TODO: TODO!
|
|
let fastify = null;
|
|
await startServiceContainers(fastify, id, teamId, destinationDocker.id, composeFileDestination);
|
|
|
|
// Workaround: Stop old minio proxies
|
|
if (service.type === 'minio') {
|
|
try {
|
|
const { stdout: containers } = await executeCommand({
|
|
dockerId: destinationDocker.id,
|
|
command: `docker container ls -a --filter 'name=${id}-' --format {{.ID}}`
|
|
});
|
|
if (containers) {
|
|
const containerArray = containers.split('\n');
|
|
if (containerArray.length > 0) {
|
|
for (const container of containerArray) {
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker stop -t 0 ${container}`
|
|
});
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker rm --force ${container}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {}
|
|
try {
|
|
const { stdout: containers } = await executeCommand({
|
|
dockerId: destinationDocker.id,
|
|
command: `docker container ls -a --filter 'name=${id}-' --format {{.ID}}`
|
|
});
|
|
if (containers) {
|
|
const containerArray = containers.split('\n');
|
|
if (containerArray.length > 0) {
|
|
for (const container of containerArray) {
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker stop -t 0 ${container}`
|
|
});
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker rm --force ${container}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} catch (error) {}
|
|
}
|
|
}),
|
|
stop: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
const teamId = ctx.user?.teamId;
|
|
const { destinationDockerId } = await getServiceFromDB({ id, teamId });
|
|
if (destinationDockerId) {
|
|
const { stdout: containers } = await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}`
|
|
});
|
|
if (containers) {
|
|
const containerArray = containers.split('\n');
|
|
if (containerArray.length > 0) {
|
|
for (const container of containerArray) {
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker stop -t 0 ${container}`
|
|
});
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker rm --force ${container}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return {};
|
|
}
|
|
}),
|
|
getServices: privateProcedure
|
|
.input(z.object({ id: z.string() }))
|
|
.query(async ({ input, ctx }) => {
|
|
const { id } = input;
|
|
const teamId = ctx.user?.teamId;
|
|
const service = await getServiceFromDB({ id, teamId });
|
|
if (!service) {
|
|
throw { status: 404, message: 'Service not found.' };
|
|
}
|
|
let template = {};
|
|
let tags = [];
|
|
if (service.type) {
|
|
template = await parseAndFindServiceTemplates(service);
|
|
tags = await getTags(service.type);
|
|
}
|
|
return {
|
|
success: true,
|
|
data: {
|
|
settings: await listSettings(),
|
|
service,
|
|
template,
|
|
tags
|
|
}
|
|
};
|
|
}),
|
|
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: 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;
|
|
}),
|
|
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;
|
|
const teamId = ctx.user?.teamId;
|
|
const { destinationDockerId } = await getServiceFromDB({ id, teamId });
|
|
if (destinationDockerId) {
|
|
const { stdout: containers } = await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}`
|
|
});
|
|
if (containers) {
|
|
const containerArray = containers.split('\n');
|
|
if (containerArray.length > 0) {
|
|
for (const container of containerArray) {
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker stop -t 0 ${container}`
|
|
});
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker rm --force ${container}`
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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 {};
|
|
})
|
|
});
|
|
|
|
export async function getServiceFromDB({
|
|
id,
|
|
teamId
|
|
}: {
|
|
id: string;
|
|
teamId: string;
|
|
}): Promise<any> {
|
|
const settings = await prisma.setting.findFirst();
|
|
const body = await prisma.service.findFirst({
|
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
|
include: {
|
|
destinationDocker: true,
|
|
persistentStorage: true,
|
|
serviceSecret: true,
|
|
serviceSetting: true,
|
|
wordpress: true,
|
|
plausibleAnalytics: true
|
|
}
|
|
});
|
|
if (!body) {
|
|
return null;
|
|
}
|
|
// body.type = fixType(body.type);
|
|
|
|
if (body?.serviceSecret.length > 0) {
|
|
body.serviceSecret = body.serviceSecret.map((s) => {
|
|
s.value = decrypt(s.value);
|
|
return s;
|
|
});
|
|
}
|
|
if (body.wordpress) {
|
|
body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
|
|
}
|
|
|
|
return { ...body, settings };
|
|
}
|