import axios from "axios"; import cuid from "cuid"; import crypto from "crypto"; import type { FastifyReply, FastifyRequest } from "fastify"; import { errorHandler, getAPIUrl, getDomain, getUIUrl, isDev, listSettings, prisma } from "../../../lib/common"; import { checkContainer, removeContainer } from "../../../lib/docker"; import { getApplicationFromDB, getApplicationFromDBWebhook } from "../../api/v1/applications/handlers"; import type { ConfigureGitLabApp, GitLabEvents } from "./types"; export async function configureGitLabApp(request: FastifyRequest, reply: FastifyReply) { try { const { code, state } = request.query; const { fqdn } = await listSettings(); const { gitSource: { gitlabApp: { appId, appSecret }, htmlUrl } }: any = await getApplicationFromDB(state, undefined); let domain = `http://${request.hostname}`; if (fqdn) domain = fqdn; if (isDev) { domain = getAPIUrl(); } const params = new URLSearchParams({ client_id: appId, client_secret: appSecret, code, state, grant_type: 'authorization_code', redirect_uri: `${domain}/webhooks/gitlab` }); const { data } = await axios.post(`${htmlUrl}/oauth/token`, params) if (isDev) { return reply.redirect(`${getUIUrl()}/webhooks/success?token=${data.access_token}`) } return reply.redirect(`/webhooks/success?token=${data.access_token}`) } catch ({ status, message, ...other }) { return errorHandler({ status, message }) } } export async function gitLabEvents(request: FastifyRequest) { const { object_kind: objectKind, ref, project_id } = request.body try { const allowedActions = ['opened', 'reopen', 'close', 'open', 'update']; const webhookToken = request.headers['x-gitlab-token']; if (!webhookToken && !isDev) { throw { status: 500, message: 'Invalid webhookToken.' } } if (objectKind === 'push') { const projectId = Number(project_id); const branch = ref.split('/')[2]; const applicationsFound = await getApplicationFromDBWebhook(projectId, branch); if (applicationsFound && applicationsFound.length > 0) { for (const application of applicationsFound) { const buildId = cuid(); if (!application.configHash) { const configHash = crypto .createHash('sha256') .update( JSON.stringify({ buildPack: application.buildPack, port: application.port, exposePort: application.exposePort, installCommand: application.installCommand, buildCommand: application.buildCommand, startCommand: application.startCommand }) ) .digest('hex'); await prisma.application.update({ where: { id: application.id }, data: { configHash } }); } await prisma.application.update({ where: { id: application.id }, data: { updatedAt: new Date() } }); await prisma.build.create({ data: { id: buildId, applicationId: application.id, destinationDockerId: application.destinationDocker.id, gitSourceId: application.gitSource.id, githubAppId: application.gitSource.githubApp?.id, gitlabAppId: application.gitSource.gitlabApp?.id, status: 'queued', type: 'webhook_commit' } }); } } } else if (objectKind === 'merge_request') { const { object_attributes: { work_in_progress: isDraft, action, source_branch: sourceBranch, target_branch: targetBranch }, project: { id } } = request.body const pullmergeRequestId = request.body.object_attributes.iid.toString(); const projectId = Number(id); if (!allowedActions.includes(action)) { throw { status: 500, message: 'Action not allowed.' } } if (isDraft) { throw { status: 500, message: 'Draft MR, do nothing.' } } const applicationsFound = await getApplicationFromDBWebhook(projectId, targetBranch); if (applicationsFound && applicationsFound.length > 0) { for (const application of applicationsFound) { const buildId = cuid(); if (application.settings.previews) { if (application.destinationDockerId) { const { found: isRunning } = await checkContainer( { dockerId: application.destinationDocker.id, container: application.id } ); if (!isRunning) { throw { status: 500, message: 'Application not running.' } } } if (!isDev && application.gitSource.gitlabApp.webhookToken !== webhookToken) { throw { status: 500, message: 'Invalid webhookToken. Are you doing something nasty?!' } } if ( action === 'opened' || action === 'reopen' || action === 'open' || action === 'update' ) { await prisma.application.update({ where: { id: application.id }, data: { updatedAt: new Date() } }); let previewApplicationId = undefined if (pullmergeRequestId) { const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } }) if (foundPreviewApplications.length > 0) { previewApplicationId = foundPreviewApplications[0].id } else { const protocol = application.fqdn.includes('https://') ? 'https://' : 'http://' const previewApplication = await prisma.previewApplication.create({ data: { pullmergeRequestId, sourceBranch, customDomain: `${protocol}${pullmergeRequestId}.${getDomain(application.fqdn)}`, application: { connect: { id: application.id } } } }) previewApplicationId = previewApplication.id } } await prisma.build.create({ data: { id: buildId, pullmergeRequestId, previewApplicationId, sourceBranch, applicationId: application.id, destinationDockerId: application.destinationDocker.id, gitSourceId: application.gitSource.id, githubAppId: application.gitSource.githubApp?.id, gitlabAppId: application.gitSource.gitlabApp?.id, status: 'queued', type: 'webhook_mr' } }); return { message: 'Queued. Thank you!' }; } else if (action === 'close') { if (application.destinationDockerId) { const id = `${application.id}-${pullmergeRequestId}`; try { await removeContainer({ id, dockerId: application.destinationDocker.id }); } catch (error) { } } const foundPreviewApplications = await prisma.previewApplication.findMany({ where: { applicationId: application.id, pullmergeRequestId } }) if (foundPreviewApplications.length > 0) { for (const preview of foundPreviewApplications) { await prisma.previewApplication.delete({ where: { id: preview.id } }) } } return { message: 'MR closed. Thank you!' }; } } } } } } catch ({ status, message }) { return errorHandler({ status, message }) } }