1688 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			1688 lines
		
	
	
		
			48 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import { exec } from 'node:child_process';
 | 
						|
import util from 'util';
 | 
						|
import fs from 'fs/promises';
 | 
						|
import yaml from 'js-yaml';
 | 
						|
import forge from 'node-forge';
 | 
						|
import { uniqueNamesGenerator, adjectives, colors, animals } from 'unique-names-generator';
 | 
						|
import type { Config } from 'unique-names-generator';
 | 
						|
import generator from 'generate-password';
 | 
						|
import crypto from 'crypto';
 | 
						|
import { promises as dns } from 'dns';
 | 
						|
import { PrismaClient } from '@prisma/client';
 | 
						|
import os from 'os';
 | 
						|
import sshConfig from 'ssh-config';
 | 
						|
 | 
						|
import { checkContainer, removeContainer } from './docker';
 | 
						|
import { day } from './dayjs';
 | 
						|
import { saveBuildLog } from './buildPacks/common';
 | 
						|
import { scheduler } from './scheduler';
 | 
						|
 | 
						|
export const version = '3.11.7';
 | 
						|
export const isDev = process.env.NODE_ENV === 'development';
 | 
						|
 | 
						|
const algorithm = 'aes-256-ctr';
 | 
						|
const customConfig: Config = {
 | 
						|
	dictionaries: [adjectives, colors, animals],
 | 
						|
	style: 'capital',
 | 
						|
	separator: ' ',
 | 
						|
	length: 3
 | 
						|
};
 | 
						|
 | 
						|
export const defaultProxyImage = `coolify-haproxy-alpine:latest`;
 | 
						|
export const defaultProxyImageTcp = `coolify-haproxy-tcp-alpine:latest`;
 | 
						|
export const defaultProxyImageHttp = `coolify-haproxy-http-alpine:latest`;
 | 
						|
export const defaultTraefikImage = `traefik:v2.8`;
 | 
						|
export function getAPIUrl() {
 | 
						|
	if (process.env.GITPOD_WORKSPACE_URL) {
 | 
						|
		const { href } = new URL(process.env.GITPOD_WORKSPACE_URL);
 | 
						|
		const newURL = href.replace('https://', 'https://3001-').replace(/\/$/, '');
 | 
						|
		return newURL;
 | 
						|
	}
 | 
						|
	if (process.env.CODESANDBOX_HOST) {
 | 
						|
		return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`;
 | 
						|
	}
 | 
						|
	return isDev ? 'http://host.docker.internal:3001' : 'http://localhost:3000';
 | 
						|
}
 | 
						|
 | 
						|
export function getUIUrl() {
 | 
						|
	if (process.env.GITPOD_WORKSPACE_URL) {
 | 
						|
		const { href } = new URL(process.env.GITPOD_WORKSPACE_URL);
 | 
						|
		const newURL = href.replace('https://', 'https://3000-').replace(/\/$/, '');
 | 
						|
		return newURL;
 | 
						|
	}
 | 
						|
	if (process.env.CODESANDBOX_HOST) {
 | 
						|
		return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}`;
 | 
						|
	}
 | 
						|
	return 'http://localhost:3000';
 | 
						|
}
 | 
						|
 | 
						|
const mainTraefikEndpoint = isDev
 | 
						|
	? `${getAPIUrl()}/webhooks/traefik/main.json`
 | 
						|
	: 'http://coolify:3000/webhooks/traefik/main.json';
 | 
						|
 | 
						|
const otherTraefikEndpoint = isDev
 | 
						|
	? `${getAPIUrl()}/webhooks/traefik/other.json`
 | 
						|
	: 'http://coolify:3000/webhooks/traefik/other.json';
 | 
						|
 | 
						|
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
 | 
						|
export const asyncExecShell = util.promisify(exec);
 | 
						|
export const asyncExecShellStream = async ({
 | 
						|
	debug,
 | 
						|
	buildId,
 | 
						|
	applicationId,
 | 
						|
	command,
 | 
						|
	engine
 | 
						|
}: {
 | 
						|
	debug: boolean;
 | 
						|
	buildId: string;
 | 
						|
	applicationId: string;
 | 
						|
	command: string;
 | 
						|
	engine: string;
 | 
						|
}) => {
 | 
						|
	return await new Promise(async (resolve, reject) => {
 | 
						|
		const { execaCommand } = await import('execa');
 | 
						|
		const subprocess = execaCommand(command, {
 | 
						|
			env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine }
 | 
						|
		});
 | 
						|
		const logs = [];
 | 
						|
		subprocess.stdout.on('data', async (data) => {
 | 
						|
			const stdout = data.toString();
 | 
						|
			const array = stdout.split('\n');
 | 
						|
			for (const line of array) {
 | 
						|
				if (line !== '\n' && line !== '') {
 | 
						|
					const log = {
 | 
						|
						line: `${line.replace('\n', '')}`,
 | 
						|
						buildId,
 | 
						|
						applicationId
 | 
						|
					}
 | 
						|
					logs.push(log);
 | 
						|
					if (debug) {
 | 
						|
						await saveBuildLog(log);
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		});
 | 
						|
		subprocess.stderr.on('data', async (data) => {
 | 
						|
			const stderr = data.toString();
 | 
						|
			const array = stderr.split('\n');
 | 
						|
			for (const line of array) {
 | 
						|
				if (line !== '\n' && line !== '') {
 | 
						|
					const log = {
 | 
						|
						line: `${line.replace('\n', '')}`,
 | 
						|
						buildId,
 | 
						|
						applicationId
 | 
						|
					}
 | 
						|
					logs.push(log);
 | 
						|
					if (debug) {
 | 
						|
						await saveBuildLog(log);
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		});
 | 
						|
		subprocess.on('exit', async (code) => {
 | 
						|
			await asyncSleep(1000);
 | 
						|
			if (code === 0) {
 | 
						|
				resolve(code);
 | 
						|
			} else {
 | 
						|
				if (!debug) {
 | 
						|
					for (const log of logs) {
 | 
						|
						await saveBuildLog(log);
 | 
						|
					}
 | 
						|
				}
 | 
						|
				reject(code);
 | 
						|
			}
 | 
						|
		});
 | 
						|
	});
 | 
						|
};
 | 
						|
 | 
						|
export const asyncSleep = (delay: number): Promise<unknown> =>
 | 
						|
	new Promise((resolve) => setTimeout(resolve, delay));
 | 
						|
export const prisma = new PrismaClient({
 | 
						|
	errorFormat: 'minimal'
 | 
						|
	// log: [
 | 
						|
	// 	{
 | 
						|
	// 	  emit: 'event',
 | 
						|
	// 	  level: 'query',
 | 
						|
	// 	},
 | 
						|
	// 	{
 | 
						|
	// 	  emit: 'stdout',
 | 
						|
	// 	  level: 'error',
 | 
						|
	// 	},
 | 
						|
	// 	{
 | 
						|
	// 	  emit: 'stdout',
 | 
						|
	// 	  level: 'info',
 | 
						|
	// 	},
 | 
						|
	// 	{
 | 
						|
	// 	  emit: 'stdout',
 | 
						|
	// 	  level: 'warn',
 | 
						|
	// 	},
 | 
						|
	//   ],
 | 
						|
});
 | 
						|
 | 
						|
// prisma.$on('query', (e) => {
 | 
						|
// console.log({e})
 | 
						|
// console.log('Query: ' + e.query)
 | 
						|
// console.log('Params: ' + e.params)
 | 
						|
// console.log('Duration: ' + e.duration + 'ms')
 | 
						|
//   })
 | 
						|
export const base64Encode = (text: string): string => {
 | 
						|
	return Buffer.from(text).toString('base64');
 | 
						|
};
 | 
						|
export const base64Decode = (text: string): string => {
 | 
						|
	return Buffer.from(text, 'base64').toString('ascii');
 | 
						|
};
 | 
						|
export const decrypt = (hashString: string) => {
 | 
						|
	if (hashString) {
 | 
						|
		try {
 | 
						|
			const hash = JSON.parse(hashString);
 | 
						|
			const decipher = crypto.createDecipheriv(
 | 
						|
				algorithm,
 | 
						|
				process.env['COOLIFY_SECRET_KEY'],
 | 
						|
				Buffer.from(hash.iv, 'hex')
 | 
						|
			);
 | 
						|
			const decrpyted = Buffer.concat([
 | 
						|
				decipher.update(Buffer.from(hash.content, 'hex')),
 | 
						|
				decipher.final()
 | 
						|
			]);
 | 
						|
			return decrpyted.toString();
 | 
						|
		} catch (error) {
 | 
						|
			console.log({ decryptionError: error.message });
 | 
						|
			return hashString;
 | 
						|
		}
 | 
						|
	}
 | 
						|
};
 | 
						|
export const encrypt = (text: string) => {
 | 
						|
	if (text) {
 | 
						|
		const iv = crypto.randomBytes(16);
 | 
						|
		const cipher = crypto.createCipheriv(algorithm, process.env['COOLIFY_SECRET_KEY'], iv);
 | 
						|
		const encrypted = Buffer.concat([cipher.update(text.trim()), cipher.final()]);
 | 
						|
		return JSON.stringify({
 | 
						|
			iv: iv.toString('hex'),
 | 
						|
			content: encrypted.toString('hex')
 | 
						|
		});
 | 
						|
	}
 | 
						|
};
 | 
						|
 | 
						|
export async function checkDoubleBranch(branch: string, projectId: number): Promise<boolean> {
 | 
						|
	const applications = await prisma.application.findMany({ where: { branch, projectId } });
 | 
						|
	return applications.length > 1;
 | 
						|
}
 | 
						|
export async function isDNSValid(hostname: any, domain: string): Promise<any> {
 | 
						|
	const { isIP } = await import('is-ip');
 | 
						|
	const { DNSServers } = await listSettings();
 | 
						|
	if (DNSServers) {
 | 
						|
		dns.setServers([...DNSServers.split(',')]);
 | 
						|
	}
 | 
						|
	let resolves = [];
 | 
						|
	try {
 | 
						|
		if (isIP(hostname)) {
 | 
						|
			resolves = [hostname];
 | 
						|
		} else {
 | 
						|
			resolves = await dns.resolve4(hostname);
 | 
						|
		}
 | 
						|
	} catch (error) {
 | 
						|
		throw 'Invalid DNS.';
 | 
						|
	}
 | 
						|
 | 
						|
	try {
 | 
						|
		let ipDomainFound = false;
 | 
						|
		const dnsResolve = await dns.resolve4(domain);
 | 
						|
		if (dnsResolve.length > 0) {
 | 
						|
			for (const ip of dnsResolve) {
 | 
						|
				if (resolves.includes(ip)) {
 | 
						|
					ipDomainFound = true;
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
		if (!ipDomainFound) throw false;
 | 
						|
	} catch (error) {
 | 
						|
		throw 'DNS not set';
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export function getDomain(domain: string): string {
 | 
						|
	if (domain) {
 | 
						|
		return domain?.replace('https://', '').replace('http://', '');
 | 
						|
	} else {
 | 
						|
		return '';
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function isDomainConfigured({
 | 
						|
	id,
 | 
						|
	fqdn,
 | 
						|
	checkOwn = false,
 | 
						|
	remoteIpAddress = undefined
 | 
						|
}: {
 | 
						|
	id: string;
 | 
						|
	fqdn: string;
 | 
						|
	checkOwn?: boolean;
 | 
						|
	remoteIpAddress?: string;
 | 
						|
}): Promise<boolean> {
 | 
						|
	const domain = getDomain(fqdn);
 | 
						|
	const nakedDomain = domain.replace('www.', '');
 | 
						|
	const foundApp = await prisma.application.findFirst({
 | 
						|
		where: {
 | 
						|
			OR: [
 | 
						|
				{ fqdn: { endsWith: `//${nakedDomain}` } },
 | 
						|
				{ fqdn: { endsWith: `//www.${nakedDomain}` } },
 | 
						|
				{ dockerComposeConfiguration: { contains: `//${nakedDomain}` } },
 | 
						|
				{ dockerComposeConfiguration: { contains: `//www.${nakedDomain}` } }
 | 
						|
			],
 | 
						|
			id: { not: id },
 | 
						|
			destinationDocker: {
 | 
						|
				remoteIpAddress
 | 
						|
			}
 | 
						|
		},
 | 
						|
		select: { fqdn: true }
 | 
						|
	});
 | 
						|
	const foundService = await prisma.service.findFirst({
 | 
						|
		where: {
 | 
						|
			OR: [
 | 
						|
				{ fqdn: { endsWith: `//${nakedDomain}` } },
 | 
						|
				{ fqdn: { endsWith: `//www.${nakedDomain}` } }
 | 
						|
			],
 | 
						|
			id: { not: checkOwn ? undefined : id },
 | 
						|
			destinationDocker: {
 | 
						|
				remoteIpAddress
 | 
						|
			}
 | 
						|
		},
 | 
						|
		select: { fqdn: true }
 | 
						|
	});
 | 
						|
 | 
						|
	const coolifyFqdn = await prisma.setting.findFirst({
 | 
						|
		where: {
 | 
						|
			OR: [
 | 
						|
				{ fqdn: { endsWith: `//${nakedDomain}` } },
 | 
						|
				{ fqdn: { endsWith: `//www.${nakedDomain}` } }
 | 
						|
			],
 | 
						|
			id: { not: id }
 | 
						|
		},
 | 
						|
		select: { fqdn: true }
 | 
						|
	});
 | 
						|
	return !!(foundApp || foundService || coolifyFqdn);
 | 
						|
}
 | 
						|
 | 
						|
export async function getContainerUsage(dockerId: string, container: string): Promise<any> {
 | 
						|
	try {
 | 
						|
		const { stdout } = await executeDockerCmd({
 | 
						|
			dockerId,
 | 
						|
			command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"`
 | 
						|
		});
 | 
						|
		return JSON.parse(stdout);
 | 
						|
	} catch (err) {
 | 
						|
		return {
 | 
						|
			MemUsage: 0,
 | 
						|
			CPUPerc: 0,
 | 
						|
			NetIO: 0
 | 
						|
		};
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function checkDomainsIsValidInDNS({ hostname, fqdn, dualCerts }): Promise<any> {
 | 
						|
	const { isIP } = await import('is-ip');
 | 
						|
	const domain = getDomain(fqdn);
 | 
						|
	const domainDualCert = domain.includes('www.') ? domain.replace('www.', '') : `www.${domain}`;
 | 
						|
 | 
						|
	const { DNSServers } = await listSettings();
 | 
						|
	if (DNSServers) {
 | 
						|
		dns.setServers([...DNSServers.split(',')]);
 | 
						|
	}
 | 
						|
 | 
						|
	let resolves = [];
 | 
						|
	try {
 | 
						|
		if (isIP(hostname)) {
 | 
						|
			resolves = [hostname];
 | 
						|
		} else {
 | 
						|
			resolves = await dns.resolve4(hostname);
 | 
						|
		}
 | 
						|
	} catch (error) {
 | 
						|
		throw { status: 500, message: `Could not determine IP address for ${hostname}.` };
 | 
						|
	}
 | 
						|
 | 
						|
	if (dualCerts) {
 | 
						|
		try {
 | 
						|
			const ipDomain = await dns.resolve4(domain);
 | 
						|
			const ipDomainDualCert = await dns.resolve4(domainDualCert);
 | 
						|
 | 
						|
			let ipDomainFound = false;
 | 
						|
			let ipDomainDualCertFound = false;
 | 
						|
 | 
						|
			for (const ip of ipDomain) {
 | 
						|
				if (resolves.includes(ip)) {
 | 
						|
					ipDomainFound = true;
 | 
						|
				}
 | 
						|
			}
 | 
						|
			for (const ip of ipDomainDualCert) {
 | 
						|
				if (resolves.includes(ip)) {
 | 
						|
					ipDomainDualCertFound = true;
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if (ipDomainFound && ipDomainDualCertFound) return { status: 200 };
 | 
						|
			throw {
 | 
						|
				status: 500,
 | 
						|
				message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
						|
			};
 | 
						|
		} catch (error) {
 | 
						|
			throw {
 | 
						|
				status: 500,
 | 
						|
				message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
						|
			};
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		try {
 | 
						|
			const ipDomain = await dns.resolve4(domain);
 | 
						|
			let ipDomainFound = false;
 | 
						|
			for (const ip of ipDomain) {
 | 
						|
				if (resolves.includes(ip)) {
 | 
						|
					ipDomainFound = true;
 | 
						|
				}
 | 
						|
			}
 | 
						|
			if (ipDomainFound) return { status: 200 };
 | 
						|
			throw {
 | 
						|
				status: 500,
 | 
						|
				message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
						|
			};
 | 
						|
		} catch (error) {
 | 
						|
			throw {
 | 
						|
				status: 500,
 | 
						|
				message: `DNS not set correctly or propogated.<br>Please check your DNS settings.`
 | 
						|
			};
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
export function generateTimestamp(): string {
 | 
						|
	return `${day().format('HH:mm:ss.SSS')}`;
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
export const supportedDatabaseTypesAndVersions = [
 | 
						|
	{
 | 
						|
		name: 'mongodb',
 | 
						|
		fancyName: 'MongoDB',
 | 
						|
		baseImage: 'bitnami/mongodb',
 | 
						|
		baseImageARM: 'mongo',
 | 
						|
		versions: ['5.0', '4.4', '4.2'],
 | 
						|
		versionsARM: ['5.0', '4.4', '4.2']
 | 
						|
	},
 | 
						|
	{
 | 
						|
		name: 'mysql',
 | 
						|
		fancyName: 'MySQL',
 | 
						|
		baseImage: 'bitnami/mysql',
 | 
						|
		baseImageARM: 'mysql',
 | 
						|
		versions: ['8.0', '5.7'],
 | 
						|
		versionsARM: ['8.0', '5.7']
 | 
						|
	},
 | 
						|
	{
 | 
						|
		name: 'mariadb',
 | 
						|
		fancyName: 'MariaDB',
 | 
						|
		baseImage: 'bitnami/mariadb',
 | 
						|
		baseImageARM: 'mariadb',
 | 
						|
		versions: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2'],
 | 
						|
		versionsARM: ['10.8', '10.7', '10.6', '10.5', '10.4', '10.3', '10.2']
 | 
						|
	},
 | 
						|
	{
 | 
						|
		name: 'postgresql',
 | 
						|
		fancyName: 'PostgreSQL',
 | 
						|
		baseImage: 'bitnami/postgresql',
 | 
						|
		baseImageARM: 'postgres',
 | 
						|
		versions: ['14.5.0', '13.8.0', '12.12.0', '11.17.0', '10.22.0'],
 | 
						|
		versionsARM: ['14.5', '13.8', '12.12', '11.17', '10.22']
 | 
						|
	},
 | 
						|
	{
 | 
						|
		name: 'redis',
 | 
						|
		fancyName: 'Redis',
 | 
						|
		baseImage: 'bitnami/redis',
 | 
						|
		baseImageARM: 'redis',
 | 
						|
		versions: ['7.0', '6.2', '6.0', '5.0'],
 | 
						|
		versionsARM: ['7.0', '6.2', '6.0', '5.0']
 | 
						|
	},
 | 
						|
	{
 | 
						|
		name: 'couchdb',
 | 
						|
		fancyName: 'CouchDB',
 | 
						|
		baseImage: 'bitnami/couchdb',
 | 
						|
		baseImageARM: 'couchdb',
 | 
						|
		versions: ['3.2.2', '3.1.2', '2.3.1'],
 | 
						|
		versionsARM: ['3.2.2', '3.1.2', '2.3.1']
 | 
						|
	},
 | 
						|
	{
 | 
						|
		name: 'edgedb',
 | 
						|
		fancyName: 'EdgeDB',
 | 
						|
		baseImage: 'edgedb/edgedb',
 | 
						|
		versions: ['latest', '2.1', '2.0', '1.4']
 | 
						|
	}
 | 
						|
];
 | 
						|
 | 
						|
export async function getFreeSSHLocalPort(id: string): Promise<number | boolean> {
 | 
						|
	const { default: isReachable } = await import('is-port-reachable');
 | 
						|
	const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({
 | 
						|
		where: { id }
 | 
						|
	});
 | 
						|
	if (sshLocalPort) {
 | 
						|
		return Number(sshLocalPort);
 | 
						|
	}
 | 
						|
 | 
						|
	const data = await prisma.setting.findFirst();
 | 
						|
	const { minPort, maxPort } = data;
 | 
						|
 | 
						|
	const ports = await prisma.destinationDocker.findMany({
 | 
						|
		where: { sshLocalPort: { not: null }, remoteIpAddress: { not: remoteIpAddress } }
 | 
						|
	});
 | 
						|
 | 
						|
	const alreadyConfigured = await prisma.destinationDocker.findFirst({
 | 
						|
		where: {
 | 
						|
			remoteIpAddress,
 | 
						|
			id: { not: id },
 | 
						|
			sshLocalPort: { not: null }
 | 
						|
		}
 | 
						|
	});
 | 
						|
	if (alreadyConfigured?.sshLocalPort) {
 | 
						|
		await prisma.destinationDocker.update({
 | 
						|
			where: { id },
 | 
						|
			data: { sshLocalPort: alreadyConfigured.sshLocalPort }
 | 
						|
		});
 | 
						|
		return Number(alreadyConfigured.sshLocalPort);
 | 
						|
	}
 | 
						|
	const range = generateRangeArray(minPort, maxPort);
 | 
						|
	const availablePorts = range.filter((port) => !ports.map((p) => p.sshLocalPort).includes(port));
 | 
						|
	for (const port of availablePorts) {
 | 
						|
		const found = await isReachable(port, { host: 'localhost' });
 | 
						|
		if (!found) {
 | 
						|
			await prisma.destinationDocker.update({
 | 
						|
				where: { id },
 | 
						|
				data: { sshLocalPort: Number(port) }
 | 
						|
			});
 | 
						|
			return Number(port);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return false;
 | 
						|
}
 | 
						|
 | 
						|
export async function createRemoteEngineConfiguration(id: string) {
 | 
						|
	const homedir = os.homedir();
 | 
						|
	const sshKeyFile = `/tmp/id_rsa-${id}`;
 | 
						|
	const localPort = await getFreeSSHLocalPort(id);
 | 
						|
	const {
 | 
						|
		sshKey: { privateKey },
 | 
						|
		network,
 | 
						|
		remoteIpAddress,
 | 
						|
		remotePort,
 | 
						|
		remoteUser
 | 
						|
	} = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } });
 | 
						|
	await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 });
 | 
						|
	// Needed for remote docker compose
 | 
						|
	// const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(
 | 
						|
	// 	`ps ax | grep [s]sh-agent | grep coolify-ssh-agent.pid | grep -v grep | wc -l`
 | 
						|
	// );
 | 
						|
	// if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
 | 
						|
	// 	try {
 | 
						|
	// 		await fs.stat(`/tmp/coolify-ssh-agent.pid`);
 | 
						|
	// 		await fs.rm(`/tmp/coolify-ssh-agent.pid`);
 | 
						|
	// 	} catch (error) { }
 | 
						|
	// 	await asyncExecShell(`eval $(ssh-agent -sa /tmp/coolify-ssh-agent.pid)`);
 | 
						|
	// }
 | 
						|
	// await asyncExecShell(`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh-add -q ${sshKeyFile}`);
 | 
						|
 | 
						|
	// const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(
 | 
						|
	// 	`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`
 | 
						|
	// );
 | 
						|
	// if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
 | 
						|
	// 	try {
 | 
						|
	// 		await asyncExecShell(
 | 
						|
	// 			`SSH_AUTH_SOCK=/tmp/coolify-ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`
 | 
						|
	// 		);
 | 
						|
	// 	} catch (error) { }
 | 
						|
	// }
 | 
						|
	const config = sshConfig.parse('');
 | 
						|
	const Host = `${remoteIpAddress}-remote`
 | 
						|
 | 
						|
	try {
 | 
						|
		await asyncExecShell(`ssh-keygen -R ${Host}`);
 | 
						|
		await asyncExecShell(`ssh-keygen -R ${remoteIpAddress}`);
 | 
						|
		await asyncExecShell(`ssh-keygen -R localhost:${localPort}`);
 | 
						|
	} catch (error) { }
 | 
						|
 | 
						|
 | 
						|
	const found = config.find({ Host });
 | 
						|
	const foundIp = config.find({ Host: remoteIpAddress });
 | 
						|
 | 
						|
	if (found) config.remove({ Host })
 | 
						|
	if (foundIp) config.remove({ Host: remoteIpAddress })
 | 
						|
 | 
						|
	config.append({
 | 
						|
		Host,
 | 
						|
		Hostname: remoteIpAddress,
 | 
						|
		Port: remotePort.toString(),
 | 
						|
		User: remoteUser,
 | 
						|
		StrictHostKeyChecking: 'no',
 | 
						|
		IdentityFile: sshKeyFile,
 | 
						|
		ControlMaster: 'auto',
 | 
						|
		ControlPath: `${homedir}/.ssh/coolify-${remoteIpAddress}-%r@%h:%p`,
 | 
						|
		ControlPersist: '10m'
 | 
						|
	});
 | 
						|
 | 
						|
	try {
 | 
						|
		await fs.stat(`${homedir}/.ssh/`);
 | 
						|
	} catch (error) {
 | 
						|
		await fs.mkdir(`${homedir}/.ssh/`);
 | 
						|
	}
 | 
						|
	return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config));
 | 
						|
}
 | 
						|
export async function executeSSHCmd({ dockerId, command }) {
 | 
						|
	const { execaCommand } = await import('execa')
 | 
						|
	let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
 | 
						|
	if (remoteEngine) {
 | 
						|
		await createRemoteEngineConfiguration(dockerId)
 | 
						|
	}
 | 
						|
	if (process.env.CODESANDBOX_HOST) {
 | 
						|
		if (command.startsWith('docker compose')) {
 | 
						|
			command = command.replace(/docker compose/gi, 'docker-compose')
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return await execaCommand(`ssh ${remoteIpAddress}-remote ${command}`)
 | 
						|
}
 | 
						|
export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise<any> {
 | 
						|
	const { execaCommand } = await import('execa')
 | 
						|
	let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
 | 
						|
	if (remoteEngine) {
 | 
						|
		await createRemoteEngineConfiguration(dockerId);
 | 
						|
		engine = `ssh://${remoteIpAddress}-remote`;
 | 
						|
	} else {
 | 
						|
		engine = 'unix:///var/run/docker.sock';
 | 
						|
	}
 | 
						|
	if (process.env.CODESANDBOX_HOST) {
 | 
						|
		if (command.startsWith('docker compose')) {
 | 
						|
			command = command.replace(/docker compose/gi, 'docker-compose');
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if (command.startsWith(`docker build`) || command.startsWith(`pack build`) || command.startsWith(`docker compose build`)) {
 | 
						|
		return await asyncExecShellStream({ debug, buildId, applicationId, command, engine });
 | 
						|
	}
 | 
						|
	return await execaCommand(command, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine }, shell: true })
 | 
						|
}
 | 
						|
export async function startTraefikProxy(id: string): Promise<void> {
 | 
						|
	const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } })
 | 
						|
	const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true });
 | 
						|
	const { id: settingsId, ipv4, ipv6 } = await listSettings();
 | 
						|
 | 
						|
	if (!found) {
 | 
						|
		const { stdout: coolifyNetwork } = await executeDockerCmd({
 | 
						|
			dockerId: id,
 | 
						|
			command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`
 | 
						|
		});
 | 
						|
 | 
						|
		if (!coolifyNetwork) {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId: id,
 | 
						|
				command: `docker network create --attachable coolify-infra`
 | 
						|
			});
 | 
						|
		}
 | 
						|
		const { stdout: Config } = await executeDockerCmd({
 | 
						|
			dockerId: id,
 | 
						|
			command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
 | 
						|
		});
 | 
						|
		const ip = JSON.parse(Config)[0].Gateway;
 | 
						|
		let traefikUrl = mainTraefikEndpoint;
 | 
						|
		if (remoteEngine) {
 | 
						|
			let ip = null;
 | 
						|
			if (isDev) {
 | 
						|
				ip = getAPIUrl();
 | 
						|
			} else {
 | 
						|
				ip = `http://${ipv4 || ipv6}:3000`;
 | 
						|
			}
 | 
						|
			traefikUrl = `${ip}/webhooks/traefik/remote/${id}`;
 | 
						|
		}
 | 
						|
		await executeDockerCmd({
 | 
						|
			dockerId: id,
 | 
						|
			command: `docker run --restart always \
 | 
						|
			--add-host 'host.docker.internal:host-gateway' \
 | 
						|
			${ip ? `--add-host 'host.docker.internal:${ip}'` : ''} \
 | 
						|
			-v coolify-traefik-letsencrypt:/etc/traefik/acme \
 | 
						|
			-v /var/run/docker.sock:/var/run/docker.sock \
 | 
						|
			--network coolify-infra \
 | 
						|
			-p "80:80" \
 | 
						|
			-p "443:443" \
 | 
						|
			--name coolify-proxy \
 | 
						|
			-d ${defaultTraefikImage} \
 | 
						|
			--entrypoints.web.address=:80 \
 | 
						|
			--entrypoints.web.forwardedHeaders.insecure=true \
 | 
						|
			--entrypoints.websecure.address=:443 \
 | 
						|
			--entrypoints.websecure.forwardedHeaders.insecure=true \
 | 
						|
			--providers.docker=true \
 | 
						|
			--providers.docker.exposedbydefault=false \
 | 
						|
			--providers.http.endpoint=${traefikUrl} \
 | 
						|
			--providers.http.pollTimeout=5s \
 | 
						|
			--certificatesresolvers.letsencrypt.acme.httpchallenge=true \
 | 
						|
			--certificatesresolvers.letsencrypt.acme.storage=/etc/traefik/acme/acme.json \
 | 
						|
			--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=web \
 | 
						|
			--log.level=error`
 | 
						|
		});
 | 
						|
		await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } });
 | 
						|
		await prisma.destinationDocker.update({
 | 
						|
			where: { id },
 | 
						|
			data: { isCoolifyProxyUsed: true }
 | 
						|
		});
 | 
						|
	}
 | 
						|
	// Configure networks for local docker engine
 | 
						|
	if (engine) {
 | 
						|
		const destinations = await prisma.destinationDocker.findMany({ where: { engine } });
 | 
						|
		for (const destination of destinations) {
 | 
						|
			await configureNetworkTraefikProxy(destination);
 | 
						|
		}
 | 
						|
	}
 | 
						|
	// Configure networks for remote docker engine
 | 
						|
	if (remoteEngine) {
 | 
						|
		const destinations = await prisma.destinationDocker.findMany({ where: { remoteIpAddress } });
 | 
						|
		for (const destination of destinations) {
 | 
						|
			await configureNetworkTraefikProxy(destination);
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function configureNetworkTraefikProxy(destination: any): Promise<void> {
 | 
						|
	const { id } = destination;
 | 
						|
	const { stdout: networks } = await executeDockerCmd({
 | 
						|
		dockerId: id,
 | 
						|
		command: `docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
 | 
						|
	});
 | 
						|
	const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
 | 
						|
	if (!configuredNetworks.includes(destination.network)) {
 | 
						|
		await executeDockerCmd({
 | 
						|
			dockerId: destination.id,
 | 
						|
			command: `docker network connect ${destination.network} coolify-proxy`
 | 
						|
		});
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function stopTraefikProxy(
 | 
						|
	id: string
 | 
						|
): Promise<{ stdout: string; stderr: string } | Error> {
 | 
						|
	const { found } = await checkContainer({ dockerId: id, container: 'coolify-proxy' });
 | 
						|
	await prisma.destinationDocker.update({
 | 
						|
		where: { id },
 | 
						|
		data: { isCoolifyProxyUsed: false }
 | 
						|
	});
 | 
						|
	const { id: settingsId } = await prisma.setting.findFirst({});
 | 
						|
	await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } });
 | 
						|
	try {
 | 
						|
		if (found) {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId: id,
 | 
						|
				command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`
 | 
						|
			});
 | 
						|
		}
 | 
						|
	} catch (error) {
 | 
						|
		return error;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function listSettings(): Promise<any> {
 | 
						|
	const settings = await prisma.setting.findFirst({});
 | 
						|
	if (settings.proxyPassword) settings.proxyPassword = decrypt(settings.proxyPassword);
 | 
						|
	return settings;
 | 
						|
}
 | 
						|
 | 
						|
export function generatePassword({
 | 
						|
	length = 24,
 | 
						|
	symbols = false,
 | 
						|
	isHex = false
 | 
						|
}: { length?: number; symbols?: boolean; isHex?: boolean } | null): string {
 | 
						|
	if (isHex) {
 | 
						|
		return crypto.randomBytes(length).toString('hex');
 | 
						|
	}
 | 
						|
	const password = generator.generate({
 | 
						|
		length,
 | 
						|
		numbers: true,
 | 
						|
		strict: true,
 | 
						|
		symbols
 | 
						|
	});
 | 
						|
 | 
						|
	return password;
 | 
						|
}
 | 
						|
 | 
						|
type DatabaseConfiguration = {
 | 
						|
	volume: string;
 | 
						|
	image: string;
 | 
						|
	command?: string;
 | 
						|
	ulimits: Record<string, unknown>;
 | 
						|
	privatePort: number;
 | 
						|
	environmentVariables: {
 | 
						|
		MYSQL_DATABASE: string;
 | 
						|
		MYSQL_PASSWORD: string;
 | 
						|
		MYSQL_ROOT_USER: string;
 | 
						|
		MYSQL_USER: string;
 | 
						|
		MYSQL_ROOT_PASSWORD: string;
 | 
						|
	};
 | 
						|
}
 | 
						|
	| {
 | 
						|
		volume: string;
 | 
						|
		image: string;
 | 
						|
		command?: string;
 | 
						|
		ulimits: Record<string, unknown>;
 | 
						|
		privatePort: number;
 | 
						|
		environmentVariables: {
 | 
						|
			MONGO_INITDB_ROOT_USERNAME?: string;
 | 
						|
			MONGO_INITDB_ROOT_PASSWORD?: string;
 | 
						|
			MONGODB_ROOT_USER?: string;
 | 
						|
			MONGODB_ROOT_PASSWORD?: string;
 | 
						|
		};
 | 
						|
	}
 | 
						|
	| {
 | 
						|
		volume: string;
 | 
						|
		image: string;
 | 
						|
		command?: string;
 | 
						|
		ulimits: Record<string, unknown>;
 | 
						|
		privatePort: number;
 | 
						|
		environmentVariables: {
 | 
						|
			MARIADB_ROOT_USER: string;
 | 
						|
			MARIADB_ROOT_PASSWORD: string;
 | 
						|
			MARIADB_USER: string;
 | 
						|
			MARIADB_PASSWORD: string;
 | 
						|
			MARIADB_DATABASE: string;
 | 
						|
		};
 | 
						|
	}
 | 
						|
	| {
 | 
						|
		volume: string;
 | 
						|
		image: string;
 | 
						|
		command?: string;
 | 
						|
		ulimits: Record<string, unknown>;
 | 
						|
		privatePort: number;
 | 
						|
		environmentVariables: {
 | 
						|
			POSTGRES_PASSWORD?: string;
 | 
						|
			POSTGRES_USER?: string;
 | 
						|
			POSTGRES_DB?: string;
 | 
						|
			POSTGRESQL_POSTGRES_PASSWORD?: string;
 | 
						|
			POSTGRESQL_USERNAME?: string;
 | 
						|
			POSTGRESQL_PASSWORD?: string;
 | 
						|
			POSTGRESQL_DATABASE?: string;
 | 
						|
		};
 | 
						|
	}
 | 
						|
	| {
 | 
						|
		volume: string;
 | 
						|
		image: string;
 | 
						|
		command?: string;
 | 
						|
		ulimits: Record<string, unknown>;
 | 
						|
		privatePort: number;
 | 
						|
		environmentVariables: {
 | 
						|
			REDIS_AOF_ENABLED: string;
 | 
						|
			REDIS_PASSWORD: string;
 | 
						|
		};
 | 
						|
	}
 | 
						|
	| {
 | 
						|
		volume: string;
 | 
						|
		image: string;
 | 
						|
		command?: string;
 | 
						|
		ulimits: Record<string, unknown>;
 | 
						|
		privatePort: number;
 | 
						|
		environmentVariables: {
 | 
						|
			COUCHDB_PASSWORD: string;
 | 
						|
			COUCHDB_USER: string;
 | 
						|
		};
 | 
						|
	}
 | 
						|
	| {
 | 
						|
		volume: string;
 | 
						|
		image: string;
 | 
						|
		command?: string;
 | 
						|
		ulimits: Record<string, unknown>;
 | 
						|
		privatePort: number;
 | 
						|
		environmentVariables: {
 | 
						|
			EDGEDB_SERVER_PASSWORD: string;
 | 
						|
			EDGEDB_SERVER_USER: string;
 | 
						|
			EDGEDB_SERVER_DATABASE: string;
 | 
						|
			EDGEDB_SERVER_TLS_CERT_MODE: string;
 | 
						|
		};
 | 
						|
	}
 | 
						|
export function generateDatabaseConfiguration(database: any, arch: string): DatabaseConfiguration {
 | 
						|
	const {
 | 
						|
		id,
 | 
						|
		dbUser,
 | 
						|
		dbUserPassword,
 | 
						|
		rootUser,
 | 
						|
		rootUserPassword,
 | 
						|
		defaultDatabase,
 | 
						|
		version,
 | 
						|
		type,
 | 
						|
	} = database;
 | 
						|
	const baseImage = getDatabaseImage(type, arch);
 | 
						|
	if (type === 'mysql') {
 | 
						|
		const configuration = {
 | 
						|
			privatePort: 3306,
 | 
						|
			environmentVariables: {
 | 
						|
				MYSQL_USER: dbUser,
 | 
						|
				MYSQL_PASSWORD: dbUserPassword,
 | 
						|
				MYSQL_ROOT_PASSWORD: rootUserPassword,
 | 
						|
				MYSQL_ROOT_USER: rootUser,
 | 
						|
				MYSQL_DATABASE: defaultDatabase
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/bitnami/mysql/data`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		if (isARM(arch)) {
 | 
						|
			configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
 | 
						|
		}
 | 
						|
		return configuration;
 | 
						|
	} else if (type === 'mariadb') {
 | 
						|
		const configuration: DatabaseConfiguration = {
 | 
						|
			privatePort: 3306,
 | 
						|
			environmentVariables: {
 | 
						|
				MARIADB_ROOT_USER: rootUser,
 | 
						|
				MARIADB_ROOT_PASSWORD: rootUserPassword,
 | 
						|
				MARIADB_USER: dbUser,
 | 
						|
				MARIADB_PASSWORD: dbUserPassword,
 | 
						|
				MARIADB_DATABASE: defaultDatabase
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/bitnami/mariadb`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		if (isARM(arch)) {
 | 
						|
			configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
 | 
						|
		}
 | 
						|
		return configuration;
 | 
						|
	} else if (type === 'mongodb') {
 | 
						|
		const configuration: DatabaseConfiguration = {
 | 
						|
			privatePort: 27017,
 | 
						|
			environmentVariables: {
 | 
						|
				MONGODB_ROOT_USER: rootUser,
 | 
						|
				MONGODB_ROOT_PASSWORD: rootUserPassword
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/bitnami/mongodb`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		if (isARM(arch)) {
 | 
						|
			configuration.environmentVariables = {
 | 
						|
				MONGO_INITDB_ROOT_USERNAME: rootUser,
 | 
						|
				MONGO_INITDB_ROOT_PASSWORD: rootUserPassword
 | 
						|
			};
 | 
						|
			configuration.volume = `${id}-${type}-data:/data/db`;
 | 
						|
		}
 | 
						|
		return configuration;
 | 
						|
	} else if (type === 'postgresql') {
 | 
						|
		const configuration: DatabaseConfiguration = {
 | 
						|
			privatePort: 5432,
 | 
						|
			environmentVariables: {
 | 
						|
				POSTGRESQL_POSTGRES_PASSWORD: rootUserPassword,
 | 
						|
				POSTGRESQL_PASSWORD: dbUserPassword,
 | 
						|
				POSTGRESQL_USERNAME: dbUser,
 | 
						|
				POSTGRESQL_DATABASE: defaultDatabase
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/bitnami/postgresql`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		if (isARM(arch)) {
 | 
						|
			configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
 | 
						|
			configuration.environmentVariables = {
 | 
						|
				POSTGRES_PASSWORD: dbUserPassword,
 | 
						|
				POSTGRES_USER: dbUser,
 | 
						|
				POSTGRES_DB: defaultDatabase
 | 
						|
			};
 | 
						|
		}
 | 
						|
		return configuration;
 | 
						|
	} else if (type === 'redis') {
 | 
						|
		const { settings: { appendOnly } } = database;
 | 
						|
		const configuration: DatabaseConfiguration = {
 | 
						|
			privatePort: 6379,
 | 
						|
			command: undefined,
 | 
						|
			environmentVariables: {
 | 
						|
				REDIS_PASSWORD: dbUserPassword,
 | 
						|
				REDIS_AOF_ENABLED: appendOnly ? 'yes' : 'no'
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/bitnami/redis/data`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		if (isARM(arch)) {
 | 
						|
			configuration.volume = `${id}-${type}-data:/data`;
 | 
						|
			configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no'
 | 
						|
				} --requirepass ${dbUserPassword}`;
 | 
						|
		}
 | 
						|
		return configuration;
 | 
						|
	} else if (type === 'couchdb') {
 | 
						|
		const configuration: DatabaseConfiguration = {
 | 
						|
			privatePort: 5984,
 | 
						|
			environmentVariables: {
 | 
						|
				COUCHDB_PASSWORD: dbUserPassword,
 | 
						|
				COUCHDB_USER: dbUser
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/bitnami/couchdb`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		if (isARM(arch)) {
 | 
						|
			configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
 | 
						|
		}
 | 
						|
		return configuration
 | 
						|
	} else if (type === 'edgedb') {
 | 
						|
		const configuration: DatabaseConfiguration = {
 | 
						|
			privatePort: 5656,
 | 
						|
			environmentVariables: {
 | 
						|
				EDGEDB_SERVER_PASSWORD: rootUserPassword,
 | 
						|
				EDGEDB_SERVER_USER: rootUser,
 | 
						|
				EDGEDB_SERVER_DATABASE: defaultDatabase,
 | 
						|
				EDGEDB_SERVER_TLS_CERT_MODE: 'generate_self_signed'
 | 
						|
			},
 | 
						|
			image: `${baseImage}:${version}`,
 | 
						|
			volume: `${id}-${type}-data:/var/lib/edgedb/data`,
 | 
						|
			ulimits: {}
 | 
						|
		};
 | 
						|
		return configuration
 | 
						|
	}
 | 
						|
}
 | 
						|
export function isARM(arch: string) {
 | 
						|
	if (arch === 'arm' || arch === 'arm64' || arch === 'aarch' || arch === 'aarch64') {
 | 
						|
		return true;
 | 
						|
	}
 | 
						|
	return false;
 | 
						|
}
 | 
						|
export function getDatabaseImage(type: string, arch: string): string {
 | 
						|
	const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
 | 
						|
	if (found) {
 | 
						|
		if (isARM(arch)) {
 | 
						|
			return found.baseImageARM || found.baseImage;
 | 
						|
		}
 | 
						|
		return found.baseImage;
 | 
						|
	}
 | 
						|
	return '';
 | 
						|
}
 | 
						|
 | 
						|
export function getDatabaseVersions(type: string, arch: string): string[] {
 | 
						|
	const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
 | 
						|
	if (found) {
 | 
						|
		if (isARM(arch)) {
 | 
						|
			return found.versionsARM || found.versions;
 | 
						|
		}
 | 
						|
		return found.versions;
 | 
						|
	}
 | 
						|
	return [];
 | 
						|
}
 | 
						|
 | 
						|
export type ComposeFile = {
 | 
						|
	version: ComposerFileVersion;
 | 
						|
	services: Record<string, ComposeFileService>;
 | 
						|
	networks: Record<string, ComposeFileNetwork>;
 | 
						|
	volumes?: Record<string, ComposeFileVolume>;
 | 
						|
};
 | 
						|
 | 
						|
export type ComposeFileService = {
 | 
						|
	container_name: string;
 | 
						|
	image?: string;
 | 
						|
	networks: string[];
 | 
						|
	environment?: Record<string, unknown>;
 | 
						|
	volumes?: string[];
 | 
						|
	ulimits?: unknown;
 | 
						|
	labels?: string[];
 | 
						|
	env_file?: string[];
 | 
						|
	extra_hosts?: string[];
 | 
						|
	restart: ComposeFileRestartOption;
 | 
						|
	depends_on?: string[];
 | 
						|
	command?: string;
 | 
						|
	ports?: string[];
 | 
						|
	build?:
 | 
						|
	| {
 | 
						|
		context: string;
 | 
						|
		dockerfile: string;
 | 
						|
		args?: Record<string, unknown>;
 | 
						|
	}
 | 
						|
	| string;
 | 
						|
	deploy?: {
 | 
						|
		restart_policy?: {
 | 
						|
			condition?: string;
 | 
						|
			delay?: string;
 | 
						|
			max_attempts?: number;
 | 
						|
			window?: string;
 | 
						|
		};
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
export type ComposerFileVersion =
 | 
						|
	| '3.8'
 | 
						|
	| '3.7'
 | 
						|
	| '3.6'
 | 
						|
	| '3.5'
 | 
						|
	| '3.4'
 | 
						|
	| '3.3'
 | 
						|
	| '3.2'
 | 
						|
	| '3.1'
 | 
						|
	| '3.0'
 | 
						|
	| '2.4'
 | 
						|
	| '2.3'
 | 
						|
	| '2.2'
 | 
						|
	| '2.1'
 | 
						|
	| '2.0';
 | 
						|
 | 
						|
export type ComposeFileRestartOption = 'no' | 'always' | 'on-failure' | 'unless-stopped';
 | 
						|
 | 
						|
export type ComposeFileNetwork = {
 | 
						|
	external: boolean;
 | 
						|
};
 | 
						|
 | 
						|
export type ComposeFileVolume = {
 | 
						|
	external?: boolean;
 | 
						|
	name?: string;
 | 
						|
};
 | 
						|
 | 
						|
export async function makeLabelForStandaloneDatabase({ id, image, volume }) {
 | 
						|
	const database = await prisma.database.findFirst({ where: { id } });
 | 
						|
	delete database.destinationDockerId;
 | 
						|
	delete database.createdAt;
 | 
						|
	delete database.updatedAt;
 | 
						|
	return [
 | 
						|
		'coolify.managed=true',
 | 
						|
		`coolify.version=${version}`,
 | 
						|
		`coolify.type=standalone-database`,
 | 
						|
		`coolify.configuration=${base64Encode(
 | 
						|
			JSON.stringify({
 | 
						|
				version,
 | 
						|
				image,
 | 
						|
				volume,
 | 
						|
				...database
 | 
						|
			})
 | 
						|
		)}`
 | 
						|
	];
 | 
						|
}
 | 
						|
export const createDirectories = async ({
 | 
						|
	repository,
 | 
						|
	buildId
 | 
						|
}: {
 | 
						|
	repository: string;
 | 
						|
	buildId: string;
 | 
						|
}): Promise<{ workdir: string; repodir: string }> => {
 | 
						|
	repository = repository.replaceAll(' ', '')
 | 
						|
	const repodir = `/tmp/build-sources/${repository}/`;
 | 
						|
	const workdir = `/tmp/build-sources/${repository}/${buildId}`;
 | 
						|
	let workdirFound = false;
 | 
						|
	try {
 | 
						|
		workdirFound = !!(await fs.stat(workdir));
 | 
						|
	} catch (error) { }
 | 
						|
	if (workdirFound) {
 | 
						|
		await asyncExecShell(`rm -fr ${workdir}`);
 | 
						|
	}
 | 
						|
	await asyncExecShell(`mkdir -p ${workdir}`);
 | 
						|
	return {
 | 
						|
		workdir,
 | 
						|
		repodir
 | 
						|
	};
 | 
						|
};
 | 
						|
 | 
						|
export async function stopDatabaseContainer(database: any): Promise<boolean> {
 | 
						|
	let everStarted = false;
 | 
						|
	const {
 | 
						|
		id,
 | 
						|
		destinationDockerId,
 | 
						|
		destinationDocker: { engine, id: dockerId }
 | 
						|
	} = database;
 | 
						|
	if (destinationDockerId) {
 | 
						|
		try {
 | 
						|
			const { stdout } = await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker inspect --format '{{json .State}}' ${id}`
 | 
						|
			});
 | 
						|
 | 
						|
			if (stdout) {
 | 
						|
				everStarted = true;
 | 
						|
				await removeContainer({ id, dockerId });
 | 
						|
			}
 | 
						|
		} catch (error) {
 | 
						|
			//
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return everStarted;
 | 
						|
}
 | 
						|
 | 
						|
export async function stopTcpHttpProxy(
 | 
						|
	id: string,
 | 
						|
	destinationDocker: any,
 | 
						|
	publicPort: number,
 | 
						|
	forceName: string = null
 | 
						|
): Promise<{ stdout: string; stderr: string } | Error> {
 | 
						|
	const { id: dockerId } = destinationDocker;
 | 
						|
	let container = `${id}-${publicPort}`;
 | 
						|
	if (forceName) container = forceName;
 | 
						|
	const { found } = await checkContainer({ dockerId, container });
 | 
						|
	try {
 | 
						|
		if (found) {
 | 
						|
			return await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker stop -t 0 ${container} && docker rm ${container}`
 | 
						|
			});
 | 
						|
		}
 | 
						|
	} catch (error) {
 | 
						|
		return error;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
 | 
						|
	const {
 | 
						|
		id,
 | 
						|
		type,
 | 
						|
		rootUser,
 | 
						|
		rootUserPassword,
 | 
						|
		dbUser,
 | 
						|
		dbUserPassword,
 | 
						|
		defaultDatabase,
 | 
						|
		destinationDockerId,
 | 
						|
		destinationDocker: { id: dockerId }
 | 
						|
	} = database;
 | 
						|
	if (destinationDockerId) {
 | 
						|
		if (type === 'mysql') {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
 | 
						|
			});
 | 
						|
		} else if (type === 'mariadb') {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"`
 | 
						|
			});
 | 
						|
		} else if (type === 'postgresql') {
 | 
						|
			if (isRoot) {
 | 
						|
				await executeDockerCmd({
 | 
						|
					dockerId,
 | 
						|
					command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
 | 
						|
				});
 | 
						|
			} else {
 | 
						|
				await executeDockerCmd({
 | 
						|
					dockerId,
 | 
						|
					command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
 | 
						|
				});
 | 
						|
			}
 | 
						|
		} else if (type === 'mongodb') {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
 | 
						|
			});
 | 
						|
		} else if (type === 'redis') {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
 | 
						|
			});
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
export async function checkExposedPort({ id, configuredPort, exposePort, engine, remoteEngine, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, engine: string, remoteEngine: boolean, remoteIpAddress?: string }) {
 | 
						|
	if (exposePort < 1024 || exposePort > 65535) {
 | 
						|
		throw { status: 500, message: `Exposed Port needs to be between 1024 and 65535.` };
 | 
						|
	}
 | 
						|
	if (configuredPort) {
 | 
						|
		if (configuredPort !== exposePort) {
 | 
						|
			const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress);
 | 
						|
			if (availablePort.toString() !== exposePort.toString()) {
 | 
						|
				throw { status: 500, message: `Port ${exposePort} is already in use.` };
 | 
						|
			}
 | 
						|
		}
 | 
						|
	} else {
 | 
						|
		const availablePort = await getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress);
 | 
						|
		if (availablePort.toString() !== exposePort.toString()) {
 | 
						|
			throw { status: 500, message: `Port ${exposePort} is already in use.` };
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
export async function getFreeExposedPort(id, exposePort, engine, remoteEngine, remoteIpAddress) {
 | 
						|
	const { default: checkPort } = await import('is-port-reachable');
 | 
						|
	if (remoteEngine) {
 | 
						|
		const applicationUsed = await (
 | 
						|
			await prisma.application.findMany({
 | 
						|
				where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
 | 
						|
				select: { exposePort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.exposePort);
 | 
						|
		const serviceUsed = await (
 | 
						|
			await prisma.service.findMany({
 | 
						|
				where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
 | 
						|
				select: { exposePort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.exposePort);
 | 
						|
		const usedPorts = [...applicationUsed, ...serviceUsed];
 | 
						|
		if (usedPorts.includes(exposePort)) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		const found = await checkPort(exposePort, { host: remoteIpAddress });
 | 
						|
		if (!found) {
 | 
						|
			return exposePort
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	} else {
 | 
						|
		const applicationUsed = await (
 | 
						|
			await prisma.application.findMany({
 | 
						|
				where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
 | 
						|
				select: { exposePort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.exposePort);
 | 
						|
		const serviceUsed = await (
 | 
						|
			await prisma.service.findMany({
 | 
						|
				where: { exposePort: { not: null }, id: { not: id }, destinationDocker: { engine } },
 | 
						|
				select: { exposePort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.exposePort);
 | 
						|
		const usedPorts = [...applicationUsed, ...serviceUsed];
 | 
						|
		if (usedPorts.includes(exposePort)) {
 | 
						|
			return false
 | 
						|
		}
 | 
						|
		const found = await checkPort(exposePort, { host: 'localhost' });
 | 
						|
		if (!found) {
 | 
						|
			return exposePort
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	}
 | 
						|
}
 | 
						|
export function generateRangeArray(start, end) {
 | 
						|
	return Array.from({ length: end - start }, (v, k) => k + start);
 | 
						|
}
 | 
						|
export async function getFreePublicPort({ id, remoteEngine, engine, remoteIpAddress }) {
 | 
						|
	const { default: isReachable } = await import('is-port-reachable');
 | 
						|
	const data = await prisma.setting.findFirst();
 | 
						|
	const { minPort, maxPort } = data;
 | 
						|
	if (remoteEngine) {
 | 
						|
		const dbUsed = await (
 | 
						|
			await prisma.database.findMany({
 | 
						|
				where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { remoteIpAddress } },
 | 
						|
				select: { publicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.publicPort);
 | 
						|
		const wpFtpUsed = await (
 | 
						|
			await prisma.wordpress.findMany({
 | 
						|
				where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
 | 
						|
				select: { ftpPublicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.ftpPublicPort);
 | 
						|
		const wpUsed = await (
 | 
						|
			await prisma.wordpress.findMany({
 | 
						|
				where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
 | 
						|
				select: { mysqlPublicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.mysqlPublicPort);
 | 
						|
		const minioUsed = await (
 | 
						|
			await prisma.minio.findMany({
 | 
						|
				where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { remoteIpAddress } } },
 | 
						|
				select: { publicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.publicPort);
 | 
						|
		const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
 | 
						|
		const range = generateRangeArray(minPort, maxPort)
 | 
						|
		const availablePorts = range.filter(port => !usedPorts.includes(port))
 | 
						|
		for (const port of availablePorts) {
 | 
						|
			const found = await isReachable(port, { host: remoteIpAddress })
 | 
						|
			if (!found) {
 | 
						|
				return port
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	} else {
 | 
						|
		const dbUsed = await (
 | 
						|
			await prisma.database.findMany({
 | 
						|
				where: { publicPort: { not: null }, id: { not: id }, destinationDocker: { engine } },
 | 
						|
				select: { publicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.publicPort);
 | 
						|
		const wpFtpUsed = await (
 | 
						|
			await prisma.wordpress.findMany({
 | 
						|
				where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } },
 | 
						|
				select: { ftpPublicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.ftpPublicPort);
 | 
						|
		const wpUsed = await (
 | 
						|
			await prisma.wordpress.findMany({
 | 
						|
				where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } },
 | 
						|
				select: { mysqlPublicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.mysqlPublicPort);
 | 
						|
		const minioUsed = await (
 | 
						|
			await prisma.minio.findMany({
 | 
						|
				where: { publicPort: { not: null }, id: { not: id }, service: { destinationDocker: { engine } } },
 | 
						|
				select: { publicPort: true }
 | 
						|
			})
 | 
						|
		).map((a) => a.publicPort);
 | 
						|
		const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
 | 
						|
		const range = generateRangeArray(minPort, maxPort)
 | 
						|
		const availablePorts = range.filter(port => !usedPorts.includes(port))
 | 
						|
		for (const port of availablePorts) {
 | 
						|
			const found = await isReachable(port, { host: 'localhost' })
 | 
						|
			if (!found) {
 | 
						|
				return port
 | 
						|
			}
 | 
						|
		}
 | 
						|
		return false
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function startTraefikTCPProxy(
 | 
						|
	destinationDocker: any,
 | 
						|
	id: string,
 | 
						|
	publicPort: number,
 | 
						|
	privatePort: number,
 | 
						|
	type?: string
 | 
						|
): Promise<{ stdout: string; stderr: string } | Error> {
 | 
						|
	const { network, id: dockerId, remoteEngine } = destinationDocker;
 | 
						|
	const container = `${id}-${publicPort}`;
 | 
						|
	const { found } = await checkContainer({ dockerId, container, remove: true });
 | 
						|
	const { ipv4, ipv6 } = await listSettings();
 | 
						|
 | 
						|
	let dependentId = id;
 | 
						|
	if (type === 'wordpressftp') dependentId = `${id}-ftp`;
 | 
						|
	const { found: foundDependentContainer } = await checkContainer({
 | 
						|
		dockerId,
 | 
						|
		container: dependentId,
 | 
						|
		remove: true
 | 
						|
	});
 | 
						|
	try {
 | 
						|
		if (foundDependentContainer && !found) {
 | 
						|
			const { stdout: Config } = await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
 | 
						|
			});
 | 
						|
 | 
						|
			const ip = JSON.parse(Config)[0].Gateway;
 | 
						|
			let traefikUrl = otherTraefikEndpoint;
 | 
						|
			if (remoteEngine) {
 | 
						|
				let ip = null;
 | 
						|
				if (isDev) {
 | 
						|
					ip = getAPIUrl();
 | 
						|
				} else {
 | 
						|
					ip = `http://${ipv4 || ipv6}:3000`;
 | 
						|
				}
 | 
						|
				traefikUrl = `${ip}/webhooks/traefik/other.json`;
 | 
						|
			}
 | 
						|
			const tcpProxy = {
 | 
						|
				version: '3.8',
 | 
						|
				services: {
 | 
						|
					[`${id}-${publicPort}`]: {
 | 
						|
						container_name: container,
 | 
						|
						image: defaultTraefikImage,
 | 
						|
						command: [
 | 
						|
							`--entrypoints.tcp.address=:${publicPort}`,
 | 
						|
							`--entryPoints.tcp.forwardedHeaders.insecure=true`,
 | 
						|
							`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
 | 
						|
							'--providers.http.pollTimeout=10s',
 | 
						|
							'--log.level=error'
 | 
						|
						],
 | 
						|
						ports: [`${publicPort}:${publicPort}`],
 | 
						|
						extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal: ${ip}`],
 | 
						|
						volumes: ['/var/run/docker.sock:/var/run/docker.sock'],
 | 
						|
						networks: ['coolify-infra', network]
 | 
						|
					}
 | 
						|
				},
 | 
						|
				networks: {
 | 
						|
					[network]: {
 | 
						|
						external: false,
 | 
						|
						name: network
 | 
						|
					},
 | 
						|
					'coolify-infra': {
 | 
						|
						external: false,
 | 
						|
						name: 'coolify-infra'
 | 
						|
					}
 | 
						|
				}
 | 
						|
			};
 | 
						|
			await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy));
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d`
 | 
						|
			});
 | 
						|
			await fs.rm(`/tmp/docker-compose-${id}.yaml`);
 | 
						|
		}
 | 
						|
		if (!foundDependentContainer && found) {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker stop -t 0 ${container} && docker rm ${container}`
 | 
						|
			});
 | 
						|
		}
 | 
						|
	} catch (error) {
 | 
						|
		return error;
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function getServiceFromDB({
 | 
						|
	id,
 | 
						|
	teamId
 | 
						|
}: {
 | 
						|
	id: string;
 | 
						|
	teamId: string;
 | 
						|
}): Promise<any> {
 | 
						|
	const settings = await prisma.setting.findFirst();
 | 
						|
	const body = await prisma.service.findFirst({
 | 
						|
		where: { id, teams: { some: { id: teamId === '0' ? undefined : teamId } } },
 | 
						|
		include: {
 | 
						|
			destinationDocker: true,
 | 
						|
			persistentStorage: true,
 | 
						|
			serviceSecret: true,
 | 
						|
			serviceSetting: true,
 | 
						|
			wordpress: true,
 | 
						|
			plausibleAnalytics: true,
 | 
						|
		}
 | 
						|
	});
 | 
						|
	if (!body) {
 | 
						|
		return null
 | 
						|
	}
 | 
						|
	// body.type = fixType(body.type);
 | 
						|
 | 
						|
	if (body?.serviceSecret.length > 0) {
 | 
						|
		body.serviceSecret = body.serviceSecret.map((s) => {
 | 
						|
			s.value = decrypt(s.value);
 | 
						|
			return s;
 | 
						|
		});
 | 
						|
	}
 | 
						|
	if (body.wordpress) {
 | 
						|
		body.wordpress.ftpPassword = decrypt(body.wordpress.ftpPassword);
 | 
						|
	}
 | 
						|
 | 
						|
	return { ...body, settings };
 | 
						|
}
 | 
						|
 | 
						|
 | 
						|
export function fixType(type) {
 | 
						|
	return type?.replaceAll(' ', '').toLowerCase() || null;
 | 
						|
}
 | 
						|
 | 
						|
export function makeLabelForServices(type) {
 | 
						|
	return [
 | 
						|
		'coolify.managed=true',
 | 
						|
		`coolify.version=${version}`,
 | 
						|
		`coolify.type=service`,
 | 
						|
		`coolify.service.type=${type}`
 | 
						|
	];
 | 
						|
}
 | 
						|
export function errorHandler({
 | 
						|
	status = 500,
 | 
						|
	message = 'Unknown error.'
 | 
						|
}: {
 | 
						|
	status: number;
 | 
						|
	message: string | any;
 | 
						|
}) {
 | 
						|
	if (message.message) message = message.message;
 | 
						|
	throw { status, message };
 | 
						|
}
 | 
						|
export async function generateSshKeyPair(): Promise<{ publicKey: string; privateKey: string }> {
 | 
						|
	return await new Promise((resolve, reject) => {
 | 
						|
		forge.pki.rsa.generateKeyPair({ bits: 4096, workers: -1 }, function (err, keys) {
 | 
						|
			if (keys) {
 | 
						|
				resolve({
 | 
						|
					publicKey: forge.ssh.publicKeyToOpenSSH(keys.publicKey),
 | 
						|
					privateKey: forge.ssh.privateKeyToOpenSSH(keys.privateKey)
 | 
						|
				});
 | 
						|
			} else {
 | 
						|
				reject(keys);
 | 
						|
			}
 | 
						|
		});
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
export async function stopBuild(buildId, applicationId) {
 | 
						|
	let count = 0;
 | 
						|
	await new Promise<void>(async (resolve, reject) => {
 | 
						|
		const { destinationDockerId, status } = await prisma.build.findFirst({
 | 
						|
			where: { id: buildId }
 | 
						|
		});
 | 
						|
		const { id: dockerId } = await prisma.destinationDocker.findFirst({
 | 
						|
			where: { id: destinationDockerId }
 | 
						|
		});
 | 
						|
		const interval = setInterval(async () => {
 | 
						|
			try {
 | 
						|
				if (status === 'failed' || status === 'canceled') {
 | 
						|
					clearInterval(interval);
 | 
						|
					return resolve();
 | 
						|
				}
 | 
						|
				if (count > 15) {
 | 
						|
					clearInterval(interval);
 | 
						|
					if (scheduler.workers.has('deployApplication')) {
 | 
						|
						scheduler.workers.get('deployApplication').postMessage('cancel');
 | 
						|
					}
 | 
						|
					await cleanupDB(buildId, applicationId);
 | 
						|
					return reject(new Error('Deployment canceled.'));
 | 
						|
				}
 | 
						|
				const { stdout: buildContainers } = await executeDockerCmd({
 | 
						|
					dockerId,
 | 
						|
					command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'`
 | 
						|
				});
 | 
						|
				if (buildContainers) {
 | 
						|
					const containersArray = buildContainers.trim().split('\n');
 | 
						|
					for (const container of containersArray) {
 | 
						|
						const containerObj = JSON.parse(container);
 | 
						|
						const id = containerObj.ID;
 | 
						|
						if (!containerObj.Names.startsWith(`${applicationId} `)) {
 | 
						|
							await removeContainer({ id, dockerId });
 | 
						|
							clearInterval(interval);
 | 
						|
							if (scheduler.workers.has('deployApplication')) {
 | 
						|
								scheduler.workers.get('deployApplication').postMessage('cancel');
 | 
						|
							}
 | 
						|
							await cleanupDB(buildId, applicationId);
 | 
						|
							return resolve();
 | 
						|
						}
 | 
						|
					}
 | 
						|
				}
 | 
						|
				count++;
 | 
						|
			} catch (error) { }
 | 
						|
		}, 100);
 | 
						|
	});
 | 
						|
}
 | 
						|
 | 
						|
async function cleanupDB(buildId: string, applicationId: string) {
 | 
						|
	const data = await prisma.build.findUnique({ where: { id: buildId } });
 | 
						|
	if (data?.status === 'queued' || data?.status === 'running') {
 | 
						|
		await prisma.build.update({ where: { id: buildId }, data: { status: 'canceled' } });
 | 
						|
	}
 | 
						|
	await saveBuildLog({ line: 'Deployment canceled.', buildId, applicationId });
 | 
						|
}
 | 
						|
 | 
						|
export function convertTolOldVolumeNames(type) {
 | 
						|
	if (type === 'nocodb') {
 | 
						|
		return 'nc';
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
 | 
						|
	// Cleanup old coolify images
 | 
						|
	try {
 | 
						|
		let { stdout: images } = await executeDockerCmd({
 | 
						|
			dockerId,
 | 
						|
			command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r`
 | 
						|
		});
 | 
						|
 | 
						|
		images = images.trim();
 | 
						|
		if (images) {
 | 
						|
			await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` });
 | 
						|
		}
 | 
						|
	} catch (error) { }
 | 
						|
	if (lowDiskSpace || force) {
 | 
						|
		// if (isDev) {
 | 
						|
		// 	if (!force) console.log(`[DEV MODE] Low disk space: ${lowDiskSpace}`);
 | 
						|
		// 	return;
 | 
						|
		// }
 | 
						|
		try {
 | 
						|
			await executeDockerCmd({
 | 
						|
				dockerId,
 | 
						|
				command: `docker container prune -f --filter "label=coolify.managed=true"`
 | 
						|
			});
 | 
						|
		} catch (error) { }
 | 
						|
		try {
 | 
						|
			await executeDockerCmd({ dockerId, command: `docker image prune -f` });
 | 
						|
		} catch (error) { }
 | 
						|
		try {
 | 
						|
			await executeDockerCmd({ dockerId, command: `docker image prune -a -f` });
 | 
						|
		} catch (error) { }
 | 
						|
		// Cleanup build caches
 | 
						|
		try {
 | 
						|
			await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` });
 | 
						|
		} catch (error) { }
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
export function persistentVolumes(id, persistentStorage, config) {
 | 
						|
	let volumeSet = new Set();
 | 
						|
	if (Object.keys(config).length > 0) {
 | 
						|
		for (const [key, value] of Object.entries(config)) {
 | 
						|
			if (value.volumes) {
 | 
						|
				for (const volume of value.volumes) {
 | 
						|
					if (!volume.startsWith('/var/run/docker.sock')) {
 | 
						|
						volumeSet.add(volume);
 | 
						|
					}
 | 
						|
				}
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	const volumesArray = Array.from(volumeSet);
 | 
						|
	const persistentVolume =
 | 
						|
		persistentStorage?.map((storage) => {
 | 
						|
			return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
 | 
						|
		}) || [];
 | 
						|
 | 
						|
	let volumes = [...persistentVolume];
 | 
						|
	if (volumesArray) volumes = [...volumesArray, ...volumes];
 | 
						|
	const composeVolumes =
 | 
						|
		(volumes.length > 0 &&
 | 
						|
			volumes.map((volume) => {
 | 
						|
				return {
 | 
						|
					[`${volume.split(':')[0]}`]: {
 | 
						|
						name: volume.split(':')[0]
 | 
						|
					}
 | 
						|
				};
 | 
						|
			})) ||
 | 
						|
		[];
 | 
						|
 | 
						|
	const volumeMounts = Object.assign({}, ...composeVolumes) || {};
 | 
						|
	return { volumeMounts };
 | 
						|
}
 | 
						|
export function defaultComposeConfiguration(network: string): any {
 | 
						|
	return {
 | 
						|
		networks: [network],
 | 
						|
		restart: 'on-failure',
 | 
						|
		deploy: {
 | 
						|
			restart_policy: {
 | 
						|
				condition: 'on-failure',
 | 
						|
				delay: '5s',
 | 
						|
				max_attempts: 10,
 | 
						|
				window: '120s'
 | 
						|
			}
 | 
						|
		}
 | 
						|
	};
 | 
						|
}
 | 
						|
export 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;
 | 
						|
	}
 | 
						|
}
 |