1889 lines
51 KiB
TypeScript
Raw Normal View History

2022-07-06 11:02:36 +02:00
import child from '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 cuid from 'cuid';
2022-07-20 13:35:26 +00:00
import os from 'os';
import sshConfig from 'ssh-config'
2022-07-06 11:02:36 +02:00
2022-07-25 12:42:10 +00:00
import { checkContainer, removeContainer } from './docker';
2022-07-06 11:02:36 +02:00
import { day } from './dayjs';
import * as serviceFields from './serviceFields'
2022-08-19 10:24:42 +00:00
export const version = '3.7.0';
export const isDev = process.env.NODE_ENV === 'development';
2022-07-06 11:02:36 +02:00
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`;
2022-07-22 12:01:07 +00:00
export const defaultTraefikImage = `traefik:v2.8`;
2022-07-12 13:08:47 +00:00
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
}
2022-08-12 09:38:02 +02:00
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3001')}`
2022-08-11 08:18:17 +00:00
}
2022-07-12 13:08:47 +00:00
return isDev ? 'http://localhost:3001' : 'http://localhost:3000';
}
2022-08-11 08:18:17 +00:00
2022-07-12 13:08:47 +00:00
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
}
2022-08-12 09:38:02 +02:00
if (process.env.CODESANDBOX_HOST) {
return `https://${process.env.CODESANDBOX_HOST.replace(/\$PORT/, '3000')}`
2022-08-11 08:18:17 +00:00
}
2022-07-12 13:08:47 +00:00
return 'http://localhost:3000';
}
2022-07-06 11:02:36 +02:00
const mainTraefikEndpoint = isDev
2022-07-12 13:08:47 +00:00
? `${getAPIUrl()}/webhooks/traefik/main.json`
2022-07-06 11:02:36 +02:00
: 'http://coolify:3000/webhooks/traefik/main.json';
const otherTraefikEndpoint = isDev
2022-07-12 13:08:47 +00:00
? `${getAPIUrl()}/webhooks/traefik/other.json`
2022-07-06 11:02:36 +02:00
: 'http://coolify:3000/webhooks/traefik/other.json';
export const include: any = {
destinationDocker: true,
persistentStorage: true,
serviceSecret: true,
minio: true,
plausibleAnalytics: true,
vscodeserver: true,
wordpress: true,
ghost: true,
meiliSearch: true,
umami: true,
hasura: true,
fider: true,
2022-08-15 14:58:10 +00:00
moodle: true,
appwrite: true,
2022-08-15 09:56:34 +00:00
glitchTip: true,
2022-07-06 11:02:36 +02:00
};
export const uniqueName = (): string => uniqueNamesGenerator(customConfig);
export const asyncExecShell = util.promisify(child.exec);
export const asyncSleep = (delay: number): Promise<unknown> =>
new Promise((resolve) => setTimeout(resolve, delay));
export const prisma = new PrismaClient({
errorFormat: 'minimal'
});
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) {
2022-08-12 19:39:03 +00:00
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
}
2022-07-06 11:02:36 +02:00
}
};
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), cipher.final()]);
return JSON.stringify({
iv: iv.toString('hex'),
content: encrypted.toString('hex')
});
}
};
export const supportedServiceTypesAndVersions = [
2022-07-20 13:35:26 +00:00
{
name: 'plausibleanalytics',
fancyName: 'Plausible Analytics',
baseImage: 'plausible/analytics',
images: ['bitnami/postgresql:13.2.0', 'yandex/clickhouse-server:21.3.2.5'],
versions: ['latest', 'stable'],
recommendedVersion: 'stable',
ports: {
main: 8000
}
},
{
name: 'nocodb',
fancyName: 'NocoDB',
baseImage: 'nocodb/nocodb',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'minio',
fancyName: 'MinIO',
baseImage: 'minio/minio',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 9001
}
},
{
name: 'vscodeserver',
fancyName: 'VSCode Server',
baseImage: 'codercom/code-server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8080
}
},
{
name: 'wordpress',
fancyName: 'Wordpress',
baseImage: 'wordpress',
images: ['bitnami/mysql:5.7'],
versions: ['latest', 'php8.1', 'php8.0', 'php7.4', 'php7.3'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'vaultwarden',
fancyName: 'Vaultwarden',
baseImage: 'vaultwarden/server',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 80
}
},
{
name: 'languagetool',
fancyName: 'LanguageTool',
baseImage: 'silviof/docker-languagetool',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8010
}
},
{
name: 'n8n',
fancyName: 'n8n',
baseImage: 'n8nio/n8n',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 5678
}
},
{
name: 'uptimekuma',
fancyName: 'Uptime Kuma',
baseImage: 'louislam/uptime-kuma',
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 3001
}
},
{
name: 'ghost',
fancyName: 'Ghost',
baseImage: 'bitnami/ghost',
images: ['bitnami/mariadb'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 2368
}
},
{
name: 'meilisearch',
fancyName: 'Meilisearch',
baseImage: 'getmeili/meilisearch',
images: [],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 7700
}
},
{
name: 'umami',
fancyName: 'Umami',
baseImage: 'ghcr.io/mikecao/umami',
images: ['postgres:12-alpine'],
versions: ['postgresql-latest'],
recommendedVersion: 'postgresql-latest',
ports: {
main: 3000
}
},
{
name: 'hasura',
fancyName: 'Hasura',
baseImage: 'hasura/graphql-engine',
images: ['postgres:12-alpine'],
2022-08-16 09:38:34 +00:00
versions: ['latest', 'v2.10.0', 'v2.5.1'],
recommendedVersion: 'v2.10.0',
2022-07-20 13:35:26 +00:00
ports: {
main: 8080
}
},
{
name: 'fider',
fancyName: 'Fider',
baseImage: 'getfider/fider',
images: ['postgres:12-alpine'],
versions: ['stable'],
recommendedVersion: 'stable',
ports: {
main: 3000
}
},
2022-08-15 14:58:10 +00:00
{
name: 'appwrite',
fancyName: 'Appwrite',
baseImage: 'appwrite/appwrite',
images: ['mariadb:10.7', 'redis:6.2-alpine', 'appwrite/telegraf:1.4.0'],
versions: ['latest', '0.15.3'],
recommendedVersion: '0.15.3',
ports: {
main: 80
}
2022-08-15 19:41:38 +00:00
},
2022-07-20 13:35:26 +00:00
// {
// name: 'moodle',
// fancyName: 'Moodle',
// baseImage: 'bitnami/moodle',
// images: [],
// versions: ['latest', 'v4.0.2'],
// recommendedVersion: 'latest',
// ports: {
// main: 8080
// }
// }
2022-08-15 09:56:34 +00:00
{
name: 'glitchTip',
fancyName: 'GlitchTip',
baseImage: 'glitchtip/glitchtip',
images: ['postgres:14-alpine', 'redis:7-alpine'],
versions: ['latest'],
recommendedVersion: 'latest',
ports: {
main: 8000
}
},
];
2022-07-06 11:02:36 +02:00
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');
2022-08-17 10:43:57 +02:00
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
}
2022-07-06 11:02:36 +02:00
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 {
return domain?.replace('https://', '').replace('http://', '');
}
export async function isDomainConfigured({
id,
fqdn,
2022-07-25 10:16:25 +00:00
checkOwn = false,
2022-08-06 09:21:16 +00:00
remoteIpAddress = undefined
2022-07-06 11:02:36 +02:00
}: {
id: string;
fqdn: string;
checkOwn?: boolean;
2022-08-06 09:21:16 +00:00
remoteIpAddress?: string;
2022-07-06 11:02:36 +02:00
}): 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}` } }
],
2022-07-25 10:16:25 +00:00
id: { not: id },
destinationDocker: {
2022-08-06 09:21:16 +00:00
remoteIpAddress,
2022-07-25 10:16:25 +00:00
}
2022-07-06 11:02:36 +02:00
},
select: { fqdn: true }
});
const foundService = await prisma.service.findFirst({
where: {
OR: [
{ fqdn: { endsWith: `//${nakedDomain}` } },
{ fqdn: { endsWith: `//www.${nakedDomain}` } },
{ minio: { apiFqdn: { endsWith: `//${nakedDomain}` } } },
{ minio: { apiFqdn: { endsWith: `//www.${nakedDomain}` } } }
],
2022-07-25 10:16:25 +00:00
id: { not: checkOwn ? undefined : id },
destinationDocker: {
2022-08-06 09:21:16 +00:00
remoteIpAddress
2022-07-25 10:16:25 +00:00
}
2022-07-06 11:02:36 +02:00
},
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);
}
2022-07-20 13:35:26 +00:00
export async function getContainerUsage(dockerId: string, container: string): Promise<any> {
2022-07-06 11:02:36 +02:00
try {
2022-07-20 13:35:26 +00:00
const { stdout } = await executeDockerCmd({ dockerId, command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` })
2022-07-06 11:02:36 +02:00
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}`;
2022-08-17 10:43:57 +02:00
const { DNSServers } = await listSettings();
if (DNSServers) {
dns.setServers([DNSServers]);
}
2022-07-06 11:02:36 +02:00
let resolves = [];
try {
if (isIP(hostname)) {
resolves = [hostname];
} else {
resolves = await dns.resolve4(hostname);
}
} catch (error) {
2022-07-06 19:34:16 +02:00
throw { status: 500, message: `Could not determine IP address for ${hostname}.` }
2022-07-06 11:02:36 +02:00
}
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 };
2022-07-06 19:04:54 +02:00
throw { status: 500, message: `DNS not set correctly or propogated.<br>Please check your DNS settings.` }
2022-07-06 11:02:36 +02:00
} catch (error) {
2022-07-06 19:04:54 +02:00
throw { status: 500, message: `DNS not set correctly or propogated.<br>Please check your DNS settings.` }
2022-07-06 11:02:36 +02:00
}
} 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 };
2022-07-06 19:04:54 +02:00
throw { status: 500, message: `DNS not set correctly or propogated.<br>Please check your DNS settings.` }
2022-07-06 11:02:36 +02:00
} catch (error) {
2022-07-06 19:04:54 +02:00
throw { status: 500, message: `DNS not set correctly or propogated.<br>Please check your DNS settings.` }
2022-07-06 11:02:36 +02:00
}
}
}
export function generateTimestamp(): string {
return `${day().format('HH:mm:ss.SSS')}`;
}
export async function listServicesWithIncludes(): Promise<any> {
return await prisma.service.findMany({
include,
orderBy: { createdAt: 'desc' }
});
}
export const supportedDatabaseTypesAndVersions = [
{
name: 'mongodb',
fancyName: 'MongoDB',
baseImage: 'bitnami/mongodb',
2022-08-12 12:13:52 +02:00
baseImageARM: 'mongo',
versions: ['5.0', '4.4', '4.2'],
versionsARM: ['5.0', '4.4', '4.2']
2022-07-06 11:02:36 +02:00
},
2022-08-12 11:48:38 +02:00
{
name: 'mysql',
fancyName: 'MySQL',
baseImage: 'bitnami/mysql',
baseImageARM: 'mysql',
versions: ['8.0', '5.7'],
versionsARM: ['8.0', '5.7']
},
2022-07-06 11:02:36 +02:00
{
name: 'mariadb',
fancyName: 'MariaDB',
baseImage: 'bitnami/mariadb',
2022-08-12 11:48:38 +02:00
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']
2022-07-06 11:02:36 +02:00
},
{
name: 'postgresql',
fancyName: 'PostgreSQL',
baseImage: 'bitnami/postgresql',
2022-08-12 11:48:38 +02:00
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']
2022-07-06 11:02:36 +02:00
},
{
name: 'redis',
fancyName: 'Redis',
baseImage: 'bitnami/redis',
2022-08-12 11:48:38 +02:00
baseImageARM: 'redis',
versions: ['7.0', '6.2', '6.0', '5.0'],
versionsARM: ['7.0', '6.2', '6.0', '5.0']
2022-07-06 11:02:36 +02:00
},
2022-08-12 11:48:38 +02:00
{
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']
}
2022-07-06 11:02:36 +02:00
];
2022-08-07 12:42:20 +00:00
export async function getFreeSSHLocalPort(id: string): Promise<number> {
const { default: getPort, portNumbers } = await import('get-port');
const { remoteIpAddress, sshLocalPort } = await prisma.destinationDocker.findUnique({ where: { id } })
if (sshLocalPort) {
return Number(sshLocalPort)
}
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 availablePort = await getPort({ port: portNumbers(10000, 10100), exclude: ports.map(p => p.sshLocalPort) })
await prisma.destinationDocker.update({ where: { id }, data: { sshLocalPort: Number(availablePort) } })
return Number(availablePort)
}
2022-07-20 13:35:26 +00:00
export async function createRemoteEngineConfiguration(id: string) {
const homedir = os.homedir();
const sshKeyFile = `/tmp/id_rsa-${id}`
2022-08-07 12:42:20 +00:00
const localPort = await getFreeSSHLocalPort(id);
2022-07-20 13:35:26 +00:00
const { sshKey: { privateKey }, remoteIpAddress, remotePort, remoteUser } = await prisma.destinationDocker.findFirst({ where: { id }, include: { sshKey: true } })
await fs.writeFile(sshKeyFile, decrypt(privateKey) + '\n', { encoding: 'utf8', mode: 400 })
2022-07-21 12:43:53 +00:00
// Needed for remote docker compose
2022-07-27 13:57:42 +00:00
const { stdout: numberOfSSHAgentsRunning } = await asyncExecShell(`ps ax | grep [s]sh-agent | grep ssh-agent.pid | grep -v grep | wc -l`)
2022-07-27 13:11:46 +00:00
if (numberOfSSHAgentsRunning !== '' && Number(numberOfSSHAgentsRunning.trim()) == 0) {
2022-07-27 13:57:42 +00:00
await asyncExecShell(`eval $(ssh-agent -sa /tmp/ssh-agent.pid)`)
2022-07-27 13:11:46 +00:00
}
2022-07-27 13:57:42 +00:00
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh-add -q ${sshKeyFile}`)
2022-08-07 12:42:20 +00:00
const { stdout: numberOfSSHTunnelsRunning } = await asyncExecShell(`ps ax | grep 'ssh -F /dev/null -o StrictHostKeyChecking no -fNL ${localPort}:localhost:${remotePort}' | grep -v grep | wc -l`)
2022-07-27 13:57:42 +00:00
if (numberOfSSHTunnelsRunning !== '' && Number(numberOfSSHTunnelsRunning.trim()) == 0) {
try {
2022-08-07 12:42:20 +00:00
await asyncExecShell(`SSH_AUTH_SOCK=/tmp/ssh-agent.pid ssh -F /dev/null -o "StrictHostKeyChecking no" -fNL ${localPort}:localhost:${remotePort} ${remoteUser}@${remoteIpAddress}`)
2022-07-27 13:57:42 +00:00
2022-08-07 12:42:20 +00:00
} catch (error) {
2022-07-27 13:57:42 +00:00
console.log(error)
}
}
2022-07-20 13:35:26 +00:00
const config = sshConfig.parse('')
const found = config.find({ Host: remoteIpAddress })
if (!found) {
config.append({
Host: remoteIpAddress,
2022-07-27 13:57:42 +00:00
Hostname: 'localhost',
2022-08-07 12:42:20 +00:00
Port: Number(localPort),
2022-07-20 13:35:26 +00:00
User: remoteUser,
IdentityFile: sshKeyFile,
StrictHostKeyChecking: 'no'
})
}
try {
await fs.stat(`${homedir}/.ssh/`)
} catch (error) {
await fs.mkdir(`${homedir}/.ssh/`)
}
2022-08-07 12:42:20 +00:00
return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config))
2022-07-20 13:35:26 +00:00
}
export async function executeDockerCmd({ dockerId, command }: { dockerId: string, command: string }) {
2022-07-27 13:57:42 +00:00
let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } })
2022-07-25 12:42:10 +00:00
if (remoteEngine) {
2022-07-20 13:35:26 +00:00
await createRemoteEngineConfiguration(dockerId)
2022-07-27 13:57:42 +00:00
engine = `ssh://${remoteIpAddress}`
2022-07-25 12:42:10 +00:00
} else {
engine = 'unix:///var/run/docker.sock'
2022-07-20 13:35:26 +00:00
}
if (process.env.CODESANDBOX_HOST) {
if (command.startsWith('docker compose')) {
command = command.replace(/docker compose/gi, 'docker-compose')
}
}
2022-07-20 13:35:26 +00:00
return await asyncExecShell(
2022-07-25 12:42:10 +00:00
`DOCKER_BUILDKIT=1 DOCKER_HOST="${engine}" ${command}`
2022-07-20 13:35:26 +00:00
);
2022-07-27 13:57:42 +00:00
2022-07-20 13:35:26 +00:00
}
export async function startTraefikProxy(id: string): Promise<void> {
2022-07-22 20:23:16 +00:00
const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } })
2022-07-20 13:35:26 +00:00
const found = await checkContainer({ dockerId: id, container: 'coolify-proxy', remove: true });
2022-07-22 21:14:12 +00:00
const { id: settingsId, ipv4, ipv6 } = await listSettings();
2022-07-06 11:02:36 +02:00
if (!found) {
const { stdout: coolifyNetwork } = await executeDockerCmd({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` })
2022-08-15 14:58:10 +00:00
if (!coolifyNetwork) {
await executeDockerCmd({ dockerId: id, command: `docker network create --attachable coolify-infra` })
}
2022-07-20 13:35:26 +00:00
const { stdout: Config } = await executeDockerCmd({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` })
2022-07-06 11:02:36 +02:00
const ip = JSON.parse(Config)[0].Gateway;
2022-07-22 20:23:16 +00:00
let traefikUrl = mainTraefikEndpoint
if (remoteEngine) {
let ip = null
if (isDev) {
ip = getAPIUrl()
} else {
2022-07-22 21:28:01 +00:00
ip = `http://${ipv4 || ipv6}:3000`
2022-07-22 20:23:16 +00:00
}
traefikUrl = `${ip}/webhooks/traefik/remote/${id}`
}
2022-07-20 13:35:26 +00:00
await executeDockerCmd({
dockerId: id,
command: `docker run --restart always \
2022-07-06 11:02:36 +02:00
--add-host 'host.docker.internal:host-gateway' \
2022-07-20 13:35:26 +00:00
${ip ? `--add-host 'host.docker.internal:${ip}'` : ''} \
2022-07-06 11:02:36 +02:00
-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 \
2022-07-22 20:23:16 +00:00
--providers.http.endpoint=${traefikUrl} \
2022-07-06 11:02:36 +02:00
--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`
2022-07-20 13:35:26 +00:00
})
await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } });
await prisma.destinationDocker.update({
where: { id },
2022-07-06 11:02:36 +02:00
data: { isCoolifyProxyUsed: true }
});
}
2022-07-22 20:23:16 +00:00
// 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);
}
}
2022-07-06 11:02:36 +02:00
}
2022-07-22 20:23:16 +00:00
export async function configureNetworkTraefikProxy(destination: any): Promise<void> {
const { id } = destination
2022-07-20 13:35:26 +00:00
const { stdout: networks } = await executeDockerCmd({
2022-07-21 12:43:53 +00:00
dockerId: id,
2022-07-20 13:35:26 +00:00
command:
`docker ps -a --filter name=coolify-proxy --format '{{json .Networks}}'`
});
2022-07-06 11:02:36 +02:00
const configuredNetworks = networks.replace(/"/g, '').replace('\n', '').split(',');
2022-07-22 20:23:16 +00:00
if (!configuredNetworks.includes(destination.network)) {
await executeDockerCmd({ dockerId: destination.id, command: `docker network connect ${destination.network} coolify-proxy` })
2022-07-06 11:02:36 +02:00
}
}
export async function stopTraefikProxy(
2022-07-20 13:35:26 +00:00
id: string
2022-07-06 11:02:36 +02:00
): Promise<{ stdout: string; stderr: string } | Error> {
2022-07-20 13:35:26 +00:00
const found = await checkContainer({ dockerId: id, container: 'coolify-proxy' });
await prisma.destinationDocker.update({
where: { id },
2022-07-06 11:02:36 +02:00
data: { isCoolifyProxyUsed: false }
});
2022-07-20 13:35:26 +00:00
const { id: settingsId } = await prisma.setting.findFirst({});
await prisma.setting.update({ where: { id: settingsId }, data: { proxyHash: null } });
2022-07-06 11:02:36 +02:00
try {
if (found) {
2022-07-20 13:35:26 +00:00
await executeDockerCmd({
dockerId: id,
command:
`docker stop -t 0 coolify-proxy && docker rm coolify-proxy`
});
2022-07-06 11:02:36 +02:00
}
} 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): string {
return generator.generate({
length,
numbers: true,
strict: true,
symbols
});
}
2022-08-12 11:48:38 +02:00
export function generateDatabaseConfiguration(database: any, arch: string):
2022-07-06 11:02:36 +02:00
| {
volume: string;
image: string;
2022-08-12 11:48:38 +02:00
command?: string;
2022-07-06 11:02:36 +02:00
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;
2022-08-12 11:48:38 +02:00
command?: string;
2022-07-06 11:02:36 +02:00
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
2022-08-12 12:13:52 +02:00
MONGO_INITDB_ROOT_USERNAME?: string;
MONGO_INITDB_ROOT_PASSWORD?: string;
MONGODB_ROOT_USER?: string;
MONGODB_ROOT_PASSWORD?: string;
2022-07-06 11:02:36 +02:00
};
}
| {
volume: string;
image: string;
2022-08-12 11:48:38 +02:00
command?: string;
2022-07-06 11:02:36 +02:00
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;
2022-08-12 11:48:38 +02:00
command?: string;
2022-07-06 11:02:36 +02:00
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRESQL_POSTGRES_PASSWORD: string;
POSTGRESQL_USERNAME: string;
POSTGRESQL_PASSWORD: string;
POSTGRESQL_DATABASE: string;
};
}
| {
volume: string;
image: string;
2022-08-12 11:48:38 +02:00
command?: string;
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
POSTGRES_USER: string;
POSTGRES_PASSWORD: string;
POSTGRES_DB: string;
};
}
| {
volume: string;
image: string;
command?: string;
2022-07-06 11:02:36 +02:00
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
REDIS_AOF_ENABLED: string;
REDIS_PASSWORD: string;
};
}
| {
volume: string;
image: string;
2022-08-12 11:48:38 +02:00
command?: string;
2022-07-06 11:02:36 +02:00
ulimits: Record<string, unknown>;
privatePort: number;
environmentVariables: {
COUCHDB_PASSWORD: string;
COUCHDB_USER: string;
};
} {
const {
id,
dbUser,
dbUserPassword,
rootUser,
rootUserPassword,
defaultDatabase,
version,
type,
settings: { appendOnly }
} = database;
2022-08-12 11:48:38 +02:00
const baseImage = getDatabaseImage(type, arch);
2022-07-06 11:02:36 +02:00
if (type === 'mysql') {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
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: {}
2022-08-12 11:48:38 +02:00
}
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if (type === 'mariadb') {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
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: {}
};
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
configuration.volume = `${id}-${type}-data:/var/lib/mysql`;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if (type === 'mongodb') {
2022-08-12 12:13:52 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort: 27017,
environmentVariables: {
MONGODB_ROOT_USER: rootUser,
MONGODB_ROOT_PASSWORD: rootUserPassword
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/mongodb`,
ulimits: {}
};
2022-08-12 12:13:52 +02:00
if (isARM(arch)) {
configuration.environmentVariables = {
MONGO_INITDB_ROOT_USERNAME: rootUser,
MONGO_INITDB_ROOT_PASSWORD: rootUserPassword
}
configuration.volume = `${id}-${type}-data:/data/db`;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if (type === 'postgresql') {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
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: {}
2022-08-12 11:48:38 +02:00
}
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
configuration.volume = `${id}-${type}-data:/var/lib/postgresql`;
configuration.environmentVariables = {
POSTGRES_PASSWORD: dbUserPassword,
POSTGRES_USER: dbUser,
POSTGRES_DB: defaultDatabase
}
2022-08-12 11:48:38 +02:00
}
return configuration
2022-07-06 11:02:36 +02:00
} else if (type === 'redis') {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort: 6379,
2022-08-12 11:48:38 +02:00
command: undefined,
2022-07-06 11:02:36 +02:00
environmentVariables: {
REDIS_PASSWORD: dbUserPassword,
REDIS_AOF_ENABLED: appendOnly ? 'yes' : 'no'
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/redis/data`,
ulimits: {}
};
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
configuration.volume = `${id}-${type}-data:/data`;
configuration.command = `/usr/local/bin/redis-server --appendonly ${appendOnly ? 'yes' : 'no'} --requirepass ${dbUserPassword}`;
}
return configuration
2022-07-06 11:02:36 +02:00
} else if (type === 'couchdb') {
2022-08-12 11:48:38 +02:00
const configuration = {
2022-07-06 11:02:36 +02:00
privatePort: 5984,
environmentVariables: {
COUCHDB_PASSWORD: dbUserPassword,
COUCHDB_USER: dbUser
},
image: `${baseImage}:${version}`,
volume: `${id}-${type}-data:/bitnami/couchdb`,
ulimits: {}
};
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
configuration.volume = `${id}-${type}-data:/opt/couchdb/data`;
}
return configuration
2022-07-06 11:02:36 +02:00
}
}
2022-08-14 20:02:18 +00:00
export function isARM(arch: string) {
2022-08-12 11:48:38 +02:00
if (arch === 'arm' || arch === 'arm64') {
return true
}
return false
}
export function getDatabaseImage(type: string, arch: string): string {
2022-07-06 11:02:36 +02:00
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
return found.baseImageARM || found.baseImage
}
2022-07-06 11:02:36 +02:00
return found.baseImage;
}
return '';
}
2022-08-12 11:48:38 +02:00
export function getDatabaseVersions(type: string, arch: string): string[] {
2022-07-06 11:02:36 +02:00
const found = supportedDatabaseTypesAndVersions.find((t) => t.name === type);
if (found) {
2022-08-12 11:54:06 +02:00
if (isARM(arch)) {
2022-08-12 11:48:38 +02:00
return found.versionsARM || found.versions
}
2022-07-06 11:02:36 +02:00
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>;
2022-07-14 12:47:26 +00:00
} | string;
2022-07-06 11:02:36 +02:00
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 }> => {
const repodir = `/tmp/build-sources/${repository}/`;
const workdir = `/tmp/build-sources/${repository}/${buildId}`;
await asyncExecShell(`mkdir -p ${workdir}`);
return {
workdir,
repodir
};
};
export async function stopDatabaseContainer(
database: any
): Promise<boolean> {
let everStarted = false;
const {
id,
destinationDockerId,
2022-07-20 13:35:26 +00:00
destinationDocker: { engine, id: dockerId }
2022-07-06 11:02:36 +02:00
} = database;
if (destinationDockerId) {
try {
2022-07-20 13:35:26 +00:00
const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` })
2022-07-06 11:02:36 +02:00
if (stdout) {
everStarted = true;
2022-07-20 13:35:26 +00:00
await removeContainer({ id, dockerId });
2022-07-06 11:02:36 +02:00
}
} catch (error) {
//
}
}
return everStarted;
}
export async function stopTcpHttpProxy(
id: string,
destinationDocker: any,
publicPort: number,
forceName: string = null
): Promise<{ stdout: string; stderr: string } | Error> {
2022-07-20 13:35:26 +00:00
const { id: dockerId } = destinationDocker;
let container = `${id}-${publicPort}`;
if (forceName) container = forceName;
const found = await checkContainer({ dockerId, container });
2022-07-06 11:02:36 +02:00
try {
if (found) {
2022-07-20 13:35:26 +00:00
return await executeDockerCmd({
dockerId,
command:
`docker stop -t 0 ${container} && docker rm ${container}`
});
2022-07-06 11:02:36 +02:00
}
} catch (error) {
return error;
}
}
export async function updatePasswordInDb(database, user, newPassword, isRoot) {
const {
id,
type,
rootUser,
rootUserPassword,
dbUser,
dbUserPassword,
defaultDatabase,
destinationDockerId,
2022-07-20 13:53:39 +00:00
destinationDocker: { id: dockerId }
2022-07-06 11:02:36 +02:00
} = database;
if (destinationDockerId) {
if (type === 'mysql') {
2022-07-20 13:53:39 +00:00
await executeDockerCmd({
dockerId,
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"ALTER USER '${user}'@'%' IDENTIFIED WITH caching_sha2_password BY '${newPassword}';\"`
})
2022-07-06 11:02:36 +02:00
} else if (type === 'mariadb') {
2022-07-20 13:53:39 +00:00
await executeDockerCmd({
dockerId,
command: `docker exec ${id} mysql -u ${rootUser} -p${rootUserPassword} -e \"SET PASSWORD FOR '${user}'@'%' = PASSWORD('${newPassword}');\"`
})
2022-07-06 11:02:36 +02:00
} else if (type === 'postgresql') {
if (isRoot) {
2022-07-20 13:53:39 +00:00
await executeDockerCmd({
dockerId,
command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"`
})
2022-07-06 11:02:36 +02:00
} else {
2022-07-20 13:53:39 +00:00
await executeDockerCmd({
dockerId,
command: `docker exec ${id} psql postgresql://${dbUser}:${dbUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role ${user} WITH PASSWORD '${newPassword}'"`
})
2022-07-06 11:02:36 +02:00
}
} else if (type === 'mongodb') {
2022-07-20 13:53:39 +00:00
await executeDockerCmd({
dockerId,
command: `docker exec ${id} mongo 'mongodb://${rootUser}:${rootUserPassword}@${id}:27017/admin?readPreference=primary&ssl=false' --eval "db.changeUserPassword('${user}','${newPassword}')"`
})
2022-07-06 11:02:36 +02:00
} else if (type === 'redis') {
2022-07-20 13:53:39 +00:00
await executeDockerCmd({
dockerId,
command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}`
})
2022-07-06 11:02:36 +02:00
}
}
}
2022-08-18 15:29:59 +02:00
export async function checkExposedPort({ id, configuredPort, exposePort, dockerId, remoteIpAddress }: { id: string, configuredPort?: number, exposePort: number, dockerId: string, 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, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
} else {
const availablePort = await getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress);
if (availablePort.toString() !== exposePort.toString()) {
throw { status: 500, message: `Port ${exposePort} is already in use.` }
}
}
}
2022-07-22 12:01:07 +00:00
export async function getFreeExposedPort(id, exposePort, dockerId, remoteIpAddress) {
2022-07-22 08:57:11 +00:00
const { default: getPort } = await import('get-port');
const applicationUsed = await (
await prisma.application.findMany({
2022-07-22 12:01:07 +00:00
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
2022-07-22 08:57:11 +00:00
select: { exposePort: true }
})
).map((a) => a.exposePort);
const serviceUsed = await (
await prisma.service.findMany({
2022-07-22 12:01:07 +00:00
where: { exposePort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
2022-07-22 08:57:11 +00:00
select: { exposePort: true }
})
).map((a) => a.exposePort);
const usedPorts = [...applicationUsed, ...serviceUsed];
2022-07-22 12:01:07 +00:00
if (remoteIpAddress) {
const { default: checkPort } = await import('is-port-reachable');
const found = await checkPort(exposePort, { host: remoteIpAddress });
if (!found) {
return exposePort
}
return false
}
return await getPort({ port: Number(exposePort), exclude: usedPorts });
2022-07-22 08:57:11 +00:00
}
2022-07-22 12:01:07 +00:00
export async function getFreePublicPort(id, dockerId) {
2022-07-06 11:02:36 +02:00
const { default: getPort, portNumbers } = await import('get-port');
const data = await prisma.setting.findFirst();
const { minPort, maxPort } = data;
const dbUsed = await (
await prisma.database.findMany({
2022-07-22 12:01:07 +00:00
where: { publicPort: { not: null }, id: { not: id }, destinationDockerId: dockerId },
2022-07-06 11:02:36 +02:00
select: { publicPort: true }
})
).map((a) => a.publicPort);
const wpFtpUsed = await (
await prisma.wordpress.findMany({
2022-07-22 12:01:07 +00:00
where: { ftpPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
2022-07-06 11:02:36 +02:00
select: { ftpPublicPort: true }
})
).map((a) => a.ftpPublicPort);
const wpUsed = await (
await prisma.wordpress.findMany({
2022-07-22 12:01:07 +00:00
where: { mysqlPublicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
2022-07-06 11:02:36 +02:00
select: { mysqlPublicPort: true }
})
).map((a) => a.mysqlPublicPort);
const minioUsed = await (
await prisma.minio.findMany({
2022-07-22 12:01:07 +00:00
where: { publicPort: { not: null }, id: { not: id }, service: { destinationDockerId: dockerId } },
2022-07-06 11:02:36 +02:00
select: { publicPort: true }
})
).map((a) => a.publicPort);
const usedPorts = [...dbUsed, ...wpFtpUsed, ...wpUsed, ...minioUsed];
return await getPort({ port: portNumbers(minPort, maxPort), exclude: usedPorts });
}
export async function startTraefikTCPProxy(
destinationDocker: any,
id: string,
publicPort: number,
privatePort: number,
type?: string
): Promise<{ stdout: string; stderr: string } | Error> {
2022-07-22 21:14:12 +00:00
const { network, id: dockerId, remoteEngine } = destinationDocker;
2022-07-20 13:35:26 +00:00
const container = `${id}-${publicPort}`;
const found = await checkContainer({ dockerId, container, remove: true });
2022-07-25 12:42:10 +00:00
const { ipv4, ipv6 } = await listSettings();
2022-07-22 21:14:12 +00:00
2022-07-06 11:02:36 +02:00
let dependentId = id;
if (type === 'wordpressftp') dependentId = `${id}-ftp`;
2022-07-20 13:35:26 +00:00
const foundDependentContainer = await checkContainer({ dockerId, container: dependentId, remove: true });
2022-07-06 11:02:36 +02:00
try {
if (foundDependentContainer && !found) {
2022-07-20 13:35:26 +00:00
const { stdout: Config } = await executeDockerCmd({
dockerId,
command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'`
})
2022-07-06 11:02:36 +02:00
const ip = JSON.parse(Config)[0].Gateway;
2022-07-22 21:14:12 +00:00
let traefikUrl = otherTraefikEndpoint
if (remoteEngine) {
let ip = null
if (isDev) {
ip = getAPIUrl()
} else {
2022-07-22 21:34:31 +00:00
ip = `http://${ipv4 || ipv6}:3000`
2022-07-22 21:14:12 +00:00
}
traefikUrl = `${ip}/webhooks/traefik/other.json`
}
2022-07-06 11:02:36 +02:00
const tcpProxy = {
2022-07-22 12:01:07 +00:00
version: '3.8',
2022-07-06 11:02:36 +02:00
services: {
[`${id}-${publicPort}`]: {
2022-07-20 13:35:26 +00:00
container_name: container,
2022-07-22 21:14:12 +00:00
image: defaultTraefikImage,
2022-07-06 11:02:36 +02:00
command: [
2022-07-22 12:01:07 +00:00
`--entrypoints.tcp.address=:${publicPort}`,
`--entryPoints.tcp.forwardedHeaders.insecure=true`,
2022-07-22 21:21:40 +00:00
`--providers.http.endpoint=${traefikUrl}?id=${id}&privatePort=${privatePort}&publicPort=${publicPort}&type=tcp&address=${dependentId}`,
2022-07-06 11:02:36 +02:00
'--providers.http.pollTimeout=2s',
'--log.level=error'
],
2022-07-22 12:01:07 +00:00
ports: [`${publicPort}:${publicPort}`],
2022-07-20 13:35:26 +00:00
extra_hosts: ['host.docker.internal:host-gateway', `host.docker.internal: ${ip}`],
2022-07-06 11:02:36 +02:00
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));
2022-07-20 13:35:26 +00:00
await executeDockerCmd({
dockerId,
command: `docker compose -f /tmp/docker-compose-${id}.yaml up -d`
})
2022-07-06 11:02:36 +02:00
await fs.rm(`/tmp/docker-compose-${id}.yaml`);
}
if (!foundDependentContainer && found) {
2022-07-20 13:35:26 +00:00
await executeDockerCmd({
dockerId,
command: `docker stop -t 0 ${container} && docker rm ${container}`
})
2022-07-06 11:02:36 +02:00
}
} catch (error) {
console.log(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
});
let { type } = body
type = fixType(type)
if (body?.serviceSecret.length > 0) {
body.serviceSecret = body.serviceSecret.map((s) => {
s.value = decrypt(s.value);
return s;
});
}
2022-08-12 19:29:53 +00:00
2022-07-06 11:02:36 +02:00
body[type] = { ...body[type], ...getUpdateableFields(type, body[type]) }
return { ...body, settings };
}
export function getServiceImage(type: string): string {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.baseImage;
}
return '';
}
export function getServiceImages(type: string): string[] {
const found = supportedServiceTypesAndVersions.find((t) => t.name === type);
if (found) {
return found.images;
}
return [];
}
export async function configureServiceType({
id,
type
}: {
id: string;
type: string;
}): Promise<void> {
if (type === 'plausibleanalytics') {
const password = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'plausibleanalytics';
const secretKeyBase = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
plausibleAnalytics: {
create: {
postgresqlDatabase,
postgresqlUser,
postgresqlPassword,
password,
secretKeyBase
}
}
}
});
} else if (type === 'nocodb') {
await prisma.service.update({
where: { id },
data: { type }
});
} else if (type === 'minio') {
const rootUser = cuid();
const rootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: { type, minio: { create: { rootUser, rootUserPassword } } }
});
} else if (type === 'vscodeserver') {
const password = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: { type, vscodeserver: { create: { password } } }
});
} else if (type === 'wordpress') {
const mysqlUser = cuid();
const mysqlPassword = encrypt(generatePassword());
const mysqlRootUser = cuid();
const mysqlRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
wordpress: { create: { mysqlPassword, mysqlRootUserPassword, mysqlRootUser, mysqlUser } }
}
});
} else if (type === 'vaultwarden') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'languagetool') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'n8n') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'uptimekuma') {
await prisma.service.update({
where: { id },
data: {
type
}
});
} else if (type === 'ghost') {
const defaultEmail = `${cuid()}@example.com`;
const defaultPassword = encrypt(generatePassword());
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
ghost: {
create: {
defaultEmail,
defaultPassword,
mariadbUser,
mariadbPassword,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
} else if (type === 'meilisearch') {
const masterKey = encrypt(generatePassword(32));
await prisma.service.update({
where: { id },
data: {
type,
meiliSearch: { create: { masterKey } }
}
});
} else if (type === 'umami') {
const umamiAdminPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'umami';
const hashSalt = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
umami: {
create: {
umamiAdminPassword,
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
hashSalt
}
}
}
});
} else if (type === 'hasura') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'hasura';
const graphQLAdminPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
hasura: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
graphQLAdminPassword
}
}
}
});
} else if (type === 'fider') {
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'fider';
const jwtSecret = encrypt(generatePassword(64, true));
await prisma.service.update({
where: { id },
data: {
type,
fider: {
create: {
postgresqlDatabase,
postgresqlPassword,
postgresqlUser,
jwtSecret
}
}
}
});
} else if (type === 'moodle') {
const defaultUsername = cuid();
const defaultPassword = encrypt(generatePassword());
2022-07-20 13:35:26 +00:00
const defaultEmail = `${cuid()} @example.com`;
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbDatabase = 'moodle_db';
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
moodle: {
create: {
defaultUsername,
defaultPassword,
defaultEmail,
mariadbUser,
mariadbPassword,
mariadbDatabase,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
2022-08-15 14:58:10 +00:00
} else if (type === 'appwrite') {
const opensslKeyV1 = encrypt(generatePassword());
2022-08-17 10:43:57 +02:00
const executorSecret = encrypt(generatePassword());
2022-08-15 14:58:10 +00:00
const redisPassword = encrypt(generatePassword());
const mariadbHost = `${id}-mariadb`
const mariadbUser = cuid();
const mariadbPassword = encrypt(generatePassword());
const mariadbDatabase = 'appwrite';
const mariadbRootUser = cuid();
const mariadbRootUserPassword = encrypt(generatePassword());
await prisma.service.update({
where: { id },
data: {
type,
appwrite: {
create: {
opensslKeyV1,
executorSecret,
redisPassword,
mariadbHost,
mariadbUser,
mariadbPassword,
mariadbDatabase,
mariadbRootUser,
mariadbRootUserPassword
}
}
}
});
2022-08-15 09:56:34 +00:00
} else if (type === 'glitchTip') {
const defaultUsername = cuid();
const defaultEmail = `${defaultUsername}@example.com`;
const defaultPassword = encrypt(generatePassword());
const postgresqlUser = cuid();
const postgresqlPassword = encrypt(generatePassword());
const postgresqlDatabase = 'glitchTip';
const secretKeyBase = encrypt(generatePassword(64));
await prisma.service.update({
where: { id },
data: {
type,
glitchTip: {
create: {
postgresqlDatabase,
postgresqlUser,
postgresqlPassword,
secretKeyBase,
defaultEmail,
defaultUsername,
defaultPassword,
}
}
}
});
} else {
await prisma.service.update({
where: { id },
data: {
type
}
});
2022-07-06 11:02:36 +02:00
}
}
export async function removeService({ id }: { id: string }): Promise<void> {
2022-08-15 14:58:10 +00:00
await prisma.serviceSecret.deleteMany({ where: { serviceId: id } });
2022-07-06 11:02:36 +02:00
await prisma.servicePersistentStorage.deleteMany({ where: { serviceId: id } });
await prisma.meiliSearch.deleteMany({ where: { serviceId: id } });
await prisma.fider.deleteMany({ where: { serviceId: id } });
await prisma.ghost.deleteMany({ where: { serviceId: id } });
await prisma.umami.deleteMany({ where: { serviceId: id } });
await prisma.hasura.deleteMany({ where: { serviceId: id } });
await prisma.plausibleAnalytics.deleteMany({ where: { serviceId: id } });
await prisma.minio.deleteMany({ where: { serviceId: id } });
await prisma.vscodeserver.deleteMany({ where: { serviceId: id } });
await prisma.wordpress.deleteMany({ where: { serviceId: id } });
2022-08-15 09:56:34 +00:00
await prisma.glitchTip.deleteMany({ where: { serviceId: id } });
2022-08-15 14:58:10 +00:00
await prisma.moodle.deleteMany({ where: { serviceId: id } });
await prisma.appwrite.deleteMany({ where: { serviceId: id } });
2022-07-06 11:02:36 +02:00
await prisma.service.delete({ where: { id } });
}
export function saveUpdateableFields(type: string, data: any) {
2022-08-11 08:18:17 +00:00
const update = {};
2022-07-06 11:02:36 +02:00
if (type && serviceFields[type]) {
serviceFields[type].map((k) => {
let temp = data[k.name]
if (temp) {
if (k.isEncrypted) {
temp = encrypt(temp)
}
if (k.isLowerCase) {
temp = temp.toLowerCase()
}
if (k.isNumber) {
temp = Number(temp)
}
if (k.isBoolean) {
temp = Boolean(temp)
}
}
update[k.name] = temp
});
}
return update
}
export function getUpdateableFields(type: string, data: any) {
2022-08-11 08:18:17 +00:00
const update = {};
2022-07-06 11:02:36 +02:00
if (type && serviceFields[type]) {
serviceFields[type].map((k) => {
let temp = data[k.name]
if (temp) {
if (k.isEncrypted) {
temp = decrypt(temp)
}
update[k.name] = temp
}
update[k.name] = temp
});
}
return update
}
export function fixType(type) {
// Hack to fix the type case sensitivity...
if (type === 'plausibleanalytics') type = 'plausibleAnalytics';
if (type === 'meilisearch') type = 'meiliSearch';
return type
}
export const getServiceMainPort = (service: string) => {
const serviceType = supportedServiceTypesAndVersions.find((s) => s.name === service);
if (serviceType) {
return serviceType.ports.main;
}
return null;
};
export function makeLabelForServices(type) {
return [
'coolify.managed=true',
2022-08-15 14:58:10 +00:00
`coolify.version = ${version}`,
2022-07-20 13:35:26 +00:00
`coolify.type = service`,
2022-08-15 14:58:10 +00:00
`coolify.service.type = ${type}`
2022-07-06 11:02:36 +02:00
];
}
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 } });
2022-07-20 13:35:26 +00:00
const { engine, id: dockerId } = await prisma.destinationDocker.findFirst({ where: { id: destinationDockerId } });
2022-08-11 08:18:17 +00:00
const interval = setInterval(async () => {
2022-07-06 11:02:36 +02:00
try {
if (status === 'failed') {
clearInterval(interval);
return resolve();
}
if (count > 100) {
clearInterval(interval);
return reject(new Error('Build canceled'));
}
2022-07-25 12:42:10 +00:00
const { stdout: buildContainers } = await executeDockerCmd({ dockerId, command: `docker container ls--filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` })
2022-07-06 11:02:36 +02:00
if (buildContainers) {
const containersArray = buildContainers.trim().split('\n');
for (const container of containersArray) {
const containerObj = JSON.parse(container);
const id = containerObj.ID;
2022-07-20 13:35:26 +00:00
if (!containerObj.Names.startsWith(`${applicationId} `)) {
await removeContainer({ id, dockerId });
2022-07-06 11:02:36 +02:00
await cleanupDB(buildId);
clearInterval(interval);
return resolve();
}
}
}
count++;
} catch (error) { }
}, 100);
});
}
async function cleanupDB(buildId: 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: 'failed' } });
}
}
export function convertTolOldVolumeNames(type) {
if (type === 'nocodb') {
return 'nc'
}
}
2022-07-15 09:18:16 +00:00
// export async function getAvailableServices(): Promise<any> {
// const { data } = await axios.get(`https://gist.githubusercontent.com/andrasbacsai/4aac36d8d6214dbfc34fa78110554a50/raw/5b27e6c37d78aaeedc1148d797112c827a2f43cf/availableServices.json`)
// return data
//
2022-07-22 15:16:51 +00:00
export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) {
// Cleanup old coolify images
try {
2022-07-22 15:16:51 +00:00
let { stdout: images } = await executeDockerCmd({ dockerId, command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs` })
images = images.trim();
if (images) {
2022-07-22 15:16:51 +00:00
await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs` })
}
} catch (error) {
//console.log(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) {
//console.log(error);
}
try {
2022-07-22 15:16:51 +00:00
await executeDockerCmd({ dockerId, command: `docker image prune -f` })
} catch (error) {
//console.log(error);
}
try {
2022-07-22 15:16:51 +00:00
await executeDockerCmd({ dockerId, command: `docker image prune -a -f` })
} catch (error) {
//console.log(error);
}
}
}
export function persistentVolumes(id, persistentStorage, config) {
const persistentVolume =
persistentStorage?.map((storage) => {
return `${id}${storage.path.replace(/\//gi, '-')}:${storage.path}`;
}) || [];
2022-08-12 09:38:02 +02:00
let volumes = [...persistentVolume]
2022-08-10 11:55:27 +00:00
if (config.volume) volumes = [config.volume, ...volumes]
const composeVolumes = volumes.length > 0 && volumes.map((volume) => {
return {
[`${volume.split(':')[0]}`]: {
name: volume.split(':')[0]
}
};
2022-08-10 11:55:27 +00:00
}) || []
const volumeMounts = config.volume && Object.assign(
{},
{
[config.volume.split(':')[0]]: {
name: config.volume.split(':')[0]
}
},
...composeVolumes
2022-08-10 11:55:27 +00:00
) || {}
return { volumes, 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'
}
}
}
}