1843 lines
52 KiB
TypeScript
1843 lines
52 KiB
TypeScript
import cuid from 'cuid';
|
|
import crypto from 'node:crypto';
|
|
import jsonwebtoken from 'jsonwebtoken';
|
|
import { FastifyReply } from 'fastify';
|
|
import fs from 'fs/promises';
|
|
import yaml from 'js-yaml';
|
|
import csv from 'csvtojson';
|
|
|
|
import { day } from '../../../../lib/dayjs';
|
|
import {
|
|
saveDockerRegistryCredentials,
|
|
setDefaultBaseImage,
|
|
setDefaultConfiguration
|
|
} from '../../../../lib/buildPacks/common';
|
|
import {
|
|
checkDomainsIsValidInDNS,
|
|
checkExposedPort,
|
|
createDirectories,
|
|
decrypt,
|
|
defaultComposeConfiguration,
|
|
encrypt,
|
|
errorHandler,
|
|
executeCommand,
|
|
generateSecrets,
|
|
generateSshKeyPair,
|
|
getContainerUsage,
|
|
getDomain,
|
|
isDev,
|
|
isDomainConfigured,
|
|
listSettings,
|
|
prisma,
|
|
stopBuild,
|
|
uniqueName
|
|
} from '../../../../lib/common';
|
|
import { checkContainer, formatLabelsOnDocker, 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,
|
|
RestartApplication
|
|
} 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 cleanupUnconfiguredApplications(request: FastifyRequest<any>) {
|
|
try {
|
|
const 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 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 {};
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message });
|
|
}
|
|
}
|
|
export async function getApplicationStatus(request: FastifyRequest<OnlyId>) {
|
|
try {
|
|
const { id } = request.params;
|
|
const { teamId } = request.user;
|
|
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
|
|
}
|
|
});
|
|
}
|
|
}
|
|
} 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;
|
|
} 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,
|
|
dockerRegistry: 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.', type: 'webhook' };
|
|
}
|
|
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, type }) {
|
|
return errorHandler({ status, message, type });
|
|
}
|
|
}
|
|
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,
|
|
gitCommitHash,
|
|
baseImage,
|
|
baseBuildImage,
|
|
deploymentType,
|
|
baseDatabaseBranch,
|
|
dockerComposeFile,
|
|
dockerComposeFileLocation,
|
|
dockerComposeConfiguration,
|
|
simpleDockerfile,
|
|
dockerRegistryImageName
|
|
} = 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,
|
|
dockerComposeFileLocation,
|
|
denoMainFile
|
|
});
|
|
if (baseDatabaseBranch) {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: {
|
|
name,
|
|
fqdn,
|
|
exposePort,
|
|
pythonWSGI,
|
|
pythonModule,
|
|
pythonVariable,
|
|
denoOptions,
|
|
baseImage,
|
|
gitCommitHash,
|
|
baseBuildImage,
|
|
deploymentType,
|
|
dockerComposeFile,
|
|
dockerComposeFileLocation,
|
|
dockerComposeConfiguration,
|
|
simpleDockerfile,
|
|
dockerRegistryImageName,
|
|
...defaultConfiguration,
|
|
connectedDatabase: { update: { hostedDatabaseDBName: baseDatabaseBranch } }
|
|
}
|
|
});
|
|
} else {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: {
|
|
name,
|
|
fqdn,
|
|
exposePort,
|
|
pythonWSGI,
|
|
pythonModule,
|
|
gitCommitHash,
|
|
pythonVariable,
|
|
denoOptions,
|
|
baseImage,
|
|
baseBuildImage,
|
|
deploymentType,
|
|
dockerComposeFile,
|
|
dockerComposeFileLocation,
|
|
dockerComposeConfiguration,
|
|
simpleDockerfile,
|
|
dockerRegistryImageName,
|
|
...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<RestartApplication>,
|
|
reply: FastifyReply
|
|
) {
|
|
try {
|
|
const { id } = request.params;
|
|
const { imageId = null } = request.body;
|
|
const { teamId } = request.user;
|
|
let application: any = await getApplicationFromDB(id, teamId);
|
|
if (application?.destinationDockerId) {
|
|
const buildId = cuid();
|
|
const { id: dockerId, network } = application.destinationDocker;
|
|
const {
|
|
dockerRegistry,
|
|
secrets,
|
|
pullmergeRequestId,
|
|
port,
|
|
repository,
|
|
persistentStorage,
|
|
id: applicationId,
|
|
buildPack,
|
|
exposePort
|
|
} = application;
|
|
let location = null;
|
|
|
|
let envs = ['NODE_ENV=production', `PORT=${port}`];
|
|
if (secrets.length > 0) {
|
|
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
|
|
}
|
|
const { workdir } = await createDirectories({ repository, buildId });
|
|
const labels = [];
|
|
let image = null;
|
|
if (imageId) {
|
|
image = imageId;
|
|
} else {
|
|
const { stdout: container } = await executeCommand({
|
|
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]}`);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
if (dockerRegistry) {
|
|
const { url, username, password } = dockerRegistry;
|
|
location = await saveDockerRegistryCredentials({ url, username, password, workdir });
|
|
}
|
|
|
|
let imageFoundLocally = false;
|
|
try {
|
|
await executeCommand({
|
|
dockerId,
|
|
command: `docker image inspect ${image}`
|
|
});
|
|
imageFoundLocally = true;
|
|
} catch (error) {
|
|
//
|
|
}
|
|
let imageFoundRemotely = false;
|
|
try {
|
|
await executeCommand({
|
|
dockerId,
|
|
command: `docker ${location ? `--config ${location}` : ''} pull ${image}`
|
|
});
|
|
imageFoundRemotely = true;
|
|
} catch (error) {
|
|
//
|
|
}
|
|
|
|
if (!imageFoundLocally && !imageFoundRemotely) {
|
|
throw { status: 500, message: 'Image not found, cannot restart application.' };
|
|
}
|
|
|
|
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,
|
|
environment: envs,
|
|
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));
|
|
try {
|
|
await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` });
|
|
await executeCommand({ dockerId, command: `docker rm ${id}` });
|
|
} catch (error) {
|
|
//
|
|
}
|
|
|
|
await executeCommand({
|
|
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;
|
|
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) {
|
|
const containerObj = JSON.parse(container);
|
|
await removeContainer({
|
|
id: containerObj.ID,
|
|
dockerId: application.destinationDocker.id
|
|
});
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
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 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 {};
|
|
} 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 getDockerImages(request) {
|
|
try {
|
|
const { id } = request.params;
|
|
const teamId = request.user?.teamId;
|
|
const application: any = await getApplicationFromDB(id, teamId);
|
|
let imagesAvailables = [];
|
|
try {
|
|
const { stdout } = await executeCommand({
|
|
dockerId: application.destinationDocker.id,
|
|
command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}' | grep -i ${id} | grep -v cache`,
|
|
shell: true
|
|
});
|
|
const { stdout: runningImage } = await executeCommand({
|
|
dockerId: application.destinationDocker.id,
|
|
command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}`
|
|
});
|
|
const images = stdout.trim().split('\n');
|
|
|
|
for (const image of images) {
|
|
const [repository, tag, createdAt] = image.split('#');
|
|
if (tag.includes('-')) {
|
|
continue;
|
|
}
|
|
const [year, time] = createdAt.split(' ');
|
|
imagesAvailables.push({
|
|
repository,
|
|
tag,
|
|
createdAt: day(year + time).unix()
|
|
});
|
|
}
|
|
|
|
imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag);
|
|
|
|
return {
|
|
imagesAvailables,
|
|
runningImage
|
|
};
|
|
} catch (error) {
|
|
return {
|
|
imagesAvailables
|
|
};
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message });
|
|
}
|
|
}
|
|
|
|
export async function getUsageByContainer(request) {
|
|
try {
|
|
const { id, containerId } = 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, containerId)
|
|
]);
|
|
}
|
|
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() } });
|
|
if (application.gitSourceId) {
|
|
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'
|
|
}
|
|
});
|
|
} else {
|
|
await prisma.build.create({
|
|
data: {
|
|
id: buildId,
|
|
applicationId: id,
|
|
branch: 'latest',
|
|
forceRebuild,
|
|
destinationDockerId: application.destinationDocker?.id,
|
|
status: 'queued',
|
|
type: '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, simpleDockerfile } = 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 } } }
|
|
});
|
|
}
|
|
if (simpleDockerfile) {
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { simpleDockerfile, settings: { update: { autodeploy: false } } }
|
|
});
|
|
}
|
|
if (gitSourceId) {
|
|
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 { default: got } = await import('got');
|
|
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 { token } = await got
|
|
.post(
|
|
`${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`,
|
|
{
|
|
headers: {
|
|
Authorization: `Bearer ${githubToken}`
|
|
}
|
|
}
|
|
)
|
|
.json();
|
|
return reply.code(201).send({
|
|
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();
|
|
|
|
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 || 'dockerRegistry',
|
|
projectId: application.projectId,
|
|
repository: application.repository,
|
|
branch: application.branch,
|
|
apiUrl: application.gitSource?.apiUrl || null,
|
|
isPublicRepository: application.settings.isPublicRepository
|
|
};
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message });
|
|
}
|
|
}
|
|
|
|
export async function saveRegistry(request, reply) {
|
|
try {
|
|
const { id } = request.params;
|
|
const { registryId } = request.body;
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { dockerRegistry: { connect: { id: registryId } } }
|
|
});
|
|
return reply.code(201).send();
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message });
|
|
}
|
|
}
|
|
export async function saveBuildPack(request, reply) {
|
|
try {
|
|
const { id } = request.params;
|
|
const { buildPack } = request.body;
|
|
const { baseImage, baseBuildImage } = setDefaultBaseImage(buildPack);
|
|
await prisma.application.update({
|
|
where: { id },
|
|
data: { buildPack, baseImage, baseBuildImage }
|
|
});
|
|
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;
|
|
const found = await prisma.secret.findMany({ where: { applicationId: id, name } });
|
|
if (found.length > 0) {
|
|
throw { message: 'Secret already exists.' };
|
|
}
|
|
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;
|
|
|
|
let envs = ['NODE_ENV=production', `PORT=${port}`];
|
|
if (secrets.length > 0) {
|
|
envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)];
|
|
}
|
|
const { workdir } = await createDirectories({ repository, buildId });
|
|
const labels = [];
|
|
let image = null;
|
|
const { stdout: container } = await executeCommand({
|
|
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 executeCommand({
|
|
dockerId,
|
|
command: `docker image inspect ${image}`
|
|
});
|
|
imageFound = true;
|
|
} catch (error) {
|
|
//
|
|
}
|
|
if (!imageFound) {
|
|
throw { status: 500, message: 'Image not found, cannot restart application.' };
|
|
}
|
|
|
|
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,
|
|
environment: envs,
|
|
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 executeCommand({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` });
|
|
await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` });
|
|
await executeCommand({
|
|
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 executeCommand({
|
|
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, containerId } = 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 { 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);
|
|
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, stderr } = error;
|
|
if (stderr.startsWith('Error: No such container')) {
|
|
return { logs: [], noContainer: true };
|
|
}
|
|
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 executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump`
|
|
});
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"`
|
|
});
|
|
await executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump`
|
|
});
|
|
await executeCommand({
|
|
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 executeCommand({
|
|
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 executeCommand({
|
|
dockerId: destinationDockerId,
|
|
command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"`
|
|
});
|
|
}
|
|
}
|
|
} catch ({ status, message }) {
|
|
return errorHandler({ status, message });
|
|
}
|
|
}
|