diff --git a/package.json b/package.json index bb42f2869..04dbc7e7d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "coolify", "description": "An open-source & self-hostable Heroku / Netlify alternative.", - "version": "2.0.28", + "version": "2.0.29", "license": "AGPL-3.0", "scripts": { "dev": "docker-compose -f docker-compose-dev.yaml up -d && NODE_ENV=development svelte-kit dev --host 0.0.0.0", diff --git a/prisma/migrations/20220311213422_autodeploy/migration.sql b/prisma/migrations/20220311213422_autodeploy/migration.sql new file mode 100644 index 000000000..d534d9372 --- /dev/null +++ b/prisma/migrations/20220311213422_autodeploy/migration.sql @@ -0,0 +1,19 @@ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_ApplicationSettings" ( + "id" TEXT NOT NULL PRIMARY KEY, + "applicationId" TEXT NOT NULL, + "dualCerts" BOOLEAN NOT NULL DEFAULT false, + "debug" BOOLEAN NOT NULL DEFAULT false, + "previews" BOOLEAN NOT NULL DEFAULT false, + "autodeploy" BOOLEAN NOT NULL DEFAULT true, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "ApplicationSettings_applicationId_fkey" FOREIGN KEY ("applicationId") REFERENCES "Application" ("id") ON DELETE RESTRICT ON UPDATE CASCADE +); +INSERT INTO "new_ApplicationSettings" ("applicationId", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt") SELECT "applicationId", "createdAt", "debug", "dualCerts", "id", "previews", "updatedAt" FROM "ApplicationSettings"; +DROP TABLE "ApplicationSettings"; +ALTER TABLE "new_ApplicationSettings" RENAME TO "ApplicationSettings"; +CREATE UNIQUE INDEX "ApplicationSettings_applicationId_key" ON "ApplicationSettings"("applicationId"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 8c4fdab53..85b78fdc4 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -104,6 +104,7 @@ model ApplicationSettings { dualCerts Boolean @default(false) debug Boolean @default(false) previews Boolean @default(false) + autodeploy Boolean @default(true) createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/lib/buildPacks/nestjs.ts b/src/lib/buildPacks/nestjs.ts index 38ef3bce1..b30c5ecd9 100644 --- a/src/lib/buildPacks/nestjs.ts +++ b/src/lib/buildPacks/nestjs.ts @@ -4,13 +4,19 @@ import { promises as fs } from 'fs'; const createDockerfile = async (data, image): Promise => { const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data; const Dockerfile: Array = []; + const isPnpm = startCommand.includes('pnpm'); Dockerfile.push(`FROM ${image}`); Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push(`LABEL coolify.image=true`); + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); + Dockerfile.push('RUN pnpm add -g pnpm'); + } Dockerfile.push( `COPY --from=${applicationId}:${tag}-cache /usr/src/app/${baseDirectory || ''} ./` ); + Dockerfile.push(`EXPOSE ${port}`); Dockerfile.push(`CMD ${startCommand}`); await fs.writeFile(`${workdir}/Dockerfile`, Dockerfile.join('\n')); diff --git a/src/lib/buildPacks/nextjs.ts b/src/lib/buildPacks/nextjs.ts index 15e6dacca..c8d16dab6 100644 --- a/src/lib/buildPacks/nextjs.ts +++ b/src/lib/buildPacks/nextjs.ts @@ -13,7 +13,10 @@ const createDockerfile = async (data, image): Promise => { pullmergeRequestId } = data; const Dockerfile: Array = []; - + const isPnpm = + installCommand.includes('pnpm') || + buildCommand.includes('pnpm') || + startCommand.includes('pnpm'); Dockerfile.push(`FROM ${image}`); Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push(`LABEL coolify.image=true`); @@ -32,6 +35,10 @@ const createDockerfile = async (data, image): Promise => { } }); } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); + Dockerfile.push('RUN pnpm add -g pnpm'); + } Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); try { await fs.stat(`${workdir}/yarn.lock`); diff --git a/src/lib/buildPacks/node.ts b/src/lib/buildPacks/node.ts index 15e6dacca..c8d16dab6 100644 --- a/src/lib/buildPacks/node.ts +++ b/src/lib/buildPacks/node.ts @@ -13,7 +13,10 @@ const createDockerfile = async (data, image): Promise => { pullmergeRequestId } = data; const Dockerfile: Array = []; - + const isPnpm = + installCommand.includes('pnpm') || + buildCommand.includes('pnpm') || + startCommand.includes('pnpm'); Dockerfile.push(`FROM ${image}`); Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push(`LABEL coolify.image=true`); @@ -32,6 +35,10 @@ const createDockerfile = async (data, image): Promise => { } }); } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); + Dockerfile.push('RUN pnpm add -g pnpm'); + } Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); try { await fs.stat(`${workdir}/yarn.lock`); diff --git a/src/lib/buildPacks/nuxtjs.ts b/src/lib/buildPacks/nuxtjs.ts index 15e6dacca..c8d16dab6 100644 --- a/src/lib/buildPacks/nuxtjs.ts +++ b/src/lib/buildPacks/nuxtjs.ts @@ -13,7 +13,10 @@ const createDockerfile = async (data, image): Promise => { pullmergeRequestId } = data; const Dockerfile: Array = []; - + const isPnpm = + installCommand.includes('pnpm') || + buildCommand.includes('pnpm') || + startCommand.includes('pnpm'); Dockerfile.push(`FROM ${image}`); Dockerfile.push('WORKDIR /usr/src/app'); Dockerfile.push(`LABEL coolify.image=true`); @@ -32,6 +35,10 @@ const createDockerfile = async (data, image): Promise => { } }); } + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); + Dockerfile.push('RUN pnpm add -g pnpm'); + } Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); try { await fs.stat(`${workdir}/yarn.lock`); diff --git a/src/lib/common.ts b/src/lib/common.ts index c36196113..db6e55e51 100644 --- a/src/lib/common.ts +++ b/src/lib/common.ts @@ -11,6 +11,7 @@ import { version as currentVersion } from '../../package.json'; import dayjs from 'dayjs'; import Cookie from 'cookie'; import os from 'os'; +import cuid from 'cuid'; try { if (!dev) { diff --git a/src/lib/components/templates.ts b/src/lib/components/templates.ts index f09272634..1b2e7346d 100644 --- a/src/lib/components/templates.ts +++ b/src/lib/components/templates.ts @@ -13,7 +13,7 @@ export function findBuildPack(pack, packageManager = 'npm') { if (pack === 'node') { return { ...metaData, - installCommand: null, + ...defaultBuildAndDeploy(packageManager), buildCommand: null, startCommand: null, publishDirectory: null, diff --git a/src/lib/database/applications.ts b/src/lib/database/applications.ts index 64cc1444e..ecbb22484 100644 --- a/src/lib/database/applications.ts +++ b/src/lib/database/applications.ts @@ -58,15 +58,6 @@ export async function removeApplication({ id, teamId }) { const id = containerObj.ID; const preview = containerObj.Image.split('-')[1]; await removeDestinationDocker({ id, engine: destinationDocker.engine }); - try { - if (preview) { - await removeProxyConfiguration({ domain: `${preview}.${domain}` }); - } else { - await removeProxyConfiguration({ domain }); - } - } catch (error) { - console.log(error); - } } } } @@ -79,8 +70,8 @@ export async function removeApplication({ id, teamId }) { export async function getApplicationWebhook({ projectId, branch }) { try { - let body = await prisma.application.findFirst({ - where: { projectId, branch }, + let application = await prisma.application.findFirst({ + where: { projectId, branch, settings: { autodeploy: true } }, include: { destinationDocker: true, settings: true, @@ -88,30 +79,38 @@ export async function getApplicationWebhook({ projectId, branch }) { secrets: true } }); - - if (body.gitSource?.githubApp?.clientSecret) { - body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret); + if (application.gitSource?.githubApp?.clientSecret) { + application.gitSource.githubApp.clientSecret = decrypt( + application.gitSource.githubApp.clientSecret + ); } - if (body.gitSource?.githubApp?.webhookSecret) { - body.gitSource.githubApp.webhookSecret = decrypt(body.gitSource.githubApp.webhookSecret); + if (application.gitSource?.githubApp?.webhookSecret) { + application.gitSource.githubApp.webhookSecret = decrypt( + application.gitSource.githubApp.webhookSecret + ); } - if (body.gitSource?.githubApp?.privateKey) { - body.gitSource.githubApp.privateKey = decrypt(body.gitSource.githubApp.privateKey); + if (application.gitSource?.githubApp?.privateKey) { + application.gitSource.githubApp.privateKey = decrypt( + application.gitSource.githubApp.privateKey + ); } - if (body?.gitSource?.gitlabApp?.appSecret) { - body.gitSource.gitlabApp.appSecret = decrypt(body.gitSource.gitlabApp.appSecret); + if (application?.gitSource?.gitlabApp?.appSecret) { + application.gitSource.gitlabApp.appSecret = decrypt( + application.gitSource.gitlabApp.appSecret + ); } - if (body?.gitSource?.gitlabApp?.webhookToken) { - body.gitSource.gitlabApp.webhookToken = decrypt(body.gitSource.gitlabApp.webhookToken); + if (application?.gitSource?.gitlabApp?.webhookToken) { + application.gitSource.gitlabApp.webhookToken = decrypt( + application.gitSource.gitlabApp.webhookToken + ); } - if (body?.secrets.length > 0) { - body.secrets = body.secrets.map((s) => { + if (application?.secrets.length > 0) { + application.secrets = application.secrets.map((s) => { s.value = decrypt(s.value); return s; }); } - - return { ...body }; + return { ...application }; } catch (e) { throw { status: 404, body: { message: e.message } }; } @@ -157,24 +156,41 @@ export async function getApplication({ id, teamId }) { return { ...body }; } -export async function configureGitRepository({ id, repository, branch, projectId, webhookToken }) { +export async function configureGitRepository({ + id, + repository, + branch, + projectId, + webhookToken, + autodeploy +}) { if (webhookToken) { const encryptedWebhookToken = encrypt(webhookToken); - return await prisma.application.update({ + await prisma.application.update({ where: { id }, data: { repository, branch, projectId, - gitSource: { update: { gitlabApp: { update: { webhookToken: encryptedWebhookToken } } } } + gitSource: { update: { gitlabApp: { update: { webhookToken: encryptedWebhookToken } } } }, + settings: { update: { autodeploy } } } }); } else { - return await prisma.application.update({ + await prisma.application.update({ where: { id }, - data: { repository, branch, projectId } + data: { repository, branch, projectId, settings: { update: { autodeploy } } } }); } + if (!autodeploy) { + const applications = await prisma.application.findMany({ where: { branch, projectId } }); + for (const application of applications) { + await prisma.applicationSettings.updateMany({ + where: { applicationId: application.id }, + data: { autodeploy: false } + }); + } + } } export async function configureBuildPack({ id, buildPack }) { @@ -209,10 +225,14 @@ export async function configureApplication({ }); } -export async function setApplicationSettings({ id, debug, previews, dualCerts }) { +export async function checkDoubleBranch(branch, projectId) { + const applications = await prisma.application.findMany({ where: { branch, projectId } }); + return applications.length > 1; +} +export async function setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }) { return await prisma.application.update({ where: { id }, - data: { settings: { update: { debug, previews, dualCerts } } }, + data: { settings: { update: { debug, previews, dualCerts, autodeploy } } }, include: { destinationDocker: true } }); } diff --git a/src/lib/docker.ts b/src/lib/docker.ts index 477491f12..a202649e2 100644 --- a/src/lib/docker.ts +++ b/src/lib/docker.ts @@ -16,6 +16,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) { secrets, pullmergeRequestId } = data; + const isPnpm = installCommand.includes('pnpm') || buildCommand.includes('pnpm'); const Dockerfile: Array = []; Dockerfile.push(`FROM ${imageForBuild}`); Dockerfile.push('WORKDIR /usr/src/app'); @@ -35,7 +36,10 @@ export async function buildCacheImageWithNode(data, imageForBuild) { } }); } - // TODO: If build command defined, install command should be the default yarn install + if (isPnpm) { + Dockerfile.push('RUN curl -f https://get.pnpm.io/v6.16.js | node - add --global pnpm'); + Dockerfile.push('RUN pnpm add -g pnpm'); + } if (installCommand) { Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`); try { diff --git a/src/lib/letsencrypt/index.ts b/src/lib/letsencrypt/index.ts index 61a8c0c37..ff189845f 100644 --- a/src/lib/letsencrypt/index.ts +++ b/src/lib/letsencrypt/index.ts @@ -99,7 +99,8 @@ export async function letsEncrypt(domain, id = null, isCoolify = false) { export async function generateSSLCerts() { const ssls = []; const applications = await db.prisma.application.findMany({ - include: { destinationDocker: true, settings: true } + include: { destinationDocker: true, settings: true }, + orderBy: { createdAt: 'desc' } }); for (const application of applications) { const { @@ -139,7 +140,8 @@ export async function generateSSLCerts() { plausibleAnalytics: true, vscodeserver: true, wordpress: true - } + }, + orderBy: { createdAt: 'desc' } }); for (const service of services) { diff --git a/src/lib/queues/index.ts b/src/lib/queues/index.ts index 9b93c6c0c..d26126d59 100644 --- a/src/lib/queues/index.ts +++ b/src/lib/queues/index.ts @@ -120,7 +120,7 @@ buildWorker.on('completed', async (job: Bullmq.Job) => { } catch (err) { console.log(err); } finally { - const workdir = `/tmp/build-sources/${job.data.repository}/`; + const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`; await asyncExecShell(`rm -fr ${workdir}`); } return; diff --git a/src/routes/applications/[id]/__layout.svelte b/src/routes/applications/[id]/__layout.svelte index 2c979a4ab..041be4a82 100644 --- a/src/routes/applications/[id]/__layout.svelte +++ b/src/routes/applications/[id]/__layout.svelte @@ -108,11 +108,9 @@ try { loading = true; await post(`/applications/${id}/stop.json`, {}); - isRunning = false; + return window.location.reload(); } catch ({ error }) { return errorNotification(error); - } finally { - loading = false; } } diff --git a/src/routes/applications/[id]/configuration/_GithubRepositories.svelte b/src/routes/applications/[id]/configuration/_GithubRepositories.svelte index 960d19d10..f69db0476 100644 --- a/src/routes/applications/[id]/configuration/_GithubRepositories.svelte +++ b/src/routes/applications/[id]/configuration/_GithubRepositories.svelte @@ -25,9 +25,11 @@ let selected = { projectId: undefined, repository: undefined, - branch: undefined + branch: undefined, + autodeploy: application.settings.autodeploy || true }; let showSave = false; + async function loadRepositoriesByPage(page = 0) { return await get(`${apiUrl}/installation/repositories?per_page=100&page=${page}`, { Authorization: `token ${$gitTokens.githubToken}` @@ -69,7 +71,14 @@ `/applications/${id}/configuration/repository.json?repository=${selected.repository}&branch=${selected.branch}` ); if (data.used) { - errorNotification('This branch is already used by another application.'); + const sure = confirm( + `This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?` + ); + if (sure) { + selected.autodeploy = false; + showSave = true; + return true; + } showSave = false; return true; } diff --git a/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte b/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte index 8fb28161a..e3a26a38d 100644 --- a/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte +++ b/src/routes/applications/[id]/configuration/_GitlabRepositories.svelte @@ -30,6 +30,7 @@ let projects = []; let branches = []; let showSave = false; + let autodeploy = application.settings.autodeploy || true; let selected = { group: undefined, @@ -138,7 +139,14 @@ `/applications/${id}/configuration/repository.json?repository=${selected.project.path_with_namespace}&branch=${selected.branch.name}` ); if (data.used) { - errorNotification('This branch is already used by another application.'); + const sure = confirm( + `This branch is already used by another application. Webhooks won't work in this case for both applications. Are you sure you want to use it?` + ); + if (sure) { + autodeploy = false; + showSave = true; + return true; + } showSave = false; return true; } @@ -235,10 +243,14 @@ const url = `/applications/${id}/configuration/repository.json`; try { + const repository = `${selected.group.full_path.replace('-personal', '')}/${ + selected.project.name + }`; await post(url, { - repository: `${selected.group.full_path}/${selected.project.name}`, + repository, branch: selected.branch.name, projectId: selected.project.id, + autodeploy, webhookToken }); return await goto(from || `/applications/${id}/configuration/buildpack`); diff --git a/src/routes/applications/[id]/configuration/repository.json.ts b/src/routes/applications/[id]/configuration/repository.json.ts index 6d133f29f..38987c4c6 100644 --- a/src/routes/applications/[id]/configuration/repository.json.ts +++ b/src/routes/applications/[id]/configuration/repository.json.ts @@ -30,14 +30,21 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - let { repository, branch, projectId, webhookToken } = await event.request.json(); + let { repository, branch, projectId, webhookToken, autodeploy } = await event.request.json(); repository = repository.toLowerCase(); branch = branch.toLowerCase(); projectId = Number(projectId); try { - await db.configureGitRepository({ id, repository, branch, projectId, webhookToken }); + await db.configureGitRepository({ + id, + repository, + branch, + projectId, + webhookToken, + autodeploy + }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/applications/[id]/index.svelte b/src/routes/applications/[id]/index.svelte index 35af22c4d..a0ccec921 100644 --- a/src/routes/applications/[id]/index.svelte +++ b/src/routes/applications/[id]/index.svelte @@ -56,6 +56,7 @@ let debug = application.settings.debug; let previews = application.settings.previews; let dualCerts = application.settings.dualCerts; + let autodeploy = application.settings.autodeploy; if (browser && window.location.hostname === 'demo.coolify.io' && !application.fqdn) { application.fqdn = `http://${cuid()}.demo.coolify.io`; @@ -75,10 +76,32 @@ if (name === 'dualCerts') { dualCerts = !dualCerts; } + if (name === 'autodeploy') { + autodeploy = !autodeploy; + } try { - await post(`/applications/${id}/settings.json`, { previews, debug, dualCerts }); + await post(`/applications/${id}/settings.json`, { + previews, + debug, + dualCerts, + autodeploy, + branch: application.branch, + projectId: application.projectId + }); return toast.push('Settings saved.'); } catch ({ error }) { + if (name === 'debug') { + debug = !debug; + } + if (name === 'previews') { + previews = !previews; + } + if (name === 'dualCerts') { + dualCerts = !dualCerts; + } + if (name === 'autodeploy') { + autodeploy = !autodeploy; + } return errorNotification(error); } } @@ -383,22 +406,23 @@
Features
-
+
+ changeSettings('autodeploy')} + title="Enable Automatic Deployment" + description="Enable automatic deployment through webhooks." + /> +
changeSettings('previews')} title="Enable MR/PR Previews" - description="Creates previews from pull and merge requests." + description="Enable preview deployments from pull or merge requests." />
diff --git a/src/routes/applications/[id]/settings.json.ts b/src/routes/applications/[id]/settings.json.ts index 6b0b3f808..aea78ff96 100644 --- a/src/routes/applications/[id]/settings.json.ts +++ b/src/routes/applications/[id]/settings.json.ts @@ -8,10 +8,17 @@ export const post: RequestHandler = async (event) => { if (status === 401) return { status, body }; const { id } = event.params; - const { debug, previews, dualCerts } = await event.request.json(); + const { debug, previews, dualCerts, autodeploy, branch, projectId } = await event.request.json(); try { - await db.setApplicationSettings({ id, debug, previews, dualCerts }); + const isDouble = await db.checkDoubleBranch(branch, projectId); + if (isDouble && autodeploy) { + throw { + message: + 'Cannot activate automatic deployments until only one application is defined for this repository / branch.' + }; + } + await db.setApplicationSettings({ id, debug, previews, dualCerts, autodeploy }); return { status: 201 }; } catch (error) { return ErrorHandler(error); diff --git a/src/routes/webhooks/github/events.ts b/src/routes/webhooks/github/events.ts index 40a60e29b..b50c19929 100644 --- a/src/routes/webhooks/github/events.ts +++ b/src/routes/webhooks/github/events.ts @@ -9,7 +9,7 @@ import { dev } from '$app/env'; export const options: RequestHandler = async () => { return { - status: 200, + status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', diff --git a/src/routes/webhooks/gitlab/events.ts b/src/routes/webhooks/gitlab/events.ts index 89aec7b4d..1c125ec02 100644 --- a/src/routes/webhooks/gitlab/events.ts +++ b/src/routes/webhooks/gitlab/events.ts @@ -9,7 +9,7 @@ import { dev } from '$app/env'; export const options: RequestHandler = async () => { return { - status: 200, + status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', diff --git a/src/routes/webhooks/gitlab/index.ts b/src/routes/webhooks/gitlab/index.ts index f3d930ace..0cc4f7ab8 100644 --- a/src/routes/webhooks/gitlab/index.ts +++ b/src/routes/webhooks/gitlab/index.ts @@ -7,7 +7,7 @@ import cookie from 'cookie'; export const options: RequestHandler = async () => { return { - status: 200, + status: 204, headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'Content-Type, Authorization', diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 8ec0aad53..e5003e5e6 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -1,5 +1,5 @@ const defaultTheme = require('tailwindcss/defaultTheme'); -const colors = require('tailwindcss/colors'); +// const colors = require('tailwindcss/colors'); module.exports = { content: ['./**/*.html', './src/**/*.{js,jsx,ts,tsx,svelte}'], important: true, @@ -18,7 +18,6 @@ module.exports = { sans: ['Poppins', ...defaultTheme.fontFamily.sans] }, colors: { - ...colors, coollabs: '#6B16ED', 'coollabs-100': '#7317FF', coolblack: '#161616',