import cuid from 'cuid'; import crypto from 'node:crypto'; import jsonwebtoken from 'jsonwebtoken'; import { FastifyReply } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import csv from 'csvtojson'; import { day } from '../../../../lib/dayjs'; import { saveDockerRegistryCredentials, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateSecrets, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker'; import type { FastifyRequest } from 'fastify'; import type { GetImages, CancelDeployment, CheckDNS, CheckRepository, DeleteApplication, DeleteSecret, DeleteStorage, GetApplicationLogs, GetBuildIdLogs, SaveApplication, SaveApplicationSettings, SaveApplicationSource, SaveDeployKey, SaveDestination, SaveSecret, SaveStorage, DeployApplication, CheckDomain, StopPreviewApplication, RestartPreviewApplication, GetBuilds, RestartApplication } from './types'; import { OnlyId } from '../../../../types'; function filterObject(obj, callback) { return Object.fromEntries(Object.entries(obj).filter(([key, val]) => callback(val, key))); } export async function listApplications(request: FastifyRequest) { try { const { teamId } = request.user; const applications = await prisma.application.findMany({ where: { teams: { some: { id: teamId === '0' ? undefined : teamId } } }, include: { teams: true, destinationDocker: true, settings: true } }); const settings = await prisma.setting.findFirst(); return { applications, settings }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getImages(request: FastifyRequest) { try { const { buildPack, deploymentType } = request.body; let publishDirectory = undefined; let port = undefined; const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( buildPack, deploymentType ); if (buildPack === 'nextjs') { if (deploymentType === 'static') { publishDirectory = 'out'; port = '80'; } else { publishDirectory = ''; port = '3000'; } } if (buildPack === 'nuxtjs') { if (deploymentType === 'static') { publishDirectory = 'dist'; port = '80'; } else { publishDirectory = ''; port = '3000'; } } return { baseImage, baseImages, baseBuildImage, baseBuildImages, publishDirectory, port }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function cleanupUnconfiguredApplications(request: FastifyRequest) { try { const teamId = request.user.teamId; const 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 === 'compose') { continue; } 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; const 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 }); } } export async function newApplication(request: FastifyRequest, reply: FastifyReply) { try { const name = uniqueName(); const { teamId } = request.user; const { id } = await prisma.application.create({ data: { name, teams: { connect: { id: teamId } }, settings: { create: { debug: false, previews: false } } } }); return reply.code(201).send({ id }); } catch ({ status, message }) { return errorHandler({ status, message }); } } function decryptApplication(application: any) { if (application) { if (application?.gitSource?.githubApp?.clientSecret) { application.gitSource.githubApp.clientSecret = decrypt(application.gitSource.githubApp.clientSecret) || null; } if (application?.gitSource?.githubApp?.webhookSecret) { application.gitSource.githubApp.webhookSecret = decrypt(application.gitSource.githubApp.webhookSecret) || null; } if (application?.gitSource?.githubApp?.privateKey) { application.gitSource.githubApp.privateKey = decrypt(application.gitSource.githubApp.privateKey) || null; } if (application?.gitSource?.gitlabApp?.appSecret) { application.gitSource.gitlabApp.appSecret = decrypt(application.gitSource.gitlabApp.appSecret) || null; } if (application?.secrets.length > 0) { application.secrets = application.secrets.map((s: any) => { s.value = decrypt(s.value) || null; return s; }); } return application; } } export async function getApplicationFromDB(id: string, teamId: string) { try { let application = await prisma.application.findFirst({ where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } }, include: { destinationDocker: true, settings: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, secrets: true, persistentStorage: true, connectedDatabase: true, previewApplication: true, dockerRegistry: true } }); if (!application) { throw { status: 404, message: 'Application not found.' }; } application = decryptApplication(application); const buildPack = application?.buildPack || null; const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage(buildPack); // Set default build images if (!application.baseImage) { application.baseImage = baseImage; } if (!application.baseBuildImage) { application.baseBuildImage = baseBuildImage; } return { ...application, baseBuildImages, baseImages }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getApplicationFromDBWebhook(projectId: number, branch: string) { try { let applications = await prisma.application.findMany({ where: { projectId, branch, settings: { autodeploy: true } }, include: { destinationDocker: true, settings: true, gitSource: { include: { githubApp: true, gitlabApp: true } }, secrets: true, persistentStorage: true, connectedDatabase: true } }); if (applications.length === 0) { throw { status: 500, message: 'Application not configured.', type: 'webhook' }; } applications = applications.map((application: any) => { application = decryptApplication(application); const { baseImage, baseBuildImage, baseBuildImages, baseImages } = setDefaultBaseImage( application.buildPack ); // Set default build images if (!application.baseImage) { application.baseImage = baseImage; } if (!application.baseBuildImage) { application.baseBuildImage = baseBuildImage; } application.baseBuildImages = baseBuildImages; application.baseImages = baseImages; return application; }); return applications; } catch ({ status, message, type }) { return errorHandler({ status, message, type }); } } export async function saveApplication( request: FastifyRequest, 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, isHttp2 } = request.body; await prisma.application.update({ where: { id }, data: { fqdn: isBot ? null : undefined, settings: { update: { debug, previews, dualCerts, autodeploy, isBot, isDBBranching, isCustomSSL, isHttp2 } } }, 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 restartApplication( request: FastifyRequest, reply: FastifyReply ) { try { const { id } = request.params; const { imageId = null } = request.body; const { teamId } = request.user; const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { const buildId = cuid(); const { id: dockerId, network } = application.destinationDocker; const { dockerRegistry, secrets, pullmergeRequestId, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; let location = null; let envs = []; if (secrets.length > 0) { envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; } const { workdir } = await createDirectories({ repository, buildId }); const labels = []; let image = null; if (imageId) { image = imageId; } else { const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` }); const containersArray = container.trim().split('\n'); for (const container of containersArray) { const containerObj = formatLabelsOnDocker(container); image = containerObj[0].Image; Object.keys(containerObj[0].Labels).forEach(function (key) { if (key.startsWith('coolify')) { labels.push(`${key}=${containerObj[0].Labels[key]}`); } }); } } if (dockerRegistry) { const { url, username, password } = dockerRegistry; location = await saveDockerRegistryCredentials({ url, username, password, workdir }); } let imageFoundLocally = false; try { await executeCommand({ dockerId, command: `docker image inspect ${image}` }); imageFoundLocally = true; } catch (error) { // } let imageFoundRemotely = false; try { await executeCommand({ dockerId, command: `docker ${location ? `--config ${location}` : ''} pull ${image}` }); imageFoundRemotely = true; } catch (error) { // } if (!imageFoundLocally && !imageFoundRemotely) { throw { status: 500, message: 'Image not found, cannot restart application.' }; } const volumes = persistentStorage?.map((storage) => { return `${applicationId}${storage.path.replace(/\//gi, '-')}:${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} -f ${workdir}/docker-compose.yml 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 }); } } export async function deleteApplication( request: FastifyRequest, reply: FastifyReply ) { try { const { id } = request.params; const { teamId } = request.user; const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true, teams: true } }); if (teamId !== '0' && !application.teams.some((team) => team.id === teamId)) { throw { status: 403, message: 'You are not allowed to delete this application.' }; } if (application?.destinationDocker?.id && 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 } }); await prisma.previewApplication.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 } }); // TODO: Disabled this because it is having problems with remote docker engines. // return await checkDomainsIsValidInDNS({ hostname: domain, fqdn, dualCerts }); return {}; } 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); 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 }); // TODO: Disabled this because it is having problems with remote docker engines. // if (isDNSCheckEnabled && !isDev && !forceSave) { // let hostname = request.hostname.split(':')[0]; // if (remoteEngine) hostname = remoteIpAddress; // return await checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }); // } return {}; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getUsage(request) { try { const { id } = request.params; const teamId = request.user?.teamId; let usage = {}; const application: any = await getApplicationFromDB(id, teamId); if (application.destinationDockerId) { [usage] = await Promise.all([getContainerUsage(application.destinationDocker.id, id)]); } return { usage }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getDockerImages(request) { try { const { id } = request.params; const teamId = request.user?.teamId; const application: any = await getApplicationFromDB(id, teamId); let imagesAvailables = []; try { const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'` }); 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') .filter((image) => image.includes(id) && !image.includes('-cache')); for (const image of images) { const [repository, tag, createdAt] = image.split('#'); if (tag.includes('-')) { continue; } const [year, time] = createdAt.split(' '); imagesAvailables.push({ repository, tag, createdAt: day(year + time).unix() }); } imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag); return { imagesAvailables, runningImage }; } catch (error) { console.log(error); return { imagesAvailables }; } } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getUsageByContainer(request) { try { const { id, containerId } = request.params; const teamId = request.user?.teamId; let usage = {}; const application: any = await getApplicationFromDB(id, teamId); if (application.destinationDockerId) { [usage] = await Promise.all([ getContainerUsage(application.destinationDocker.id, containerId) ]); } return { usage }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function deployApplication(request: FastifyRequest) { try { const { id } = request.params; const teamId = request.user?.teamId; const { pullmergeRequestId = null, branch, forceRebuild } = request.body; const buildId = cuid(); const application = await getApplicationFromDB(id, teamId); if (application) { if (!application?.configHash) { const configHash = crypto .createHash('sha256') .update( JSON.stringify({ buildPack: application.buildPack, port: application.port, exposePort: application.exposePort, installCommand: application.installCommand, buildCommand: application.buildCommand, startCommand: application.startCommand }) ) .digest('hex'); await prisma.application.update({ where: { id }, data: { configHash } }); } await prisma.application.update({ where: { id }, data: { updatedAt: new Date() } }); if (application.gitSourceId) { await prisma.build.create({ data: { id: buildId, applicationId: id, sourceBranch: branch, branch: application.branch, pullmergeRequestId: pullmergeRequestId?.toString(), forceRebuild, destinationDockerId: application.destinationDocker?.id, gitSourceId: application.gitSource?.id, githubAppId: application.gitSource?.githubApp?.id, gitlabAppId: application.gitSource?.gitlabApp?.id, status: 'queued', type: pullmergeRequestId ? application.gitSource?.githubApp?.id ? 'manual_pr' : 'manual_mr' : 'manual' } }); } else { await prisma.build.create({ data: { id: buildId, applicationId: id, branch: 'latest', forceRebuild, destinationDockerId: application.destinationDocker?.id, status: 'queued', type: 'manual' } }); } return { buildId }; } throw { status: 500, message: 'Application not found!' }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function saveApplicationSource( request: FastifyRequest, reply: FastifyReply ) { try { const { id } = request.params; const { gitSourceId, forPublic, type, simpleDockerfile } = request.body; if (forPublic) { const publicGit = await prisma.gitSource.findFirst({ where: { type, forPublic } }); await prisma.application.update({ where: { id }, data: { gitSource: { connect: { id: publicGit.id } } } }); } if (simpleDockerfile) { await prisma.application.update({ where: { id }, data: { simpleDockerfile, settings: { update: { autodeploy: false } } } }); } if (gitSourceId) { await prisma.application.update({ where: { id }, data: { gitSource: { connect: { id: gitSourceId } } } }); } return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getGitHubToken(request: FastifyRequest, reply: FastifyReply) { try { const { default: got } = await import('got'); const { id } = request.params; const { teamId } = request.user; const application: any = await getApplicationFromDB(id, teamId); const payload = { iat: Math.round(new Date().getTime() / 1000), exp: Math.round(new Date().getTime() / 1000 + 60), iss: application.gitSource.githubApp.appId }; const githubToken = jsonwebtoken.sign(payload, application.gitSource.githubApp.privateKey, { algorithm: 'RS256' }); const { token } = await got .post( `${application.gitSource.apiUrl}/app/installations/${application.gitSource.githubApp.installationId}/access_tokens`, { headers: { Authorization: `Bearer ${githubToken}` } } ) .json(); return reply.code(201).send({ token }); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function checkRepository(request: FastifyRequest) { try { const { id } = request.params; const { repository, branch } = request.query; const application = await prisma.application.findUnique({ where: { id }, include: { gitSource: true } }); const found = await prisma.application.findFirst({ where: { branch, repository, gitSource: { type: application.gitSource.type }, id: { not: id } } }); return { used: found ? true : false }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function saveRepository(request, reply) { try { const { id } = request.params; let { repository, branch, projectId, autodeploy, webhookToken, isPublicRepository = false } = request.body; repository = repository.toLowerCase(); projectId = Number(projectId); if (webhookToken) { await prisma.application.update({ where: { id }, data: { repository, branch, projectId, gitSource: { update: { gitlabApp: { update: { webhookToken: webhookToken ? webhookToken : undefined } } } }, settings: { update: { autodeploy, isPublicRepository } } } }); } else { await prisma.application.update({ where: { id }, data: { repository, branch, projectId, settings: { update: { autodeploy, isPublicRepository } } } }); } // if (!isPublicRepository) { // const isDouble = await checkDoubleBranch(branch, projectId); // if (isDouble) { // await prisma.applicationSettings.updateMany({ where: { application: { branch, projectId } }, data: { autodeploy: false, isPublicRepository } }) // } // } return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function saveDestination( request: FastifyRequest, reply: FastifyReply ) { try { const { id } = request.params; const { destinationId } = request.body; await prisma.application.update({ where: { id }, data: { destinationDocker: { connect: { id: destinationId } } } }); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getBuildPack(request) { try { const { id } = request.params; const teamId = request.user?.teamId; const application: any = await getApplicationFromDB(id, teamId); return { type: application.gitSource?.type || 'dockerRegistry', projectId: application.projectId, repository: application.repository, branch: application.branch, apiUrl: application.gitSource?.apiUrl || null, isPublicRepository: application.settings.isPublicRepository }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function saveRegistry(request, reply) { try { const { id } = request.params; const { registryId } = request.body; await prisma.application.update({ where: { id }, data: { dockerRegistry: { connect: { id: registryId } } } }); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function saveBuildPack(request, reply) { try { const { id } = request.params; const { buildPack } = request.body; const { baseImage, baseBuildImage } = setDefaultBaseImage(buildPack); await prisma.application.update({ where: { id }, data: { buildPack, baseImage, baseBuildImage } }); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function saveConnectedDatabase(request, reply) { try { const { id } = request.params; const { databaseId, type } = request.body; await prisma.application.update({ where: { id }, data: { connectedDatabase: { upsert: { create: { database: { connect: { id: databaseId } }, hostedDatabaseType: type }, update: { database: { connect: { id: databaseId } }, hostedDatabaseType: type } } } } }); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getSecrets(request: FastifyRequest) { try { const { id } = request.params; let secrets = await prisma.secret.findMany({ where: { applicationId: id, isPRMRSecret: false }, orderBy: { createdAt: 'asc' } }); let previewSecrets = await prisma.secret.findMany({ where: { applicationId: id, isPRMRSecret: true }, orderBy: { createdAt: 'asc' } }); secrets = secrets.map((secret) => { secret.value = decrypt(secret.value); return secret; }); previewSecrets = previewSecrets.map((secret) => { secret.value = decrypt(secret.value); return secret; }); return { previewSecrets: previewSecrets.sort((a, b) => { return ('' + a.name).localeCompare(b.name); }), secrets: secrets.sort((a, b) => { return ('' + a.name).localeCompare(b.name); }) }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function updatePreviewSecret( request: FastifyRequest, 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 }); } } 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 }); } } 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 }); } } 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 }); } } export async function saveStorage(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params; const { hostPath, path, newStorage, storageId } = request.body; if (newStorage) { await prisma.applicationPersistentStorage.create({ data: { hostPath, path, application: { connect: { id } } } }); } else { await prisma.applicationPersistentStorage.update({ where: { id: storageId }, data: { hostPath, 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 }); } } export async function restartPreview( request: FastifyRequest, reply: FastifyReply ) { try { const { id, pullmergeRequestId } = request.params; const { teamId } = request.user; const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { const buildId = cuid(); const { id: dockerId, network } = application.destinationDocker; const { secrets, port, repository, persistentStorage, id: applicationId, buildPack, exposePort } = application; let envs = []; if (secrets.length > 0) { envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; } const { workdir } = await createDirectories({ repository, buildId }); const labels = []; let image = null; const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` }); const containersArray = container.trim().split('\n'); for (const container of containersArray) { const containerObj = formatLabelsOnDocker(container); image = containerObj[0].Image; Object.keys(containerObj[0].Labels).forEach(function (key) { if (key.startsWith('coolify')) { labels.push(`${key}=${containerObj[0].Labels[key]}`); } }); } let imageFound = false; try { await executeCommand({ dockerId, command: `docker image inspect ${image}` }); imageFound = true; } catch (error) { // } if (!imageFound) { throw { status: 500, message: 'Image not found, cannot restart application.' }; } const volumes = persistentStorage?.map((storage) => { return `${applicationId}${storage.path.replace(/\//gi, '-')}:${buildPack !== 'docker' ? '/app' : '' }${storage.path}`; }) || []; const composeVolumes = volumes.map((volume) => { return { [`${volume.split(':')[0]}`]: { name: volume.split(':')[0] } }; }); const composeFile = { version: '3.8', services: { [`${applicationId}-${pullmergeRequestId}`]: { image, container_name: `${applicationId}-${pullmergeRequestId}`, volumes, environment: envs, labels, depends_on: [], expose: [port], ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), ...defaultComposeConfiguration(network) } }, networks: { [network]: { external: true } }, volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); await executeCommand({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` }); await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` }); await executeCommand({ dockerId, command: `docker compose --project-directory ${workdir} -f ${workdir}/docker-compose.yml 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 }); } } 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' ); 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; }); 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); return { logs: stripLogsStderr.concat(stripLogsStdout) }; } 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); } let builds = []; const buildCount = await prisma.build.count({ where: { applicationId: id } }); if (buildId) { builds = await prisma.build.findMany({ where: { applicationId: id, id: buildId } }); } else { builds = await prisma.build.findMany({ where: { applicationId: id }, orderBy: { createdAt: 'desc' }, take: 5 + skip }); } builds = builds.map((build) => { if (build.status === 'running') { build.elapsed = (day().utc().diff(day(build.createdAt)) / 1000).toFixed(0); } return build; }); return { builds, buildCount }; } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function getBuildIdLogs(request: FastifyRequest) { 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) { const 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' }; } const fileLogs = (await fs.readFile(file)).toString(); const decryptedLogs = await csv({ noheader: true }).fromString(fileLogs); const 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 }); } } 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 }); } } export async function saveDeployKey(request: FastifyRequest, reply: FastifyReply) { try { const { id } = request.params; let { deployKeyId } = request.body; deployKeyId = Number(deployKeyId); const application = await prisma.application.findUnique({ where: { id }, include: { gitSource: { include: { gitlabApp: true } } } }); await prisma.gitlabApp.update({ where: { id: application.gitSource.gitlabApp.id }, data: { deployKeyId } }); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function cancelDeployment( request: FastifyRequest, reply: FastifyReply ) { try { const { buildId, applicationId } = request.body; if (!buildId) { throw { status: 500, message: 'buildId is required' }; } await stopBuild(buildId, applicationId); return reply.code(201).send(); } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function createdBranchDatabase( database: any, baseDatabaseBranch: string, pullmergeRequestId: string ) { try { if (!baseDatabaseBranch) return; const { id, type, destinationDockerId, rootUser, rootUserPassword, dbUser } = database; if (destinationDockerId) { if (type === 'postgresql') { const decryptedRootUserPassword = decrypt(rootUserPassword); await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump` }); await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"` }); await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump` }); await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"` }); } } } catch ({ status, message }) { return errorHandler({ status, message }); } } export async function removeBranchDatabase(database: any, pullmergeRequestId: string) { try { const { id, type, destinationDockerId, rootUser, rootUserPassword } = database; if (destinationDockerId) { if (type === 'postgresql') { const decryptedRootUserPassword = decrypt(rootUserPassword); // Terminate all connections to the database await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"` }); await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"` }); } } } catch ({ status, message }) { return errorHandler({ status, message }); } }