From b45ad19732256d9dded2073dbf9e4f48862695ca Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Tue, 6 Dec 2022 10:27:51 +0100 Subject: [PATCH] fix: security hole --- apps/api/package.json | 2 + apps/api/src/index.ts | 41 ++-- apps/api/src/jobs/deployApplication.ts | 79 ++++--- apps/api/src/lib/buildPacks/common.ts | 4 +- apps/api/src/lib/buildPacks/compose.ts | 6 +- apps/api/src/lib/buildPacks/heroku.ts | 4 +- apps/api/src/lib/buildPacks/rust.ts | 4 +- apps/api/src/lib/common.ts | 218 ++++++++++++------ apps/api/src/lib/docker.ts | 18 +- apps/api/src/lib/importers/github.ts | 22 +- apps/api/src/lib/importers/gitlab.ts | 20 +- apps/api/src/lib/services/handlers.ts | 58 +++-- .../routes/api/v1/applications/handlers.ts | 52 ++--- .../src/routes/api/v1/databases/handlers.ts | 10 +- .../routes/api/v1/destinations/handlers.ts | 20 +- apps/api/src/routes/api/v1/handlers.ts | 18 +- apps/api/src/routes/api/v1/index.ts | 6 +- .../api/src/routes/api/v1/servers/handlers.ts | 2 +- .../src/routes/api/v1/services/handlers.ts | 76 +++--- .../src/routes/api/v1/settings/handlers.ts | 4 +- .../src/routes/webhooks/traefik/handlers.ts | 16 +- pnpm-lock.yaml | 13 +- 22 files changed, 416 insertions(+), 277 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 62da72325..ad987b719 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -49,6 +49,7 @@ "is-port-reachable": "4.0.0", "js-yaml": "4.1.0", "jsonwebtoken": "8.5.1", + "minimist": "^1.2.7", "node-forge": "1.3.1", "node-os-utils": "1.3.7", "p-all": "4.0.0", @@ -56,6 +57,7 @@ "prisma": "4.6.1", "public-ip": "6.0.1", "pump": "3.0.0", + "shell-quote": "^1.7.4", "socket.io": "4.5.3", "ssh-config": "4.1.6", "strip-ansi": "7.0.1", diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index a5983012a..0d5cfe9e0 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -9,7 +9,7 @@ import autoLoad from '@fastify/autoload'; import socketIO from 'fastify-socket.io' import socketIOServer from './realtime' -import { asyncExecShell, cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeDockerCmd, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, sentryDSN, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; +import { cleanupDockerStorage, createRemoteEngineConfiguration, decrypt, executeCommand, executeSSHCmd, generateDatabaseConfiguration, isDev, listSettings, prisma, sentryDSN, startTraefikProxy, startTraefikTCPProxy, version } from './lib/common'; import { scheduler } from './lib/scheduler'; import { compareVersions } from 'compare-versions'; import Graceful from '@ladjs/graceful' @@ -261,7 +261,7 @@ async function initServer() { } try { console.log(`[001] Initializing server...`); - await asyncExecShell(`docker network create --attachable coolify`); + await executeCommand({ command: `docker network create --attachable coolify` }); } catch (error) { } try { console.log(`[002] Cleanup stucked builds...`); @@ -272,7 +272,7 @@ async function initServer() { } catch (error) { } try { console.log('[003] Cleaning up old build sources under /tmp/build-sources/...'); - if (!isDev) await fs.rm('/tmp/build-sources', { recursive: true, force: true }) + await fs.rm('/tmp/build-sources', { recursive: true, force: true }) } catch (error) { console.log(error) } @@ -323,14 +323,10 @@ async function autoUpdater() { if (!isDev) { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); if (isAutoUpdateEnabled) { - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep '^COOLIFY' > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` - ); + await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }) + await executeCommand({ command: `env | grep '^COOLIFY' > .env` }) + await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }) + await executeCommand({ command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` }) } } else { console.log('Updating (not really in dev mode).'); @@ -351,8 +347,8 @@ async function checkFluentBit() { }); const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit', remove: true }); if (!found) { - await asyncExecShell(`env | grep '^COOLIFY' > .env`); - await asyncExecShell(`docker compose up -d fluent-bit`); + await executeCommand({ command: `env | grep '^COOLIFY' > .env` }); + await executeCommand({ command: `docker compose up -d fluent-bit` }); } } } catch (error) { @@ -462,13 +458,13 @@ async function copySSLCertificates() { } catch (error) { console.log(error) } finally { - await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`) + await executeCommand({ command: `find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete` }) } } async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) { try { - await asyncExecShell(`scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/`) + await executeCommand({ command: `scp /tmp/${id}-cert.pem /tmp/${id}-key.pem ${remoteIpAddress}:/tmp/` }) await executeSSHCmd({ dockerId, command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'` }) await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) await executeSSHCmd({ dockerId, command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) @@ -478,9 +474,9 @@ async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddr } async function copyLocalCertificates(id: string) { try { - await asyncExecShell(`docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`) - await asyncExecShell(`docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/`) - await asyncExecShell(`docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/`) + await executeCommand({ command: `docker exec coolify-proxy sh -c 'test -d /etc/traefik/acme/custom/ || mkdir -p /etc/traefik/acme/custom/'`, shell: true }) + await executeCommand({ command: `docker cp /tmp/${id}-key.pem coolify-proxy:/etc/traefik/acme/custom/` }) + await executeCommand({ command: `docker cp /tmp/${id}-cert.pem coolify-proxy:/etc/traefik/acme/custom/` }) } catch (error) { console.log({ error }) } @@ -498,12 +494,13 @@ async function cleanupStorage() { try { let stdout = null if (!isDev) { - const output = await executeDockerCmd({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'` }) + const output = await executeCommand({ dockerId: destination.id, command: `CONTAINER=$(docker ps -lq | head -1) && docker exec $CONTAINER sh -c 'df -kPT /'`, shell: true }) stdout = output.stdout; } else { - const output = await asyncExecShell( - `df -kPT /` - ); + const output = await executeCommand({ + command: + `df -kPT /` + }); stdout = output.stdout; } let lines = stdout.trim().split('\n'); diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index cc56f6139..f6843281f 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -4,7 +4,7 @@ import fs from 'fs/promises'; import yaml from 'js-yaml'; import { copyBaseConfigurationFiles, makeLabelForSimpleDockerfile, makeLabelForStandaloneApplication, saveBuildLog, saveDockerRegistryCredentials, setDefaultConfiguration } from '../lib/buildPacks/common'; -import { createDirectories, decrypt, defaultComposeConfiguration, executeDockerCmd, getDomain, prisma, decryptApplication, isDev, pushToRegistry } from '../lib/common'; +import { createDirectories, decrypt, defaultComposeConfiguration, getDomain, prisma, decryptApplication, isDev, pushToRegistry, executeCommand } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; @@ -70,14 +70,19 @@ import * as buildpacks from '../lib/buildPacks'; if (destinationDockerId) { await prisma.build.update({ where: { id: buildId }, data: { status: 'running' } }); try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.service=${applicationId}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { // } @@ -154,7 +159,7 @@ import * as buildpacks from '../lib/buildPacks'; volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await executeCommand({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) await saveBuildLog({ line: 'Deployed successfully', buildId, applicationId }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); @@ -187,9 +192,7 @@ import * as buildpacks from '../lib/buildPacks'; if (error instanceof Error) { await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); } - if (!isDev) { - await fs.rm(workdir, { recursive: true, force: true }); - } + await fs.rm(workdir, { recursive: true, force: true }); return; } try { @@ -208,9 +211,7 @@ import * as buildpacks from '../lib/buildPacks'; await saveBuildLog({ line: error.stderr, buildId, applicationId }); } } finally { - if (!isDev) { - await fs.rm(workdir, { recursive: true, force: true }); - } + await fs.rm(workdir, { recursive: true, force: true }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); } return; @@ -409,7 +410,7 @@ import * as buildpacks from '../lib/buildPacks'; } try { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker image inspect ${applicationId}:${tag}` }) @@ -423,7 +424,7 @@ import * as buildpacks from '../lib/buildPacks'; } try { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker ${location ? `--config ${location}` : ''} pull ${imageName}:${customTag}` }) @@ -514,19 +515,24 @@ import * as buildpacks from '../lib/buildPacks'; if (buildPack === 'compose') { try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=coolify.applicationId=${applicationId}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { // } try { - await executeDockerCmd({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await executeCommand({ debug, buildId, applicationId, dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) await saveBuildLog({ line: 'Deployed successfully', buildId, applicationId }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); await prisma.application.update({ @@ -549,14 +555,19 @@ import * as buildpacks from '../lib/buildPacks'; } else { try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.service=${pullmergeRequestId ? imageId : applicationId}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { // } @@ -622,7 +633,7 @@ import * as buildpacks from '../lib/buildPacks'; volumes: Object.assign({}, ...composeVolumes) }; await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) + await executeCommand({ dockerId: destinationDocker.id, command: `docker compose --project-directory ${workdir} up -d` }) await saveBuildLog({ line: 'Deployed successfully', buildId, applicationId }); } catch (error) { await saveBuildLog({ line: error, buildId, applicationId }); @@ -660,9 +671,7 @@ import * as buildpacks from '../lib/buildPacks'; if (error instanceof Error) { await saveBuildLog({ line: error.message, buildId, applicationId: application.id }); } - if (!isDev) { - await fs.rm(workdir, { recursive: true, force: true }); - } + await fs.rm(workdir, { recursive: true, force: true }); return; } try { @@ -680,9 +689,7 @@ import * as buildpacks from '../lib/buildPacks'; } } finally { - if (!isDev) { - await fs.rm(workdir, { recursive: true, force: true }); - } + await fs.rm(workdir, { recursive: true, force: true }); await prisma.build.update({ where: { id: buildId }, data: { status: 'success' } }); } }); diff --git a/apps/api/src/lib/buildPacks/common.ts b/apps/api/src/lib/buildPacks/common.ts index 005a03d8b..9800ccc70 100644 --- a/apps/api/src/lib/buildPacks/common.ts +++ b/apps/api/src/lib/buildPacks/common.ts @@ -1,4 +1,4 @@ -import { base64Encode, decrypt, encrypt, executeDockerCmd, generateTimestamp, getDomain, isARM, isDev, prisma, version } from "../common"; +import { base64Encode, decrypt, encrypt, executeCommand, generateTimestamp, getDomain, isARM, isDev, prisma, version } from "../common"; import { promises as fs } from 'fs'; import { day } from "../dayjs"; @@ -656,7 +656,7 @@ export async function buildImage({ location = await saveDockerRegistryCredentials({ url, username, password, workdir }) } - await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker ${location ? `--config ${location}` : ''} build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` }) + await executeCommand({ debug, buildId, applicationId, dockerId, command: `docker ${location ? `--config ${location}` : ''} build --progress plain -f ${workdir}/${dockerFile} -t ${cache} --build-arg SOURCE_COMMIT=${commit} ${workdir}` }) const { status } = await prisma.build.findUnique({ where: { id: buildId } }) if (status === 'canceled') { diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index 432447f99..d7e00a8d9 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -1,5 +1,5 @@ import { promises as fs } from 'fs'; -import { defaultComposeConfiguration, executeDockerCmd } from '../common'; +import { defaultComposeConfiguration, executeCommand } from '../common'; import { buildImage, saveBuildLog } from './common'; import yaml from 'js-yaml'; @@ -108,8 +108,8 @@ export default async function (data) { } dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }) await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); - await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) + await executeCommand({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} pull` }) await saveBuildLog({ line: 'Pulling images from Compose file', buildId, applicationId }); - await executeDockerCmd({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` }) + await executeCommand({ debug, buildId, applicationId, dockerId, command: `docker compose --project-directory ${workdir} build --progress plain` }) await saveBuildLog({ line: 'Building images from Compose file', buildId, applicationId }); } diff --git a/apps/api/src/lib/buildPacks/heroku.ts b/apps/api/src/lib/buildPacks/heroku.ts index c108f3203..bb26d79c0 100644 --- a/apps/api/src/lib/buildPacks/heroku.ts +++ b/apps/api/src/lib/buildPacks/heroku.ts @@ -1,11 +1,11 @@ -import { executeDockerCmd, prisma } from "../common" +import { executeCommand } from "../common" import { saveBuildLog } from "./common"; export default async function (data: any): Promise { const { buildId, applicationId, tag, dockerId, debug, workdir, baseDirectory, baseImage } = data try { await saveBuildLog({ line: `Building image started.`, buildId, applicationId }); - await executeDockerCmd({ + await executeCommand({ buildId, debug, dockerId, diff --git a/apps/api/src/lib/buildPacks/rust.ts b/apps/api/src/lib/buildPacks/rust.ts index 1af215869..931a4524a 100644 --- a/apps/api/src/lib/buildPacks/rust.ts +++ b/apps/api/src/lib/buildPacks/rust.ts @@ -1,6 +1,6 @@ import { promises as fs } from 'fs'; import TOML from '@iarna/toml'; -import { asyncExecShell } from '../common'; +import { executeCommand } from '../common'; import { buildCacheImageWithCargo, buildImage } from './common'; const createDockerfile = async (data, image, name): Promise => { @@ -28,7 +28,7 @@ const createDockerfile = async (data, image, name): Promise => { export default async function (data) { try { const { workdir, baseImage, baseBuildImage } = data; - const { stdout: cargoToml } = await asyncExecShell(`cat ${workdir}/Cargo.toml`); + const { stdout: cargoToml } = await executeCommand({ command: `cat ${workdir}/Cargo.toml` }); const parsedToml: any = TOML.parse(cargoToml); const name = parsedToml.package.name; await buildCacheImageWithCargo(data, baseBuildImage); diff --git a/apps/api/src/lib/common.ts b/apps/api/src/lib/common.ts index 723368941..7dcd58e73 100644 --- a/apps/api/src/lib/common.ts +++ b/apps/api/src/lib/common.ts @@ -17,6 +17,7 @@ import { checkContainer, removeContainer } from './docker'; import { day } from './dayjs'; import { saveBuildLog, saveDockerRegistryCredentials } from './buildPacks/common'; import { scheduler } from './scheduler'; +import type { ExecaChildProcess } from 'execa'; export const version = '3.12.0'; export const isDev = process.env.NODE_ENV === 'development'; @@ -63,7 +64,6 @@ const otherTraefikEndpoint = isDev : '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, @@ -303,7 +303,7 @@ export async function isDomainConfigured({ export async function getContainerUsage(dockerId: string, container: string): Promise { try { - const { stdout } = await executeDockerCmd({ + const { stdout } = await executeCommand({ dockerId, command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` }); @@ -508,36 +508,13 @@ export async function createRemoteEngineConfiguration(id: string) { 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}`); + await executeCommand({ command: `ssh-keygen -R ${Host}` }); + await executeCommand({ command: `ssh-keygen -R ${remoteIpAddress}` }); + await executeCommand({ command: `ssh-keygen -R localhost:${localPort}` }); } catch (error) { } @@ -566,8 +543,102 @@ export async function createRemoteEngineConfiguration(id: string) { } return await fs.writeFile(`${homedir}/.ssh/config`, sshConfig.stringify(config)); } +export async function executeCommand({ command, dockerId = null, sshCommand = false, shell = false, buildId, applicationId, debug }: { command: string, sshCommand?: boolean, shell?: boolean, dockerId?: string, buildId?: string, applicationId?: string, debug?: boolean }): Promise> { + const { execa, execaCommand } = await import('execa') + const { parse } = await import('shell-quote') + const parsedCommand = parse(command); + const dockerCommand = parsedCommand[0]; + const dockerArgs = parsedCommand.slice(1); + + if (dockerId) { + 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 (sshCommand) { + if (shell) { + return execaCommand(`ssh ${remoteIpAddress}-remote ${command}`, { shell: true, stdio: 'inherit' }); + } + return await execa('ssh', [`${remoteIpAddress}-remote`, ...dockerArgs]); + } + return await new Promise(async (resolve, reject) => { + let subprocess = null; + if (shell) { + subprocess = execaCommand(command, { + env: { DOCKER_BUILDKIT: '1', DOCKER_HOST: engine } + }); + } else { + subprocess = execa(dockerCommand, dockerArgs, { + 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); + } + }); + }) + } else { + if (shell) { + return execaCommand(command, { shell: true }); + } + return await execa(dockerCommand, dockerArgs); + } +} export async function executeSSHCmd({ dockerId, command }) { - const { execaCommand } = await import('execa') + const { execaCommand, execa } = await import('execa') + const { parse } = await import('shell-quote') let { remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId) @@ -577,10 +648,12 @@ export async function executeSSHCmd({ dockerId, command }) { command = command.replace(/docker compose/gi, 'docker-compose') } } - return await execaCommand(`ssh ${remoteIpAddress}-remote ${command}`) + const dockerArgs = parse(command); + return await execa('ssh', [`${remoteIpAddress}-remote`, ...dockerArgs]); } export async function executeDockerCmd({ debug, buildId, applicationId, dockerId, command }: { debug?: boolean, buildId?: string, applicationId?: string, dockerId: string, command: string }): Promise { - const { execaCommand } = await import('execa') + const { execa } = await import('execa') + const { parse } = await import('shell-quote') let { remoteEngine, remoteIpAddress, engine } = await prisma.destinationDocker.findUnique({ where: { id: dockerId } }) if (remoteEngine) { await createRemoteEngineConfiguration(dockerId); @@ -593,10 +666,13 @@ export async function executeDockerCmd({ debug, buildId, applicationId, dockerId command = command.replace(/docker compose/gi, 'docker-compose'); } } + const parsedCommand = parse(command); + const dockerCommand = parsedCommand[0]; + const dockerArgs = parsedCommand.slice(1); 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 }) + return await execa(dockerCommand, dockerArgs, { env: { DOCKER_BUILDKIT: "1", DOCKER_HOST: engine } }); } export async function startTraefikProxy(id: string): Promise { const { engine, network, remoteEngine, remoteIpAddress } = await prisma.destinationDocker.findUnique({ where: { id } }) @@ -604,18 +680,18 @@ export async function startTraefikProxy(id: string): Promise { const { id: settingsId, ipv4, ipv6 } = await listSettings(); if (!found) { - const { stdout: coolifyNetwork } = await executeDockerCmd({ + const { stdout: coolifyNetwork } = await executeCommand({ dockerId: id, command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"` }); if (!coolifyNetwork) { - await executeDockerCmd({ + await executeCommand({ dockerId: id, command: `docker network create --attachable coolify-infra` }); } - const { stdout: Config } = await executeDockerCmd({ + const { stdout: Config } = await executeCommand({ dockerId: id, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }); @@ -630,7 +706,7 @@ export async function startTraefikProxy(id: string): Promise { } traefikUrl = `${ip}/webhooks/traefik/remote/${id}`; } - await executeDockerCmd({ + await executeCommand({ dockerId: id, command: `docker run --restart always \ --add-host 'host.docker.internal:host-gateway' \ @@ -655,7 +731,6 @@ export async function startTraefikProxy(id: string): Promise { --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 } @@ -679,13 +754,13 @@ export async function startTraefikProxy(id: string): Promise { export async function configureNetworkTraefikProxy(destination: any): Promise { const { id } = destination; - const { stdout: networks } = await executeDockerCmd({ + const { stdout: networks } = await executeCommand({ 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({ + await executeCommand({ dockerId: destination.id, command: `docker network connect ${destination.network} coolify-proxy` }); @@ -700,13 +775,12 @@ export async function stopTraefikProxy( 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({ + await executeCommand({ dockerId: id, - command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy` + command: `docker stop -t 0 coolify-proxy && docker rm coolify-proxy`, + shell: true }); } } catch (error) { @@ -1099,9 +1173,9 @@ export const createDirectories = async ({ workdirFound = !!(await fs.stat(workdir)); } catch (error) { } if (workdirFound) { - await asyncExecShell(`rm -fr ${workdir}`); + await executeCommand({ command: `rm -fr ${workdir}` }); } - await asyncExecShell(`mkdir -p ${workdir}`); + await executeCommand({ command: `mkdir -p ${workdir}` }); return { workdir, repodir @@ -1117,7 +1191,7 @@ export async function stopDatabaseContainer(database: any): Promise { } = database; if (destinationDockerId) { try { - const { stdout } = await executeDockerCmd({ + const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }); @@ -1145,9 +1219,10 @@ export async function stopTcpHttpProxy( const { found } = await checkContainer({ dockerId, container }); try { if (found) { - return await executeDockerCmd({ + return await executeCommand({ dockerId, - command: `docker stop -t 0 ${container} && docker rm ${container}` + command: `docker stop -t 0 ${container} && docker rm ${container}`, + shell: true }); } } catch (error) { @@ -1169,34 +1244,34 @@ export async function updatePasswordInDb(database, user, newPassword, isRoot) { } = database; if (destinationDockerId) { if (type === 'mysql') { - await executeDockerCmd({ + await executeCommand({ 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({ + await executeCommand({ 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({ + await executeCommand({ dockerId, command: `docker exec ${id} psql postgresql://postgres:${rootUserPassword}@${id}:5432/${defaultDatabase} -c "ALTER role postgres WITH PASSWORD '${newPassword}'"` }); } else { - await executeDockerCmd({ + await executeCommand({ 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({ + await executeCommand({ 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({ + await executeCommand({ dockerId, command: `docker exec ${id} redis-cli -u redis://${dbUserPassword}@${id}:6379 --raw CONFIG SET requirepass ${newPassword}` }); @@ -1370,7 +1445,7 @@ export async function startTraefikTCPProxy( }); try { if (foundDependentContainer && !found) { - const { stdout: Config } = await executeDockerCmd({ + const { stdout: Config } = await executeCommand({ dockerId, command: `docker network inspect ${network} --format '{{json .IPAM.Config }}'` }); @@ -1417,16 +1492,17 @@ export async function startTraefikTCPProxy( } }; await fs.writeFile(`/tmp/docker-compose-${id}.yaml`, yaml.dump(tcpProxy)); - await executeDockerCmd({ + await executeCommand({ 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({ + await executeCommand({ dockerId, - command: `docker stop -t 0 ${container} && docker rm ${container}` + command: `docker stop -t 0 ${container} && docker rm ${container}`, + shell: true }); } } catch (error) { @@ -1537,7 +1613,7 @@ export async function stopBuild(buildId, applicationId) { await cleanupDB(buildId, applicationId); return reject(new Error('Canceled.')); } - const { stdout: buildContainers } = await executeDockerCmd({ + const { stdout: buildContainers } = await executeCommand({ dockerId, command: `docker container ls --filter "label=coolify.buildId=${buildId}" --format '{{json .}}'` }); @@ -1580,26 +1656,28 @@ export function convertTolOldVolumeNames(type) { export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { // Cleanup old coolify images try { - let { stdout: images } = await executeDockerCmd({ + let { stdout: images } = await executeCommand({ dockerId, - command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r` + command: `docker images coollabsio/coolify --filter before="coollabsio/coolify:${version}" -q | xargs -r`, + shell: true }); images = images.trim(); if (images) { - await executeDockerCmd({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r` }); + await executeCommand({ dockerId, command: `docker rmi -f ${images}" -q | xargs -r`, shell: true }); } } catch (error) { } if (lowDiskSpace || force) { // Cleanup images that are not used try { - await executeDockerCmd({ dockerId, command: `docker image prune -f` }); + await executeCommand({ dockerId, command: `docker image prune -f` }); } catch (error) { } const { numberOfDockerImagesKeptLocally } = await prisma.setting.findUnique({ where: { id: '0' } }) - const { stdout: images } = await executeDockerCmd({ + const { stdout: images } = await executeCommand({ dockerId, - command: `docker images | grep -v "" | grep -v REPOSITORY | awk '{print $1, $2}'` + command: `docker images | grep -v "" | grep -v REPOSITORY | awk '{print $1, $2}'`, + shell: true }); const imagesArray = images.trim().replaceAll(' ', ':').split('\n'); const imagesSet = new Set(imagesArray.map((image) => image.split(':')[0])); @@ -1618,12 +1696,12 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { } } for (const image of deleteImage) { - await executeDockerCmd({ dockerId, command: `docker image rm -f ${image}` }); + await executeCommand({ dockerId, command: `docker image rm -f ${image}` }); } // Prune coolify managed containers try { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker container prune -f --filter "label=coolify.managed=true"` }); @@ -1631,7 +1709,7 @@ export async function cleanupDockerStorage(dockerId, lowDiskSpace, force) { // Cleanup build caches try { - await executeDockerCmd({ dockerId, command: `docker builder prune -a -f` }); + await executeCommand({ dockerId, command: `docker builder prune -a -f` }); } catch (error) { } } } @@ -1718,11 +1796,11 @@ export async function pushToRegistry(application: any, workdir: string, tag: str const location = `${workdir}/.docker` const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}` const pushCommand = `docker --config ${location} push ${imageName}:${customTag}` - await executeDockerCmd({ + await executeCommand({ dockerId: application.destinationDockerId, command: tagCommand }) - await executeDockerCmd({ + await executeCommand({ dockerId: application.destinationDockerId, command: pushCommand }) diff --git a/apps/api/src/lib/docker.ts b/apps/api/src/lib/docker.ts index c826bd4e7..0e2822d59 100644 --- a/apps/api/src/lib/docker.ts +++ b/apps/api/src/lib/docker.ts @@ -1,4 +1,4 @@ -import { executeDockerCmd } from './common'; +import { executeCommand } from './common'; export function formatLabelsOnDocker(data) { return data.trim().split('\n').map(a => JSON.parse(a)).map((container) => { @@ -16,7 +16,7 @@ export function formatLabelsOnDocker(data) { export async function checkContainer({ dockerId, container, remove = false }: { dockerId: string, container: string, remove?: boolean }): Promise<{ found: boolean, status?: { isExited: boolean, isRunning: boolean, isRestarting: boolean } }> { let containerFound = false; try { - const { stdout } = await executeDockerCmd({ + const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${container}` @@ -28,14 +28,14 @@ export async function checkContainer({ dockerId, container, remove = false }: { const isRestarting = status === 'restarting' const isExited = status === 'exited' if (status === 'created') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker rm ${container}` }); } if (remove && status === 'exited') { - await executeDockerCmd({ + await executeCommand({ dockerId, command: `docker rm ${container}` @@ -62,7 +62,7 @@ export async function checkContainer({ dockerId, container, remove = false }: { export async function isContainerExited(dockerId: string, containerName: string): Promise { let isExited = false; try { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` }) + const { stdout } = await executeCommand({ dockerId, command: `docker inspect -f '{{.State.Status}}' ${containerName}` }) if (stdout.trim() === 'exited') { isExited = true; } @@ -81,13 +81,13 @@ export async function removeContainer({ dockerId: string; }): Promise { try { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) + const { stdout } = await executeCommand({ dockerId, command: `docker inspect --format '{{json .State}}' ${id}` }) if (JSON.parse(stdout).Running) { - await executeDockerCmd({ dockerId, command: `docker stop -t 0 ${id}` }) - await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }) + await executeCommand({ dockerId, command: `docker rm ${id}` }) } if (JSON.parse(stdout).Status === 'exited') { - await executeDockerCmd({ dockerId, command: `docker rm ${id}` }) + await executeCommand({ dockerId, command: `docker rm ${id}` }) } } catch (error) { throw error; diff --git a/apps/api/src/lib/importers/github.ts b/apps/api/src/lib/importers/github.ts index 62adf413b..774752b32 100644 --- a/apps/api/src/lib/importers/github.ts +++ b/apps/api/src/lib/importers/github.ts @@ -1,7 +1,7 @@ import jsonwebtoken from 'jsonwebtoken'; import { saveBuildLog } from '../buildPacks/common'; -import { asyncExecShell, decrypt, prisma } from '../common'; +import { decrypt, executeCommand, prisma } from '../common'; export default async function ({ applicationId, @@ -43,9 +43,11 @@ export default async function ({ applicationId }); } - await asyncExecShell( - `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. ` - ); + await executeCommand({ + command: + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, + shell: true + }); } else { const body = await prisma.githubApp.findUnique({ where: { id: githubAppId } }); @@ -81,11 +83,13 @@ export default async function ({ applicationId }); } - await asyncExecShell( - `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. ` - ); + await executeCommand({ + command: + `git clone -q -b ${branch} https://x-access-token:${token}@${url}/${repository}.git --config core.sshCommand="ssh -p ${customPort}" ${workdir}/ && cd ${workdir} && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, + shell: true + }); } - const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); - + const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true }); + console.log({ commit }) return commit.replace('\n', ''); } diff --git a/apps/api/src/lib/importers/gitlab.ts b/apps/api/src/lib/importers/gitlab.ts index 0e2ef7b32..525ca9703 100644 --- a/apps/api/src/lib/importers/gitlab.ts +++ b/apps/api/src/lib/importers/gitlab.ts @@ -1,5 +1,5 @@ import { saveBuildLog } from "../buildPacks/common"; -import { asyncExecShell } from "../common"; +import { executeCommand } from "../common"; export default async function ({ applicationId, @@ -28,8 +28,8 @@ export default async function ({ }): Promise { const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); if (!forPublic) { - await asyncExecShell(`echo '${privateSshKey}' > ${repodir}/id.rsa`); - await asyncExecShell(`chmod 600 ${repodir}/id.rsa`); + await executeCommand({ command: `echo '${privateSshKey}' > ${repodir}/id.rsa`, shell: true }); + await executeCommand({ command: `chmod 600 ${repodir}/id.rsa` }); } await saveBuildLog({ @@ -45,15 +45,19 @@ export default async function ({ }); } if (forPublic) { - await asyncExecShell( - `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. ` + await executeCommand({ + command: + `git clone -q -b ${branch} https://${url}/${repository}.git ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true + } ); } else { - await asyncExecShell( - `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. ` + await executeCommand({ + command: + `git clone -q -b ${branch} git@${url}:${repository}.git --config core.sshCommand="ssh -p ${customPort} -q -i ${repodir}id.rsa -o StrictHostKeyChecking=no" ${workdir}/ && cd ${workdir}/ && git checkout ${gitCommitHash || ""} && git submodule update --init --recursive && git lfs pull && cd .. `, shell: true + } ); } - const { stdout: commit } = await asyncExecShell(`cd ${workdir}/ && git rev-parse HEAD`); + const { stdout: commit } = await executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true }); return commit.replace('\n', ''); } diff --git a/apps/api/src/lib/services/handlers.ts b/apps/api/src/lib/services/handlers.ts index ec015ea4f..866f9b17a 100644 --- a/apps/api/src/lib/services/handlers.ts +++ b/apps/api/src/lib/services/handlers.ts @@ -2,7 +2,7 @@ import type { FastifyReply, FastifyRequest } from 'fastify'; import fs from 'fs/promises'; import yaml from 'js-yaml'; import path from 'path'; -import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeDockerCmd, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common'; +import { asyncSleep, ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, errorHandler, executeCommand, getServiceFromDB, isARM, makeLabelForServices, persistentVolumes, prisma, stopTcpHttpProxy } from '../common'; import { parseAndFindServiceTemplates } from '../../routes/api/v1/services/handlers'; import { ServiceStartStop } from '../../routes/api/v1/services/types'; @@ -15,14 +15,19 @@ export async function stopService(request: FastifyRequest) { const teamId = request.user.teamId; const { destinationDockerId } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.project=${id}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } return {} } throw { status: 500, message: 'Could not stop containers.' } @@ -182,19 +187,36 @@ export async function startService(request: FastifyRequest, fa // Workaround: Stop old minio proxies if (service.type === 'minio') { try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDocker.id, command: - `docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container stop -t 0` + `docker container ls -a --filter 'name=${id}-' --format {{.ID}}` }); - + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { } try { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: destinationDocker.id, command: - `docker container ls -a --filter 'name=${id}-' --format {{.ID}}|xargs -r -n 1 docker container rm -f` + `docker container ls -a --filter 'name=${id}-' --format {{.ID}}` }); + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } catch (error) { } } return {} @@ -205,16 +227,16 @@ export async function startService(request: FastifyRequest, fa async function startServiceContainers(fastify, id, teamId, dockerId, composeFileDestination) { try { fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Pulling images...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} pull` }) } catch (error) { } fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Building images...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} build --no-cache` }) fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Creating containers...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} create` }) fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 'Starting containers...' }) - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} start` }) await asyncSleep(1000); - await executeDockerCmd({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) + await executeCommand({ dockerId, command: `docker compose -f ${composeFileDestination} up -d` }) fastify.io.to(teamId).emit(`start-service`, { serviceId: id, state: 0 }) } export async function migrateAppwriteDB(request: FastifyRequest, reply: FastifyReply) { @@ -226,7 +248,7 @@ export async function migrateAppwriteDB(request: FastifyRequest, reply: destinationDocker, } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker exec ${id} migrate` }) diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index 038f4ba6b..82a8613ab 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -8,7 +8,7 @@ import csv from 'csvtojson'; import { day } from '../../../../lib/dayjs'; import { saveDockerRegistryCredentials, setDefaultBaseImage, setDefaultConfiguration } from '../../../../lib/buildPacks/common'; -import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; +import { checkDomainsIsValidInDNS, checkExposedPort, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateSshKeyPair, getContainerUsage, getDomain, isDev, isDomainConfigured, listSettings, prisma, stopBuild, uniqueName } from '../../../../lib/common'; import { checkContainer, formatLabelsOnDocker, removeContainer } from '../../../../lib/docker'; import type { FastifyRequest } from 'fastify'; @@ -78,7 +78,7 @@ export async function cleanupUnconfiguredApplications(request: FastifyRequest) { const application: any = await getApplicationFromDB(id, teamId); if (application?.destinationDockerId) { if (application.buildPack === 'compose') { - const { stdout: containers } = await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` @@ -485,7 +485,7 @@ export async function restartApplication(request: FastifyRequest, reply: Fa if (application?.destinationDockerId) { const { id: dockerId } = application.destinationDocker; if (application.buildPack === 'compose') { - const { stdout: containers } = await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker ps -a --filter "label=coolify.applicationId=${id}" --format '{{json .}}'` @@ -627,7 +627,7 @@ export async function deleteApplication(request: FastifyRequest) { try { const { id } = request.params const application = await prisma.application.findUnique({ where: { id }, include: { destinationDocker: true } }); - const { stdout } = await executeDockerCmd({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) + const { stdout } = await executeCommand({ dockerId: application.destinationDocker.id, command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` }) if (stdout === '') { throw { status: 500, message: 'No previews found.' } } @@ -1369,7 +1369,7 @@ export async function getApplicationLogs(request: FastifyRequest ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const logs = stripLogsStderr.concat(stripLogsStdout) @@ -1560,19 +1560,19 @@ export async function createdBranchDatabase(database: any, baseDatabaseBranch: s if (destinationDockerId) { if (type === 'postgresql') { const decryptedRootUserPassword = decrypt(rootUserPassword); - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} pg_dump -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/${baseDatabaseBranch}" --encoding=UTF8 --schema-only -f /tmp/${baseDatabaseBranch}.dump` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "CREATE DATABASE branch_${pullmergeRequestId}"` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql -d "postgresql://postgres:${decryptedRootUserPassword}@${id}:5432/branch_${pullmergeRequestId}" -f /tmp/${baseDatabaseBranch}.dump` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "ALTER DATABASE branch_${pullmergeRequestId} OWNER TO ${dbUser}"` }) @@ -1591,12 +1591,12 @@ export async function removeBranchDatabase(database: any, pullmergeRequestId: st if (type === 'postgresql') { const decryptedRootUserPassword = decrypt(rootUserPassword); // Terminate all connections to the database - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "SELECT pg_terminate_backend(pg_stat_activity.pid) FROM pg_stat_activity WHERE pg_stat_activity.datname = 'branch_${pullmergeRequestId}' AND pid <> pg_backend_pid();"` }) - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDockerId, command: `docker exec ${id} psql postgresql://postgres:${decryptedRootUserPassword}@${id}:5432 -c "DROP DATABASE branch_${pullmergeRequestId}"` }) diff --git a/apps/api/src/routes/api/v1/databases/handlers.ts b/apps/api/src/routes/api/v1/databases/handlers.ts index 48a29a900..20bf285b1 100644 --- a/apps/api/src/routes/api/v1/databases/handlers.ts +++ b/apps/api/src/routes/api/v1/databases/handlers.ts @@ -3,7 +3,7 @@ import type { FastifyRequest } from 'fastify'; import { FastifyReply } from 'fastify'; import yaml from 'js-yaml'; import fs from 'fs/promises'; -import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeDockerCmd, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; +import { ComposeFile, createDirectories, decrypt, defaultComposeConfiguration, encrypt, errorHandler, executeCommand, generateDatabaseConfiguration, generatePassword, getContainerUsage, getDatabaseImage, getDatabaseVersions, getFreePublicPort, listSettings, makeLabelForStandaloneDatabase, prisma, startTraefikTCPProxy, stopDatabaseContainer, stopTcpHttpProxy, supportedDatabaseTypesAndVersions, uniqueName, updatePasswordInDb } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import type { OnlyId } from '../../../../types'; @@ -89,7 +89,7 @@ export async function getDatabaseStatus(request: FastifyRequest) { const { destinationDockerId, destinationDocker } = database; if (destinationDockerId) { try { - const { stdout } = await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` }) + const { stdout } = await executeCommand({ dockerId: destinationDocker.id, command: `docker inspect --format '{{json .State}}' ${id}` }) if (JSON.parse(stdout).Running) { isRunning = true; @@ -208,7 +208,7 @@ export async function saveDatabaseDestination(request: FastifyRequest) { }; const composeFileDestination = `${workdir}/docker-compose.yaml`; await fs.writeFile(composeFileDestination, yaml.dump(composeFile)); - await executeDockerCmd({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) + await executeCommand({ dockerId: destinationDocker.id, command: `docker compose -f ${composeFileDestination} up -d` }) if (isPublic) await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); return {}; @@ -347,7 +347,7 @@ export async function getDatabaseLogs(request: FastifyRequest) // const found = await checkContainer({ dockerId, container: id }) // if (found) { const { default: ansi } = await import('strip-ansi') - const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` }) + const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${id}` }) const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const logs = stripLogsStderr.concat(stripLogsStdout) diff --git a/apps/api/src/routes/api/v1/destinations/handlers.ts b/apps/api/src/routes/api/v1/destinations/handlers.ts index 9006dc336..c4bcf71f5 100644 --- a/apps/api/src/routes/api/v1/destinations/handlers.ts +++ b/apps/api/src/routes/api/v1/destinations/handlers.ts @@ -4,7 +4,7 @@ import sshConfig from 'ssh-config' import fs from 'fs/promises' import os from 'os'; -import { asyncExecShell, createRemoteEngineConfiguration, decrypt, errorHandler, executeDockerCmd, executeSSHCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common'; +import { createRemoteEngineConfiguration, decrypt, errorHandler, executeCommand, executeSSHCmd, listSettings, prisma, startTraefikProxy, stopTraefikProxy } from '../../../../lib/common'; import { checkContainer } from '../../../../lib/docker'; import type { OnlyId } from '../../../../types'; @@ -79,9 +79,9 @@ export async function newDestination(request: FastifyRequest, re let { name, network, engine, isCoolifyProxyUsed, remoteIpAddress, remoteUser, remotePort } = request.body if (id === 'new') { if (engine) { - const { stdout } = await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network ls --filter 'name=^${network}$' --format '{{json .}}'`); + const { stdout } = await await executeCommand({ command: `docker network ls --filter 'name=^${network}$' --format '{{json .}}'` }); if (stdout === '') { - await asyncExecShell(`DOCKER_HOST=unix:///var/run/docker.sock docker network create --attachable ${network}`); + await await executeCommand({ command: `docker network create --attachable ${network}` }); } await prisma.destinationDocker.create({ data: { name, teams: { connect: { id: teamId } }, engine, network, isCoolifyProxyUsed } @@ -122,13 +122,13 @@ export async function deleteDestination(request: FastifyRequest) { const { network, remoteVerified, engine, isCoolifyProxyUsed } = await prisma.destinationDocker.findUnique({ where: { id } }); if (isCoolifyProxyUsed) { if (engine || remoteVerified) { - const { stdout: found } = await executeDockerCmd({ + const { stdout: found } = await executeCommand({ dockerId: id, command: `docker ps -a --filter network=${network} --filter name=coolify-proxy --format '{{.}}'` }) if (found) { - await executeDockerCmd({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` }) - await executeDockerCmd({ dockerId: id, command: `docker network rm ${network}` }) + await executeCommand({ dockerId: id, command: `docker network disconnect ${network} coolify-proxy` }) + await executeCommand({ dockerId: id, command: `docker network rm ${network}` }) } } } @@ -206,13 +206,13 @@ export async function verifyRemoteDockerEngineFn(id: string) { await createRemoteEngineConfiguration(id); const { remoteIpAddress, network, isCoolifyProxyUsed } = await prisma.destinationDocker.findFirst({ where: { id } }) const host = `ssh://${remoteIpAddress}-remote` - const { stdout } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`); + const { stdout } = await executeCommand({ command: `docker network ls --filter 'name=${network}' --no-trunc --format "{{json .}}"`, dockerId: id }); if (!stdout) { - await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable ${network}`); + await executeCommand({ command: `docker network create --attachable ${network}`, dockerId: id }); } - const { stdout: coolifyNetwork } = await asyncExecShell(`DOCKER_HOST=${host} docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`); + const { stdout: coolifyNetwork } = await executeCommand({ command: `docker network ls --filter 'name=coolify-infra' --no-trunc --format "{{json .}}"`, dockerId: id }); if (!coolifyNetwork) { - await asyncExecShell(`DOCKER_HOST=${host} docker network create --attachable coolify-infra`); + await executeCommand({ command: `docker network create --attachable coolify-infra`, dockerId: id }); } if (isCoolifyProxyUsed) await startTraefikProxy(id); try { diff --git a/apps/api/src/routes/api/v1/handlers.ts b/apps/api/src/routes/api/v1/handlers.ts index 2b5f7e521..01686f5cb 100644 --- a/apps/api/src/routes/api/v1/handlers.ts +++ b/apps/api/src/routes/api/v1/handlers.ts @@ -4,7 +4,6 @@ import bcrypt from "bcryptjs"; import fs from 'fs/promises'; import yaml from 'js-yaml'; import { - asyncExecShell, asyncSleep, cleanupDockerStorage, errorHandler, @@ -15,6 +14,7 @@ import { version, sentryDSN, executeDockerCmd, + executeCommand, } from "../../../lib/common"; import { scheduler } from "../../../lib/scheduler"; import type { FastifyReply, FastifyRequest } from "fastify"; @@ -38,7 +38,7 @@ export async function backup(request: FastifyRequest) { // dockerId: database.destinationDockerId, // command: `docker pull coollabsio/backup:latest`, // }) - std = await executeDockerCmd({ + std = await executeCommand({ dockerId: database.destinationDockerId, command: `docker run --rm -v /var/run/docker.sock:/var/run/docker.sock -v coolify-local-backup:/app/backups -e CONTAINERS_TO_BACKUP="${backupData}" coollabsio/backup` }) @@ -141,14 +141,10 @@ export async function update(request: FastifyRequest) { try { if (!isDev) { const { isAutoUpdateEnabled } = await prisma.setting.findFirst(); - await asyncExecShell(`docker pull coollabsio/coolify:${latestVersion}`); - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell( - `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` - ); - await asyncExecShell( - `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` - ); + await executeCommand({ command: `docker pull coollabsio/coolify:${latestVersion}` }); + await executeCommand({ command: `env | grep COOLIFY > .env` }); + await executeCommand({ command: `sed -i '/COOLIFY_AUTO_UPDATE=/cCOOLIFY_AUTO_UPDATE=${isAutoUpdateEnabled}' .env` }); + await executeCommand({ command: `docker run --rm -tid --env-file .env -v /var/run/docker.sock:/var/run/docker.sock -v coolify-db coollabsio/coolify:${latestVersion} /bin/sh -c "env | grep COOLIFY > .env && echo 'TAG=${latestVersion}' >> .env && docker stop -t 0 coolify coolify-fluentbit && docker rm coolify coolify-fluentbit && docker compose pull && docker compose up -d --force-recreate"` }); return {}; } else { await asyncSleep(2000); @@ -177,7 +173,7 @@ export async function restartCoolify(request: FastifyRequest) { const teamId = request.user.teamId; if (teamId === "0") { if (!isDev) { - asyncExecShell(`docker restart coolify`); + await executeCommand({ command: `docker restart coolify` }); return {}; } else { return {}; diff --git a/apps/api/src/routes/api/v1/index.ts b/apps/api/src/routes/api/v1/index.ts index d691bef08..243683a59 100644 --- a/apps/api/src/routes/api/v1/index.ts +++ b/apps/api/src/routes/api/v1/index.ts @@ -53,9 +53,9 @@ const root: FastifyPluginAsync = async (fastify): Promise => { onRequest: [fastify.authenticate] }, async (request) => await cleanupManually(request)); - fastify.get('/internal/backup/:backupData', { - onRequest: [fastify.authenticate] - }, async (request) => await backup(request)); + // fastify.get('/internal/backup/:backupData', { + // onRequest: [fastify.authenticate] + // }, async (request) => await backup(request)); }; export default root; diff --git a/apps/api/src/routes/api/v1/servers/handlers.ts b/apps/api/src/routes/api/v1/servers/handlers.ts index 874f5a9f7..f05c2b965 100644 --- a/apps/api/src/routes/api/v1/servers/handlers.ts +++ b/apps/api/src/routes/api/v1/servers/handlers.ts @@ -1,5 +1,5 @@ import type { FastifyRequest } from 'fastify'; -import { errorHandler, executeDockerCmd, prisma, createRemoteEngineConfiguration, executeSSHCmd } from '../../../../lib/common'; +import { errorHandler, prisma, executeSSHCmd } from '../../../../lib/common'; import os from 'node:os'; import osu from 'node-os-utils'; diff --git a/apps/api/src/routes/api/v1/services/handlers.ts b/apps/api/src/routes/api/v1/services/handlers.ts index 659e02890..88c027051 100644 --- a/apps/api/src/routes/api/v1/services/handlers.ts +++ b/apps/api/src/routes/api/v1/services/handlers.ts @@ -4,7 +4,7 @@ import yaml from 'js-yaml'; import bcrypt from 'bcryptjs'; import cuid from 'cuid'; -import { prisma, uniqueName, asyncExecShell, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, executeDockerCmd, checkDomainsIsValidInDNS, checkExposedPort, listSettings, generateToken } from '../../../../lib/common'; +import { prisma, uniqueName, getServiceFromDB, getContainerUsage, isDomainConfigured, fixType, decrypt, encrypt, ComposeFile, getFreePublicPort, getDomain, errorHandler, generatePassword, isDev, stopTcpHttpProxy, checkDomainsIsValidInDNS, checkExposedPort, listSettings, generateToken, executeCommand } from '../../../../lib/common'; import { day } from '../../../../lib/dayjs'; import { checkContainer, } from '../../../../lib/docker'; import { removeService } from '../../../../lib/services/common'; @@ -48,14 +48,19 @@ export async function cleanupUnconfiguredServices(request: FastifyRequest) { for (const service of services) { if (!service.fqdn) { if (service.destinationDockerId) { - await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: service.destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker stop -t 0` - }) - await executeDockerCmd({ - dockerId: service.destinationDockerId, - command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}|xargs -r -n 1 docker rm --force` + command: `docker ps -a --filter 'label=com.docker.compose.project=${service.id}' --format {{.ID}}` }) + if (containers) { + const containerArray = containers.split('\n'); + if (containerArray.length > 0) { + for (const container of containerArray) { + await executeCommand({ dockerId: service.destinationDockerId, command: `docker stop -t 0 ${container}` }) + await executeCommand({ dockerId: service.destinationDockerId, command: `docker rm --force ${container}` }) + } + } + } } await removeService({ id: service.id }); } @@ -73,7 +78,7 @@ export async function getServiceStatus(request: FastifyRequest) { const { destinationDockerId, settings } = service; let payload = {} if (destinationDockerId) { - const { stdout: containers } = await executeDockerCmd({ + const { stdout: containers } = await executeCommand({ dockerId: service.destinationDocker.id, command: `docker ps -a --filter "label=com.docker.compose.project=${id}" --format '{{json .}}'` @@ -443,7 +448,7 @@ export async function getServiceLogs(request: FastifyRequest) { if (destinationDockerId) { try { const { default: ansi } = await import('strip-ansi') - const { stdout, stderr } = await executeDockerCmd({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` }) + const { stdout, stderr } = await executeCommand({ dockerId, command: `docker logs --since ${since} --tail 5000 --timestamps ${containerId}` }) const stripLogsStdout = stdout.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const stripLogsStderr = stderr.toString().split('\n').map((l) => ansi(l)).filter((a) => a); const logs = stripLogsStderr.concat(stripLogsStdout) @@ -749,7 +754,7 @@ export async function activatePlausibleUsers(request: FastifyRequest, re if (destinationDockerId) { const databaseUrl = serviceSecret.find((secret) => secret.name === 'DATABASE_URL'); if (databaseUrl) { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, command: `docker exec ${id}-postgresql psql -H ${databaseUrl.value} -c "UPDATE users SET email_verified = true;"` }) @@ -770,9 +775,10 @@ export async function cleanupPlausibleLogs(request: FastifyRequest, repl destinationDocker, } = await getServiceFromDB({ id, teamId }); if (destinationDockerId) { - await executeDockerCmd({ + await executeCommand({ dockerId: destinationDocker.id, - command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"` + command: `docker exec ${id}-clickhouse /usr/bin/clickhouse-client -q \\"SELECT name FROM system.tables WHERE name LIKE '%log%';\\"| xargs -I{} /usr/bin/clickhouse-client -q \"TRUNCATE TABLE system.{};\"`, + shell: true }) return await reply.code(201).send() } @@ -812,36 +818,42 @@ export async function activateWordpressFtp(request: FastifyRequest ${hostkeyDir}/${id}.ed25519`); + await executeCommand({ command: `echo "${decrypt(ftpHostKey)}" > ${hostkeyDir}/${id}.ed25519`, shell: true }); } if (!ftpHostKeyPrivate) { - await asyncExecShell(`ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa`); - const { stdout: ftpHostKeyPrivate } = await asyncExecShell(`cat ${hostkeyDir}/${id}.rsa`); + await executeCommand({ command: `ssh-keygen -t rsa -b 4096 -N "" -f ${hostkeyDir}/${id}.rsa` }); + const { stdout: ftpHostKeyPrivate } = await executeCommand({ command: `cat ${hostkeyDir}/${id}.rsa` }); await prisma.wordpress.update({ where: { serviceId: id }, data: { ftpHostKeyPrivate: encrypt(ftpHostKeyPrivate) } }); } else { - await asyncExecShell(`echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`); + await executeCommand({ command: `echo "${decrypt(ftpHostKeyPrivate)}" > ${hostkeyDir}/${id}.rsa`, shell: true }); } await prisma.wordpress.update({ @@ -856,9 +868,10 @@ export async function activateWordpressFtp(request: FastifyRequest, try { const teamId = request.user.teamId; const { id } = request.body; - await asyncExecShell(`docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`) + await executeCommand({ command: `docker exec coolify-proxy sh -c 'rm -f /etc/traefik/acme/custom/${id}-key.pem /etc/traefik/acme/custom/${id}-cert.pem'`, shell: true }) await prisma.certificate.deleteMany({ where: { id, teamId } }) return reply.code(201).send() } catch ({ status, message }) { diff --git a/apps/api/src/routes/webhooks/traefik/handlers.ts b/apps/api/src/routes/webhooks/traefik/handlers.ts index caa13c559..e46c9d376 100644 --- a/apps/api/src/routes/webhooks/traefik/handlers.ts +++ b/apps/api/src/routes/webhooks/traefik/handlers.ts @@ -1,5 +1,5 @@ import { FastifyRequest } from "fastify"; -import { errorHandler, getDomain, isDev, prisma, executeDockerCmd, fixType } from "../../../lib/common"; +import { errorHandler, getDomain, isDev, prisma, executeCommand } from "../../../lib/common"; import { getTemplates } from "../../../lib/services"; import { OnlyId } from "../../../types"; @@ -263,10 +263,12 @@ export async function proxyConfiguration(request: FastifyRequest, remote const runningContainers = {} applications.forEach((app) => dockerIds.add(app.destinationDocker.id)); for (const dockerId of dockerIds) { - const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) - const containersArray = container.trim().split('\n'); - if (containersArray.length > 0) { - runningContainers[dockerId] = containersArray + const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) + if (container) { + const containersArray = container.trim().split('\n'); + if (containersArray.length > 0) { + runningContainers[dockerId] = containersArray + } } } for (const application of applications) { @@ -332,7 +334,7 @@ export async function proxyConfiguration(request: FastifyRequest, remote traefik.http.routers = { ...traefik.http.routers, ...generateRouters(serviceId, domain, nakedDomain, pathPrefix, isHttps, isWWW, dualCerts, isCustomSSL) } traefik.http.services = { ...traefik.http.services, ...generateServices(serviceId, id, port) } if (previews) { - const { stdout } = await executeDockerCmd({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` }) + const { stdout } = await executeCommand({ dockerId, command: `docker container ls --filter="status=running" --filter="network=${network}" --filter="name=${id}-" --format="{{json .Names}}"` }) const containers = stdout .trim() .split('\n') @@ -359,7 +361,7 @@ export async function proxyConfiguration(request: FastifyRequest, remote const runningContainers = {} services.forEach((app) => dockerIds.add(app.destinationDocker.id)); for (const dockerId of dockerIds) { - const { stdout: container } = await executeDockerCmd({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) + const { stdout: container } = await executeCommand({ dockerId, command: `docker container ls --filter 'label=coolify.managed=true' --format '{{ .Names}}'` }) const containersArray = container.trim().split('\n'); if (containersArray.length > 0) { runningContainers[dockerId] = containersArray diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f8082420..ab6a85025 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,6 +56,7 @@ importers: is-port-reachable: 4.0.0 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 + minimist: ^1.2.7 node-forge: 1.3.1 node-os-utils: 1.3.7 nodemon: 2.0.20 @@ -66,6 +67,7 @@ importers: public-ip: 6.0.1 pump: 3.0.0 rimraf: 3.0.2 + shell-quote: ^1.7.4 socket.io: 4.5.3 ssh-config: 4.1.6 strip-ansi: 7.0.1 @@ -108,6 +110,7 @@ importers: is-port-reachable: 4.0.0 js-yaml: 4.1.0 jsonwebtoken: 8.5.1 + minimist: 1.2.7 node-forge: 1.3.1 node-os-utils: 1.3.7 p-all: 4.0.0 @@ -115,6 +118,7 @@ importers: prisma: 4.6.1 public-ip: 6.0.1 pump: 3.0.0 + shell-quote: 1.7.4 socket.io: 4.5.3 ssh-config: 4.1.6 strip-ansi: 7.0.1 @@ -5599,6 +5603,9 @@ packages: /minimist/1.2.6: resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} + /minimist/1.2.7: + resolution: {integrity: sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==} + /mkdirp-classic/0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} dev: false @@ -6702,6 +6709,10 @@ packages: resolution: {integrity: sha512-Vpfqwm4EnqGdlsBFNmHhxhElJYrdfcxPThu+ryKS5J8L/fhAwLazFZtq+S+TWZ9ANj2piSQLGj6NQg+lKPmxrw==} dev: true + /shell-quote/1.7.4: + resolution: {integrity: sha512-8o/QEhSSRb1a5i7TFR0iM4G16Z0vYB2OQVs4G3aAFXjn3T6yEx8AZxy1PgDF7I00LZHYA3WxaSYIf5e5sAX8Rw==} + dev: false + /side-channel/1.0.4: resolution: {integrity: sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==} dependencies: @@ -7310,7 +7321,7 @@ packages: engines: {node: '>=6'} dependencies: json5: 2.2.1 - minimist: 1.2.6 + minimist: 1.2.7 strip-bom: 3.0.0 /tslib/1.14.1: