2023-02-17 14:35:42 +01:00

262 lines
12 KiB
TypeScript

import type { FastifyReply, FastifyRequest } from 'fastify';
import fs from 'fs/promises';
import yaml from 'js-yaml';
import path from 'path';
import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeCommand, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common';
import { parseAndFindServiceTemplates } from '../../routes/api/v1/services/handlers';
import { ServiceStartStop } from '../../routes/api/v1/services/types';
import { OnlyId } from '../../types';
import { verifyAndDecryptServiceSecrets } from './common';
export async function stopService(request: FastifyRequest<ServiceStartStop>) {
try {
const { id } = request.params;
const teamId = request.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 {}
}
throw { status: 500, message: 'Could not stop containers.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
export async function startService(request: FastifyRequest<ServiceStartStop>, fastify: any) {
try {
const { id } = request.params;
const teamId = request.user.teamId;
const service = await getServiceFromDB({ id, teamId });
const arm = isARM();
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: 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));
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) { }
}
return {}
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}
async function startServiceContainers(fastify, id, teamId, dockerId, composeFileDestination) {
try {
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Pulling images...' })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} pull` })
} catch (error) { }
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Building images...' })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` })
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Creating containers...' })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} create` })
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Starting containers...' })
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} start` })
await asyncSleep(1000);
await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` })
fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 0 })
}
export async function migrateAppwriteDB(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
try {
const { id } = request.params
const teamId = request.user.teamId;
const {
destinationDockerId,
destinationDocker,
} = await getServiceFromDB({ id, teamId });
if (destinationDockerId) {
await executeCommand({
dockerId: destinationDocker.id,
command: `docker exec ${id} migrate`
})
return await reply.code(201).send()
}
throw { status: 500, message: 'Could cleanup logs.' }
} catch ({ status, message }) {
return errorHandler({ status, message })
}
}