From 8980598085edd9af23547ed9956114e751bb5e4d Mon Sep 17 00:00:00 2001 From: Andras Bacsai Date: Thu, 12 Jan 2023 16:43:41 +0100 Subject: [PATCH] wip: trpc --- .../applications/[id]/danger/+page.svelte | 60 ++ .../applications/[id]/previews/+page.svelte | 323 +++++++ .../applications/[id]/revert/+page.svelte | 151 ++++ .../routes/applications/[id]/revert/+page.ts | 16 + .../applications/[id]/usage/+page.svelte | 116 +++ apps/client/static/icons/directus.svg | 4 + apps/client/static/icons/libretranslate.png | Bin 0 -> 2329 bytes apps/client/static/icons/openblocks.png | Bin 0 -> 6641 bytes apps/client/static/icons/whoogle.png | Bin 0 -> 10193 bytes apps/server/package.json | 1 + ...pplication.ts => applicationBuildQueue.ts} | 58 +- apps/server/src/jobs/worker.ts | 9 - apps/server/src/lib/buildPacks/common.ts | 843 ++++++++++++++++++ apps/server/src/lib/buildPacks/compose.ts | 111 +++ apps/server/src/lib/buildPacks/deno.ts | 52 ++ apps/server/src/lib/buildPacks/docker.ts | 27 + apps/server/src/lib/buildPacks/gatsby.ts | 28 + apps/server/src/lib/buildPacks/heroku.ts | 17 + apps/server/src/lib/buildPacks/index.ts | 41 + apps/server/src/lib/buildPacks/laravel.ts | 46 + apps/server/src/lib/buildPacks/nestjs.ts | 31 + apps/server/src/lib/buildPacks/nextjs.ts | 66 ++ apps/server/src/lib/buildPacks/node.ts | 49 + apps/server/src/lib/buildPacks/nuxtjs.ts | 66 ++ apps/server/src/lib/buildPacks/php.ts | 50 ++ apps/server/src/lib/buildPacks/python.ts | 67 ++ apps/server/src/lib/buildPacks/react.ts | 28 + apps/server/src/lib/buildPacks/rust.ts | 40 + apps/server/src/lib/buildPacks/static.ts | 54 ++ apps/server/src/lib/buildPacks/svelte.ts | 28 + apps/server/src/lib/buildPacks/vuejs.ts | 28 + apps/server/src/lib/common.ts | 131 +++ apps/server/src/lib/importers/github.ts | 96 ++ apps/server/src/lib/importers/gitlab.ts | 65 ++ apps/server/src/lib/importers/index.ts | 4 + apps/server/src/scheduler.ts | 14 +- apps/server/src/server.ts | 6 +- .../src/trpc/routers/applications/index.ts | 646 +++++++++++--- .../src/trpc/routers/applications/lib.ts | 12 +- pnpm-lock.yaml | 20 +- 40 files changed, 3210 insertions(+), 194 deletions(-) create mode 100644 apps/client/src/routes/applications/[id]/danger/+page.svelte create mode 100644 apps/client/src/routes/applications/[id]/previews/+page.svelte create mode 100644 apps/client/src/routes/applications/[id]/revert/+page.svelte create mode 100644 apps/client/src/routes/applications/[id]/revert/+page.ts create mode 100644 apps/client/src/routes/applications/[id]/usage/+page.svelte create mode 100644 apps/client/static/icons/directus.svg create mode 100644 apps/client/static/icons/libretranslate.png create mode 100644 apps/client/static/icons/openblocks.png create mode 100644 apps/client/static/icons/whoogle.png rename apps/server/src/jobs/{deployApplication.ts => applicationBuildQueue.ts} (94%) delete mode 100644 apps/server/src/jobs/worker.ts create mode 100644 apps/server/src/lib/buildPacks/common.ts create mode 100644 apps/server/src/lib/buildPacks/compose.ts create mode 100644 apps/server/src/lib/buildPacks/deno.ts create mode 100644 apps/server/src/lib/buildPacks/docker.ts create mode 100644 apps/server/src/lib/buildPacks/gatsby.ts create mode 100644 apps/server/src/lib/buildPacks/heroku.ts create mode 100644 apps/server/src/lib/buildPacks/index.ts create mode 100644 apps/server/src/lib/buildPacks/laravel.ts create mode 100644 apps/server/src/lib/buildPacks/nestjs.ts create mode 100644 apps/server/src/lib/buildPacks/nextjs.ts create mode 100644 apps/server/src/lib/buildPacks/node.ts create mode 100644 apps/server/src/lib/buildPacks/nuxtjs.ts create mode 100644 apps/server/src/lib/buildPacks/php.ts create mode 100644 apps/server/src/lib/buildPacks/python.ts create mode 100644 apps/server/src/lib/buildPacks/react.ts create mode 100644 apps/server/src/lib/buildPacks/rust.ts create mode 100644 apps/server/src/lib/buildPacks/static.ts create mode 100644 apps/server/src/lib/buildPacks/svelte.ts create mode 100644 apps/server/src/lib/buildPacks/vuejs.ts create mode 100644 apps/server/src/lib/importers/github.ts create mode 100644 apps/server/src/lib/importers/gitlab.ts create mode 100644 apps/server/src/lib/importers/index.ts diff --git a/apps/client/src/routes/applications/[id]/danger/+page.svelte b/apps/client/src/routes/applications/[id]/danger/+page.svelte new file mode 100644 index 000000000..4fede0195 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/danger/+page.svelte @@ -0,0 +1,60 @@ + + +
+
+
Danger Zone
+
+ + {#if forceDelete} + + {:else} + + {/if} +
diff --git a/apps/client/src/routes/applications/[id]/previews/+page.svelte b/apps/client/src/routes/applications/[id]/previews/+page.svelte new file mode 100644 index 000000000..a4095f8b1 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/previews/+page.svelte @@ -0,0 +1,323 @@ + + +
+
+
+
Preview Deployments
+
+ +
+
+
+
+ +{#if loading.init} +
+
Loading...
+
+{:else if application.previewApplication.length > 0} +
+ {#each application.previewApplication as preview} +
+
+ {#await getStatus(preview)} + + {:then} + {#if status[preview.id] === 'running'} + + {:else} + + {/if} + {/await} +
+
+

+ PR #{preview.pullmergeRequestId} + {#if status[preview.id] === 'building'} + + BUILDING + + {/if} +

+
+

{preview.customDomain.replace('https://', '').replace('http://', '')}

+
+ +
+ {#if preview.customDomain} + + + + + + + + + {/if} + Open Preview + {#if loading.restart} + + {:else} + + {/if} + + Restart (useful to change secrets) + + Force redeploy (without cache) + + Delete Preview +
+
+
+
+
+ {/each} +
+{:else} + No previews found. +{/if} diff --git a/apps/client/src/routes/applications/[id]/revert/+page.svelte b/apps/client/src/routes/applications/[id]/revert/+page.svelte new file mode 100644 index 000000000..80013e0f0 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/revert/+page.svelte @@ -0,0 +1,151 @@ + + +
+
+
+
+ Revert +
+
+
+ If you do not want the next commit to overwrite the reverted application, temporary disable Automatic Deployment + feature here. +
+ {#if imagesAvailables.length > 0} +
Local Images
+
+ {#each imagesAvailables as image} +
+
+
+ {image.tag} +
+
+ + + {#if image.repository + ':' + image.tag !== runningImage} + + {:else} + + {/if} +
+
+
+ {/each} +
+ {:else} +
+
No Local images available
+
+ {/if} +
+ Remote Images (Docker Registry) +
+
+ + +
+
+
diff --git a/apps/client/src/routes/applications/[id]/revert/+page.ts b/apps/client/src/routes/applications/[id]/revert/+page.ts new file mode 100644 index 000000000..2fc882718 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/revert/+page.ts @@ -0,0 +1,16 @@ +import { error } from '@sveltejs/kit'; +import { trpc } from '$lib/store'; +import type { PageLoad } from './$types'; +export const ssr = false; + +export const load: PageLoad = async ({ params }) => { + try { + const { id } = params; + const { data } = await trpc.applications.getLocalImages.query({ id }); + return data; + } catch (err) { + throw error(500, { + message: 'An unexpected error occurred, please try again later.' + }); + } +}; diff --git a/apps/client/src/routes/applications/[id]/usage/+page.svelte b/apps/client/src/routes/applications/[id]/usage/+page.svelte new file mode 100644 index 000000000..656c697e3 --- /dev/null +++ b/apps/client/src/routes/applications/[id]/usage/+page.svelte @@ -0,0 +1,116 @@ + + +
+
+
Monitoring
+
+
+
+ {#each services as service} + + {/each} +
+{#if selectedService} +
+ {#if usageLoading} +
+{/if} diff --git a/apps/client/static/icons/directus.svg b/apps/client/static/icons/directus.svg new file mode 100644 index 000000000..e530a836b --- /dev/null +++ b/apps/client/static/icons/directus.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/client/static/icons/libretranslate.png b/apps/client/static/icons/libretranslate.png new file mode 100644 index 0000000000000000000000000000000000000000..b22316b68597295cb1ae8dcd04c4608b6b1f62d0 GIT binary patch literal 2329 zcmZ{mc{~%0AIE1TjkUz&CdZt=4AH8s9COSy2`zJGO;L`BWx1DJsX1e`+{zUqSDCr* zEa!xY5IHJ6tdytckKgmx@At>&`+0pn|9-!(*Y_Q7YhwlmNrC_X02pJAw)^9_e zGk@=tcmeDZz=t))iq-c4x?B$H*joF=b z&Ar9hFhKJQa)?)GYXto4&d|nY&hRhP(1OthVx7+zKza#|gG$Ih5;|heFQCNBm%;<= z1w;`d4Rj3T|E+b{{CsZ(8`BC63S7KDW#&fP#B@o=am7AoNhdG>kylI*bFh|6We1ZU z<0YqWc8DK%Xr+X}na~3~1^KKQA`+`g=}<((C6?F9B=CzpEGzqI_9#mI?EC!)T?Lfz z&o&Duva&MI`G#1Q*EWtv*o;XXv_h_v!?0rCbA?dB+m8W;?ps}AFh#XUZ3O>$GGz#x z*pNGNQ2Tyt0e3sY{>q1hnIik(=VT>gh7`>j>qSL~8S2JumqduP(djmVnz#|qV!dw; zAYb9bnYH^3ip4N@%kq`eR0Ogq?x}ynxQ1Cz%C(`mAdSDKWd?LMp#jBFelqra5;o&o zw^6QM%fF$cmOg6gIxsVEN0-Si`62C?GD*UAL@kV^nH&)uzcqffvbVBE9zIF8e(l0Q z5KH;nhr>b7x>SM!si@e)xKT0+Ckm)hR8~?-kWW@oT2yypZ-b zzXxod6tbZe+a}2q5%oL+G{?>Oe9-xEIQk;my3`ubC21>LpdXxVzVCEG_1 z;vmNPL&ux)~6rJF=Mid#c({MMY!m-f(kjY|Ruh4kUOBZ}+z4H_c+v_j^n zVCDv9&dEZ0cJZ=0h#_CrayNOMHnj3|>0-Jm_Z6VWCD^YjD$w^y`a|#r&XKaWPmLci0I%F`1FkBj2x$-AxL( zC@l^l9Cz@VI@qo^_3={wWcRyxdX*O}KPCK>sKc$wBWq+`o@%)4`IkI*RC(X;T7csA z7hu|6veb0+-g^Dk9_{bIj z4W6nsG4ey9179SthB>gQnr2`#`-82Mw60KP{-dA~gi`mjZp<2E8v>`ceK^^vaRPW? zMm5qrxxF01GBb=GZ~J7Z8P|R7K~bVeyI-)VLAhJ$li;w*rzt(38(o8!iue<-#1hrW z9_+1iaX8yRy zXDi6EO`b*^w3kGDy3{#TMDUOmfg5`YkOstdM>hM5HRoxfP0^F7CP!0+8uZdt2RsMD z1zQ=WIK8#}N<8@Z65fBm#kY$E>F6UMv4O68-|9(?@5)MFp1!yHUCcBU&vUQ6_Vn|z z!>3wYdT)cvx(8-&sLx0~Suj>PqICGYQ$@WJPWYVf(PW3Mecx|as+H3x=@m?Nu1WE= z)n8B-CLB`qz#H0U#moa+;m*jMi6Wi2KU!ZqR0l%Ym^g9>;E3W2k51-w1U3dmJrJ+j zmhHK);B7-`O|C=sO&2AutG|Kv?pEn@zb6}K%F4f~rjSe*QZ8;0Ru<^W2P{_avG~Ik znnbUPFQuiC%KbGw$3Fu~q_{)X;`WZ5kV{;fLEzLIil#tN4W%$H2It-BdM9nU$>KUxwD z2lR~R9ZjXUqdOK}v@pR<5NW*U*lSk2_eNjXr$J;@Y-#F!k+B@?=CWg}vt3n2_<|}T z+s`Qr85p7nzi7qT9lMQ6VJHIQ05|zV{g2e$o%x%gboi73ZAw z!lT!T9(rwOr>)nH4ZJN7h(#Ku#MrLuS%7e~2?txO1zcXwWWwX@FEj6H{^V12DuSxwws`V)B7j;nBq z6y?ut`z?TP`8VW@jhADjp9~j0TzVIzI%ERi;|~-co;#HODpAgv{(SK5vYm4_uu`>M zuIiiIj8MIXXeVNB;x>#>56)Y2+ULH**bETmS$7 literal 0 HcmV?d00001 diff --git a/apps/client/static/icons/openblocks.png b/apps/client/static/icons/openblocks.png new file mode 100644 index 0000000000000000000000000000000000000000..b64d1bab30aba85a707b99db4f932e6d321ee04f GIT binary patch literal 6641 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 Y7JR_`w5g2#U7!Q0D!za=$Xi7IFV|i#_5c6? literal 0 HcmV?d00001 diff --git a/apps/server/package.json b/apps/server/package.json index a4ee12018..45177559a 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -24,6 +24,7 @@ "@fastify/jwt": "6.5.0", "@fastify/static": "6.6.0", "@fastify/websocket": "7.1.1", + "@iarna/toml": "2.2.5", "@ladjs/graceful": "3.0.2", "@prisma/client": "4.6.1", "@trpc/client": "10.1.0", diff --git a/apps/server/src/jobs/deployApplication.ts b/apps/server/src/jobs/applicationBuildQueue.ts similarity index 94% rename from apps/server/src/jobs/deployApplication.ts rename to apps/server/src/jobs/applicationBuildQueue.ts index 6f79b94e8..fd7bf076a 100644 --- a/apps/server/src/jobs/deployApplication.ts +++ b/apps/server/src/jobs/applicationBuildQueue.ts @@ -14,17 +14,16 @@ import { import { createDirectories, decrypt, - defaultComposeConfiguration, getDomain, - prisma, + generateSecrets, decryptApplication, - isDev, - pushToRegistry, - executeCommand, - generateSecrets + pushToRegistry } from '../lib/common'; import * as importers from '../lib/importers'; import * as buildpacks from '../lib/buildPacks'; +import { prisma } from '../prisma'; +import { executeCommand } from '../lib/executeCommand'; +import { defaultComposeConfiguration } from '../lib/docker'; (async () => { if (parentPort) { @@ -532,6 +531,48 @@ import * as buildpacks from '../lib/buildPacks'; }); if (forceRebuild) deployNeeded = true; if ((!imageFoundLocally && !imageFoundRemotely) || deployNeeded) { + if (buildPack === 'static') { + await buildpacks.staticApp({ + dockerId: destinationDocker.id, + network: destinationDocker.network, + buildId, + applicationId, + domain, + name, + type, + volumes, + labels, + pullmergeRequestId, + buildPack, + repository, + branch, + projectId, + publishDirectory, + debug, + commit, + tag, + workdir, + port: exposePort ? `${exposePort}:${port}` : port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + secrets, + phpModules, + pythonWSGI, + pythonModule, + pythonVariable, + dockerFileLocation, + dockerComposeConfiguration, + dockerComposeFileLocation, + denoMainFile, + denoOptions, + baseImage, + baseBuildImage, + deploymentType, + forceRebuild + }); + } if (buildpacks[buildPack]) await buildpacks[buildPack]({ dockerId: destinationDocker.id, @@ -803,5 +844,8 @@ import * as buildpacks from '../lib/buildPacks'; while (true) { await th(); } - } else process.exit(0); + } else { + console.log('hello'); + process.exit(0); + } })(); diff --git a/apps/server/src/jobs/worker.ts b/apps/server/src/jobs/worker.ts deleted file mode 100644 index 981171c92..000000000 --- a/apps/server/src/jobs/worker.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { parentPort } from 'node:worker_threads'; -import process from 'node:process'; - -console.log('Hello TypeScript!'); - -// signal to parent that the job is done -if (parentPort) parentPort.postMessage('done'); -// eslint-disable-next-line unicorn/no-process-exit -else process.exit(0); diff --git a/apps/server/src/lib/buildPacks/common.ts b/apps/server/src/lib/buildPacks/common.ts new file mode 100644 index 000000000..ebbd83583 --- /dev/null +++ b/apps/server/src/lib/buildPacks/common.ts @@ -0,0 +1,843 @@ +import { + base64Encode, + decrypt, + encrypt, + generateSecrets, + generateTimestamp, + getDomain, + isARM, + isDev, + version +} from '../common'; +import { promises as fs } from 'fs'; +import { day } from '../dayjs'; +import { prisma } from '../../prisma'; +import { executeCommand } from '../executeCommand'; + +const staticApps = ['static', 'react', 'vuejs', 'svelte', 'gatsby', 'astro', 'eleventy']; +const nodeBased = [ + 'react', + 'preact', + 'vuejs', + 'svelte', + 'gatsby', + 'astro', + 'eleventy', + 'node', + 'nestjs', + 'nuxtjs', + 'nextjs' +]; + +export function setDefaultBaseImage( + buildPack: string | null, + deploymentType: string | null = null +) { + const nodeVersions = [ + { + value: 'node:lts', + label: 'node:lts' + }, + { + value: 'node:18', + label: 'node:18' + }, + { + value: 'node:17', + label: 'node:17' + }, + { + value: 'node:16', + label: 'node:16' + }, + { + value: 'node:14', + label: 'node:14' + }, + { + value: 'node:12', + label: 'node:12' + } + ]; + const staticVersions = [ + { + value: 'webdevops/nginx:alpine', + label: 'webdevops/nginx:alpine' + }, + { + value: 'webdevops/apache:alpine', + label: 'webdevops/apache:alpine' + }, + { + value: 'nginx:alpine', + label: 'nginx:alpine' + }, + { + value: 'httpd:alpine', + label: 'httpd:alpine (Apache)' + } + ]; + const rustVersions = [ + { + value: 'rust:latest', + label: 'rust:latest' + }, + { + value: 'rust:1.60', + label: 'rust:1.60' + }, + { + value: 'rust:1.60-buster', + label: 'rust:1.60-buster' + }, + { + value: 'rust:1.60-bullseye', + label: 'rust:1.60-bullseye' + }, + { + value: 'rust:1.60-slim-buster', + label: 'rust:1.60-slim-buster' + }, + { + value: 'rust:1.60-slim-bullseye', + label: 'rust:1.60-slim-bullseye' + }, + { + value: 'rust:1.60-alpine3.14', + label: 'rust:1.60-alpine3.14' + }, + { + value: 'rust:1.60-alpine3.15', + label: 'rust:1.60-alpine3.15' + } + ]; + const phpVersions = [ + { + value: 'webdevops/php-apache:8.2', + label: 'webdevops/php-apache:8.2' + }, + { + value: 'webdevops/php-nginx:8.2', + label: 'webdevops/php-nginx:8.2' + }, + { + value: 'webdevops/php-apache:8.1', + label: 'webdevops/php-apache:8.1' + }, + { + value: 'webdevops/php-nginx:8.1', + label: 'webdevops/php-nginx:8.1' + }, + { + value: 'webdevops/php-apache:8.0', + label: 'webdevops/php-apache:8.0' + }, + { + value: 'webdevops/php-nginx:8.0', + label: 'webdevops/php-nginx:8.0' + }, + { + value: 'webdevops/php-apache:7.4', + label: 'webdevops/php-apache:7.4' + }, + { + value: 'webdevops/php-nginx:7.4', + label: 'webdevops/php-nginx:7.4' + }, + { + value: 'webdevops/php-apache:7.3', + label: 'webdevops/php-apache:7.3' + }, + { + value: 'webdevops/php-nginx:7.3', + label: 'webdevops/php-nginx:7.3' + }, + { + value: 'webdevops/php-apache:7.2', + label: 'webdevops/php-apache:7.2' + }, + { + value: 'webdevops/php-nginx:7.2', + label: 'webdevops/php-nginx:7.2' + }, + { + value: 'webdevops/php-apache:7.1', + label: 'webdevops/php-apache:7.1' + }, + { + value: 'webdevops/php-nginx:7.1', + label: 'webdevops/php-nginx:7.1' + }, + { + value: 'webdevops/php-apache:7.0', + label: 'webdevops/php-apache:7.0' + }, + { + value: 'webdevops/php-nginx:7.0', + label: 'webdevops/php-nginx:7.0' + }, + { + value: 'webdevops/php-apache:5.6', + label: 'webdevops/php-apache:5.6' + }, + { + value: 'webdevops/php-nginx:5.6', + label: 'webdevops/php-nginx:5.6' + }, + { + value: 'webdevops/php-apache:8.2-alpine', + label: 'webdevops/php-apache:8.2-alpine' + }, + { + value: 'webdevops/php-nginx:8.2-alpine', + label: 'webdevops/php-nginx:8.2-alpine' + }, + { + value: 'webdevops/php-apache:8.1-alpine', + label: 'webdevops/php-apache:8.1-alpine' + }, + { + value: 'webdevops/php-nginx:8.1-alpine', + label: 'webdevops/php-nginx:8.1-alpine' + }, + { + value: 'webdevops/php-apache:8.0-alpine', + label: 'webdevops/php-apache:8.0-alpine' + }, + { + value: 'webdevops/php-nginx:8.0-alpine', + label: 'webdevops/php-nginx:8.0-alpine' + }, + { + value: 'webdevops/php-apache:7.4-alpine', + label: 'webdevops/php-apache:7.4-alpine' + }, + { + value: 'webdevops/php-nginx:7.4-alpine', + label: 'webdevops/php-nginx:7.4-alpine' + }, + { + value: 'webdevops/php-apache:7.3-alpine', + label: 'webdevops/php-apache:7.3-alpine' + }, + { + value: 'webdevops/php-nginx:7.3-alpine', + label: 'webdevops/php-nginx:7.3-alpine' + }, + { + value: 'webdevops/php-apache:7.2-alpine', + label: 'webdevops/php-apache:7.2-alpine' + }, + { + value: 'webdevops/php-nginx:7.2-alpine', + label: 'webdevops/php-nginx:7.2-alpine' + }, + { + value: 'webdevops/php-apache:7.1-alpine', + label: 'webdevops/php-apache:7.1-alpine' + }, + { + value: 'php:8.1-fpm', + label: 'php:8.1-fpm' + }, + { + value: 'php:8.0-fpm', + label: 'php:8.0-fpm' + }, + { + value: 'php:8.1-fpm-alpine', + label: 'php:8.1-fpm-alpine' + }, + { + value: 'php:8.0-fpm-alpine', + label: 'php:8.0-fpm-alpine' + } + ]; + const pythonVersions = [ + { + value: 'python:3.10-alpine', + label: 'python:3.10-alpine' + }, + { + value: 'python:3.10-buster', + label: 'python:3.10-buster' + }, + { + value: 'python:3.10-bullseye', + label: 'python:3.10-bullseye' + }, + { + value: 'python:3.10-slim-bullseye', + label: 'python:3.10-slim-bullseye' + }, + { + value: 'python:3.9-alpine', + label: 'python:3.9-alpine' + }, + { + value: 'python:3.9-buster', + label: 'python:3.9-buster' + }, + { + value: 'python:3.9-bullseye', + label: 'python:3.9-bullseye' + }, + { + value: 'python:3.9-slim-bullseye', + label: 'python:3.9-slim-bullseye' + }, + { + value: 'python:3.8-alpine', + label: 'python:3.8-alpine' + }, + { + value: 'python:3.8-buster', + label: 'python:3.8-buster' + }, + { + value: 'python:3.8-bullseye', + label: 'python:3.8-bullseye' + }, + { + value: 'python:3.8-slim-bullseye', + label: 'python:3.8-slim-bullseye' + }, + { + value: 'python:3.7-alpine', + label: 'python:3.7-alpine' + }, + { + value: 'python:3.7-buster', + label: 'python:3.7-buster' + }, + { + value: 'python:3.7-bullseye', + label: 'python:3.7-bullseye' + }, + { + value: 'python:3.7-slim-bullseye', + label: 'python:3.7-slim-bullseye' + } + ]; + const herokuVersions = [ + { + value: 'heroku/builder:22', + label: 'heroku/builder:22' + }, + { + value: 'heroku/buildpacks:20', + label: 'heroku/buildpacks:20' + }, + { + value: 'heroku/builder-classic:22', + label: 'heroku/builder-classic:22' + } + ]; + let payload: any = { + baseImage: null, + baseBuildImage: null, + baseImages: [], + baseBuildImages: [] + }; + if (nodeBased.includes(buildPack)) { + if (deploymentType === 'static') { + payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; + payload.baseImages = isARM(process.arch) + ? staticVersions.filter((version) => !version.value.includes('webdevops')) + : staticVersions; + payload.baseBuildImage = 'node:lts'; + payload.baseBuildImages = nodeVersions; + } else { + payload.baseImage = 'node:lts'; + payload.baseImages = nodeVersions; + payload.baseBuildImage = 'node:lts'; + payload.baseBuildImages = nodeVersions; + } + } + if (staticApps.includes(buildPack)) { + payload.baseImage = isARM(process.arch) ? 'nginx:alpine' : 'webdevops/nginx:alpine'; + payload.baseImages = isARM(process.arch) + ? staticVersions.filter((version) => !version.value.includes('webdevops')) + : staticVersions; + payload.baseBuildImage = 'node:lts'; + payload.baseBuildImages = nodeVersions; + } + if (buildPack === 'python') { + payload.baseImage = 'python:3.10-alpine'; + payload.baseImages = pythonVersions; + } + if (buildPack === 'rust') { + payload.baseImage = 'rust:latest'; + payload.baseBuildImage = 'rust:latest'; + payload.baseImages = rustVersions; + payload.baseBuildImages = rustVersions; + } + if (buildPack === 'deno') { + payload.baseImage = 'denoland/deno:latest'; + } + if (buildPack === 'php') { + payload.baseImage = isARM(process.arch) + ? 'php:8.1-fpm-alpine' + : 'webdevops/php-apache:8.2-alpine'; + payload.baseImages = isARM(process.arch) + ? phpVersions.filter((version) => !version.value.includes('webdevops')) + : phpVersions; + } + if (buildPack === 'laravel') { + payload.baseImage = isARM(process.arch) + ? 'php:8.1-fpm-alpine' + : 'webdevops/php-apache:8.2-alpine'; + payload.baseImages = isARM(process.arch) + ? phpVersions.filter((version) => !version.value.includes('webdevops')) + : phpVersions; + payload.baseBuildImage = 'node:18'; + payload.baseBuildImages = nodeVersions; + } + if (buildPack === 'heroku') { + payload.baseImage = 'heroku/buildpacks:20'; + payload.baseImages = herokuVersions; + } + return payload; +} + +export const setDefaultConfiguration = async (data: any) => { + let { + buildPack, + port, + installCommand, + startCommand, + buildCommand, + publishDirectory, + baseDirectory, + dockerFileLocation, + dockerComposeFileLocation, + denoMainFile + } = data; + //@ts-ignore + const template = scanningTemplates[buildPack]; + if (!port) { + port = template?.port || 3000; + + if (buildPack === 'static') port = 80; + else if (buildPack === 'node') port = 3000; + else if (buildPack === 'php') port = 80; + else if (buildPack === 'python') port = 8000; + } + if (!installCommand && buildPack !== 'static' && buildPack !== 'laravel') + installCommand = template?.installCommand || 'yarn install'; + if (!startCommand && buildPack !== 'static' && buildPack !== 'laravel') + startCommand = template?.startCommand || 'yarn start'; + if (!buildCommand && buildPack !== 'static' && buildPack !== 'laravel') + buildCommand = template?.buildCommand || null; + if (!publishDirectory) publishDirectory = template?.publishDirectory || null; + if (baseDirectory) { + if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`; + if (baseDirectory.endsWith('/') && baseDirectory !== '/') + baseDirectory = baseDirectory.slice(0, -1); + } + if (dockerFileLocation) { + if (!dockerFileLocation.startsWith('/')) dockerFileLocation = `/${dockerFileLocation}`; + if (dockerFileLocation.endsWith('/')) dockerFileLocation = dockerFileLocation.slice(0, -1); + } else { + dockerFileLocation = '/Dockerfile'; + } + if (dockerComposeFileLocation) { + if (!dockerComposeFileLocation.startsWith('/')) + dockerComposeFileLocation = `/${dockerComposeFileLocation}`; + if (dockerComposeFileLocation.endsWith('/')) + dockerComposeFileLocation = dockerComposeFileLocation.slice(0, -1); + } else { + dockerComposeFileLocation = '/Dockerfile'; + } + if (!denoMainFile) { + denoMainFile = 'main.ts'; + } + + return { + buildPack, + port, + installCommand, + startCommand, + buildCommand, + publishDirectory, + baseDirectory, + dockerFileLocation, + dockerComposeFileLocation, + denoMainFile + }; +}; + +export const scanningTemplates = { + '@sveltejs/kit': { + buildPack: 'nodejs' + }, + astro: { + buildPack: 'astro' + }, + '@11ty/eleventy': { + buildPack: 'eleventy' + }, + svelte: { + buildPack: 'svelte' + }, + '@nestjs/core': { + buildPack: 'nestjs' + }, + next: { + buildPack: 'nextjs' + }, + nuxt: { + buildPack: 'nuxtjs' + }, + 'react-scripts': { + buildPack: 'react' + }, + 'parcel-bundler': { + buildPack: 'static' + }, + '@vue/cli-service': { + buildPack: 'vuejs' + }, + vuejs: { + buildPack: 'vuejs' + }, + gatsby: { + buildPack: 'gatsby' + }, + 'preact-cli': { + buildPack: 'react' + } +}; + +export const saveBuildLog = async ({ + line, + buildId, + applicationId +}: { + line: string; + buildId: string; + applicationId: string; +}): Promise => { + if (buildId === 'undefined' || buildId === 'null' || !buildId) return; + if (applicationId === 'undefined' || applicationId === 'null' || !applicationId) return; + const { default: got } = await import('got'); + if (typeof line === 'object' && line) { + if (line.shortMessage) { + line = line.shortMessage + '\n' + line.stderr; + } else { + line = JSON.stringify(line); + } + } + if (line && typeof line === 'string' && line.includes('ghs_')) { + const regex = /ghs_.*@/g; + line = line.replace(regex, '@'); + } + const addTimestamp = `[${generateTimestamp()}] ${line}`; + const fluentBitUrl = isDev + ? process.env.COOLIFY_CONTAINER_DEV === 'true' + ? 'http://coolify-fluentbit:24224' + : 'http://localhost:24224' + : 'http://coolify-fluentbit:24224'; + + if (isDev && !process.env.COOLIFY_CONTAINER_DEV) { + console.debug(`[${applicationId}] ${addTimestamp}`); + } + try { + return await got.post(`${fluentBitUrl}/${applicationId}_buildlog_${buildId}.csv`, { + json: { + line: encrypt(line) + } + }); + } catch (error) { + return await prisma.buildLog.create({ + data: { + line: addTimestamp, + buildId, + time: Number(day().valueOf()), + applicationId + } + }); + } +}; + +export async function copyBaseConfigurationFiles( + buildPack, + workdir, + buildId, + applicationId, + baseImage +) { + try { + if (buildPack === 'php') { + await fs.writeFile(`${workdir}/entrypoint.sh`, `chown -R 1000 /app`); + await saveBuildLog({ + line: 'Copied default configuration file for PHP.', + buildId, + applicationId + }); + } else if (baseImage?.includes('nginx')) { + await fs.writeFile( + `${workdir}/nginx.conf`, + `user nginx; + worker_processes auto; + + error_log /docker.stdout; + pid /run/nginx.pid; + + events { + worker_connections 1024; + } + + http { + log_format main '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent" "$http_x_forwarded_for"'; + + access_log /docker.stdout main; + + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 2048; + + include /etc/nginx/mime.types; + default_type application/octet-stream; + + server { + listen 80; + server_name localhost; + + location / { + root /app; + index index.html; + try_files $uri $uri/index.html $uri/ /index.html =404; + } + + error_page 404 /50x.html; + + # redirect server error pages to the static page /50x.html + # + error_page 500 502 503 504 /50x.html; + location = /50x.html { + root /app; + } + + } + + } + ` + ); + } + // TODO: Add more configuration files for other buildpacks, like apache2, etc. + } catch (error) { + throw new Error(error); + } +} + +export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) { + return ( + installCommand?.includes('pnpm') || + buildCommand?.includes('pnpm') || + startCommand?.includes('pnpm') + ); +} + +export async function saveDockerRegistryCredentials({ url, username, password, workdir }) { + if (!username || !password) { + return null; + } + + let decryptedPassword = decrypt(password); + const location = `${workdir}/.docker`; + + try { + await fs.mkdir(`${workdir}/.docker`); + } catch (error) { + // console.log(error); + } + const payload = JSON.stringify({ + auths: { + [url]: { + auth: Buffer.from(`${username}:${decryptedPassword}`).toString('base64') + } + } + }); + await fs.writeFile(`${location}/config.json`, payload); + return location; +} +export async function buildImage({ + applicationId, + tag, + workdir, + buildId, + dockerId, + isCache = false, + debug = false, + dockerFileLocation = '/Dockerfile', + commit, + forceRebuild = false +}) { + if (isCache) { + await saveBuildLog({ line: `Building cache image...`, buildId, applicationId }); + } else { + await saveBuildLog({ line: `Building production image...`, buildId, applicationId }); + } + const dockerFile = isCache ? `${dockerFileLocation}-cache` : `${dockerFileLocation}`; + const cache = `${applicationId}:${tag}${isCache ? '-cache' : ''}`; + let location = null; + + const { dockerRegistry } = await prisma.application.findUnique({ + where: { id: applicationId }, + select: { dockerRegistry: true } + }); + if (dockerRegistry) { + const { url, username, password } = dockerRegistry; + location = await saveDockerRegistryCredentials({ url, username, password, workdir }); + } + + await executeCommand({ + stream: true, + debug, + buildId, + applicationId, + dockerId, + command: `docker ${location ? `--config ${location}` : ''} build ${ + forceRebuild ? '--no-cache' : '' + } --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') { + throw new Error('Canceled.'); + } +} +export function makeLabelForSimpleDockerfile({ applicationId, port, type }) { + return [ + 'coolify.managed=true', + `coolify.version=${version}`, + `coolify.applicationId=${applicationId}`, + `coolify.type=standalone-application` + ]; +} +export function makeLabelForStandaloneApplication({ + applicationId, + fqdn, + name, + type, + pullmergeRequestId = null, + buildPack, + repository, + branch, + projectId, + port, + commit, + installCommand, + buildCommand, + startCommand, + baseDirectory, + publishDirectory +}) { + if (pullmergeRequestId) { + const protocol = fqdn.startsWith('https://') ? 'https' : 'http'; + const domain = getDomain(fqdn); + fqdn = `${protocol}://${pullmergeRequestId}.${domain}`; + } + return [ + 'coolify.managed=true', + `coolify.version=${version}`, + `coolify.applicationId=${applicationId}`, + `coolify.type=standalone-application`, + `coolify.name=${name}`, + `coolify.configuration=${base64Encode( + JSON.stringify({ + applicationId, + fqdn, + name, + type, + pullmergeRequestId, + buildPack, + repository, + branch, + projectId, + port, + commit, + installCommand, + buildCommand, + startCommand, + baseDirectory, + publishDirectory + }) + )}` + ]; +} + +export async function buildCacheImageWithNode(data, imageForBuild) { + const { + workdir, + buildId, + baseDirectory, + installCommand, + buildCommand, + secrets, + pullmergeRequestId + } = data; + const isPnpm = checkPnpm(installCommand, buildCommand); + const Dockerfile: Array = []; + Dockerfile.push(`FROM ${imageForBuild}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); + } + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + if (installCommand) { + Dockerfile.push(`RUN ${installCommand}`); + } + Dockerfile.push(`RUN ${buildCommand}`); + await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); + await buildImage({ ...data, isCache: true }); +} + +export async function buildCacheImageForLaravel(data, imageForBuild) { + const { workdir, buildId, secrets, pullmergeRequestId } = data; + const Dockerfile: Array = []; + Dockerfile.push(`FROM ${imageForBuild}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + Dockerfile.push(`COPY *.json *.mix.js /app/`); + Dockerfile.push(`COPY resources /app/resources`); + Dockerfile.push(`RUN yarn install && yarn production`); + await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); + await buildImage({ ...data, isCache: true }); +} + +export async function buildCacheImageWithCargo(data, imageForBuild) { + const { applicationId, workdir, buildId } = data; + + const Dockerfile: Array = []; + Dockerfile.push(`FROM ${imageForBuild} as planner-${applicationId}`); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push('RUN cargo install cargo-chef'); + Dockerfile.push('COPY . .'); + Dockerfile.push('RUN cargo chef prepare --recipe-path recipe.json'); + Dockerfile.push(`FROM ${imageForBuild}`); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push('RUN cargo install cargo-chef'); + Dockerfile.push(`COPY --from=planner-${applicationId} /app/recipe.json recipe.json`); + Dockerfile.push('RUN cargo chef cook --release --recipe-path recipe.json'); + await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n')); + await buildImage({ ...data, isCache: true }); +} diff --git a/apps/server/src/lib/buildPacks/compose.ts b/apps/server/src/lib/buildPacks/compose.ts new file mode 100644 index 000000000..695e205c9 --- /dev/null +++ b/apps/server/src/lib/buildPacks/compose.ts @@ -0,0 +1,111 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { saveBuildLog } from './common'; +import yaml from 'js-yaml'; +import { defaultComposeConfiguration } from '../docker'; +import { executeCommand } from '../executeCommand'; + +export default async function (data) { + let { + applicationId, + debug, + buildId, + dockerId, + network, + volumes, + labels, + workdir, + baseDirectory, + secrets, + pullmergeRequestId, + dockerComposeConfiguration, + dockerComposeFileLocation + } = data; + const fileYaml = `${workdir}${baseDirectory}${dockerComposeFileLocation}`; + const dockerComposeRaw = await fs.readFile(fileYaml, 'utf8'); + const dockerComposeYaml = yaml.load(dockerComposeRaw); + if (!dockerComposeYaml.services) { + throw 'No Services found in docker-compose file.'; + } + let envs = []; + if (secrets.length > 0) { + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, null)]; + } + + const composeVolumes = []; + if (volumes.length > 0) { + for (const volume of volumes) { + let [v, path] = volume.split(':'); + composeVolumes[v] = { + name: v + }; + } + } + + let networks = {}; + for (let [key, value] of Object.entries(dockerComposeYaml.services)) { + value['container_name'] = `${applicationId}-${key}`; + let environment = typeof value['environment'] === 'undefined' ? [] : value['environment'] + value['environment'] = [...environment, ...envs]; + value['labels'] = labels; + // TODO: If we support separated volume for each service, we need to add it here + if (value['volumes']?.length > 0) { + value['volumes'] = value['volumes'].map((volume) => { + let [v, path, permission] = volume.split(':'); + if (!path) { + path = v; + v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; + } else { + v = `${applicationId}${v.replace(/\//gi, '-').replace(/\./gi, '')}`; + } + composeVolumes[v] = { + name: v + }; + return `${v}:${path}${permission ? ':' + permission : ''}`; + }); + } + if (volumes.length > 0) { + for (const volume of volumes) { + value['volumes'].push(volume); + } + } + if (dockerComposeConfiguration[key].port) { + value['expose'] = [dockerComposeConfiguration[key].port]; + } + if (value['networks']?.length > 0) { + value['networks'].forEach((network) => { + networks[network] = { + name: network + }; + }); + } + value['networks'] = [...(value['networks'] || ''), network]; + dockerComposeYaml.services[key] = { + ...dockerComposeYaml.services[key], + restart: defaultComposeConfiguration(network).restart, + deploy: defaultComposeConfiguration(network).deploy + }; + } + if (Object.keys(composeVolumes).length > 0) { + dockerComposeYaml['volumes'] = { ...composeVolumes }; + } + dockerComposeYaml['networks'] = Object.assign({ ...networks }, { [network]: { external: true } }); + + await fs.writeFile(fileYaml, yaml.dump(dockerComposeYaml)); + await executeCommand({ + debug, + buildId, + applicationId, + dockerId, + command: `docker compose --project-directory ${workdir} pull` + }); + await saveBuildLog({ line: 'Pulling images from Compose file...', buildId, applicationId }); + 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/server/src/lib/buildPacks/deno.ts b/apps/server/src/lib/buildPacks/deno.ts new file mode 100644 index 000000000..2649e3d0a --- /dev/null +++ b/apps/server/src/lib/buildPacks/deno.ts @@ -0,0 +1,52 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { + workdir, + port, + baseDirectory, + secrets, + pullmergeRequestId, + denoMainFile, + denoOptions, + buildId + } = data; + const Dockerfile: Array = []; + + let depsFound = false; + try { + await fs.readFile(`${workdir}${baseDirectory || ''}/deps.ts`); + depsFound = true; + } catch (error) {} + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (depsFound) { + Dockerfile.push(`COPY .${baseDirectory || ''}/deps.ts /app`); + Dockerfile.push(`RUN deno cache deps.ts`); + } + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + Dockerfile.push(`RUN deno cache ${denoMainFile}`); + Dockerfile.push(`ENV NO_COLOR true`); + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD deno run ${denoOptions || ''} ${denoMainFile}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/docker.ts b/apps/server/src/lib/buildPacks/docker.ts new file mode 100644 index 000000000..e02103f88 --- /dev/null +++ b/apps/server/src/lib/buildPacks/docker.ts @@ -0,0 +1,27 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildImage } from './common'; + +export default async function (data) { + let { workdir, buildId, baseDirectory, secrets, pullmergeRequestId, dockerFileLocation } = data; + const file = `${workdir}${baseDirectory}${dockerFileLocation}`; + data.workdir = `${workdir}${baseDirectory}`; + const DockerfileRaw = await fs.readFile(`${file}`, 'utf8'); + const Dockerfile: Array = DockerfileRaw.toString().trim().split('\n'); + Dockerfile.forEach((line, index) => { + if (line.startsWith('FROM')) { + Dockerfile.splice(index + 1, 0, `LABEL coolify.buildId=${buildId}`); + } + }); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.forEach((line, index) => { + if (line.startsWith('FROM')) { + Dockerfile.splice(index + 1, 0, env); + } + }); + }); + } + await fs.writeFile(`${data.workdir}${dockerFileLocation}`, Dockerfile.join('\n')); + await buildImage(data); +} diff --git a/apps/server/src/lib/buildPacks/gatsby.ts b/apps/server/src/lib/buildPacks/gatsby.ts new file mode 100644 index 000000000..fbb0a933f --- /dev/null +++ b/apps/server/src/lib/buildPacks/gatsby.ts @@ -0,0 +1,28 @@ +import { promises as fs } from 'fs'; +import { buildCacheImageWithNode, buildImage } from './common'; + +const createDockerfile = async (data, imageforBuild): Promise => { + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; + const Dockerfile: Array = []; + + Dockerfile.push(`FROM ${imageforBuild}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/heroku.ts b/apps/server/src/lib/buildPacks/heroku.ts new file mode 100644 index 000000000..a58fd8fcf --- /dev/null +++ b/apps/server/src/lib/buildPacks/heroku.ts @@ -0,0 +1,17 @@ +import { executeCommand } from "../executeCommand"; +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 production image...`, buildId, applicationId }); + await executeCommand({ + buildId, + debug, + dockerId, + command: `pack build -p ${workdir}${baseDirectory} ${applicationId}:${tag} --builder ${baseImage}` + }) + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/index.ts b/apps/server/src/lib/buildPacks/index.ts new file mode 100644 index 000000000..fa10e4bcd --- /dev/null +++ b/apps/server/src/lib/buildPacks/index.ts @@ -0,0 +1,41 @@ +import node from './node'; +import staticApp from './static'; +import docker from './docker'; +import gatsby from './gatsby'; +import svelte from './svelte'; +import react from './react'; +import nestjs from './nestjs'; +import nextjs from './nextjs'; +import nuxtjs from './nuxtjs'; +import vuejs from './vuejs'; +import php from './php'; +import rust from './rust'; +import astro from './static'; +import eleventy from './static'; +import python from './python'; +import deno from './deno'; +import laravel from './laravel'; +import heroku from './heroku'; +import compose from './compose'; + +export { + node, + staticApp, + docker, + gatsby, + svelte, + react, + nestjs, + nextjs, + nuxtjs, + vuejs, + php, + rust, + astro, + eleventy, + python, + deno, + laravel, + heroku, + compose +}; diff --git a/apps/server/src/lib/buildPacks/laravel.ts b/apps/server/src/lib/buildPacks/laravel.ts new file mode 100644 index 000000000..159e8d9ca --- /dev/null +++ b/apps/server/src/lib/buildPacks/laravel.ts @@ -0,0 +1,46 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildCacheImageForLaravel, buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { workdir, applicationId, tag, buildId, port, secrets, pullmergeRequestId } = data; + const Dockerfile: Array = []; + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`ENV WEB_DOCUMENT_ROOT /app/public`); + Dockerfile.push(`COPY --chown=application:application composer.* ./`); + Dockerfile.push(`COPY --chown=application:application database/ database/`); + Dockerfile.push( + `RUN composer install --ignore-platform-reqs --no-interaction --no-plugins --no-scripts --prefer-dist` + ); + Dockerfile.push( + `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/js/ /app/public/js/` + ); + Dockerfile.push( + `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/public/css/ /app/public/css/` + ); + Dockerfile.push( + `COPY --chown=application:application --from=${applicationId}:${tag}-cache /app/mix-manifest.json /app/public/mix-manifest.json` + ); + Dockerfile.push(`COPY --chown=application:application . ./`); + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + const { baseImage, baseBuildImage } = data; + try { + await buildCacheImageForLaravel(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/nestjs.ts b/apps/server/src/lib/buildPacks/nestjs.ts new file mode 100644 index 000000000..90c99301b --- /dev/null +++ b/apps/server/src/lib/buildPacks/nestjs.ts @@ -0,0 +1,31 @@ +import { promises as fs } from 'fs'; +import { buildCacheImageWithNode, buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { buildId, applicationId, tag, port, startCommand, workdir, baseDirectory } = data; + const Dockerfile: Array = []; + const isPnpm = startCommand.includes('pnpm'); + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); + } + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${baseDirectory || ''} ./`); + + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD ${startCommand}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/nextjs.ts b/apps/server/src/lib/buildPacks/nextjs.ts new file mode 100644 index 000000000..957dc5bce --- /dev/null +++ b/apps/server/src/lib/buildPacks/nextjs.ts @@ -0,0 +1,66 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { + applicationId, + buildId, + tag, + workdir, + publishDirectory, + port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + secrets, + pullmergeRequestId, + deploymentType, + baseImage + } = data; + const Dockerfile: Array = []; + const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); + } + if (deploymentType === 'node') { + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + Dockerfile.push(`RUN ${installCommand}`); + Dockerfile.push(`RUN ${buildCommand}`); + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD ${startCommand}`); + } else if (deploymentType === 'static') { + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + Dockerfile.push(`EXPOSE 80`); + } + + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage, deploymentType, buildCommand } = data; + if (deploymentType === 'node') { + await createDockerfile(data, baseImage); + await buildImage(data); + } else if (deploymentType === 'static') { + if (buildCommand) await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/node.ts b/apps/server/src/lib/buildPacks/node.ts new file mode 100644 index 000000000..8ccfcc68e --- /dev/null +++ b/apps/server/src/lib/buildPacks/node.ts @@ -0,0 +1,49 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildImage, checkPnpm } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { + workdir, + port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + secrets, + pullmergeRequestId, + buildId + } = data; + const Dockerfile: Array = []; + const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); + } + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + Dockerfile.push(`RUN ${installCommand}`); + if (buildCommand) { + Dockerfile.push(`RUN ${buildCommand}`); + } + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD ${startCommand}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage } = data; + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/nuxtjs.ts b/apps/server/src/lib/buildPacks/nuxtjs.ts new file mode 100644 index 000000000..957dc5bce --- /dev/null +++ b/apps/server/src/lib/buildPacks/nuxtjs.ts @@ -0,0 +1,66 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildCacheImageWithNode, buildImage, checkPnpm } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { + applicationId, + buildId, + tag, + workdir, + publishDirectory, + port, + installCommand, + buildCommand, + startCommand, + baseDirectory, + secrets, + pullmergeRequestId, + deploymentType, + baseImage + } = data; + const Dockerfile: Array = []; + const isPnpm = checkPnpm(installCommand, buildCommand, startCommand); + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm@7'); + } + if (deploymentType === 'node') { + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + Dockerfile.push(`RUN ${installCommand}`); + Dockerfile.push(`RUN ${buildCommand}`); + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD ${startCommand}`); + } else if (deploymentType === 'static') { + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + Dockerfile.push(`EXPOSE 80`); + } + + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage, deploymentType, buildCommand } = data; + if (deploymentType === 'node') { + await createDockerfile(data, baseImage); + await buildImage(data); + } else if (deploymentType === 'static') { + if (buildCommand) await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/php.ts b/apps/server/src/lib/buildPacks/php.ts new file mode 100644 index 000000000..abfd7af4f --- /dev/null +++ b/apps/server/src/lib/buildPacks/php.ts @@ -0,0 +1,50 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildImage } from './common'; + +const createDockerfile = async (data, image, htaccessFound): Promise => { + const { workdir, baseDirectory, buildId, port, secrets, pullmergeRequestId } = data; + const Dockerfile: Array = []; + let composerFound = false; + try { + await fs.readFile(`${workdir}${baseDirectory || ''}/composer.json`); + composerFound = true; + } catch (error) {} + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`COPY .${baseDirectory || ''} /app`); + if (htaccessFound) { + Dockerfile.push(`COPY .${baseDirectory || ''}/.htaccess ./`); + } + if (composerFound) { + Dockerfile.push(`RUN composer install`); + } + + Dockerfile.push(`COPY /entrypoint.sh /opt/docker/provision/entrypoint.d/30-entrypoint.sh`); + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + const { workdir, baseDirectory, baseImage } = data; + try { + let htaccessFound = false; + try { + await fs.readFile(`${workdir}${baseDirectory || ''}/.htaccess`); + htaccessFound = true; + } catch (e) { + // + } + await createDockerfile(data, baseImage, htaccessFound); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/python.ts b/apps/server/src/lib/buildPacks/python.ts new file mode 100644 index 000000000..56294660f --- /dev/null +++ b/apps/server/src/lib/buildPacks/python.ts @@ -0,0 +1,67 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { + workdir, + port, + baseDirectory, + secrets, + pullmergeRequestId, + pythonWSGI, + pythonModule, + pythonVariable, + buildId + } = data; + const Dockerfile: Array = []; + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (pythonWSGI?.toLowerCase() === 'gunicorn') { + Dockerfile.push(`RUN pip install gunicorn`); + } else if (pythonWSGI?.toLowerCase() === 'uvicorn') { + Dockerfile.push(`RUN pip install uvicorn`); + } else if (pythonWSGI?.toLowerCase() === 'uwsgi') { + Dockerfile.push(`RUN apk add --no-cache uwsgi-python3`); + // Dockerfile.push(`RUN pip install --no-cache-dir uwsgi`) + } + + try { + await fs.stat(`${workdir}${baseDirectory || ''}/requirements.txt`); + Dockerfile.push(`COPY .${baseDirectory || ''}/requirements.txt ./`); + Dockerfile.push(`RUN pip install --no-cache-dir -r .${baseDirectory || ''}/requirements.txt`); + } catch (e) { + // + } + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + Dockerfile.push(`EXPOSE ${port}`); + if (pythonWSGI?.toLowerCase() === 'gunicorn') { + Dockerfile.push(`CMD gunicorn -w=4 -b=0.0.0.0:8000 ${pythonModule}:${pythonVariable}`); + } else if (pythonWSGI?.toLowerCase() === 'uvicorn') { + Dockerfile.push(`CMD uvicorn ${pythonModule}:${pythonVariable} --port ${port} --host 0.0.0.0`); + } else if (pythonWSGI?.toLowerCase() === 'uwsgi') { + Dockerfile.push( + `CMD uwsgi --master -p 4 --http-socket 0.0.0.0:8000 --uid uwsgi --plugins python3 --protocol uwsgi --wsgi ${pythonModule}:${pythonVariable}` + ); + } else { + Dockerfile.push(`CMD python ${pythonModule}`); + } + + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/react.ts b/apps/server/src/lib/buildPacks/react.ts new file mode 100644 index 000000000..e85704d3f --- /dev/null +++ b/apps/server/src/lib/buildPacks/react.ts @@ -0,0 +1,28 @@ +import { promises as fs } from 'fs'; +import { buildCacheImageWithNode, buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; + const Dockerfile: Array = []; + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/rust.ts b/apps/server/src/lib/buildPacks/rust.ts new file mode 100644 index 000000000..f3c9ef918 --- /dev/null +++ b/apps/server/src/lib/buildPacks/rust.ts @@ -0,0 +1,40 @@ +import { promises as fs } from 'fs'; +import TOML from '@iarna/toml'; +import { buildCacheImageWithCargo, buildImage } from './common'; +import { executeCommand } from '../executeCommand'; + +const createDockerfile = async (data, image, name): Promise => { + const { workdir, port, applicationId, tag, buildId } = data; + const Dockerfile: Array = []; + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target target`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /usr/local/cargo /usr/local/cargo`); + Dockerfile.push(`COPY . .`); + Dockerfile.push(`RUN cargo build --release --bin ${name}`); + Dockerfile.push('FROM debian:buster-slim'); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push( + `RUN apt-get update -y && apt-get install -y --no-install-recommends openssl libcurl4 ca-certificates && apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/*` + ); + Dockerfile.push(`RUN update-ca-certificates`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/target/release/${name} ${name}`); + Dockerfile.push(`EXPOSE ${port}`); + Dockerfile.push(`CMD ["/app/${name}"]`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { workdir, baseImage, baseBuildImage } = data; + 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); + await createDockerfile(data, baseImage, name); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/static.ts b/apps/server/src/lib/buildPacks/static.ts new file mode 100644 index 000000000..19b13cef3 --- /dev/null +++ b/apps/server/src/lib/buildPacks/static.ts @@ -0,0 +1,54 @@ +import { promises as fs } from 'fs'; +import { generateSecrets } from '../common'; +import { buildCacheImageWithNode, buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { + applicationId, + tag, + workdir, + buildCommand, + baseDirectory, + publishDirectory, + secrets, + pullmergeRequestId, + baseImage, + buildId, + port + } = data; + const Dockerfile: Array = []; + + Dockerfile.push(`FROM ${image}`); + if (baseImage?.includes('httpd')) { + Dockerfile.push('WORKDIR /usr/local/apache2/htdocs/'); + } else { + Dockerfile.push('WORKDIR /app'); + } + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + if (secrets.length > 0) { + generateSecrets(secrets, pullmergeRequestId, true).forEach((env) => { + Dockerfile.push(env); + }); + } + if (buildCommand) { + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + } else { + Dockerfile.push(`COPY .${baseDirectory || ''} ./`); + } + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + if (data.buildCommand) await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/svelte.ts b/apps/server/src/lib/buildPacks/svelte.ts new file mode 100644 index 000000000..56fc12d7a --- /dev/null +++ b/apps/server/src/lib/buildPacks/svelte.ts @@ -0,0 +1,28 @@ +import { promises as fs } from 'fs'; +import { buildCacheImageWithNode, buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; + const Dockerfile: Array = []; + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/buildPacks/vuejs.ts b/apps/server/src/lib/buildPacks/vuejs.ts new file mode 100644 index 000000000..56fc12d7a --- /dev/null +++ b/apps/server/src/lib/buildPacks/vuejs.ts @@ -0,0 +1,28 @@ +import { promises as fs } from 'fs'; +import { buildCacheImageWithNode, buildImage } from './common'; + +const createDockerfile = async (data, image): Promise => { + const { applicationId, tag, workdir, publishDirectory, baseImage, buildId, port } = data; + const Dockerfile: Array = []; + + Dockerfile.push(`FROM ${image}`); + Dockerfile.push('WORKDIR /app'); + Dockerfile.push(`LABEL coolify.buildId=${buildId}`); + Dockerfile.push(`COPY --from=${applicationId}:${tag}-cache /app/${publishDirectory} ./`); + if (baseImage?.includes('nginx')) { + Dockerfile.push(`COPY /nginx.conf /etc/nginx/nginx.conf`); + } + Dockerfile.push(`EXPOSE ${port}`); + await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); +}; + +export default async function (data) { + try { + const { baseImage, baseBuildImage } = data; + await buildCacheImageWithNode(data, baseBuildImage); + await createDockerfile(data, baseImage); + await buildImage(data); + } catch (error) { + throw error; + } +} diff --git a/apps/server/src/lib/common.ts b/apps/server/src/lib/common.ts index 5b200a5b2..b415790aa 100644 --- a/apps/server/src/lib/common.ts +++ b/apps/server/src/lib/common.ts @@ -535,3 +535,134 @@ export async function cleanupDB(buildId: string, applicationId: string) { } await saveBuildLog({ line: 'Canceled.', buildId, applicationId }); } + +export const base64Encode = (text: string): string => { + return Buffer.from(text).toString('base64'); +}; +export const base64Decode = (text: string): string => { + return Buffer.from(text, 'base64').toString('ascii'); +}; +function parseSecret(secret, isBuild) { + if (secret.value.includes('$')) { + secret.value = secret.value.replaceAll('$', '$$$$'); + } + if (secret.value.includes('\\n')) { + if (isBuild) { + return `ARG ${secret.name}=${secret.value}`; + } else { + return `${secret.name}=${secret.value}`; + } + } else if (secret.value.includes(' ')) { + if (isBuild) { + return `ARG ${secret.name}='${secret.value}'`; + } else { + return `${secret.name}='${secret.value}'`; + } + } else { + if (isBuild) { + return `ARG ${secret.name}=${secret.value}`; + } else { + return `${secret.name}=${secret.value}`; + } + } +} +export function generateSecrets( + secrets: Array, + pullmergeRequestId: string, + isBuild = false, + port = null +): Array { + const envs = []; + const isPRMRSecret = secrets.filter((s) => s.isPRMRSecret); + const normalSecrets = secrets.filter((s) => !s.isPRMRSecret); + if (pullmergeRequestId && isPRMRSecret.length > 0) { + isPRMRSecret.forEach((secret) => { + if (isBuild && !secret.isBuildSecret) { + return; + } + const build = isBuild && secret.isBuildSecret; + envs.push(parseSecret(secret, build)); + }); + } + if (!pullmergeRequestId && normalSecrets.length > 0) { + normalSecrets.forEach((secret) => { + if (isBuild && !secret.isBuildSecret) { + return; + } + const build = isBuild && secret.isBuildSecret; + envs.push(parseSecret(secret, build)); + }); + } + const portFound = envs.filter((env) => env.startsWith('PORT')); + if (portFound.length === 0 && port && !isBuild) { + envs.push(`PORT=${port}`); + } + const nodeEnv = envs.filter((env) => env.startsWith('NODE_ENV')); + if (nodeEnv.length === 0 && !isBuild) { + envs.push(`NODE_ENV=production`); + } + return envs; +} +export function decryptApplication(application: any) { + if (application) { + if (application?.gitSource?.githubApp?.clientSecret) { + application.gitSource.githubApp.clientSecret = + decrypt(application.gitSource.githubApp.clientSecret) || null; + } + if (application?.gitSource?.githubApp?.webhookSecret) { + application.gitSource.githubApp.webhookSecret = + decrypt(application.gitSource.githubApp.webhookSecret) || null; + } + if (application?.gitSource?.githubApp?.privateKey) { + application.gitSource.githubApp.privateKey = + decrypt(application.gitSource.githubApp.privateKey) || null; + } + if (application?.gitSource?.gitlabApp?.appSecret) { + application.gitSource.gitlabApp.appSecret = + decrypt(application.gitSource.gitlabApp.appSecret) || null; + } + if (application?.secrets.length > 0) { + application.secrets = application.secrets.map((s: any) => { + s.value = decrypt(s.value) || null; + return s; + }); + } + + return application; + } +} +export async function pushToRegistry( + application: any, + workdir: string, + tag: string, + imageName: string, + customTag: string +) { + const location = `${workdir}/.docker`; + const tagCommand = `docker tag ${application.id}:${tag} ${imageName}:${customTag}`; + const pushCommand = `docker --config ${location} push ${imageName}:${customTag}`; + await executeCommand({ + dockerId: application.destinationDockerId, + command: tagCommand + }); + await executeCommand({ + dockerId: application.destinationDockerId, + command: pushCommand + }); +} + +export async function getContainerUsage(dockerId: string, container: string): Promise { + try { + const { stdout } = await executeCommand({ + dockerId, + command: `docker container stats ${container} --no-stream --no-trunc --format "{{json .}}"` + }); + return JSON.parse(stdout); + } catch (err) { + return { + MemUsage: 0, + CPUPerc: 0, + NetIO: 0 + }; + } +} \ No newline at end of file diff --git a/apps/server/src/lib/importers/github.ts b/apps/server/src/lib/importers/github.ts new file mode 100644 index 000000000..dfaa4c724 --- /dev/null +++ b/apps/server/src/lib/importers/github.ts @@ -0,0 +1,96 @@ + +import jsonwebtoken from 'jsonwebtoken'; +import { prisma } from '../../prisma'; +import { saveBuildLog } from '../buildPacks/common'; +import { decrypt } from '../common'; +import { executeCommand } from '../executeCommand'; + +export default async function ({ + applicationId, + workdir, + githubAppId, + repository, + apiUrl, + gitCommitHash, + htmlUrl, + branch, + buildId, + customPort, + forPublic +}: { + applicationId: string; + workdir: string; + githubAppId: string; + repository: string; + apiUrl: string; + gitCommitHash?: string; + htmlUrl: string; + branch: string; + buildId: string; + customPort: number; + forPublic?: boolean; +}): Promise { + const { default: got } = await import('got') + const url = htmlUrl.replace('https://', '').replace('http://', ''); + if (forPublic) { + await saveBuildLog({ + line: `Cloning ${repository}:${branch}...`, + buildId, + applicationId + }); + if (gitCommitHash) { + await saveBuildLog({ + line: `Checking out ${gitCommitHash} commit...`, + buildId, + applicationId + }); + } + 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 } }); + if (body.privateKey) body.privateKey = decrypt(body.privateKey); + const { privateKey, appId, installationId } = body + const githubPrivateKey = privateKey.replace(/\\n/g, '\n').replace(/"/g, ''); + + const payload = { + iat: Math.round(new Date().getTime() / 1000), + exp: Math.round(new Date().getTime() / 1000 + 60), + iss: appId + }; + const jwtToken = jsonwebtoken.sign(payload, githubPrivateKey, { + algorithm: 'RS256' + }); + const { token } = await got + .post(`${apiUrl}/app/installations/${installationId}/access_tokens`, { + headers: { + Authorization: `Bearer ${jwtToken}`, + Accept: 'application/vnd.github.machine-man-preview+json' + } + }) + .json(); + await saveBuildLog({ + line: `Cloning ${repository}:${branch}...`, + buildId, + applicationId + }); + if (gitCommitHash) { + await saveBuildLog({ + line: `Checking out ${gitCommitHash} commit...`, + buildId, + applicationId + }); + } + 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 executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true }); + return commit.replace('\n', ''); +} diff --git a/apps/server/src/lib/importers/gitlab.ts b/apps/server/src/lib/importers/gitlab.ts new file mode 100644 index 000000000..4f49fbd0e --- /dev/null +++ b/apps/server/src/lib/importers/gitlab.ts @@ -0,0 +1,65 @@ +import { saveBuildLog } from "../buildPacks/common"; +import { executeCommand } from "../executeCommand"; + +export default async function ({ + applicationId, + workdir, + repodir, + htmlUrl, + gitCommitHash, + repository, + branch, + buildId, + privateSshKey, + customPort, + forPublic, + customUser, +}: { + applicationId: string; + workdir: string; + repository: string; + htmlUrl: string; + branch: string; + buildId: string; + repodir: string; + gitCommitHash: string; + privateSshKey: string; + customPort: number; + forPublic: boolean; + customUser: string; +}): Promise { + const url = htmlUrl.replace('https://', '').replace('http://', '').replace(/\/$/, ''); + if (!forPublic) { + await executeCommand({ command: `echo '${privateSshKey}' > ${repodir}/id.rsa`, shell: true }); + await executeCommand({ command: `chmod 600 ${repodir}/id.rsa` }); + } + + await saveBuildLog({ + line: `Cloning ${repository}:${branch}...`, + buildId, + applicationId + }); + if (gitCommitHash) { + await saveBuildLog({ + line: `Checking out ${gitCommitHash} commit...`, + buildId, + applicationId + }); + } + if (forPublic) { + 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 executeCommand({ + command: + `git clone -q -b ${branch} ${customUser}@${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 executeCommand({ command: `cd ${workdir}/ && git rev-parse HEAD`, shell: true }); + return commit.replace('\n', ''); +} diff --git a/apps/server/src/lib/importers/index.ts b/apps/server/src/lib/importers/index.ts new file mode 100644 index 000000000..193443890 --- /dev/null +++ b/apps/server/src/lib/importers/index.ts @@ -0,0 +1,4 @@ +import github from './github'; +import gitlab from './gitlab'; + +export { github, gitlab }; diff --git a/apps/server/src/scheduler.ts b/apps/server/src/scheduler.ts index 38067e196..3ec5c7990 100644 --- a/apps/server/src/scheduler.ts +++ b/apps/server/src/scheduler.ts @@ -1,6 +1,6 @@ import Bree from 'bree'; import path from 'path'; -// import Cabin from 'cabin'; +import Cabin from 'cabin'; import TSBree from '@breejs/ts-worker'; export const isDev = process.env['NODE_ENV'] === 'development'; @@ -9,16 +9,8 @@ Bree.extend(TSBree); const options: any = { defaultExtension: 'js', - logger: false, - // logger: false, - // workerMessageHandler: async ({ name, message }) => { - // if (name === 'deployApplication' && message?.deploying) { - // if (scheduler.workers.has('autoUpdater') || scheduler.workers.has('cleanupStorage')) { - // scheduler.workers.get('deployApplication').postMessage('cancel') - // } - // } - // }, - jobs: [{ name: 'deployApplication' }, { name: 'worker' }] + logger: new Cabin({}), + jobs: [{ name: 'applicationBuildQueue' }] }; if (isDev) options.root = path.join(__dirname, './jobs'); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index e9dd64876..6b2d9d87d 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -64,7 +64,11 @@ export function createServer(opts: ServerOptions) { console.log('Coolify server is listening on port', port, 'at 0.0.0.0 🚀'); const graceful = new Graceful({ brees: [scheduler] }); graceful.listen(); - scheduler.run('worker'); + setInterval(async () => { + if (!scheduler.workers.has('applicationBuildQueue')) { + scheduler.run('applicationBuildQueue'); + } + }, 2000); } catch (err) { server.log.error(err); process.exit(1); diff --git a/apps/server/src/trpc/routers/applications/index.ts b/apps/server/src/trpc/routers/applications/index.ts index 0b51690f5..bfa71d848 100644 --- a/apps/server/src/trpc/routers/applications/index.ts +++ b/apps/server/src/trpc/routers/applications/index.ts @@ -24,6 +24,8 @@ import { createDirectories, decrypt, encrypt, + generateSecrets, + getContainerUsage, getDomain, isDev, isDomainConfigured, @@ -32,8 +34,345 @@ import { } from '../../../lib/common'; import { day } from '../../../lib/dayjs'; import csv from 'csvtojson'; +import { scheduler } from '../../../scheduler'; export const applicationsRouter = router({ + deleteApplication: privateProcedure + .input( + z.object({ + id: z.string(), + force: z.boolean().default(false) + }) + ) + .mutation(async ({ input, ctx }) => { + const { id, force } = input; + const teamId = ctx.user.teamId; + const application = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + if (!force && application?.destinationDockerId && application.destinationDocker?.network) { + const { stdout: containers } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter network=${application.destinationDocker.network} --filter name=${id} --format '{{json .}}'` + }); + if (containers) { + const containersArray = containers.trim().split('\n'); + for (const container of containersArray) { + const containerObj = JSON.parse(container); + const id = containerObj.ID; + await removeContainer({ id, dockerId: application.destinationDocker.id }); + } + } + } + await prisma.applicationSettings.deleteMany({ where: { application: { id } } }); + await prisma.buildLog.deleteMany({ where: { applicationId: id } }); + await prisma.build.deleteMany({ where: { applicationId: id } }); + await prisma.secret.deleteMany({ where: { applicationId: id } }); + await prisma.applicationPersistentStorage.deleteMany({ where: { applicationId: id } }); + await prisma.applicationConnectedDatabase.deleteMany({ where: { applicationId: id } }); + if (teamId === '0') { + await prisma.application.deleteMany({ where: { id } }); + } else { + await prisma.application.deleteMany({ where: { id, teams: { some: { id: teamId } } } }); + } + return {} + }), + restartPreview: privateProcedure + .input( + z.object({ + id: z.string(), + pullmergeRequestId: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id, pullmergeRequestId } = input; + const teamId = ctx.user.teamId; + let application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const buildId = cuid(); + const { id: dockerId, network } = application.destinationDocker; + const { + secrets, + port, + repository, + persistentStorage, + id: applicationId, + buildPack, + exposePort + } = application; + + let envs = []; + if (secrets.length > 0) { + envs = [...envs, ...generateSecrets(secrets, pullmergeRequestId, false, port)]; + } + const { workdir } = await createDirectories({ repository, buildId }); + const labels = []; + let image = null; + const { stdout: container } = await executeCommand({ + dockerId, + command: `docker container ls --filter 'label=com.docker.compose.service=${id}-${pullmergeRequestId}' --format '{{json .}}'` + }); + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image; + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`); + } + }); + } + let imageFound = false; + try { + await executeCommand({ + dockerId, + command: `docker image inspect ${image}` + }); + imageFound = true; + } catch (error) { + // + } + if (!imageFound) { + throw { status: 500, message: 'Image not found, cannot restart application.' }; + } + + const volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ + buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] + } + }; + }); + const composeFile = { + version: '3.8', + services: { + [`${applicationId}-${pullmergeRequestId}`]: { + image, + container_name: `${applicationId}-${pullmergeRequestId}`, + volumes, + environment: envs, + labels, + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network) + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) + }; + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}-${pullmergeRequestId}` }); + await executeCommand({ dockerId, command: `docker rm ${id}-${pullmergeRequestId}` }); + await executeCommand({ + dockerId, + command: `docker compose --project-directory ${workdir} up -d` + }); + } + }), + getPreviewStatus: privateProcedure + .input( + z.object({ + id: z.string(), + pullmergeRequestId: z.string() + }) + ) + .query(async ({ input, ctx }) => { + const { id, pullmergeRequestId } = input; + const teamId = ctx.user.teamId; + let isRunning = false; + let isExited = false; + let isRestarting = false; + let isBuilding = false; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const status = await checkContainer({ + dockerId: application.destinationDocker.id, + container: `${id}-${pullmergeRequestId}` + }); + if (status?.found) { + isRunning = status.status.isRunning; + isExited = status.status.isExited; + isRestarting = status.status.isRestarting; + } + const building = await prisma.build.findMany({ + where: { applicationId: id, pullmergeRequestId, status: { in: ['queued', 'running'] } } + }); + isBuilding = building.length > 0; + } + return { + success: true, + data: { + isBuilding, + isRunning, + isRestarting, + isExited + } + }; + }), + loadPreviews: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id } = input; + const application = await prisma.application.findUnique({ + where: { id }, + include: { destinationDocker: true } + }); + const { stdout } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker container ls --filter 'name=${id}-' --format "{{json .}}"` + }); + if (stdout === '') { + throw { status: 500, message: 'No previews found.' }; + } + const containers = formatLabelsOnDocker(stdout).filter( + (container) => + container.Labels['coolify.configuration'] && + container.Labels['coolify.type'] === 'standalone-application' + ); + + const jsonContainers = containers + .map((container) => + JSON.parse(Buffer.from(container.Labels['coolify.configuration'], 'base64').toString()) + ) + .filter((container) => { + return container.pullmergeRequestId && container.applicationId === id; + }); + for (const container of jsonContainers) { + const found = await prisma.previewApplication.findMany({ + where: { + applicationId: container.applicationId, + pullmergeRequestId: container.pullmergeRequestId + } + }); + if (found.length === 0) { + await prisma.previewApplication.create({ + data: { + pullmergeRequestId: container.pullmergeRequestId, + sourceBranch: container.branch, + customDomain: container.fqdn, + application: { connect: { id: container.applicationId } } + } + }); + } + } + return { + success: true, + data: { + previews: await prisma.previewApplication.findMany({ where: { applicationId: id } }) + } + }; + }), + stopPreview: privateProcedure + .input( + z.object({ + id: z.string(), + pullmergeRequestId: z.string() + }) + ) + .mutation(async ({ input, ctx }) => { + const { id, pullmergeRequestId } = input; + const teamId = ctx.user.teamId; + const application: any = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const container = `${id}-${pullmergeRequestId}`; + const { id: dockerId } = application.destinationDocker; + const { found } = await checkContainer({ dockerId, container }); + if (found) { + await removeContainer({ id: container, dockerId: application.destinationDocker.id }); + } + await prisma.previewApplication.deleteMany({ + where: { applicationId: application.id, pullmergeRequestId } + }); + } + return {}; + }), + getUsage: privateProcedure + .input( + z.object({ + id: z.string(), + containerId: z.string() + }) + ) + .query(async ({ input, ctx }) => { + const { id, containerId } = input; + const teamId = ctx.user.teamId; + let usage = {}; + + const application: any = await getApplicationFromDB(id, teamId); + if (application.destinationDockerId) { + [usage] = await Promise.all([ + getContainerUsage(application.destinationDocker.id, containerId) + ]); + } + return { + success: true, + data: { + usage + } + }; + }), + getLocalImages: privateProcedure + .input( + z.object({ + id: z.string() + }) + ) + .query(async ({ input, ctx }) => { + const { id } = input; + const teamId = ctx.user.teamId; + const application: any = await getApplicationFromDB(id, teamId); + let imagesAvailables = []; + const { stdout } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker images --format '{{.Repository}}#{{.Tag}}#{{.CreatedAt}}'` + }); + const { stdout: runningImage } = await executeCommand({ + dockerId: application.destinationDocker.id, + command: `docker ps -a --filter 'label=com.docker.compose.service=${id}' --format {{.Image}}` + }); + const images = stdout + .trim() + .split('\n') + .filter((image) => image.includes(id) && !image.includes('-cache')); + for (const image of images) { + const [repository, tag, createdAt] = image.split('#'); + if (tag.includes('-')) { + continue; + } + const [year, time] = createdAt.split(' '); + imagesAvailables.push({ + repository, + tag, + createdAt: day(year + time).unix() + }); + } + + imagesAvailables = imagesAvailables.sort((a, b) => b.tag - a.tag); + + return { + success: true, + data: { + imagesAvailables, + runningImage + } + }; + }), resetQueue: privateProcedure.mutation(async ({ ctx }) => { const teamId = ctx.user.teamId; if (teamId === '0') { @@ -41,7 +380,7 @@ export const applicationsRouter = router({ where: { status: { in: ['queued', 'running'] } }, data: { status: 'canceled' } }); - // scheduler.workers.get("deployApplication").postMessage("cancel"); + // scheduler.workers.get("").postMessage("cancel"); } }), cancelBuild: privateProcedure @@ -813,170 +1152,183 @@ export const applicationsRouter = router({ } return {}; }), - restart: privateProcedure.input(z.object({ id: z.string() })).mutation(async ({ ctx, input }) => { - const { id } = input; - const teamId = ctx.user?.teamId; - let application = await getApplicationFromDB(id, teamId); - if (application?.destinationDockerId) { - const buildId = cuid(); - const { id: dockerId, network } = application.destinationDocker; - const { - dockerRegistry, - secrets, - pullmergeRequestId, - port, - repository, - persistentStorage, - id: applicationId, - buildPack, - exposePort - } = application; - let location = null; - const labels = []; - let image = null; - const envs = [`PORT=${port}`, 'NODE_ENV=production']; + restart: privateProcedure + .input(z.object({ id: z.string(), imageId: z.string().nullable() })) + .mutation(async ({ ctx, input }) => { + const { id, imageId } = input; + const teamId = ctx.user?.teamId; + let application = await getApplicationFromDB(id, teamId); + if (application?.destinationDockerId) { + const buildId = cuid(); + const { id: dockerId, network } = application.destinationDocker; + const { + dockerRegistry, + secrets, + pullmergeRequestId, + port, + repository, + persistentStorage, + id: applicationId, + buildPack, + exposePort + } = application; + let location = null; + const labels = []; + let image = null; + const envs = [`PORT=${port}`, 'NODE_ENV=production']; - if (secrets.length > 0) { - secrets.forEach((secret) => { - if (pullmergeRequestId) { - const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); - if (isSecretFound.length > 0) { - if (isSecretFound[0].value.includes('\\n') || isSecretFound[0].value.includes("'")) { - envs.push(`${secret.name}=${isSecretFound[0].value}`); + if (secrets.length > 0) { + secrets.forEach((secret) => { + if (pullmergeRequestId) { + const isSecretFound = secrets.filter((s) => s.name === secret.name && s.isPRMRSecret); + if (isSecretFound.length > 0) { + if ( + isSecretFound[0].value.includes('\\n') || + isSecretFound[0].value.includes("'") + ) { + envs.push(`${secret.name}=${isSecretFound[0].value}`); + } else { + envs.push(`${secret.name}='${isSecretFound[0].value}'`); + } } else { - envs.push(`${secret.name}='${isSecretFound[0].value}'`); + if (secret.value.includes('\\n') || secret.value.includes("'")) { + envs.push(`${secret.name}=${secret.value}`); + } else { + envs.push(`${secret.name}='${secret.value}'`); + } } } else { - if (secret.value.includes('\\n') || secret.value.includes("'")) { - envs.push(`${secret.name}=${secret.value}`); - } else { - envs.push(`${secret.name}='${secret.value}'`); + if (!secret.isPRMRSecret) { + if (secret.value.includes('\\n') || secret.value.includes("'")) { + envs.push(`${secret.name}=${secret.value}`); + } else { + envs.push(`${secret.name}='${secret.value}'`); + } } } - } else { - if (!secret.isPRMRSecret) { - if (secret.value.includes('\\n') || secret.value.includes("'")) { - envs.push(`${secret.name}=${secret.value}`); - } else { - envs.push(`${secret.name}='${secret.value}'`); + }); + } + const { workdir } = await createDirectories({ repository, buildId }); + + if (imageId) { + image = imageId; + } else { + const { stdout: container } = await executeCommand({ + dockerId, + command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` + }); + const containersArray = container.trim().split('\n'); + for (const container of containersArray) { + const containerObj = formatLabelsOnDocker(container); + image = containerObj[0].Image; + Object.keys(containerObj[0].Labels).forEach(function (key) { + if (key.startsWith('coolify')) { + labels.push(`${key}=${containerObj[0].Labels[key]}`); } + }); + } + } + + if (dockerRegistry) { + const { url, username, password } = dockerRegistry; + location = await saveDockerRegistryCredentials({ url, username, password, workdir }); + } + + let imageFoundLocally = false; + try { + await executeCommand({ + dockerId, + command: `docker image inspect ${image}` + }); + imageFoundLocally = true; + } catch (error) { + // + } + let imageFoundRemotely = false; + try { + await executeCommand({ + dockerId, + command: `docker ${location ? `--config ${location}` : ''} pull ${image}` + }); + imageFoundRemotely = true; + } catch (error) { + // + } + + if (!imageFoundLocally && !imageFoundRemotely) { + throw { status: 500, message: 'Image not found, cannot restart application.' }; + } + await fs.writeFile(`${workdir}/.env`, envs.join('\n')); + + let envFound = false; + try { + envFound = !!(await fs.stat(`${workdir}/.env`)); + } catch (error) { + // + } + const volumes = + persistentStorage?.map((storage) => { + return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ + buildPack !== 'docker' ? '/app' : '' + }${storage.path}`; + }) || []; + const composeVolumes = volumes.map((volume) => { + return { + [`${volume.split(':')[0]}`]: { + name: volume.split(':')[0] } - } + }; }); - } - const { workdir } = await createDirectories({ repository, buildId }); - - const { stdout: container } = await executeCommand({ - dockerId, - command: `docker container ls --filter 'label=com.docker.compose.service=${id}' --format '{{json .}}'` - }); - const containersArray = container.trim().split('\n'); - for (const container of containersArray) { - const containerObj = formatLabelsOnDocker(container); - image = containerObj[0].Image; - Object.keys(containerObj[0].Labels).forEach(function (key) { - if (key.startsWith('coolify')) { - labels.push(`${key}=${containerObj[0].Labels[key]}`); - } - }); - } - if (dockerRegistry) { - const { url, username, password } = dockerRegistry; - location = await saveDockerRegistryCredentials({ url, username, password, workdir }); - } - - let imageFoundLocally = false; - try { - await executeCommand({ - dockerId, - command: `docker image inspect ${image}` - }); - imageFoundLocally = true; - } catch (error) { - // - } - let imageFoundRemotely = false; - try { - await executeCommand({ - dockerId, - command: `docker ${location ? `--config ${location}` : ''} pull ${image}` - }); - imageFoundRemotely = true; - } catch (error) { - // - } - - if (!imageFoundLocally && !imageFoundRemotely) { - throw { status: 500, message: 'Image not found, cannot restart application.' }; - } - await fs.writeFile(`${workdir}/.env`, envs.join('\n')); - - let envFound = false; - try { - envFound = !!(await fs.stat(`${workdir}/.env`)); - } catch (error) { - // - } - const volumes = - persistentStorage?.map((storage) => { - return `${applicationId}${storage.path.replace(/\//gi, '-')}:${ - buildPack !== 'docker' ? '/app' : '' - }${storage.path}`; - }) || []; - const composeVolumes = volumes.map((volume) => { - return { - [`${volume.split(':')[0]}`]: { - name: volume.split(':')[0] - } + const composeFile = { + version: '3.8', + services: { + [applicationId]: { + image, + container_name: applicationId, + volumes, + env_file: envFound ? [`${workdir}/.env`] : [], + labels, + depends_on: [], + expose: [port], + ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), + ...defaultComposeConfiguration(network) + } + }, + networks: { + [network]: { + external: true + } + }, + volumes: Object.assign({}, ...composeVolumes) }; - }); - const composeFile = { - version: '3.8', - services: { - [applicationId]: { - image, - container_name: applicationId, - volumes, - env_file: envFound ? [`${workdir}/.env`] : [], - labels, - depends_on: [], - expose: [port], - ...(exposePort ? { ports: [`${exposePort}:${port}`] } : {}), - ...defaultComposeConfiguration(network) - } - }, - networks: { - [network]: { - external: true - } - }, - volumes: Object.assign({}, ...composeVolumes) - }; - await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); - try { - await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }); - await executeCommand({ dockerId, command: `docker rm ${id}` }); - } catch (error) { - // - } + await fs.writeFile(`${workdir}/docker-compose.yml`, yaml.dump(composeFile)); + try { + await executeCommand({ dockerId, command: `docker stop -t 0 ${id}` }); + await executeCommand({ dockerId, command: `docker rm ${id}` }); + } catch (error) { + // + } - await executeCommand({ - dockerId, - command: `docker compose --project-directory ${workdir} up -d` - }); - } - return {}; - }), + await executeCommand({ + dockerId, + command: `docker compose --project-directory ${workdir} up -d` + }); + } + return {}; + }), deploy: privateProcedure .input( z.object({ - id: z.string() + id: z.string(), + forceRebuild: z.boolean().default(false), + pullmergeRequestId: z.string().nullable(), + branch: z.string().nullable() }) ) .mutation(async ({ ctx, input }) => { - const { id } = input; + const { id, pullmergeRequestId, branch, forceRebuild } = input; const teamId = ctx.user?.teamId; - const buildId = await deployApplication(id, teamId); + const buildId = await deployApplication(id, teamId, forceRebuild, pullmergeRequestId, branch); return { buildId }; diff --git a/apps/server/src/trpc/routers/applications/lib.ts b/apps/server/src/trpc/routers/applications/lib.ts index dfedc1bc1..ee4b9a809 100644 --- a/apps/server/src/trpc/routers/applications/lib.ts +++ b/apps/server/src/trpc/routers/applications/lib.ts @@ -7,7 +7,9 @@ import { prisma } from '../../../prisma'; export async function deployApplication( id: string, teamId: string, - forceRebuild: boolean = false + forceRebuild: boolean, + pullmergeRequestId: string | null = null, + branch: string | null = null ): Promise { const buildId = cuid(); const application = await getApplicationFromDB(id, teamId); @@ -29,14 +31,20 @@ export async function deployApplication( data: { id: buildId, applicationId: id, + sourceBranch: branch, branch: application.branch, + pullmergeRequestId: pullmergeRequestId?.toString(), forceRebuild, destinationDockerId: application.destinationDocker?.id, gitSourceId: application.gitSource?.id, githubAppId: application.gitSource?.githubApp?.id, gitlabAppId: application.gitSource?.gitlabApp?.id, status: 'queued', - type: 'manual' + type: pullmergeRequestId + ? application.gitSource?.githubApp?.id + ? 'manual_pr' + : 'manual_mr' + : 'manual' } }); } else { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index af24eef4f..e19fab50f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -245,6 +245,7 @@ importers: '@fastify/jwt': 6.5.0 '@fastify/static': 6.6.0 '@fastify/websocket': 7.1.1 + '@iarna/toml': 2.2.5 '@ladjs/graceful': 3.0.2 '@prisma/client': 4.6.1 '@trpc/client': 10.1.0 @@ -301,6 +302,7 @@ importers: '@fastify/jwt': 6.5.0 '@fastify/static': 6.6.0 '@fastify/websocket': 7.1.1 + '@iarna/toml': 2.2.5 '@ladjs/graceful': 3.0.2 '@prisma/client': 4.6.1_prisma@4.6.1 '@trpc/client': 10.1.0_@trpc+server@10.1.0 @@ -1453,11 +1455,10 @@ packages: engines: {node: '>= 12.11'} peerDependencies: bree: '>=9.0.0' - tsconfig-paths: '>= 4' dependencies: bree: 9.1.2 ts-node: 10.8.2_wup25etrarvlqkprac7h35hj7u - tsconfig-paths: 4.1.0 + tsconfig-paths: 4.1.2 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' @@ -6305,12 +6306,6 @@ packages: hasBin: true dev: true - /json5/2.2.1: - resolution: {integrity: sha512-1hqLFMSrGHRHxav9q9gNjJ5EXznIxGVO09xQRrwplcS8qs28pZ8s8hupZAmqDwZUmVZ2Qb2jnyPOWcDH8m8dlA==} - engines: {node: '>=6'} - hasBin: true - dev: false - /json5/2.2.3: resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} engines: {node: '>=6'} @@ -8771,15 +8766,6 @@ packages: yn: 3.1.1 dev: false - /tsconfig-paths/4.1.0: - resolution: {integrity: sha512-AHx4Euop/dXFC+Vx589alFba8QItjF+8hf8LtmuiCwHyI4rHXQtOOENaM8kvYf5fR0dRChy3wzWIZ9WbB7FWow==} - engines: {node: '>=6'} - dependencies: - json5: 2.2.1 - minimist: 1.2.7 - strip-bom: 3.0.0 - dev: false - /tsconfig-paths/4.1.2: resolution: {integrity: sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==} engines: {node: '>=6'}