2021-06-07 23:44:36 +02:00

230 lines
6.1 KiB
TypeScript

import type { Request } from '@sveltejs/kit';
import crypto from 'crypto';
import Deployment from '$models/Deployment';
import { docker } from '$lib/api/docker';
import { precheckDeployment, setDefaultConfiguration } from '$lib/api/applications/configuration';
import cloneRepository from '$lib/api/applications/cloneRepository';
import { cleanupTmp, execShellAsync } from '$lib/api/common';
import queueAndBuild from '$lib/api/applications/queueAndBuild';
import Configuration from '$models/Configuration';
import ApplicationLog from '$models/ApplicationLog';
import { cleanupStuckedDeploymentsInDB } from '$lib/api/applications/cleanup';
export async function post(request: Request) {
let configuration;
const allowedGithubEvents = ['push', 'pull_request'];
const allowedPRActions = ['opened', 'reopened', 'synchronize', 'closed'];
const githubEvent = request.headers['x-github-event'];
const { GITHUP_APP_WEBHOOK_SECRET } = process.env;
const hmac = crypto.createHmac('sha256', GITHUP_APP_WEBHOOK_SECRET);
const digest = Buffer.from(
'sha256=' + hmac.update(JSON.stringify(request.body)).digest('hex'),
'utf8'
);
const checksum = Buffer.from(request.headers['x-hub-signature-256'], 'utf8');
if (checksum.length !== digest.length || !crypto.timingSafeEqual(digest, checksum)) {
return {
status: 500,
body: {
error: 'Invalid request.'
}
};
}
if (!allowedGithubEvents.includes(githubEvent)) {
return {
status: 500,
body: {
error: 'Event not allowed.'
}
};
}
try {
const applications = await Configuration.find({
'repository.id': request.body.repository.id
}).select('-_id -__v -createdAt -updatedAt');
if (githubEvent === 'push') {
configuration = applications.find((r) => {
if (request.body.ref.startsWith('refs')) {
if (r.repository.branch === request.body.ref.split('/')[2]) {
return r;
}
}
return null;
});
} else if (githubEvent === 'pull_request') {
if (!allowedPRActions.includes(request.body.action)) {
return {
status: 500,
body: {
error: 'PR action is not allowed.'
}
};
}
configuration = applications.find(
(r) => r.repository.branch === request.body['pull_request'].base.ref
);
if (configuration) {
if (!configuration.general.isPreviewDeploymentEnabled) {
return {
status: 500,
body: {
error: 'PR deployments are not enabled.'
}
};
}
configuration.general.pullRequest = request.body.number;
}
}
if (!configuration) {
return {
status: 500,
body: {
error: 'No configuration found.'
}
};
}
configuration = setDefaultConfiguration(configuration);
const { id, organization, name, branch } = configuration.repository;
const { domain } = configuration.publish;
const { deployId, nickname, pullRequest } = configuration.general;
if (request.body.action === 'closed') {
const deploys = await Deployment.find({ organization, branch, name, domain });
for (const deploy of deploys) {
await ApplicationLog.deleteMany({ deployId: deploy.deployId });
await Deployment.deleteMany({ deployId: deploy.deployId });
}
await Configuration.findOneAndRemove({
'repository.id': id,
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'general.pullRequest': pullRequest
});
await execShellAsync(`docker stack rm ${configuration.build.container.name}`);
return {
status: 200,
body: {
success: true,
message: 'Removed'
}
};
}
await cloneRepository(configuration);
const { foundService, imageChanged, configChanged, forceUpdate } = await precheckDeployment(
configuration
);
if (foundService && !forceUpdate && !imageChanged && !configChanged) {
cleanupTmp(configuration.general.workdir);
return {
status: 200,
body: {
success: false,
message: 'Nothing changed, no need to redeploy.'
}
};
}
const alreadyQueued = await Deployment.find({
repoId: id,
branch: branch,
organization: organization,
name: name,
domain: domain,
progress: { $in: ['queued', 'inprogress'] }
});
if (alreadyQueued.length > 0) {
return {
status: 200,
body: {
success: false,
message: 'Already in the queue.'
}
};
}
await new Deployment({
repoId: id,
branch,
deployId,
domain,
organization,
name,
nickname
}).save();
if (githubEvent === 'pull_request') {
await Configuration.findOneAndUpdate(
{
'repository.id': id,
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'general.pullRequest': pullRequest
},
{ ...configuration },
{ upsert: true, new: true }
);
} else {
await Configuration.findOneAndUpdate(
{
'repository.id': id,
'repository.organization': organization,
'repository.name': name,
'repository.branch': branch,
'general.pullRequest': { $in: [null, 0] }
},
{ ...configuration },
{ upsert: true, new: true }
);
}
queueAndBuild(configuration, imageChanged);
return {
status: 201,
body: {
message: 'Deployment queued.',
nickname: configuration.general.nickname,
name: configuration.build.container.name,
deployId: configuration.general.deployId
}
};
} catch (error) {
console.log(error);
// console.log(configuration)
if (configuration) {
cleanupTmp(configuration.general.workdir);
await Deployment.findOneAndUpdate(
{
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain
},
{
repoId: configuration.repository.id,
branch: configuration.repository.branch,
organization: configuration.repository.organization,
name: configuration.repository.name,
domain: configuration.publish.domain,
progress: 'failed'
}
);
}
return {
status: 500,
body: {
error: error.message || error
}
};
} finally {
try {
await cleanupStuckedDeploymentsInDB();
} catch (error) {
console.log(error);
}
}
}