diff --git a/apps/api/src/jobs/deployApplication.ts b/apps/api/src/jobs/deployApplication.ts index f25c4bbc3..b2d2c8ec9 100644 --- a/apps/api/src/jobs/deployApplication.ts +++ b/apps/api/src/jobs/deployApplication.ts @@ -85,6 +85,7 @@ import * as buildpacks from '../lib/buildPacks'; baseDirectory, publishDirectory, dockerFileLocation, + dockerComposeConfiguration, denoMainFile } = application const currentHash = crypto @@ -112,17 +113,6 @@ import * as buildpacks from '../lib/buildPacks'; ) .digest('hex'); const { debug } = settings; - // if (concurrency === 1) { - // await prisma.build.updateMany({ - // where: { - // status: { in: ['queued', 'running'] }, - // id: { not: buildId }, - // applicationId, - // createdAt: { lt: new Date(new Date().getTime() - 10 * 1000) } - // }, - // data: { status: 'failed' } - // }); - // } let imageId = applicationId; let domain = getDomain(fqdn); const volumes = @@ -138,6 +128,10 @@ import * as buildpacks from '../lib/buildPacks'; repository = sourceRepository || repository; } + try { + dockerComposeConfiguration = JSON.parse(dockerComposeConfiguration) + } catch (error) { } + let deployNeeded = true; let destinationType; @@ -264,6 +258,7 @@ import * as buildpacks from '../lib/buildPacks'; pythonModule, pythonVariable, dockerFileLocation, + dockerComposeConfiguration, denoMainFile, denoOptions, baseImage, diff --git a/apps/api/src/jobs/infrastructure.ts b/apps/api/src/jobs/infrastructure.ts deleted file mode 100644 index 2bd9c329d..000000000 --- a/apps/api/src/jobs/infrastructure.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { parentPort } from 'node:worker_threads'; -import axios from 'axios'; -import { compareVersions } from 'compare-versions'; -import { asyncExecShell, cleanupDockerStorage, executeDockerCmd, isDev, prisma, startTraefikTCPProxy, generateDatabaseConfiguration, startTraefikProxy, listSettings, version, createRemoteEngineConfiguration, decrypt, executeSSHCmd } from '../lib/common'; -import { checkContainer } from '../lib/docker'; -import fs from 'fs/promises' -async function autoUpdater() { - try { - const currentVersion = version; - const { data: versions } = await axios - .get( - `https://get.coollabs.io/versions.json` - , { - params: { - appId: process.env['COOLIFY_APP_ID'] || undefined, - version: currentVersion - } - }) - const latestVersion = versions['coolify'].main.version; - const isUpdateAvailable = compareVersions(latestVersion, currentVersion); - if (isUpdateAvailable === 1) { - const activeCount = 0 - if (activeCount === 0) { - 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"` - ); - } - } else { - console.log('Updating (not really in dev mode).'); - } - } - } - } catch (error) { } -} -async function checkFluentBit() { - if (!isDev) { - const engine = '/var/run/docker.sock'; - const { id } = await prisma.destinationDocker.findFirst({ - where: { engine, network: 'coolify' } - }); - const { found } = await checkContainer({ dockerId: id, container: 'coolify-fluentbit' }); - if (!found) { - await asyncExecShell(`env | grep COOLIFY > .env`); - await asyncExecShell(`docker compose up -d fluent-bit`); - } - } -} -async function copyRemoteCertificates(id: string, dockerId: string, remoteIpAddress: string) { - try { - await asyncExecShell(`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/` }) - } catch (error) { - console.log({ error }) - } -} -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/`) - } catch (error) { - console.log({ error }) - } -} -async function copySSLCertificates() { - try { - const pAll = await import('p-all'); - const actions = [] - const certificates = await prisma.certificate.findMany({ include: { team: true } }) - const teamIds = certificates.map(c => c.teamId) - const destinations = await prisma.destinationDocker.findMany({ where: { isCoolifyProxyUsed: true, teams: { some: { id: { in: [...teamIds] } } } } }) - for (const certificate of certificates) { - const { id, key, cert } = certificate - const decryptedKey = decrypt(key) - await fs.writeFile(`/tmp/${id}-key.pem`, decryptedKey) - await fs.writeFile(`/tmp/${id}-cert.pem`, cert) - for (const destination of destinations) { - if (destination.remoteEngine) { - if (destination.remoteVerified) { - const { id: dockerId, remoteIpAddress } = destination - actions.push(async () => copyRemoteCertificates(id, dockerId, remoteIpAddress)) - } - } else { - actions.push(async () => copyLocalCertificates(id)) - } - } - } - await pAll.default(actions, { concurrency: 1 }) - } catch (error) { - console.log(error) - } finally { - await asyncExecShell(`find /tmp/ -maxdepth 1 -type f -name '*-*.pem' -delete`) - } -} -async function checkProxies() { - try { - const { default: isReachable } = await import('is-port-reachable'); - let portReachable; - - const { arch, ipv4, ipv6 } = await listSettings(); - - // Coolify Proxy local - const engine = '/var/run/docker.sock'; - const localDocker = await prisma.destinationDocker.findFirst({ - where: { engine, network: 'coolify', isCoolifyProxyUsed: true } - }); - if (localDocker) { - portReachable = await isReachable(80, { host: ipv4 || ipv6 }) - if (!portReachable) { - await startTraefikProxy(localDocker.id); - } - } - // Coolify Proxy remote - const remoteDocker = await prisma.destinationDocker.findMany({ - where: { remoteEngine: true, remoteVerified: true } - }); - if (remoteDocker.length > 0) { - for (const docker of remoteDocker) { - if (docker.isCoolifyProxyUsed) { - portReachable = await isReachable(80, { host: docker.remoteIpAddress }) - if (!portReachable) { - await startTraefikProxy(docker.id); - } - } - try { - await createRemoteEngineConfiguration(docker.id) - } catch (error) { } - } - } - // TCP Proxies - const databasesWithPublicPort = await prisma.database.findMany({ - where: { publicPort: { not: null } }, - include: { settings: true, destinationDocker: true } - }); - for (const database of databasesWithPublicPort) { - const { destinationDockerId, destinationDocker, publicPort, id } = database; - if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { - const { privatePort } = generateDatabaseConfiguration(database, arch); - await startTraefikTCPProxy(destinationDocker, id, publicPort, privatePort); - } - } - const wordpressWithFtp = await prisma.wordpress.findMany({ - where: { ftpPublicPort: { not: null } }, - include: { service: { include: { destinationDocker: true } } } - }); - for (const ftp of wordpressWithFtp) { - const { service, ftpPublicPort } = ftp; - const { destinationDockerId, destinationDocker, id } = service; - if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { - await startTraefikTCPProxy(destinationDocker, id, ftpPublicPort, 22, 'wordpressftp'); - } - } - - // HTTP Proxies - const minioInstances = await prisma.minio.findMany({ - where: { publicPort: { not: null } }, - include: { service: { include: { destinationDocker: true } } } - }); - for (const minio of minioInstances) { - const { service, publicPort } = minio; - const { destinationDockerId, destinationDocker, id } = service; - if (destinationDockerId && destinationDocker.isCoolifyProxyUsed) { - await startTraefikTCPProxy(destinationDocker, id, publicPort, 9000); - } - } - } catch (error) { - - } -} -async function cleanupPrismaEngines() { - if (!isDev) { - try { - const { stdout } = await asyncExecShell(`ps -ef | grep /app/prisma-engines/query-engine | grep -v grep | wc -l | xargs`) - if (stdout.trim() != null && stdout.trim() != '' && Number(stdout.trim()) > 1) { - await asyncExecShell(`killall -q -e /app/prisma-engines/query-engine -o 1m`) - } - } catch (error) { } - } -} -async function cleanupStorage() { - const destinationDockers = await prisma.destinationDocker.findMany(); - let enginesDone = new Set() - for (const destination of destinationDockers) { - if (enginesDone.has(destination.engine) || enginesDone.has(destination.remoteIpAddress)) return - if (destination.engine) enginesDone.add(destination.engine) - if (destination.remoteIpAddress) enginesDone.add(destination.remoteIpAddress) - - let lowDiskSpace = false; - 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 /'` }) - stdout = output.stdout; - } else { - const output = await asyncExecShell( - `df -kPT /` - ); - stdout = output.stdout; - } - let lines = stdout.trim().split('\n'); - let header = lines[0]; - let regex = - /^Filesystem\s+|Type\s+|1024-blocks|\s+Used|\s+Available|\s+Capacity|\s+Mounted on\s*$/g; - const boundaries = []; - let match; - - while ((match = regex.exec(header))) { - boundaries.push(match[0].length); - } - - boundaries[boundaries.length - 1] = -1; - const data = lines.slice(1).map((line) => { - const cl = boundaries.map((boundary) => { - const column = boundary > 0 ? line.slice(0, boundary) : line; - line = line.slice(boundary); - return column.trim(); - }); - return { - capacity: Number.parseInt(cl[5], 10) / 100 - }; - }); - if (data.length > 0) { - const { capacity } = data[0]; - if (capacity > 0.8) { - lowDiskSpace = true; - } - } - } catch (error) { } - await cleanupDockerStorage(destination.id, lowDiskSpace, false) - } -} - -(async () => { - let status = { - cleanupStorage: false, - autoUpdater: false, - copySSLCertificates: false, - } - if (parentPort) { - parentPort.on('message', async (message) => { - if (parentPort) { - if (message === 'error') throw new Error('oops'); - if (message === 'cancel') { - parentPort.postMessage('cancelled'); - process.exit(1); - } - if (message === 'action:cleanupStorage') { - if (!status.autoUpdater) { - status.cleanupStorage = true - await cleanupStorage(); - status.cleanupStorage = false - } - return; - } - if (message === 'action:cleanupPrismaEngines') { - await cleanupPrismaEngines(); - return; - } - if (message === 'action:checkProxies') { - await checkProxies(); - return; - } - if (message === 'action:checkFluentBit') { - await checkFluentBit(); - return; - } - if (message === 'action:copySSLCertificates') { - if (!status.copySSLCertificates) { - status.copySSLCertificates = true - await copySSLCertificates(); - status.copySSLCertificates = false - } - return; - } - if (message === 'action:autoUpdater') { - if (!status.cleanupStorage) { - status.autoUpdater = true - await autoUpdater(); - status.autoUpdater = false - } - return; - } - } - }); - } else process.exit(0); -})(); diff --git a/apps/api/src/lib/buildPacks/compose.ts b/apps/api/src/lib/buildPacks/compose.ts index 67f971304..26a946596 100644 --- a/apps/api/src/lib/buildPacks/compose.ts +++ b/apps/api/src/lib/buildPacks/compose.ts @@ -16,7 +16,8 @@ export default async function (data) { baseDirectory, secrets, pullmergeRequestId, - port + port, + dockerComposeConfiguration } = data const fileYml = `${workdir}${baseDirectory}/docker-compose.yml`; const fileYaml = `${workdir}${baseDirectory}/docker-compose.yaml`; @@ -76,6 +77,9 @@ export default async function (data) { value['env_file'] = envFound ? [`${workdir}/.env`] : [] value['labels'] = labels value['volumes'] = volumes + if (dockerComposeConfiguration[key].port) { + value['expose'] = [dockerComposeConfiguration[key].port] + } if (value['networks']?.length > 0) { value['networks'].forEach((network) => { networks[network] = { diff --git a/apps/api/src/routes/api/v1/applications/handlers.ts b/apps/api/src/routes/api/v1/applications/handlers.ts index c5425e25f..588ad98be 100644 --- a/apps/api/src/routes/api/v1/applications/handlers.ts +++ b/apps/api/src/routes/api/v1/applications/handlers.ts @@ -1223,7 +1223,7 @@ export async function getPreviews(request: FastifyRequest) { export async function getApplicationLogs(request: FastifyRequest) { try { - const { id } = request.params; + const { id, containerId } = request.params; let { since = 0 } = request.query if (since !== 0) { since = day(since).unix(); @@ -1234,10 +1234,8 @@ 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) diff --git a/apps/api/src/routes/api/v1/applications/index.ts b/apps/api/src/routes/api/v1/applications/index.ts index 26e7ff16a..1c230210f 100644 --- a/apps/api/src/routes/api/v1/applications/index.ts +++ b/apps/api/src/routes/api/v1/applications/index.ts @@ -45,7 +45,8 @@ const root: FastifyPluginAsync = async (fastify): Promise => { fastify.get('/:id/previews/:pullmergeRequestId/status', async (request) => await getPreviewStatus(request)); fastify.post('/:id/previews/:pullmergeRequestId/restart', async (request, reply) => await restartPreview(request, reply)); - fastify.get('/:id/logs', async (request) => await getApplicationLogs(request)); + // fastify.get('/:id/logs', async (request) => await getApplicationLogs(request)); + fastify.get('/:id/logs/:containerId', async (request) => await getApplicationLogs(request)); fastify.get('/:id/logs/build', async (request) => await getBuilds(request)); fastify.get('/:id/logs/build/:buildId', async (request) => await getBuildIdLogs(request)); diff --git a/apps/api/src/routes/api/v1/applications/types.ts b/apps/api/src/routes/api/v1/applications/types.ts index 14ba30b78..b282b0647 100644 --- a/apps/api/src/routes/api/v1/applications/types.ts +++ b/apps/api/src/routes/api/v1/applications/types.ts @@ -87,7 +87,11 @@ export interface DeleteStorage extends OnlyId { path: string, } } -export interface GetApplicationLogs extends OnlyId { +export interface GetApplicationLogs { + Params: { + id: string, + containerId: string + } Querystring: { since: number, } diff --git a/apps/ui/src/routes/_NewResource.svelte b/apps/ui/src/routes/_NewResource.svelte index 84e115c5c..acdebb162 100644 --- a/apps/ui/src/routes/_NewResource.svelte +++ b/apps/ui/src/routes/_NewResource.svelte @@ -18,7 +18,7 @@ diff --git a/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte b/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte index f7771e026..fb913b5a9 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_BuildPack.svelte @@ -14,8 +14,9 @@ export let foundConfig: any; export let scanning: any; export let packageManager: any; - export let dockerComposeFile: any = null; + export let dockerComposeFile: string | null = null; export let dockerComposeFileLocation: string | null = null; + export let dockerComposeConfiguration: any = null; async function handleSubmit(name: string) { try { @@ -27,11 +28,19 @@ delete tempBuildPack.fancyName; delete tempBuildPack.color; delete tempBuildPack.hoverColor; + let composeConfiguration: any = {} + if (!dockerComposeConfiguration && dockerComposeFile) { + for (const [name, _] of Object.entries(JSON.parse(dockerComposeFile).services)) { + composeConfiguration[name] = {}; + } + + } await post(`/applications/${id}`, { ...tempBuildPack, buildPack: name, dockerComposeFile, - dockerComposeFileLocation + dockerComposeFileLocation, + dockerComposeConfiguration: JSON.stringify(composeConfiguration) || JSON.stringify({}) }); await post(`/applications/${id}/configuration/buildpack`, { buildPack: name }); return await goto(from || `/applications/${id}`); diff --git a/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte b/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte index 28d0ef1ce..32d5627d9 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/_PublicRepository.svelte @@ -165,7 +165,7 @@ placeholder="eg: https://github.com/coollabsio/nodejs-example/tree/main" bind:value={publicRepositoryLink} /> - diff --git a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte index ad9b70be4..30ec09fe9 100644 --- a/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte +++ b/apps/ui/src/routes/applications/[id]/configuration/buildpack.svelte @@ -12,6 +12,7 @@ const response = await get(`/applications/${params.id}/configuration/buildpack`); return { props: { + application, ...response } }; @@ -25,6 +26,14 @@
-
+ handleSubmit()}>
General
@@ -440,7 +441,7 @@ diff --git a/apps/ui/src/routes/applications/[id]/logs/index.svelte b/apps/ui/src/routes/applications/[id]/logs/index.svelte index 4ec6ab28d..a0ef249b2 100644 --- a/apps/ui/src/routes/applications/[id]/logs/index.svelte +++ b/apps/ui/src/routes/applications/[id]/logs/index.svelte @@ -3,11 +3,11 @@ import { get } from '$lib/api'; import { t } from '$lib/translations'; import { errorNotification } from '$lib/common'; - import LoadingLogs from '$lib/components/LoadingLogs.svelte'; import { onMount, onDestroy } from 'svelte'; import Tooltip from '$lib/components/Tooltip.svelte'; import { status } from '$lib/store'; import { goto } from '$app/navigation'; + let application: any = {}; let logsLoading = false; let loadLogsInterval: any = null; @@ -17,26 +17,39 @@ let followingLogs: any; let logsEl: any; let position = 0; - if ( - !$status.application.isExited && - !$status.application.isRestarting && - !$status.application.isRunning - ) { - goto(`/applications/${$page.params.id}/`, { replaceState: true }); - } + let services: any = []; + let selectedService: any = null; const { id } = $page.params; onMount(async () => { const response = await get(`/applications/${id}`); application = response.application; - loadAllLogs(); - loadLogsInterval = setInterval(() => { - loadLogs(); - }, 1000); + if (response.application.dockerComposeFile) { + services = normalizeDockerServices( + JSON.parse(response.application.dockerComposeFile).services + ); + } else { + services = [ + { + name: '' + } + ]; + await selectService('') + } }); onDestroy(() => { clearInterval(loadLogsInterval); clearInterval(followingInterval); }); + function normalizeDockerServices(services: any[]) { + const tempdockerComposeServices = []; + for (const [name, data] of Object.entries(services)) { + tempdockerComposeServices.push({ + name, + data + }); + } + return tempdockerComposeServices; + } async function loadAllLogs() { try { logsLoading = true; @@ -55,7 +68,7 @@ if (logsLoading) return; try { const newLogs: any = await get( - `/applications/${id}/logs?since=${lastLog?.split(' ')[0] || 0}` + `/applications/${id}/logs/${selectedService}?since=${lastLog?.split(' ')[0] || 0}` ); if (newLogs?.logs && newLogs.logs[newLogs.logs.length - 1] !== logs[logs.length - 1]) { @@ -89,6 +102,22 @@ clearInterval(followingInterval); } } + async function selectService(service: any, init: boolean = false) { + if (services.length === 1 && init) return + + if (loadLogsInterval) clearInterval(loadLogsInterval); + if (followingInterval) clearInterval(followingInterval); + + logs = []; + lastLog = null; + followingLogs = false; + + selectedService = `${application.id}${service.name ? `-${service.name}` : ''}`; + loadLogs(); + loadLogsInterval = setInterval(() => { + loadLogs(); + }, 1000); + }
@@ -96,50 +125,67 @@
Application Logs
-
- {#if logs.length === 0} -
{$t('application.build.waiting_logs')}
- {:else} -
-
- - {#if loadLogsInterval} -
-
- {#each logs as log} -

{log + '\n'}

- {/each} -
-
- {/if} +
+ {#each services as service} + + {/each}
+ +{#if selectedService} +
+ {#if logs.length === 0} +
{$t('application.build.waiting_logs')}
+ {:else} +
+
+ + {#if loadLogsInterval} +
+
+ {#each logs as log} +

{log + '\n'}

+ {/each} +
+
+ {/if} +
+{/if}