1406 lines
57 KiB
TypeScript
1406 lines
57 KiB
TypeScript
import cuid from 'cuid';
|
|
import crypto from 'node:crypto'
|
|
import jsonwebtoken from 'jsonwebtoken';
|
|
import axios from 'axios';
|
|
import { FastifyReply } from 'fastify';
|
|
import fs from 'fs/promises';
|
|
import yaml from 'js-yaml';
|
|
import csv from 'csvtojson';
|
|
|
|
import { day } from '../../../../lib/dayjs';
|
|
import { makeLabelForStandaloneApplication, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common';
|
|
import { checkDomainsIsValidInDNS, checkDoubleBranch, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common';
|
|
import { checkContainer, formatLabelsOnDocker, isContainerExited, removeContainer } from '../../../../lib/docker';
|
|
|
|
import type { FastifyRequest } from 'fastify';
|
|
import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds } from './types';
|
|
import { OnlyId } from '../../../../types';
|
|
|
|
function filterObject(obj, callback) {
|
|
return Object.fromEntries(Object.entries(obj).
|
|
filter(([key, val]) => callback(val, key)));
|
|
}
|
|
|
|
export async function listApplications(request: FastifyRequest) {
|
|
try {
|
|
const { teamId } = request.user
|
|
const applications = await prisma.application.findMany({
|
|
where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
|
include: { teams: true, destinationDocker: true, settings: true }
|
|
});
|
|
const settings = await prisma.setting.findFirst()
|
|
return {
|
|
applications,
|
|
settings
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function getImages(request: FastifyRequest<GetImages>) {
|
|
try {
|
|
const { buildPack, deploymentType } = request.body
|
|
let publishDirectory = undefined;
|
|
let port = undefined
|
|
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
|
|
buildPack, deploymentType
|
|
);
|
|
if (buildPack === 'nextjs') {
|
|
if (deploymentType === 'static') {
|
|
publishDirectory = 'out'
|
|
port = '80'
|
|
} else {
|
|
publishDirectory = ''
|
|
port = '3000'
|
|
}
|
|
}
|
|
if (buildPack === 'nuxtjs') {
|
|
if (deploymentType === 'static') {
|
|
publishDirectory = 'dist'
|
|
port = '80'
|
|
} else {
|
|
publishDirectory = ''
|
|
port = '3000'
|
|
}
|
|
}
|
|
|
|
return { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port }
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function cleanupUnconfigured(request: FastifyRequest<any>) {
|
|
try {
|
|
let teamId = request.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 executeDockerCmd({
|
|
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 {}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { teamId } = request.user
|
|
let isRunning = false;
|
|
let isExited = false;
|
|
let isRestarting = false;
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
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
|
|
}
|
|
}
|
|
return {
|
|
isRunning,
|
|
isRestarting,
|
|
isExited,
|
|
};
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getApplication(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { teamId } = request.user
|
|
const appId = process.env['COOLIFY_APP_ID'];
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
const settings = await listSettings();
|
|
return {
|
|
application,
|
|
appId,
|
|
settings
|
|
};
|
|
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function newApplication(request: FastifyRequest, reply: FastifyReply) {
|
|
try {
|
|
const name = uniqueName();
|
|
const { teamId } = request.user
|
|
const { id } = await prisma.application.create({
|
|
data: {
|
|
name,
|
|
teams: { connect: { id: teamId } },
|
|
settings: { create: { debug: false, previews: false } }
|
|
}
|
|
});
|
|
return reply.code(201).send({ id });
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
function decryptApplication(application: any) {
|
|
if (application) {
|
|
if (application?.gitSource?.githubApp?.clientSecret) {
|
|
application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null;
|
|
}
|
|
if (application?.gitSource?.githubApp?.webhookSecret) {
|
|
application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null;
|
|
}
|
|
if (application?.gitSource?.githubApp?.privateKey) {
|
|
application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null;
|
|
}
|
|
if (application?.gitSource?.gitlabApp?.appSecret) {
|
|
application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null;
|
|
}
|
|
if (application?.secrets.length > 0) {
|
|
application.secrets = application.secrets.map((s: any) => {
|
|
s.value = decrypt(s.value) || null
|
|
return s;
|
|
});
|
|
}
|
|
|
|
return application;
|
|
}
|
|
}
|
|
export async function getApplicationFromDB(id: string, teamId: string) {
|
|
try {
|
|
let application = await prisma.application.findFirst({
|
|
where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
|
|
include: {
|
|
destinationDocker: true,
|
|
settings: true,
|
|
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
|
secrets: true,
|
|
persistentStorage: true,
|
|
connectedDatabase: true,
|
|
previewApplication: true
|
|
}
|
|
});
|
|
if (!application) {
|
|
throw { status: 404, message: 'Application not found.' };
|
|
}
|
|
application = decryptApplication(application);
|
|
const buildPack = application?.buildPack || null;
|
|
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
|
|
buildPack
|
|
);
|
|
|
|
// Set default build images
|
|
if (!application.baseImage) {
|
|
application.baseImage = baseImage;
|
|
}
|
|
if (!application.baseBuildImage) {
|
|
application.baseBuildImage = baseBuildImage;
|
|
}
|
|
return { ...application, baseBuildImages, baseImages };
|
|
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function getApplicationFromDBWebhook(projectId: number, branch: string) {
|
|
try {
|
|
let applications = await prisma.application.findMany({
|
|
where: { projectId, branch, settings: { autodeploy: true } },
|
|
include: {
|
|
destinationDocker: true,
|
|
settings: true,
|
|
gitSource: { include: { githubApp: true, gitlabApp: true } },
|
|
secrets: true,
|
|
persistentStorage: true,
|
|
connectedDatabase: true
|
|
}
|
|
});
|
|
if (applications.length === 0) {
|
|
throw { status: 500, message: 'Application not configured.' }
|
|
}
|
|
applications = applications.map((application: any) => {
|
|
application = decryptApplication(application);
|
|
const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(
|
|
application.buildPack
|
|
);
|
|
|
|
// Set default build images
|
|
if (!application.baseImage) {
|
|
application.baseImage = baseImage;
|
|
}
|
|
if (!application.baseBuildImage) {
|
|
application.baseBuildImage = baseBuildImage;
|
|
}
|
|
application.baseBuildImages = baseBuildImages;
|
|
application.baseImages = baseImages;
|
|
return application
|
|
})
|
|
|
|
return applications;
|
|
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function saveApplication(request: FastifyRequest<SaveApplication>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
let {
|
|
name,
|
|
buildPack,
|
|
fqdn,
|
|
port,
|
|
exposePort,
|
|
installCommand,
|
|
buildCommand,
|
|
startCommand,
|
|
baseDirectory,
|
|
publishDirectory,
|
|
pythonWSGI,
|
|
pythonModule,
|
|
pythonVariable,
|
|
dockerFileLocation,
|
|
denoMainFile,
|
|
denoOptions,
|
|
baseImage,
|
|
baseBuildImage,
|
|
deploymentType,
|
|
baseDatabaseBranch
|
|
} = request.body
|
|
if (port) port = Number(port);
|
|
if (exposePort) {
|
|
exposePort = Number(exposePort);
|
|
}
|
|
|
|
const { destinationDocker: { engine, remoteEngine, remoteIpAddress }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
|
if (denoOptions) denoOptions = denoOptions.trim();
|
|
const defaultConfiguration = await setDefaultConfiguration({
|
|
buildPack,
|
|
port,
|
|
installCommand,
|
|
startCommand,
|
|
buildCommand,
|
|
publishDirectory,
|
|
baseDirectory,
|
|
dockerFileLocation,
|
|
denoMainFile
|
|
});
|
|
if (baseDatabaseBranch) {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: {
|
|
name,
|
|
fqdn,
|
|
exposePort,
|
|
pythonWSGI,
|
|
pythonModule,
|
|
pythonVariable,
|
|
denoOptions,
|
|
baseImage,
|
|
baseBuildImage,
|
|
deploymentType,
|
|
...defaultConfiguration,
|
|
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
|
|
}
|
|
});
|
|
} else {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: {
|
|
name,
|
|
fqdn,
|
|
exposePort,
|
|
pythonWSGI,
|
|
pythonModule,
|
|
pythonVariable,
|
|
denoOptions,
|
|
baseImage,
|
|
baseBuildImage,
|
|
deploymentType,
|
|
...defaultConfiguration
|
|
}
|
|
});
|
|
}
|
|
|
|
return reply.code(201).send();
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
|
|
}
|
|
|
|
export async function saveApplicationSettings(request: FastifyRequest<SaveApplicationSettings>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { debug, previews, dualCerts, autodeploy, branch, projectId, isBot, isDBBranching, isCustomSSL } = request.body
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL } } },
|
|
include: { destinationDocker: true }
|
|
});
|
|
return reply.code(201).send();
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function stopPreviewApplication(request: FastifyRequest<StopPreviewApplication>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { pullmergeRequestId } = request.body
|
|
const { teamId } = request.user
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
const container = `${id}-${pullmergeRequestId}`
|
|
const { id: dockerId } = application.destinationDocker;
|
|
const { found } = await checkContainer({ dockerId, container });
|
|
if (found) {
|
|
await removeContainer({ id: container, dockerId: application.destinationDocker.id });
|
|
}
|
|
await prisma.previewApplication.deleteMany({ where: { applicationId: application.id, pullmergeRequestId } })
|
|
}
|
|
return reply.code(201).send();
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function restartApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { teamId } = request.user
|
|
let application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
const buildId = cuid();
|
|
const { id: dockerId, network } = application.destinationDocker;
|
|
const { secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
|
|
|
|
const envs = [
|
|
`PORT=${port}`
|
|
];
|
|
if (secrets.length > 0) {
|
|
secrets.forEach((secret) => {
|
|
if (pullmergeRequestId) {
|
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
|
if (isSecretFound.length > 0) {
|
|
envs.push(`${secret.name}=${isSecretFound[0].value}`);
|
|
} else {
|
|
envs.push(`${secret.name}=${secret.value}`);
|
|
}
|
|
} else {
|
|
if (!secret.isPRMRSecret) {
|
|
envs.push(`${secret.name}=${secret.value}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
const { workdir } = await createDirectories({ repository, buildId });
|
|
const labels = []
|
|
let image = null
|
|
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` })
|
|
const containersArray = container.trim().split('\n');
|
|
for (const container of containersArray) {
|
|
const containerObj = formatLabelsOnDocker(container);
|
|
image = containerObj[0].Image
|
|
Object.keys(containerObj[0].Labels).forEach(function (key) {
|
|
if (key.startsWith('coolify')) {
|
|
labels.push(`${key}=${containerObj[0].Labels[key]}`)
|
|
}
|
|
})
|
|
}
|
|
let imageFound = false;
|
|
try {
|
|
await executeDockerCmd({
|
|
dockerId,
|
|
command: `docker image inspect ${image}`
|
|
})
|
|
imageFound = true;
|
|
} catch (error) {
|
|
//
|
|
}
|
|
if (!imageFound) {
|
|
throw { status: 500, message: 'Image not found, cannot restart application.' }
|
|
}
|
|
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
|
|
|
let envFound = false;
|
|
try {
|
|
envFound = !!(await fs.stat(`${workdir}/.env`));
|
|
} catch (error) {
|
|
//
|
|
}
|
|
const volumes =
|
|
persistentStorage?.map((storage) => {
|
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
|
}${storage.path}`;
|
|
}) || [];
|
|
const composeVolumes = volumes.map((volume) => {
|
|
return {
|
|
[`${volume.split(':')[0]}`]: {
|
|
name: volume.split(':')[0]
|
|
}
|
|
};
|
|
});
|
|
const composeFile = {
|
|
version: '3.8',
|
|
services: {
|
|
[applicationId]: {
|
|
image,
|
|
container_name: applicationId,
|
|
volumes,
|
|
env_file: envFound ? [`${workdir}/.env`] : [],
|
|
labels,
|
|
depends_on: [],
|
|
expose: [port],
|
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
|
...defaultComposeConfiguration(network),
|
|
}
|
|
},
|
|
networks: {
|
|
[network]: {
|
|
external: true
|
|
}
|
|
},
|
|
volumes: Object.assign({}, ...composeVolumes)
|
|
};
|
|
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
|
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` })
|
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}` })
|
|
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
|
|
return reply.code(201).send();
|
|
}
|
|
throw { status: 500, message: 'Application cannot be restarted.' }
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function stopApplication(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { teamId } = request.user
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
const { id: dockerId } = application.destinationDocker;
|
|
const { found } = await checkContainer({ dockerId, container: id });
|
|
if (found) {
|
|
await removeContainer({ id, dockerId: application.destinationDocker.id });
|
|
}
|
|
}
|
|
return reply.code(201).send();
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function deleteApplication(request: FastifyRequest<DeleteApplication>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { force } = request.body
|
|
|
|
const { teamId } = request.user
|
|
const application = await prisma.application.findUnique({
|
|
where: { id },
|
|
include: { destinationDocker: true }
|
|
});
|
|
if (!force && application?.destinationDockerId && application.destinationDocker?.network) {
|
|
const { stdout: containers } = await executeDockerCmd({
|
|
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 {}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function checkDomain(request: FastifyRequest<CheckDomain>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { domain } = request.query
|
|
const { fqdn, settings: { dualCerts } } = await prisma.application.findUnique({ where: { id }, include: { settings: true } })
|
|
return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts });
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function checkDNS(request: FastifyRequest<CheckDNS>) {
|
|
try {
|
|
const { id } = request.params
|
|
let { exposePort, fqdn, forceSave, dualCerts } = request.body
|
|
if (!fqdn) {
|
|
return {}
|
|
} else {
|
|
fqdn = fqdn.toLowerCase();
|
|
}
|
|
if (exposePort) exposePort = Number(exposePort);
|
|
|
|
const { destinationDocker: { engine, remoteIpAddress, remoteEngine }, exposePort: configuredPort } = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } })
|
|
const { isDNSCheckEnabled } = await prisma.setting.findFirst({});
|
|
|
|
const found = await isDomainConfigured({ id, fqdn, remoteIpAddress });
|
|
if (found) {
|
|
throw { status: 500, message: `Domain ${getDomain(fqdn).replace('www.', '')} is already in use!` }
|
|
}
|
|
if (exposePort) await checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress })
|
|
if (isDNSCheckEnabled && !isDev && !forceSave) {
|
|
let hostname = request.hostname.split(':')[0];
|
|
if (remoteEngine) hostname = remoteIpAddress;
|
|
return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts });
|
|
}
|
|
return {}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getUsage(request) {
|
|
try {
|
|
const { id } = request.params
|
|
const teamId = request.user?.teamId;
|
|
let usage = {};
|
|
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
if (application.destinationDockerId) {
|
|
[usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]);
|
|
}
|
|
return {
|
|
usage
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function deployApplication(request: FastifyRequest<DeployApplication>) {
|
|
try {
|
|
const { id } = request.params
|
|
const teamId = request.user?.teamId;
|
|
const { pullmergeRequestId = null, branch, forceRebuild } = request.body
|
|
const buildId = cuid();
|
|
const application = await getApplicationFromDB(id, teamId);
|
|
if (application) {
|
|
if (!application?.configHash) {
|
|
const configHash = crypto.createHash('sha256')
|
|
.update(
|
|
JSON.stringify({
|
|
buildPack: application.buildPack,
|
|
port: application.port,
|
|
exposePort: application.exposePort,
|
|
installCommand: application.installCommand,
|
|
buildCommand: application.buildCommand,
|
|
startCommand: application.startCommand
|
|
})
|
|
)
|
|
.digest('hex');
|
|
await prisma.application.update({ where: { id }, data: { configHash } });
|
|
}
|
|
await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
|
|
await prisma.build.create({
|
|
data: {
|
|
id: buildId,
|
|
applicationId: id,
|
|
sourceBranch: branch,
|
|
branch: application.branch,
|
|
pullmergeRequestId: pullmergeRequestId?.toString(),
|
|
forceRebuild,
|
|
destinationDockerId: application.destinationDocker?.id,
|
|
gitSourceId: application.gitSource?.id,
|
|
githubAppId: application.gitSource?.githubApp?.id,
|
|
gitlabAppId: application.gitSource?.gitlabApp?.id,
|
|
status: 'queued',
|
|
type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual'
|
|
}
|
|
});
|
|
return {
|
|
buildId
|
|
};
|
|
}
|
|
throw { status: 500, message: 'Application not found!' }
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
|
|
export async function saveApplicationSource(request: FastifyRequest<SaveApplicationSource>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { gitSourceId, forPublic, type } = request.body
|
|
if (forPublic) {
|
|
const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } });
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { gitSource: { connect: { id: publicGit.id } } }
|
|
});
|
|
} else {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { gitSource: { connect: { id: gitSourceId } } }
|
|
});
|
|
}
|
|
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getGitHubToken(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { teamId } = request.user
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
const payload = {
|
|
iat: Math.round(new Date().getTime() / 1000),
|
|
exp: Math.round(new Date().getTime() / 1000 + 60),
|
|
iss: application.gitSource.githubApp.appId
|
|
};
|
|
const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, {
|
|
algorithm: 'RS256'
|
|
});
|
|
const { data } = await axios.post(`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, {}, {
|
|
headers: {
|
|
Authorization: `Bearer ${githubToken}`
|
|
}
|
|
})
|
|
return reply.code(201).send({
|
|
token: data.token
|
|
})
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function checkRepository(request: FastifyRequest<CheckRepository>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { repository, branch } = request.query
|
|
const application = await prisma.application.findUnique({
|
|
where: { id },
|
|
include: { gitSource: true }
|
|
});
|
|
const found = await prisma.application.findFirst({
|
|
where: { branch, repository, gitSource: { type: application.gitSource.type }, id: { not: id } }
|
|
});
|
|
return {
|
|
used: found ? true : false
|
|
};
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function saveRepository(request, reply) {
|
|
try {
|
|
const { id } = request.params
|
|
let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body
|
|
|
|
repository = repository.toLowerCase();
|
|
branch = branch.toLowerCase();
|
|
projectId = Number(projectId);
|
|
if (webhookToken) {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy, isPublicRepository } } }
|
|
});
|
|
} else {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } }
|
|
});
|
|
}
|
|
// if (!isPublicRepository) {
|
|
// const isDouble = await checkDoubleBranch(branch, projectId);
|
|
// if (isDouble) {
|
|
// await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } })
|
|
// }
|
|
// }
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function saveDestination(request: FastifyRequest<SaveDestination>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { destinationId } = request.body
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { destinationDocker: { connect: { id: destinationId } } }
|
|
});
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getBuildPack(request) {
|
|
try {
|
|
const { id } = request.params
|
|
const teamId = request.user?.teamId;
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
return {
|
|
type: application.gitSource.type,
|
|
projectId: application.projectId,
|
|
repository: application.repository,
|
|
branch: application.branch,
|
|
apiUrl: application.gitSource.apiUrl,
|
|
isPublicRepository: application.settings.isPublicRepository
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function saveBuildPack(request, reply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { buildPack } = request.body
|
|
await prisma.application.update({ where: { id }, data: { buildPack } });
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function saveConnectedDatabase(request, reply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { databaseId, type } = request.body
|
|
await prisma.application.update({ where: { id }, data: { connectedDatabase: { upsert: { create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } } } } })
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getSecrets(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
|
|
let secrets = await prisma.secret.findMany({
|
|
where: { applicationId: id, isPRMRSecret: false },
|
|
orderBy: { createdAt: 'asc' }
|
|
});
|
|
let previewSecrets = await prisma.secret.findMany({
|
|
where: { applicationId: id, isPRMRSecret: true },
|
|
orderBy: { createdAt: 'asc' }
|
|
});
|
|
|
|
secrets = secrets.map((secret) => {
|
|
secret.value = decrypt(secret.value);
|
|
return secret;
|
|
});
|
|
previewSecrets = previewSecrets.map((secret) => {
|
|
secret.value = decrypt(secret.value);
|
|
return secret;
|
|
});
|
|
|
|
return {
|
|
previewSecrets: previewSecrets.sort((a, b) => {
|
|
return ('' + a.name).localeCompare(b.name);
|
|
}),
|
|
secrets: secrets.sort((a, b) => {
|
|
return ('' + a.name).localeCompare(b.name);
|
|
})
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function updatePreviewSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
let { name, value } = request.body
|
|
if (value) {
|
|
value = encrypt(value.trim())
|
|
} else {
|
|
value = ''
|
|
}
|
|
await prisma.secret.updateMany({
|
|
where: { applicationId: id, name, isPRMRSecret: true },
|
|
data: { value }
|
|
});
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function updateSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { name, value, isBuildSecret = undefined } = request.body
|
|
await prisma.secret.updateMany({
|
|
where: { applicationId: id, name },
|
|
data: { value: encrypt(value.trim()), isBuildSecret }
|
|
});
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function saveSecret(request: FastifyRequest<SaveSecret>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { name, value, isBuildSecret = false } = request.body
|
|
await prisma.secret.create({
|
|
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: false, application: { connect: { id } } }
|
|
});
|
|
await prisma.secret.create({
|
|
data: { name, value: encrypt(value.trim()), isBuildSecret, isPRMRSecret: true, application: { connect: { id } } }
|
|
});
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function deleteSecret(request: FastifyRequest<DeleteSecret>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { name } = request.body
|
|
await prisma.secret.deleteMany({ where: { applicationId: id, name } });
|
|
return {}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getStorages(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
const persistentStorages = await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } });
|
|
return {
|
|
persistentStorages
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function saveStorage(request: FastifyRequest<SaveStorage>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const { path, newStorage, storageId } = request.body
|
|
|
|
if (newStorage) {
|
|
await prisma.applicationPersistentStorage.create({
|
|
data: { path, application: { connect: { id } } }
|
|
});
|
|
} else {
|
|
await prisma.applicationPersistentStorage.update({
|
|
where: { id: storageId },
|
|
data: { path }
|
|
});
|
|
}
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function deleteStorage(request: FastifyRequest<DeleteStorage>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { path } = request.body
|
|
await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id, path } });
|
|
return {}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function restartPreview(request: FastifyRequest<RestartPreviewApplication>, reply: FastifyReply) {
|
|
try {
|
|
const { id, pullmergeRequestId } = request.params
|
|
const { teamId } = request.user
|
|
let application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
const buildId = cuid();
|
|
const { id: dockerId, network } = application.destinationDocker;
|
|
const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application;
|
|
|
|
const envs = [
|
|
`PORT=${port}`
|
|
];
|
|
if (secrets.length > 0) {
|
|
secrets.forEach((secret) => {
|
|
if (pullmergeRequestId) {
|
|
const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret)
|
|
if (isSecretFound.length > 0) {
|
|
envs.push(`${secret.name}=${isSecretFound[0].value}`);
|
|
} else {
|
|
envs.push(`${secret.name}=${secret.value}`);
|
|
}
|
|
} else {
|
|
if (!secret.isPRMRSecret) {
|
|
envs.push(`${secret.name}=${secret.value}`);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
const { workdir } = await createDirectories({ repository, buildId });
|
|
const labels = []
|
|
let image = null
|
|
const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` })
|
|
const containersArray = container.trim().split('\n');
|
|
for (const container of containersArray) {
|
|
const containerObj = formatLabelsOnDocker(container);
|
|
image = containerObj[0].Image
|
|
Object.keys(containerObj[0].Labels).forEach(function (key) {
|
|
if (key.startsWith('coolify')) {
|
|
labels.push(`${key}=${containerObj[0].Labels[key]}`)
|
|
}
|
|
})
|
|
}
|
|
let imageFound = false;
|
|
try {
|
|
await executeDockerCmd({
|
|
dockerId,
|
|
command: `docker image inspect ${image}`
|
|
})
|
|
imageFound = true;
|
|
} catch (error) {
|
|
//
|
|
}
|
|
if (!imageFound) {
|
|
throw { status: 500, message: 'Image not found, cannot restart application.' }
|
|
}
|
|
await fs.writeFile(`${workdir}/.env`, envs.join('\n'));
|
|
|
|
let envFound = false;
|
|
try {
|
|
envFound = !!(await fs.stat(`${workdir}/.env`));
|
|
} catch (error) {
|
|
//
|
|
}
|
|
const volumes =
|
|
persistentStorage?.map((storage) => {
|
|
return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : ''
|
|
}${storage.path}`;
|
|
}) || [];
|
|
const composeVolumes = volumes.map((volume) => {
|
|
return {
|
|
[`${volume.split(':')[0]}`]: {
|
|
name: volume.split(':')[0]
|
|
}
|
|
};
|
|
});
|
|
const composeFile = {
|
|
version: '3.8',
|
|
services: {
|
|
[`${applicationId}-${pullmergeRequestId}`]: {
|
|
image,
|
|
container_name: `${applicationId}-${pullmergeRequestId}`,
|
|
volumes,
|
|
env_file: envFound ? [`${workdir}/.env`] : [],
|
|
labels,
|
|
depends_on: [],
|
|
expose: [port],
|
|
...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}),
|
|
...defaultComposeConfiguration(network),
|
|
}
|
|
},
|
|
networks: {
|
|
[network]: {
|
|
external: true
|
|
}
|
|
},
|
|
volumes: Object.assign({}, ...composeVolumes)
|
|
};
|
|
await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile));
|
|
await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` })
|
|
await executeDockerCmd({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` })
|
|
await executeDockerCmd({ dockerId, command: `docker compose --project-directory ${workdir} up -d` })
|
|
return reply.code(201).send();
|
|
}
|
|
throw { status: 500, message: 'Application cannot be restarted.' }
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function getPreviewStatus(request: FastifyRequest<RestartPreviewApplication>) {
|
|
try {
|
|
const { id, pullmergeRequestId } = request.params
|
|
const { teamId } = request.user
|
|
let isRunning = false;
|
|
let isExited = false;
|
|
let isRestarting = false;
|
|
let isBuilding = false
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
const status = await checkContainer({ dockerId: application.destinationDocker.id, container: `${id}-${pullmergeRequestId}` });
|
|
if (status?.found) {
|
|
isRunning = status.status.isRunning;
|
|
isExited = status.status.isExited;
|
|
isRestarting = status.status.isRestarting
|
|
}
|
|
const building = await prisma.build.findMany({ where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } })
|
|
isBuilding = building.length > 0
|
|
}
|
|
return {
|
|
isBuilding,
|
|
isRunning,
|
|
isRestarting,
|
|
isExited,
|
|
};
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function loadPreviews(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } });
|
|
const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` })
|
|
if (stdout === '') {
|
|
throw { status: 500, message: 'No previews found.' }
|
|
}
|
|
const containers = formatLabelsOnDocker(stdout).filter(container => container.Labels['coolify.configuration'] && container.Labels['coolify.type'] === 'standalone-application')
|
|
|
|
const jsonContainers = containers
|
|
.map((container) =>
|
|
JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString())
|
|
)
|
|
.filter((container) => {
|
|
return container.pullmergeRequestId && container.applicationId === id;
|
|
});
|
|
for (const container of jsonContainers) {
|
|
const found = await prisma.previewApplication.findMany({ where: { applicationId: container.applicationId, pullmergeRequestId: container.pullmergeRequestId } })
|
|
if (found.length === 0) {
|
|
await prisma.previewApplication.create({
|
|
data: {
|
|
pullmergeRequestId: container.pullmergeRequestId,
|
|
sourceBranch: container.branch,
|
|
customDomain: container.fqdn,
|
|
application: { connect: { id: container.applicationId } }
|
|
}
|
|
})
|
|
}
|
|
}
|
|
return {
|
|
previews: await prisma.previewApplication.findMany({ where: { applicationId: id } })
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function getPreviews(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
const { teamId } = request.user
|
|
let secrets = await prisma.secret.findMany({
|
|
where: { applicationId: id },
|
|
orderBy: { createdAt: 'desc' }
|
|
});
|
|
secrets = secrets.map((secret) => {
|
|
secret.value = decrypt(secret.value);
|
|
return secret;
|
|
});
|
|
|
|
const applicationSecrets = secrets.filter((secret) => !secret.isPRMRSecret);
|
|
const PRMRSecrets = secrets.filter((secret) => secret.isPRMRSecret);
|
|
return {
|
|
applicationSecrets: applicationSecrets.sort((a, b) => {
|
|
return ('' + a.name).localeCompare(b.name);
|
|
}),
|
|
PRMRSecrets: PRMRSecrets.sort((a, b) => {
|
|
return ('' + a.name).localeCompare(b.name);
|
|
})
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getApplicationLogs(request: FastifyRequest<GetApplicationLogs>) {
|
|
try {
|
|
const { id } = request.params;
|
|
let { since = 0 } = request.query
|
|
if (since !== 0) {
|
|
since = day(since).unix();
|
|
}
|
|
const { destinationDockerId, destinationDocker: { id: dockerId } } = await prisma.application.findUnique({
|
|
where: { id },
|
|
include: { destinationDocker: true }
|
|
});
|
|
if (destinationDockerId) {
|
|
try {
|
|
// const found = await checkContainer({ dockerId, container: id })
|
|
// if (found) {
|
|
const { default: ansi } = await import('strip-ansi')
|
|
const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` })
|
|
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);
|
|
const logs = stripLogsStderr.concat(stripLogsStdout)
|
|
const sortedLogs = logs.sort((a, b) => (day(a.split(' ')[0]).isAfter(day(b.split(' ')[0])) ? 1 : -1))
|
|
return { logs: sortedLogs }
|
|
// }
|
|
} catch (error) {
|
|
const { statusCode } = error;
|
|
if (statusCode === 404) {
|
|
return {
|
|
logs: []
|
|
};
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
message: 'No logs found.'
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function getBuilds(request: FastifyRequest<GetBuilds>) {
|
|
try {
|
|
const { id } = request.params
|
|
let { buildId, skip = 0 } = request.query
|
|
if (typeof skip !== 'number') {
|
|
skip = Number(skip)
|
|
}
|
|
|
|
let builds = [];
|
|
|
|
const buildCount = await prisma.build.count({ where: { applicationId: id } });
|
|
if (buildId) {
|
|
builds = await prisma.build.findMany({ where: { applicationId: id, id: buildId } });
|
|
} else {
|
|
builds = await prisma.build.findMany({
|
|
where: { applicationId: id },
|
|
orderBy: { createdAt: 'desc' },
|
|
take: 5 + skip
|
|
});
|
|
}
|
|
builds = builds.map((build) => {
|
|
if (build.status === 'running') {
|
|
build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0);
|
|
}
|
|
return build
|
|
})
|
|
return {
|
|
builds,
|
|
buildCount
|
|
};
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getBuildIdLogs(request: FastifyRequest<GetBuildIdLogs>) {
|
|
try {
|
|
// TODO: Fluentbit could still hold the logs, so we need to check if the logs are done
|
|
const { buildId, id } = request.params
|
|
let { sequence = 0 } = request.query
|
|
if (typeof sequence !== 'number') {
|
|
sequence = Number(sequence)
|
|
}
|
|
let file = `/app/logs/${id}_buildlog_${buildId}.csv`
|
|
if (isDev) {
|
|
file = `${process.cwd()}/../../logs/${id}_buildlog_${buildId}.csv`
|
|
}
|
|
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
|
const createdAt = day(data.createdAt).utc();
|
|
try {
|
|
await fs.stat(file)
|
|
} catch (error) {
|
|
let logs = await prisma.buildLog.findMany({
|
|
where: { buildId, time: { gt: sequence } },
|
|
orderBy: { time: 'asc' }
|
|
});
|
|
const data = await prisma.build.findFirst({ where: { id: buildId } });
|
|
const createdAt = day(data.createdAt).utc();
|
|
return {
|
|
logs: logs.map(log => {
|
|
log.time = Number(log.time)
|
|
return log
|
|
}),
|
|
fromDb: true,
|
|
took: day().diff(createdAt) / 1000,
|
|
status: data?.status || 'queued'
|
|
}
|
|
}
|
|
let fileLogs = (await fs.readFile(file)).toString()
|
|
let decryptedLogs = await csv({ noheader: true }).fromString(fileLogs)
|
|
let logs = decryptedLogs.map(log => {
|
|
const parsed = {
|
|
time: log['field1'],
|
|
line: decrypt(log['field2'] + '","' + log['field3'])
|
|
}
|
|
return parsed
|
|
}).filter(log => log.time > sequence)
|
|
return {
|
|
logs,
|
|
fromDb: false,
|
|
took: day().diff(createdAt) / 1000,
|
|
status: data?.status || 'queued'
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function getGitLabSSHKey(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params
|
|
const application = await prisma.application.findUnique({
|
|
where: { id },
|
|
include: { gitSource: { include: { gitlabApp: true } } }
|
|
});
|
|
return { publicKey: application.gitSource.gitlabApp.publicSshKey };
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function saveGitLabSSHKey(request: FastifyRequest<OnlyId>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
const application = await prisma.application.findUnique({
|
|
where: { id },
|
|
include: { gitSource: { include: { gitlabApp: true } } }
|
|
});
|
|
if (!application.gitSource?.gitlabApp?.privateSshKey) {
|
|
const keys = await generateSshKeyPair();
|
|
const encryptedPrivateKey = encrypt(keys.privateKey);
|
|
await prisma.gitlabApp.update({
|
|
where: { id: application.gitSource.gitlabApp.id },
|
|
data: { privateSshKey: encryptedPrivateKey, publicSshKey: keys.publicKey }
|
|
});
|
|
return reply.code(201).send({ publicKey: keys.publicKey })
|
|
}
|
|
return { message: 'SSH key already exists' }
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function saveDeployKey(request: FastifyRequest<SaveDeployKey>, reply: FastifyReply) {
|
|
try {
|
|
const { id } = request.params
|
|
let { deployKeyId } = request.body;
|
|
|
|
deployKeyId = Number(deployKeyId);
|
|
const application = await prisma.application.findUnique({
|
|
where: { id },
|
|
include: { gitSource: { include: { gitlabApp: true } } }
|
|
});
|
|
await prisma.gitlabApp.update({
|
|
where: { id: application.gitSource.gitlabApp.id },
|
|
data: { deployKeyId }
|
|
});
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
export async function cancelDeployment(request: FastifyRequest<CancelDeployment>, reply: FastifyReply) {
|
|
try {
|
|
const { buildId, applicationId } = request.body;
|
|
if (!buildId) {
|
|
throw { status: 500, message: 'buildId is required' }
|
|
|
|
}
|
|
await stopBuild(buildId, applicationId);
|
|
return reply.code(201).send()
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
|
|
|
|
export async function createdBranchDatabase(database: any, baseDatabaseBranch: string, pullmergeRequestId: string) {
|
|
try {
|
|
if (!baseDatabaseBranch) return
|
|
const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database;
|
|
if (destinationDockerId) {
|
|
if (type === 'postgresql') {
|
|
const decryptedRootUserPassword = decrypt(rootUserPassword);
|
|
await executeDockerCmd({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump`
|
|
})
|
|
await executeDockerCmd({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"`
|
|
})
|
|
await executeDockerCmd({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump`
|
|
})
|
|
await executeDockerCmd({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"`
|
|
})
|
|
}
|
|
}
|
|
|
|
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
}
|
|
export async function removeBranchDatabase(database: any, pullmergeRequestId: string) {
|
|
try {
|
|
const { id, type, destinationDockerId, rootUser, rootUserPassword } = database;
|
|
if (destinationDockerId) {
|
|
if (type === 'postgresql') {
|
|
const decryptedRootUserPassword = decrypt(rootUserPassword);
|
|
// Terminate all connections to the database
|
|
await executeDockerCmd({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"`
|
|
})
|
|
|
|
await executeDockerCmd({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"`
|
|
})
|
|
}
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message })
|
|
}
|
|
} |