Compare commits

...

20 Commits

Author SHA1 Message Date
Andras Bacsai
e5537a33fb Merge pull request #223 from coollabsio/v2.0.30
v2.0.30
2022-03-19 15:11:42 +01:00
Andras Bacsai
35384deb68 fix: Remove build logs in case of app removed 2022-03-19 15:06:25 +01:00
Andras Bacsai
547ca60c2a fix: Better queue system + more support on monorepos 2022-03-19 15:04:52 +01:00
Andras Bacsai
376f6f7455 fix: Basedir for dockerfiles 2022-03-19 13:33:31 +01:00
Andras Bacsai
abe92dedff fix: no webhook secret found? 2022-03-15 17:35:37 +01:00
Andras Bacsai
4b521ceedc chore: version++ 2022-03-15 17:25:17 +01:00
Andras Bacsai
6dfcb9e52b fix: No error if GitSource is missing 2022-03-15 17:22:28 +01:00
Andras Bacsai
335e3216e2 fix: Missing session data 2022-03-15 17:21:18 +01:00
Andras Bacsai
5b22bb4818 fix: No cookie found 2022-03-15 17:04:15 +01:00
Andras Bacsai
0097004882 Merge pull request #217 from coollabsio/v2.0.29
v2.0.29
2022-03-12 00:28:26 +01:00
Andras Bacsai
1bc9e4c2d3 fix: Autodeploy true by default for GH repos 2022-03-11 23:56:11 +01:00
Andras Bacsai
36c7e1a3c3 feat: Install pnpm into docker image if pnpm lock file is used 2022-03-11 23:55:57 +01:00
Andras Bacsai
c6b4d04e26 Revert double build 2022-03-11 22:48:55 +01:00
Andras Bacsai
fa6cf068c7 feat: Autodeploy pause 2022-03-11 22:36:21 +01:00
Andras Bacsai
7c273a3a48 feat: Check ssl for new apps/services first 2022-03-11 21:28:27 +01:00
Andras Bacsai
3de2ea1523 chore: version++ 2022-03-11 21:19:03 +01:00
Andras Bacsai
c5c9f84503 feat: Webhooks inititate all applications with the correct branch 2022-03-11 21:18:12 +01:00
Andras Bacsai
16ea9a3e07 Update options request 2022-03-11 20:52:11 +01:00
Andras Bacsai
48f952c798 fix: Personal Gitlab repos 2022-03-11 20:47:26 +01:00
Andras Bacsai
f78ea5de07 Remove colors Tailwind 2022-03-11 20:47:13 +01:00
33 changed files with 356 additions and 172 deletions

View File

@@ -1,4 +1,5 @@
FROM node:16.14.0-alpine
RUN apk add --no-cache g++ cmake make python3
WORKDIR /app
COPY package*.json .
RUN yarn install

View File

@@ -1,7 +1,7 @@
{
"name": "coolify",
"description": "An open-source & self-hostable Heroku / Netlify alternative.",
"version": "2.0.28",
"version": "2.0.30",
"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",

View File

@@ -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;

View File

@@ -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
}

View File

@@ -84,7 +84,15 @@ export function makeLabelForServices(type) {
}
export const setDefaultConfiguration = async (data) => {
let { buildPack, port, installCommand, startCommand, buildCommand, publishDirectory } = data;
let {
buildPack,
port,
installCommand,
startCommand,
buildCommand,
publishDirectory,
baseDirectory
} = data;
const template = scanningTemplates[buildPack];
if (!port) {
port = template?.port || 3000;
@@ -97,6 +105,10 @@ export const setDefaultConfiguration = async (data) => {
if (!startCommand) startCommand = template?.startCommand || 'yarn start';
if (!buildCommand) buildCommand = template?.buildCommand || null;
if (!publishDirectory) publishDirectory = template?.publishDirectory || null;
if (baseDirectory) {
if (!baseDirectory.startsWith('/')) baseDirectory = `/${baseDirectory}`;
if (!baseDirectory.endsWith('/')) baseDirectory = `${baseDirectory}/`;
}
return {
buildPack,
@@ -104,7 +116,8 @@ export const setDefaultConfiguration = async (data) => {
installCommand,
startCommand,
buildCommand,
publishDirectory
publishDirectory,
baseDirectory
};
};
@@ -175,3 +188,11 @@ export async function copyBaseConfigurationFiles(buildPack, workdir, buildId, ap
throw new Error(error);
}
}
export function checkPnpm(installCommand = null, buildCommand = null, startCommand = null) {
return (
installCommand?.includes('pnpm') ||
buildCommand?.includes('pnpm') ||
startCommand?.includes('pnpm')
);
}

View File

@@ -16,6 +16,7 @@ export default async function ({
let file = `${workdir}/Dockerfile`;
if (baseDirectory) {
file = `${workdir}/${baseDirectory}/Dockerfile`;
workdir = `${workdir}/${baseDirectory}`;
}
const Dockerfile: Array<string> = (await fs.readFile(`${file}`, 'utf8'))

View File

@@ -4,13 +4,19 @@ import { promises as fs } from 'fs';
const createDockerfile = async (data, image): Promise<void> => {
const { applicationId, tag, port, startCommand, workdir, baseDirectory } = data;
const Dockerfile: Array<string> = [];
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'));

View File

@@ -1,5 +1,6 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
@@ -13,7 +14,7 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
@@ -32,17 +33,13 @@ const createDockerfile = async (data, image): Promise<void> => {
}
});
}
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);
try {
await fs.stat(`${workdir}/yarn.lock`);
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`);
} catch (error) {}
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
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 || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`);
}

View File

@@ -1,5 +1,6 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
@@ -13,6 +14,7 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app');
@@ -32,17 +34,12 @@ const createDockerfile = async (data, image): Promise<void> => {
}
});
}
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);
try {
await fs.stat(`${workdir}/yarn.lock`);
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`);
} catch (error) {}
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
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 || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`);
}

View File

@@ -1,5 +1,6 @@
import { buildImage } from '$lib/docker';
import { promises as fs } from 'fs';
import { checkPnpm } from './common';
const createDockerfile = async (data, image): Promise<void> => {
const {
@@ -13,7 +14,7 @@ const createDockerfile = async (data, image): Promise<void> => {
pullmergeRequestId
} = data;
const Dockerfile: Array<string> = [];
const isPnpm = checkPnpm(installCommand, buildCommand, startCommand);
Dockerfile.push(`FROM ${image}`);
Dockerfile.push('WORKDIR /usr/src/app');
Dockerfile.push(`LABEL coolify.image=true`);
@@ -32,17 +33,12 @@ const createDockerfile = async (data, image): Promise<void> => {
}
});
}
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);
try {
await fs.stat(`${workdir}/yarn.lock`);
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`);
} catch (error) {}
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
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 || ''} ./`);
Dockerfile.push(`RUN ${installCommand}`);
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
if (buildCommand) {
Dockerfile.push(`RUN ${buildCommand}`);
}

View File

@@ -9,7 +9,7 @@ const createDockerfile = async (data, image): Promise<void> => {
Dockerfile.push(`LABEL coolify.image=true`);
Dockerfile.push('RUN a2enmod rewrite');
Dockerfile.push('WORKDIR /var/www/html');
Dockerfile.push(`COPY ./${baseDirectory || ''} /var/www/html`);
Dockerfile.push(`COPY .${baseDirectory || ''} /var/www/html`);
Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["apache2-foreground"]');
Dockerfile.push('RUN chown -R www-data /var/www/html');

View File

@@ -37,7 +37,7 @@ const createDockerfile = async (data, image): Promise<void> => {
`COPY --from=${applicationId}:${tag}-cache /usr/src/app/${publishDirectory} ./`
);
} else {
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
}
Dockerfile.push(`EXPOSE 80`);
Dockerfile.push('CMD ["nginx", "-g", "daemon off;"]');

View File

@@ -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) {
@@ -68,9 +69,9 @@ export const isTeamIdTokenAvailable = (request) => {
export const getTeam = (event) => {
const cookies = Cookie.parse(event.request.headers.get('cookie'));
if (cookies.teamId) {
if (cookies?.teamId) {
return cookies.teamId;
} else if (event.locals.session.data.teamId) {
} else if (event.locals.session.data?.teamId) {
return event.locals.session.data.teamId;
}
return null;

View File

@@ -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,

View File

@@ -58,29 +58,21 @@ 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);
}
}
}
}
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.application.deleteMany({ where: { id, teams: { some: { 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 +80,41 @@ export async function getApplicationWebhook({ projectId, branch }) {
secrets: true
}
});
if (body.gitSource?.githubApp?.clientSecret) {
body.gitSource.githubApp.clientSecret = decrypt(body.gitSource.githubApp.clientSecret);
if (!application) {
return null;
}
if (body.gitSource?.githubApp?.webhookSecret) {
body.gitSource.githubApp.webhookSecret = decrypt(body.gitSource.githubApp.webhookSecret);
if (application?.gitSource?.githubApp?.clientSecret) {
application.gitSource.githubApp.clientSecret = decrypt(
application.gitSource.githubApp.clientSecret
);
}
if (body.gitSource?.githubApp?.privateKey) {
body.gitSource.githubApp.privateKey = decrypt(body.gitSource.githubApp.privateKey);
if (application?.gitSource?.githubApp?.webhookSecret) {
application.gitSource.githubApp.webhookSecret = decrypt(
application.gitSource.githubApp.webhookSecret
);
}
if (body?.gitSource?.gitlabApp?.appSecret) {
body.gitSource.gitlabApp.appSecret = decrypt(body.gitSource.gitlabApp.appSecret);
if (application?.gitSource?.githubApp?.privateKey) {
application.gitSource.githubApp.privateKey = decrypt(
application.gitSource.githubApp.privateKey
);
}
if (body?.gitSource?.gitlabApp?.webhookToken) {
body.gitSource.gitlabApp.webhookToken = decrypt(body.gitSource.gitlabApp.webhookToken);
if (application?.gitSource?.gitlabApp?.appSecret) {
application.gitSource.gitlabApp.appSecret = decrypt(
application.gitSource.gitlabApp.appSecret
);
}
if (body?.secrets.length > 0) {
body.secrets = body.secrets.map((s) => {
if (application?.gitSource?.gitlabApp?.webhookToken) {
application.gitSource.gitlabApp.webhookToken = decrypt(
application.gitSource.gitlabApp.webhookToken
);
}
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 +160,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 +229,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 }
});
}

View File

@@ -1,5 +1,6 @@
import Dockerode from 'dockerode';
import { promises as fs } from 'fs';
import { checkPnpm } from './buildPacks/common';
import { saveBuildLog } from './common';
export async function buildCacheImageWithNode(data, imageForBuild) {
@@ -16,6 +17,7 @@ export async function buildCacheImageWithNode(data, imageForBuild) {
secrets,
pullmergeRequestId
} = data;
const isPnpm = checkPnpm(installCommand, buildCommand);
const Dockerfile: Array<string> = [];
Dockerfile.push(`FROM ${imageForBuild}`);
Dockerfile.push('WORKDIR /usr/src/app');
@@ -35,20 +37,14 @@ 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');
}
Dockerfile.push(`COPY .${baseDirectory || ''} ./`);
if (installCommand) {
Dockerfile.push(`COPY ./${baseDirectory || ''}package*.json ./`);
try {
await fs.stat(`${workdir}/yarn.lock`);
Dockerfile.push(`COPY ./${baseDirectory || ''}yarn.lock ./`);
} catch (error) {}
try {
await fs.stat(`${workdir}/pnpm-lock.yaml`);
Dockerfile.push(`COPY ./${baseDirectory || ''}pnpm-lock.yaml ./`);
} catch (error) {}
Dockerfile.push(`RUN ${installCommand}`);
}
Dockerfile.push(`COPY ./${baseDirectory || ''} ./`);
Dockerfile.push(`RUN ${buildCommand}`);
await fs.writeFile(`${workdir}/Dockerfile-cache`, Dockerfile.join('\n'));
await buildImage({ applicationId, tag, workdir, docker, buildId, isCache: true, debug });

View File

@@ -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) {

View File

@@ -3,7 +3,14 @@ import fs from 'fs/promises';
import * as buildpacks from '../buildPacks';
import * as importers from '../importers';
import { dockerInstance } from '../docker';
import { asyncExecShell, createDirectories, getDomain, getEngine, saveBuildLog } from '../common';
import {
asyncExecShell,
asyncSleep,
createDirectories,
getDomain,
getEngine,
saveBuildLog
} from '../common';
import * as db from '$lib/database';
import { decrypt } from '$lib/crypto';
import { sentry } from '$lib/common';
@@ -12,7 +19,6 @@ import {
makeLabelForStandaloneApplication,
setDefaultConfiguration
} from '$lib/buildPacks/common';
import { letsEncrypt } from '$lib/letsencrypt';
export default async function (job) {
/*
@@ -45,7 +51,7 @@ export default async function (job) {
settings
} = job.data;
const { debug } = settings;
await asyncSleep(1000);
let imageId = applicationId;
let domain = getDomain(fqdn);
const isHttps = fqdn.startsWith('https://');
@@ -67,17 +73,8 @@ export default async function (job) {
const docker = dockerInstance({ destinationDocker });
const host = getEngine(destinationDocker.engine);
const build = await db.createBuild({
id: buildId,
applicationId,
destinationDockerId: destinationDocker.id,
gitSourceId: gitSource.id,
githubAppId: gitSource.githubApp?.id,
gitlabAppId: gitSource.gitlabApp?.id,
type
});
const { workdir, repodir } = await createDirectories({ repository, buildId: build.id });
await db.prisma.build.update({ where: { id: buildId }, data: { status: 'running' } });
const { workdir, repodir } = await createDirectories({ repository, buildId });
const configuration = await setDefaultConfiguration(job.data);
@@ -87,6 +84,7 @@ export default async function (job) {
startCommand = configuration.startCommand;
buildCommand = configuration.buildCommand;
publishDirectory = configuration.publishDirectory;
baseDirectory = configuration.baseDirectory;
let commit = await importers[gitSource.type]({
applicationId,
@@ -97,7 +95,7 @@ export default async function (job) {
gitlabAppId: gitSource.gitlabApp?.id,
repository,
branch,
buildId: build.id,
buildId,
apiUrl: gitSource.apiUrl,
projectId,
deployKeyId: gitSource.gitlabApp?.deployKeyId || null,
@@ -109,7 +107,7 @@ export default async function (job) {
}
try {
await db.prisma.build.update({ where: { id: build.id }, data: { commit } });
db.prisma.build.update({ where: { id: buildId }, data: { commit } });
} catch (err) {
console.log(err);
}
@@ -160,7 +158,7 @@ export default async function (job) {
await copyBaseConfigurationFiles(buildPack, workdir, buildId, applicationId);
if (buildpacks[buildPack])
await buildpacks[buildPack]({
buildId: build.id,
buildId,
applicationId,
domain,
name,

View File

@@ -87,7 +87,7 @@ const cron = async () => {
await queue.proxy.add('proxy', {}, { repeat: { every: 10000 } });
await queue.ssl.add('ssl', {}, { repeat: { every: dev ? 10000 : 60000 } });
await queue.cleanup.add('cleanup', {}, { repeat: { every: dev ? 10000 : 300000 } });
if (!dev) await queue.cleanup.add('cleanup', {}, { repeat: { every: 300000 } });
await queue.sslRenew.add('sslRenew', {}, { repeat: { every: 1800000 } });
const events = {
@@ -110,18 +110,18 @@ cron().catch((error) => {
const buildQueueName = 'build_queue';
const buildQueue = new Queue(buildQueueName, connectionOptions);
const buildWorker = new Worker(buildQueueName, async (job) => await builder(job), {
concurrency: 2,
concurrency: 1,
...connectionOptions
});
buildWorker.on('completed', async (job: Bullmq.Job) => {
try {
await prisma.build.update({ where: { id: job.data.build_id }, data: { status: 'success' } });
} catch (err) {
console.log(err);
} catch (error) {
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}/`;
await asyncExecShell(`rm -fr ${workdir}`);
const workdir = `/tmp/build-sources/${job.data.repository}/${job.data.build_id}`;
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
}
return;
});
@@ -133,7 +133,7 @@ buildWorker.on('failed', async (job: Bullmq.Job, failedReason) => {
console.log(error);
} finally {
const workdir = `/tmp/build-sources/${job.data.repository}`;
await asyncExecShell(`rm -fr ${workdir}`);
if (!dev) await asyncExecShell(`rm -fr ${workdir}`);
}
saveBuildLog({
line: 'Failed to deploy!',

View File

@@ -76,6 +76,7 @@
import { del, post } from '$lib/api';
import { goto } from '$app/navigation';
import { gitTokens } from '$lib/store';
import { toast } from '@zerodevx/svelte-toast';
if (githubToken) $gitTokens.githubToken = githubToken;
if (gitlabToken) $gitTokens.gitlabToken = gitlabToken;
@@ -86,6 +87,7 @@
async function handleDeploySubmit() {
try {
const { buildId } = await post(`/applications/${id}/deploy.json`, { ...application });
toast.push('Deployment queued.');
return await goto(`/applications/${id}/logs/build?buildId=${buildId}`);
} catch ({ error }) {
return errorNotification(error);
@@ -108,11 +110,9 @@
try {
loading = true;
await post(`/applications/${id}/stop.json`, {});
isRunning = false;
return window.location.reload();
} catch ({ error }) {
return errorNotification(error);
} finally {
loading = false;
}
}
</script>

View File

@@ -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;
}

View File

@@ -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`);

View File

@@ -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);

View File

@@ -30,6 +30,18 @@ export const post: RequestHandler = async (event) => {
await db.prisma.application.update({ where: { id }, data: { configHash } });
}
await db.prisma.application.update({ where: { id }, data: { updatedAt: new Date() } });
await db.prisma.build.create({
data: {
id: buildId,
applicationId: id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'manual'
}
});
await buildQueue.add(buildId, { build_id: buildId, type: 'manual', ...applicationFound });
return {
status: 200,

View File

@@ -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 @@
<div class="flex space-x-1 pb-5 font-bold">
<div class="title">Features</div>
</div>
<!-- <ul class="mt-2 divide-y divide-stone-800">
<Setting
bind:setting={forceSSL}
on:click={() => changeSettings('forceSSL')}
title="Force https"
description="Creates a https redirect for all requests from http and also generates a https certificate for the domain through Let's Encrypt."
/>
</ul> -->
<div class="px-10 pb-10">
<div class="grid grid-cols-2 items-center">
<Setting
isCenter={false}
bind:setting={autodeploy}
on:click={() => changeSettings('autodeploy')}
title="Enable Automatic Deployment"
description="Enable automatic deployment through webhooks."
/>
</div>
<div class="grid grid-cols-2 items-center">
<Setting
isCenter={false}
bind:setting={previews}
on:click={() => changeSettings('previews')}
title="Enable MR/PR Previews"
description="Creates previews from pull and merge requests."
description="Enable preview deployments from pull or merge requests."
/>
</div>
<div class="grid grid-cols-2 items-center">

View File

@@ -43,11 +43,11 @@
logs = logs.concat(responseLogs.map((log) => ({ ...log, line: cleanAnsiCodes(log.line) })));
loading = false;
streamInterval = setInterval(async () => {
if (status !== 'running') {
if (status !== 'running' && status !== 'queued') {
clearInterval(streamInterval);
return;
}
const nextSequence = logs[logs.length - 1].time;
const nextSequence = logs[logs.length - 1]?.time || 0;
try {
const data = await get(
`/applications/${id}/logs/build/build.json?buildId=${buildId}&sequence=${nextSequence}`
@@ -83,38 +83,42 @@
{#if currentStatus === 'running'}
<LoadingLogs />
{/if}
<div class="flex justify-end sticky top-0 p-2">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
{#if currentStatus === 'queued'}
<div class="text-center">Queued and waiting for execution.</div>
{:else}
<div class="flex justify-end sticky top-0 p-2">
<button
on:click={followBuild}
class="bg-transparent"
data-tooltip="Follow logs"
class:text-green-500={followingBuild}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
</div>
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
<svg
xmlns="http://www.w3.org/2000/svg"
class="w-6 h-6"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<circle cx="12" cy="12" r="9" />
<line x1="8" y1="12" x2="12" y2="16" />
<line x1="12" y1="8" x2="12" y2="16" />
<line x1="16" y1="12" x2="12" y2="16" />
</svg>
</button>
</div>
<div
class="font-mono leading-6 text-left text-md tracking-tighter rounded bg-coolgray-200 py-5 px-6 whitespace-pre-wrap break-words overflow-auto max-h-[80vh] -mt-12 overflow-y-scroll scrollbar-w-1 scrollbar-thumb-coollabs scrollbar-track-coolgray-200"
bind:this={logsEl}
>
{#each logs as log}
<div>{log.line + '\n'}</div>
{/each}
</div>
{/if}
</div>
{/if}

View File

@@ -19,7 +19,7 @@ export const get: RequestHandler = async (event) => {
return {
body: {
logs,
status: data?.status || 'running'
status: data?.status
}
};
} catch (error) {

View File

@@ -121,6 +121,8 @@
<div class="w-48 text-center text-xs">
{#if build.status === 'running'}
<div class="font-bold">Running</div>
{:else if build.status === 'queued'}
<div class="font-bold">Queued</div>
{:else}
<div>{build.since}</div>
<div>Finished in <span class="font-bold">{build.took}s</span></div>

View File

@@ -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);

View File

@@ -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',
@@ -47,7 +47,7 @@ export const post: RequestHandler = async (event) => {
const applicationFound = await db.getApplicationWebhook({ projectId, branch });
if (applicationFound) {
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret;
const webhookSecret = applicationFound.gitSource.githubApp.webhookSecret || null;
const hmac = crypto.createHmac('sha256', webhookSecret);
const digest = Buffer.from(
'sha256=' + hmac.update(JSON.stringify(body)).digest('hex'),
@@ -88,6 +88,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_commit',
@@ -136,6 +148,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_pr'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_pr',

View File

@@ -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',
@@ -52,6 +52,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_commit'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_commit',
@@ -133,6 +145,18 @@ export const post: RequestHandler = async (event) => {
where: { id: applicationFound.id },
data: { updatedAt: new Date() }
});
await db.prisma.build.create({
data: {
id: buildId,
applicationId: applicationFound.id,
destinationDockerId: applicationFound.destinationDocker.id,
gitSourceId: applicationFound.gitSource.id,
githubAppId: applicationFound.gitSource.githubApp?.id,
gitlabAppId: applicationFound.gitSource.gitlabApp?.id,
status: 'queued',
type: 'webhook_mr'
}
});
await buildQueue.add(buildId, {
build_id: buildId,
type: 'webhook_mr',

View File

@@ -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',

View File

@@ -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',