diff --git a/services/webhook-handler/README.md b/services/webhook-handler/README.md index 5cc9e31633..cc8ce699e0 100644 --- a/services/webhook-handler/README.md +++ b/services/webhook-handler/README.md @@ -13,7 +13,7 @@ The main Lagoon entrypoint for webhooks originating from other services. Every incoming webhook is parsed and validated before being queued for processing later. -Examples of webhooks Lagoon is interested in: GitHub/Bitbucket/GitLab repository +Examples of webhooks Lagoon is interested in: GitHub/Gitea/Bitbucket/GitLab repository activity, Lagoon project environment backup events. ## Technology diff --git a/services/webhook-handler/src/extractWebhookData.ts b/services/webhook-handler/src/extractWebhookData.ts index 5187b80a75..ce2eb76b7d 100644 --- a/services/webhook-handler/src/extractWebhookData.ts +++ b/services/webhook-handler/src/extractWebhookData.ts @@ -34,7 +34,12 @@ export function extractWebhookData(req: IncomingMessage, body: string): WebhookR throw new Error(`Request body is not parsable as JSON. Are you sure you have enabled application/json as the webhook content type? ${e}.`); } - if ('x-github-event' in req.headers) { + if ('x-gitea-event' in req.headers) { + webhooktype = 'gitea'; + event = req.headers['x-gitea-event']; + uuid = req.headers['x-gitea-delivery']; + giturl = R.path(['repository', 'ssh_url'], bodyObj); + } else if ('x-github-event' in req.headers) { webhooktype = 'github'; event = req.headers['x-github-event']; uuid = req.headers['x-github-delivery']; diff --git a/services/webhook-handler/src/types.ts b/services/webhook-handler/src/types.ts index 6f4c03d770..03aee6a6e6 100644 --- a/services/webhook-handler/src/types.ts +++ b/services/webhook-handler/src/types.ts @@ -24,6 +24,20 @@ export interface GithubPushEvent { }, }; +// See: https://docs.gitea.io/en-us/webhooks/ +export interface GiteaPushEvent { + event: 'push', + webhooktype: 'gitea', + uuid: string, + body: { + repository: { + ssh_url: string, + }, + ref: string, + after: string, + }, +}; + // See: https://developer.github.com/v3/activity/events/types/#pullrequestevent export interface GithubPullRequestEvent { event: 'pull_request', @@ -38,6 +52,20 @@ export interface GithubPullRequestEvent { } }; +// See: https://docs.gitea.io/en-us/webhooks/ +export interface GiteaPullRequestEvent { + event: 'pull_request', + webhooktype: 'gitea', + uuid: string, + body: { + action: string, + repository: { + ssh_url: string, + }, + number: number, + } +}; + // See: https://developer.github.com/v3/activity/events/types/#deleteevent export interface GithubDeleteEvent { event: 'delete', @@ -52,6 +80,19 @@ export interface GithubDeleteEvent { } }; +// See: https://docs.gitea.io/en-us/webhooks/ +export interface GiteaDeleteEvent { + event: 'delete', + webhooktype: 'gitea', + uuid: string, + body: { + ref_type: string, + repository: { + ssh_url: string, + } + } +}; + export interface CustomPushEvent { event: 'push', webhooktype: 'custom', @@ -78,6 +119,9 @@ export type RawData = GithubPushEvent | GithubPullRequestEvent | GithubDeleteEvent + | GiteaPushEvent + | GiteaPullRequestEvent + | GiteaDeleteEvent | CustomPushEvent | any; diff --git a/services/webhooks2tasks/src/handlers/giteaBranchDeleted.ts b/services/webhooks2tasks/src/handlers/giteaBranchDeleted.ts new file mode 100644 index 0000000000..fc0c78dd7e --- /dev/null +++ b/services/webhooks2tasks/src/handlers/giteaBranchDeleted.ts @@ -0,0 +1,59 @@ +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { createRemoveTask } from '@lagoon/commons/dist/tasks'; + +import { WebhookRequestData, removeData, Project } from '../types'; + +export async function giteaBranchDeleted(webhook: WebhookRequestData, project: Project) { + const { + webhooktype, + event, + giturl, + uuid, + body, + } = webhook; + + const meta: { [key: string]: any } = { + projectName: project.name, + branch: body.ref.replace('refs/heads/',''), + branchName: body.ref.replace('refs/heads/','') + } + + const data: removeData = { + projectName: project.name, + branch: meta.branch, + branchName: meta.branchName, + forceDeleteProductionEnvironment: false, + type: 'branch' + } + + try { + await createRemoveTask(data); + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:delete:handled`, meta, + `*[${project.name}]* \`${meta.branch}\` deleted in <${body.repository.html_url}|${body.repository.full_name}>` + ) + return; + } catch (error) { + meta.error = error + switch (error.name) { + case "ProjectNotFound": + case "NoActiveSystemsDefined": + case "UnknownActiveSystem": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* \`${meta.branch}\` deleted. No remove task created, reason: ${error}` + ) + return; + + case "CannotDeleteProductionEnvironment": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('warning', project.name, uuid, `${webhooktype}:${event}:CannotDeleteProductionEnvironment`, meta, + `*[${project.name}]* \`${meta.branch}\` not deleted. ${error}` + ) + return; + + default: + // Other messages are real errors and should reschedule the message in RabbitMQ in order to try again + throw error + } + } +} diff --git a/services/webhooks2tasks/src/handlers/giteaPullRequestClosed.ts b/services/webhooks2tasks/src/handlers/giteaPullRequestClosed.ts new file mode 100644 index 0000000000..e32eef4f32 --- /dev/null +++ b/services/webhooks2tasks/src/handlers/giteaPullRequestClosed.ts @@ -0,0 +1,64 @@ +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { createRemoveTask } from '@lagoon/commons/dist/tasks'; + +import { WebhookRequestData, removeData, Project } from '../types'; + +export async function giteaPullRequestClosed(webhook: WebhookRequestData, project: Project) { + + const { + webhooktype, + event, + giturl, + uuid, + body, + user, + sender, + } = webhook; + + const meta: { [key: string]: any } = { + projectName: project.name, + pullrequestTitle: body.pull_request.title, + pullrequestNumber: body.number, + pullrequestUrl: body.pull_request.html_url, + repoName: body.repository.full_name, + repoUrl: body.repository.html_url, + } + + const data: removeData = { + projectName: project.name, + pullrequestNumber: body.number, + pullrequestTitle: body.pull_request.title, + type: 'pullrequest' + } + + try { + await createRemoveTask(data); + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:closed:handled`, meta, + `*[${project.name}]* PR <${body.pull_request.html_url}|#${body.number} (${body.pull_request.title})> by <${body.pull_request.user.login}> changed by <${body.sender.login}> closed in <${body.repository.html_url}|${body.repository.full_name}>` + ) + return; + } catch (error) { + meta.error = error + switch (error.name) { + case "ProjectNotFound": + case "NoActiveSystemsDefined": + case "UnknownActiveSystem": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* PR ${body.number} closed. No remove task created, reason: ${error}` + ) + return; + + case "CannotDeleteProductionEnvironment": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('warning', project.name, uuid, `${webhooktype}:${event}:CannotDeleteProductionEnvironment`, meta, + `*[${project.name}]* \`${meta.branch}\` not deleted. ${error}` + ) + return; + + default: + // Other messages are real errors and should reschedule the message in RabbitMQ in order to try again + throw error + } + } +} diff --git a/services/webhooks2tasks/src/handlers/giteaPullRequestOpened.ts b/services/webhooks2tasks/src/handlers/giteaPullRequestOpened.ts new file mode 100644 index 0000000000..1fa010256a --- /dev/null +++ b/services/webhooks2tasks/src/handlers/giteaPullRequestOpened.ts @@ -0,0 +1,78 @@ +import R from 'ramda'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { createDeployTask } from '@lagoon/commons/dist/tasks'; + +import { WebhookRequestData, deployData, Project } from '../types'; + +export async function giteaPullRequestOpened(webhook: WebhookRequestData, project: Project) { + + const { + webhooktype, + event, + giturl, + uuid, + body, + } = webhook; + + const headRepoId = body.pull_request.head.repo.id + const headBranchName = body.pull_request.head.ref + const headSha = body.pull_request.head.sha + const baseRepoId = body.pull_request.base.repo.id + const baseBranchName = body.pull_request.base.ref + const baseSha = body.pull_request.base.sha + + const meta = { + projectName: project.name, + pullrequestTitle: body.pull_request.title, + pullrequestNumber: body.number, + pullrequestUrl: body.pull_request.html_url, + repoName: body.repository.full_name, + repoUrl: body.repository.html_url, + } + + // Don't trigger deploy if the head and base repos are different + if (!R.equals(headRepoId, baseRepoId)) { + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* PR ${body.number} opened. No deploy task created, reason: Head/Base not same repo` + ) + return; + } + + const data: deployData = { + repoUrl: body.repository.html_url, + repoName: body.repository.full_name, + pullrequestTitle: body.pull_request.title, + pullrequestNumber: body.number, + pullrequestUrl: body.pull_request.html_url, + projectName: project.name, + type: 'pullrequest', + headBranchName: headBranchName, + headSha: headSha, + baseBranchName: baseBranchName, + baseSha: baseSha, + branchName: `pr-${body.number}`, + } + + try { + await createDeployTask(data); + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:opened:handled`, data, + `*[${project.name}]* PR <${body.pull_request.html_url}|#${body.number} (${body.pull_request.title})> opened in <${body.repository.html_url}|${body.repository.full_name}>` + ) + return; + } catch (error) { + switch (error.name) { + case "ProjectNotFound": + case "NoActiveSystemsDefined": + case "UnknownActiveSystem": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* PR ${body.number} opened. No deploy task created, reason: ${error}` + ) + return; + + default: + // Other messages are real errors and should reschedule the message in RabbitMQ in order to try again + throw error + } + } +} diff --git a/services/webhooks2tasks/src/handlers/giteaPullRequestSynchronize.ts b/services/webhooks2tasks/src/handlers/giteaPullRequestSynchronize.ts new file mode 100644 index 0000000000..54d0fd0858 --- /dev/null +++ b/services/webhooks2tasks/src/handlers/giteaPullRequestSynchronize.ts @@ -0,0 +1,96 @@ +import R from 'ramda'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { createDeployTask } from '@lagoon/commons/dist/tasks'; + +import { WebhookRequestData, deployData, Project } from '../types'; + +const isEditAction = R.propEq('action', 'edited'); + +const onlyBodyChanges = R.pipe( + R.propOr({}, 'changes'), + R.keys, + R.equals(['body']), +); + +const skipRedeploy = R.and(isEditAction, onlyBodyChanges); + +export async function giteaPullRequestSynchronize(webhook: WebhookRequestData, project: Project) { + + const { + webhooktype, + event, + giturl, + uuid, + body, + } = webhook; + + const headRepoId = body.pull_request.head.repo.id + const headBranchName = body.pull_request.head.ref + const headSha = body.pull_request.head.sha + const baseRepoId = body.pull_request.base.repo.id + const baseBranchName = body.pull_request.base.ref + const baseSha = body.pull_request.base.sha + + const meta = { + projectName: project.name, + pullrequestTitle: body.pull_request.title, + pullrequestNumber: body.number, + pullrequestUrl: body.pull_request.html_url, + repoName: body.repository.full_name, + repoUrl: body.repository.html_url, + } + + // Don't trigger deploy if only the PR body was edited. + if (skipRedeploy(body)) { + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* PR ${body.number} updated. No deploy task created, reason: Only body changed` + ) + return; + } + + // Don't trigger deploy if the head and base repos are different + if (!R.equals(headRepoId, baseRepoId)) { + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* PR ${body.number} updated. No deploy task created, reason: Head/Base not same repo` + ) + return; + } + + const data: deployData = { + repoName: body.repository.full_name, + repoUrl: body.repository.html_url, + pullrequestUrl: body.pull_request.html_url, + pullrequestTitle: body.pull_request.title, + pullrequestNumber: body.number, + projectName: project.name, + type: 'pullrequest', + headBranchName: headBranchName, + headSha: headSha, + baseBranchName: baseBranchName, + baseSha: baseSha, + branchName: `pr-${body.number}`, + } + + try { + await createDeployTask(data); + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:synchronize:handled`, data, + `*[${project.name}]* PR <${body.pull_request.html_url}|#${body.number} (${body.pull_request.title})> updated in <${body.repository.html_url}|${body.repository.full_name}>` + ) + return; + } catch (error) { + switch (error.name) { + case "ProjectNotFound": + case "NoActiveSystemsDefined": + case "UnknownActiveSystem": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* PR ${body.number} opened. No deploy task created, reason: ${error}` + ) + return; + + default: + // Other messages are real errors and should reschedule the message in RabbitMQ in order to try again + throw error + } + } +} diff --git a/services/webhooks2tasks/src/handlers/giteaPush.ts b/services/webhooks2tasks/src/handlers/giteaPush.ts new file mode 100644 index 0000000000..97eef04fa2 --- /dev/null +++ b/services/webhooks2tasks/src/handlers/giteaPush.ts @@ -0,0 +1,80 @@ +import R from 'ramda'; +import { sendToLagoonLogs } from '@lagoon/commons/dist/logs'; +import { createDeployTask } from '@lagoon/commons/dist/tasks'; + +import { WebhookRequestData, deployData, Project } from '../types'; + +export async function giteaPush(webhook: WebhookRequestData, project: Project) { + + const { + webhooktype, + event, + giturl, + uuid, + body, + } = webhook; + + const branchName = body.ref.toLowerCase().replace('refs/heads/','') + const sha = body.after + var afterUrl = `${body.repository.html_url}/commit/${body.after}` + + // @ts-ignore + const skip_deploy = R.pathOr('',['head_commit','message'], body).match(/\[skip deploy\]|\[deploy skip\]/i) + + const meta = { + projectName: project.name, + branch: branchName, + sha: sha, + shortSha: sha.substring(0, 7), + repoFullName: body.repository.full_name, + repoUrl: body.repository.html_url, + branchName: branchName, + commitUrl: afterUrl, + event: event, + } + + const data: deployData = { + projectName: project.name, + type: 'branch', + branchName: branchName, + sha: sha, + } + + let logMessage = `\`<${body.repository.html_url}/tree/${meta.branch}|${meta.branch}>\`` + if (sha) { + const shortSha: string = sha.substring(0, 7) + logMessage = `${logMessage} (<${afterUrl}|${shortSha}>)` + } + + if (skip_deploy) { + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:skipped`, meta, + `*[${project.name}]* ${logMessage} pushed in <${body.repository.html_url}|${body.repository.full_name}> *deployment skipped*` + ) + return; + } + + try { + await createDeployTask(data); + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handled`, meta, + `*[${project.name}]* ${logMessage} pushed in <${body.repository.html_url}|${body.repository.full_name}>` + ) + return; + } catch (error) { + switch (error.name) { + case "ProjectNotFound": + case "NoActiveSystemsDefined": + case "UnknownActiveSystem": + case "NoNeedToDeployBranch": + // These are not real errors and also they will happen many times. We just log them locally but not throw an error + sendToLagoonLogs('info', project.name, uuid, `${webhooktype}:${event}:handledButNoTask`, meta, + `*[${project.name}]* ${logMessage}. No deploy task created, reason: ${error}` + ) + return; + + default: + // Other messages are real errors and should reschedule the message in RabbitMQ in order to try again + throw error + } + } + +} diff --git a/services/webhooks2tasks/src/webhooks/projects.ts b/services/webhooks2tasks/src/webhooks/projects.ts index 38bd61f04d..cd9ed15a35 100644 --- a/services/webhooks2tasks/src/webhooks/projects.ts +++ b/services/webhooks2tasks/src/webhooks/projects.ts @@ -8,6 +8,11 @@ import { githubPullRequestOpened } from '../handlers/githubPullRequestOpened'; import { githubPullRequestSynchronize } from '../handlers/githubPullRequestSynchronize'; import { githubBranchDeleted } from '../handlers/githubBranchDeleted'; import { githubPush } from '../handlers/githubPush'; +import { giteaPullRequestClosed } from '../handlers/giteaPullRequestClosed'; +import { giteaPullRequestOpened } from '../handlers/giteaPullRequestOpened'; +import { giteaPullRequestSynchronize } from '../handlers/giteaPullRequestSynchronize'; +import { giteaBranchDeleted } from '../handlers/giteaBranchDeleted'; +import { giteaPush } from '../handlers/giteaPush'; import { bitbucketPush } from '../handlers/bitbucketPush'; import { bitbucketBranchDeleted } from '../handlers/bitbucketBranchDeleted'; import { bitbucketPullRequestUpdated } from '../handlers/bitbucketPullRequestUpdated'; @@ -160,7 +165,49 @@ export async function processProjects( } break; + case 'gitea:pull_request': + switch (body.action) { + case 'closed': + await handle( + giteaPullRequestClosed, + webhook, + project, + `${webhooktype}:${event}:${body.action}` + ); + break; + + case 'opened': + case 'reopened': + await handle( + giteaPullRequestOpened, + webhook, + project, + `${webhooktype}:${event}:${body.action}` + ); + break; + + case 'synchronize': + case 'edited': + await handle( + giteaPullRequestSynchronize, + webhook, + project, + `${webhooktype}:${event}:${body.action}` + ); + break; + + default: + unhandled( + webhook, + project, + `${webhooktype}:${event}:${body.action}` + ); + break; + } + break; + case 'github:delete': + case 'gitea:delete': switch (body.ref_type) { case 'branch': // We do not handle branch deletes via github delete push event, as github also sends a regular push event with 'deleted=true'. It's handled there (see below inside "github:push") @@ -194,6 +241,21 @@ export async function processProjects( } break; + + case 'gitea:push': + if (body.deleted === true) { + await handle( + giteaBranchDeleted, + webhook, + project, + `${webhooktype}:${event}` + ); + } else { + await handle(giteaPush, webhook, project, `${webhooktype}:${event}`); + } + + break; + case 'bitbucket:repo:push': if (body.push.changes[0].closed === true) { await handle(