diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index 7f0977043..c8fea7548 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -20,7 +20,8 @@ import { decryptApplication, isDev, pushToRegistry, - executeCommand + executeCommand, + generateSecrets } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -140,33 +141,10 @@ import * as buildpacks from '../lib/buildPacks'; } catch (error) { // } - const envs = [`PORT='${port}'`]; + let envs = ['NODE_ENV=production', `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}'`); - } - } - }); + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - await fs.writeFile(`${workdir}/Dockerfile`, simpleDockerfile); if (dockerRegistry) { const { url, username, password } = dockerRegistry; @@ -197,7 +175,7 @@ import * as buildpacks from '../lib/buildPacks'; container_name: applicationId, volumes, labels, - env_file: envFound ? [`${workdir}/.env`] : [], + environment: envs, depends_on: [], expose: [port], ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), @@ -698,37 +676,14 @@ import * as buildpacks from '../lib/buildPacks'; } catch (error) { // } - const envs = [`PORT='${port}'`]; + let envs = ['NODE_ENV=production', `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}'`); - } - } - }); + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); if (dockerRegistry) { const { url, username, password } = dockerRegistry; await saveDockerRegistryCredentials({ url, username, password, workdir }); } - - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } try { const composeVolumes = volumes.map((volume) => { return { @@ -744,7 +699,7 @@ import * as buildpacks from '../lib/buildPacks'; image: imageFound, container_name: imageId, volumes, - env_file: envFound ? [`${workdir}/.env`] : [], + environment: envs, labels, depends_on: [], expose: [port], diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index f25e15e7d..1a0d18d73 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -3,6 +3,7 @@ import { decrypt, encrypt, executeCommand, + generateSecrets, generateTimestamp, getDomain, isARM, @@ -653,7 +654,7 @@ export async function saveDockerRegistryCredentials({ url, username, password, w try { await fs.mkdir(`${workdir}/.docker`); } catch (error) { - console.log(error); + // console.log(error); } const payload = JSON.stringify({ auths: { @@ -787,21 +788,8 @@ export async function buildCacheImageWithNode(data, imageForBuild) { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { @@ -811,7 +799,6 @@ export async function buildCacheImageWithNode(data, imageForBuild) { if (installCommand) { Dockerfile.push(`RUN ${installCommand}`); } - // Dockerfile.push(`ARG CACHEBUST=1`); Dockerfile.push(`RUN ${buildCommand}`); await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); await buildImage({ ...data, isCache: true }); @@ -819,27 +806,13 @@ export async function buildCacheImageWithNode(data, imageForBuild) { export async function buildCacheImageForLaravel(data, imageForBuild) { const { workdir, buildId, secrets, pullmergeRequestId } = data; - const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } Dockerfile.push(`COPY *.json *.mix.js /app/`); diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index bda83bd9e..d4adb796a 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -1,5 +1,5 @@ import { promises as fs } from 'fs'; -import { defaultComposeConfiguration, executeCommand } from '../common'; +import { defaultComposeConfiguration, executeCommand, generateSecrets } from '../common'; import { saveBuildLog } from './common'; import yaml from 'js-yaml'; @@ -25,30 +25,11 @@ export default async function (data) { if (!dockerComposeYaml.services) { throw 'No Services found in docker-compose file.'; } - const envs = []; + let envs = ['NODE_ENV=production']; 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}'`); - } - } - }); - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId)]; } + const composeVolumes = []; if (volumes.length > 0) { for (const volume of volumes) { @@ -62,7 +43,7 @@ export default async function (data) { let networks = {}; for (let [key, value] of Object.entries(dockerComposeYaml.services)) { value['container_name'] = `${applicationId}-${key}`; - value['env_file'] = envFound ? [`${workdir}/.env`] : []; + value['environment'] = [...value['environment'], ...envs]; value['labels'] = labels; // TODO: If we support separated volume for each service, we need to add it here if (value['volumes']?.length > 0) { @@ -106,6 +87,7 @@ export default async function (data) { dockerComposeYaml['volumes'] = { ...composeVolumes }; } dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }); + await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); await executeCommand({ debug, diff --git a/apps/api/src/lib/buildPacks/deno.ts b/apps/api/src/lib/buildPacks/deno.ts index 90107e4d2..2649e3d0a 100644 --- a/apps/api/src/lib/buildPacks/deno.ts +++ b/apps/api/src/lib/buildPacks/deno.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; const createDockerfile = async (data, image): Promise => { @@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (depsFound) { diff --git a/apps/api/src/lib/buildPacks/docker.ts b/apps/api/src/lib/buildPacks/docker.ts index 2ef265fe3..e02103f88 100644 --- a/apps/api/src/lib/buildPacks/docker.ts +++ b/apps/api/src/lib/buildPacks/docker.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; export default async function (data) { @@ -13,19 +14,12 @@ export default async function (data) { } }); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if ( - (pullmergeRequestId && secret.isPRMRSecret) || - (!pullmergeRequestId && !secret.isPRMRSecret) - ) { - Dockerfile.forEach((line, index) => { - if (line.startsWith('FROM')) { - Dockerfile.splice(index + 1, 0, `ARG ${secret.name}='${secret.value}'`); - } - }); + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.forEach((line, index) => { + if (line.startsWith('FROM')) { + Dockerfile.splice(index + 1, 0, env); } - } + }); }); } await fs.writeFile(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n')); diff --git a/apps/api/src/lib/buildPacks/nextjs.ts b/apps/api/src/lib/buildPacks/nextjs.ts index c7aaa9dcd..957dc5bce 100644 --- a/apps/api/src/lib/buildPacks/nextjs.ts +++ b/apps/api/src/lib/buildPacks/nextjs.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; const createDockerfile = async (data, image): Promise => { @@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { diff --git a/apps/api/src/lib/buildPacks/node.ts b/apps/api/src/lib/buildPacks/node.ts index c2e77c9f5..8ccfcc68e 100644 --- a/apps/api/src/lib/buildPacks/node.ts +++ b/apps/api/src/lib/buildPacks/node.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage, checkPnpm } from './common'; const createDockerfile = async (data, image): Promise => { @@ -20,21 +21,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { diff --git a/apps/api/src/lib/buildPacks/nuxtjs.ts b/apps/api/src/lib/buildPacks/nuxtjs.ts index c7aaa9dcd..957dc5bce 100644 --- a/apps/api/src/lib/buildPacks/nuxtjs.ts +++ b/apps/api/src/lib/buildPacks/nuxtjs.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; const createDockerfile = async (data, image): Promise => { @@ -24,21 +25,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (isPnpm) { diff --git a/apps/api/src/lib/buildPacks/php.ts b/apps/api/src/lib/buildPacks/php.ts index a5e2737ac..abfd7af4f 100644 --- a/apps/api/src/lib/buildPacks/php.ts +++ b/apps/api/src/lib/buildPacks/php.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; const createDockerfile = async (data, image, htaccessFound): Promise => { @@ -13,21 +14,8 @@ const createDockerfile = async (data, image, htaccessFound): Promise => { Dockerfile.push(`FROM ${image}`); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } Dockerfile.push('WORKDIR /app'); diff --git a/apps/api/src/lib/buildPacks/python.ts b/apps/api/src/lib/buildPacks/python.ts index 3b5dfc264..56294660f 100644 --- a/apps/api/src/lib/buildPacks/python.ts +++ b/apps/api/src/lib/buildPacks/python.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildImage } from './common'; const createDockerfile = async (data, image): Promise => { @@ -18,21 +19,8 @@ const createDockerfile = async (data, image): Promise => { Dockerfile.push('WORKDIR /app'); Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (pythonWSGI?.toLowerCase() === 'gunicorn') { diff --git a/apps/api/src/lib/buildPacks/static.ts b/apps/api/src/lib/buildPacks/static.ts index 235175c7a..19b13cef3 100644 --- a/apps/api/src/lib/buildPacks/static.ts +++ b/apps/api/src/lib/buildPacks/static.ts @@ -1,4 +1,5 @@ import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; import { buildCacheImageWithNode, buildImage } from './common'; const createDockerfile = async (data, image): Promise => { @@ -25,21 +26,8 @@ const createDockerfile = async (data, image): Promise => { } Dockerfile.push(`LABEL coolify.buildId=${buildId}`); if (secrets.length > 0) { - secrets.forEach((secret) => { - if (secret.isBuildSecret) { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter(s => s.name === secret.name && s.isPRMRSecret) - if (isSecretFound.length > 0) { - Dockerfile.push(`ARG ${secret.name}='${isSecretFound[0].value}'`); - } else { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } else { - if (!secret.isPRMRSecret) { - Dockerfile.push(`ARG ${secret.name}='${secret.value}'`); - } - } - } + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); }); } if (buildCommand) { diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 52dd2dd53..69a77aa9a 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -19,7 +19,7 @@ import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common import { scheduler } from './scheduler'; import type { ExecaChildProcess } from 'execa'; -export const version = '3.12.2'; +export const version = '3.12.3'; export const isDev = process.env.NODE_ENV === 'development'; export const sentryDSN = 'https://409f09bcb7af47928d3e0f46b78987f3@o1082494.ingest.sentry.io/4504236622217216'; @@ -1875,3 +1875,48 @@ export async function pushToRegistry( command: pushCommand }); } + +export function generateSecrets( + secrets: Array, + pullmergeRequestId: string, + isBuild = false +): Array { + const envs = []; + const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret); + const normalSecrets = secrets.filter((s) => !s.isPRMRSecret); + if (pullmergeRequestId && isPRMRSecret.length > 0) { + isPRMRSecret.forEach((secret) => { + if (isBuild && !secret.isBuildSecret) { + return; + } + const build = isBuild && secret.isBuildSecret; + if (build) { + if (secret.value.includes(' ') || secret.value.includes('\\n')) { + envs.push(`ARG ${secret.name}='${secret.value}'`); + } else { + envs.push(`ARG ${secret.name}=${secret.value}`); + } + } else { + envs.push(`${secret.name}=${secret.value}`); + } + }); + } + if (!pullmergeRequestId && normalSecrets.length > 0) { + normalSecrets.forEach((secret) => { + if (isBuild && !secret.isBuildSecret) { + return; + } + const build = isBuild && secret.isBuildSecret; + if (build) { + if (secret.value.includes(' ') || secret.value.includes('\\n')) { + envs.push(`ARG ${secret.name}='${secret.value}'`); + } else { + envs.push(`ARG ${secret.name}=${secret.value}`); + } + } else { + envs.push(`${secret.name}=${secret.value}`); + } + }); + } + return envs; +} diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 3e2739ec5..ebc70e713 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -1,5 +1,5 @@ import cuid from 'cuid'; -import crypto from 'node:crypto' +import crypto from 'node:crypto'; import jsonwebtoken from 'jsonwebtoken'; import { FastifyReply } from 'fastify'; import fs from 'fs/promises'; @@ -7,1601 +7,1836 @@ 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, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +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 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))); + 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 }) - } + 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) { - 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' - } - } + 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 }) - } + return { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function cleanupUnconfiguredApplications(request: FastifyRequest) { - 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 }) - } + 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) { - 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 }) - } + 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) { - 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 }) - } + 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 }) - } + 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; - }); - } + 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; - } + 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 - ); + 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 }) - } + // 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 - ); + 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 - }) + // 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 }) - } + return applications; + } catch ({ status, message, type }) { + return errorHandler({ status, message, type }); + } } -export async function saveApplication(request: FastifyRequest, 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 saveApplication( + request: FastifyRequest, + 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, 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 saveApplicationSettings( + request: FastifyRequest, + 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, 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 stopPreviewApplication( + request: FastifyRequest, + 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, 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; - 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 - 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 }) - } +export async function restartApplication( + request: FastifyRequest, + 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 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) { - // - } + 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 }); + } - if (!imageFoundLocally && !imageFoundRemotely) { - throw { status: 500, message: 'Image not found, cannot restart application.' } - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + 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) { + // + } - 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)); - try { - await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }) - await executeCommand({ dockerId, command: `docker rm ${id}` }) - } catch (error) { - // - } + if (!imageFoundLocally && !imageFoundRemotely) { + throw { status: 500, message: 'Image not found, cannot restart application.' }; + } - 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 }) - } + 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, 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 }) - } + 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, reply: FastifyReply) { - try { - const { id } = request.params - const { force } = request.body +export async function deleteApplication( + request: FastifyRequest, + 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 }) - } + 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) { - 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 }) - } + 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) { - try { - const { id } = request.params - let { exposePort, fqdn, forceSave, dualCerts } = request.body - if (!fqdn) { - return {} - } else { - fqdn = fqdn.toLowerCase(); - } - if (exposePort) exposePort = Number(exposePort); + 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 { + 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 }) - } + 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 = {}; + 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 }) - } + 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'); + 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() - }) - } + 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); + imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag); - return { - imagesAvailables, - runningImage - } - } catch (error) { - return { - imagesAvailables, - } - } - - } catch ({ status, message }) { - - return errorHandler({ status, message }) - } + 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 = {}; + 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 }) - } + 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) { - 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' - } - }); - } + 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 }) - } + return { + buildId + }; + } + throw { status: 500, message: 'Application not found!' }; + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } +export async function saveApplicationSource( + request: FastifyRequest, + 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 } } } + }); + } -export async function saveApplicationSource(request: FastifyRequest, 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 }) - } + return reply.code(201).send(); + } catch ({ status, message }) { + return errorHandler({ status, message }); + } } export async function getGitHubToken(request: FastifyRequest, 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 }) - } + 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) { - 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 }) - } + 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 + try { + const { id } = request.params; + let { + repository, + branch, + projectId, + autodeploy, + webhookToken, + isPublicRepository = false + } = request.body; - repository = repository.toLowerCase(); + 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 }) - } + 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, 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 saveDestination( + request: FastifyRequest, + 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 }) - } + 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 }) - } + 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 }) - } + 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 }) - } + 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) { - try { - const { id } = request.params + 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' } - }); + 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; - }); + 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 }) - } + 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, 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 updatePreviewSecret( + request: FastifyRequest, + 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, 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 }) - } + 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, 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 }) - } + 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) { - 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 }) - } + 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) { - try { - const { id } = request.params - const persistentStorages = await prisma.applicationPersistentStorage.findMany({ where: { applicationId: id } }); - return { - persistentStorages - } - } catch ({ status, message }) { - return errorHandler({ status, message }) - } + 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, reply: FastifyReply) { - try { - const { id } = request.params - const { path, newStorage, storageId } = request.body + 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 }) - } + 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) { - 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 }) - } + 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, 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; +export async function restartPreview( + request: FastifyRequest, + 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 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.' } - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + 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.' }; + } - 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 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 }) - } + 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) { - 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 }) - } + 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) { - 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') + 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 }) - } + 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) { - 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; - }); + 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 }) - } + 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) { - 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 }) - } + 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) { - try { - const { id } = request.params - let { buildId, skip = 0 } = request.query - if (typeof skip !== 'number') { - skip = Number(skip) - } + try { + const { id } = request.params; + let { buildId, skip = 0 } = request.query; + if (typeof skip !== 'number') { + skip = Number(skip); + } - let builds = []; + 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 }) - } + 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) { - 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 }) - } + 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) { - 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 }) - } + 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, 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 }) - } + 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, reply: FastifyReply) { - try { - const { id } = request.params - let { deployKeyId } = request.body; + 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 }) - } + 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, 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 cancelDeployment( + request: FastifyRequest, + 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 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();"` - }) + 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 }) - } -} \ No newline at end of file + 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 }); + } +} diff --git a/apps/server/src/trpc/routers/applications/index.ts b/apps/server/src/trpc/routers/applications/index.ts index 2674bdd79..563f9fb12 100644 --- a/apps/server/src/trpc/routers/applications/index.ts +++ b/apps/server/src/trpc/routers/applications/index.ts @@ -203,21 +203,33 @@ export const applicationsRouter = router({ let location = null; const labels = []; let image = null; - const envs = [`PORT=${port}`]; + const envs = [`PORT=${port}`, 'NODE_ENV=production']; if (secrets.length > 0) { secrets.forEach((secret) => { if (pullmergeRequestId) { const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); if (isSecretFound.length > 0) { + if (isSecretFound[0].value.includes('\\n') || isSecretFound[0].value.includes("'")) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { envs.push(`${secret.name}='${isSecretFound[0].value}'`); + } } else { + if (secret.value.includes('\\n')|| secret.value.includes("'")) { + envs.push(`${secret.name}=${secret.value}`); + } else { envs.push(`${secret.name}='${secret.value}'`); + } } } else { if (!secret.isPRMRSecret) { + if (secret.value.includes('\\n')|| secret.value.includes("'")) { + envs.push(`${secret.name}=${secret.value}`); + } else { envs.push(`${secret.name}='${secret.value}'`); - } + } + } } }); } diff --git a/package.json b/package.json index 361b63719..0b6546855 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "3.12.2", + "version": "3.12.3", "license": "Apache-2.0", "repository": "github:coollabsio/coolify", "scripts": {