From 15bb156e819c7960638adcecf4652f2db449d784 Mon Sep 17 00:00:00 2001 From: Luca Faggianelli Date: Thu, 17 Oct 2019 01:49:09 +0200 Subject: [PATCH] gitlab credentials and triggers --- .../credentials/GitlabApi.credentials.ts | 24 ++ .../nodes/Gitlab/GenericFunctions.ts | 52 ++++ .../nodes/Gitlab/GitlabTrigger.node.ts | 252 ++++++++++++++++++ packages/nodes-base/nodes/Gitlab/gitlab.png | Bin 0 -> 2234 bytes 4 files changed, 328 insertions(+) create mode 100644 packages/nodes-base/credentials/GitlabApi.credentials.ts create mode 100644 packages/nodes-base/nodes/Gitlab/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts create mode 100644 packages/nodes-base/nodes/Gitlab/gitlab.png diff --git a/packages/nodes-base/credentials/GitlabApi.credentials.ts b/packages/nodes-base/credentials/GitlabApi.credentials.ts new file mode 100644 index 000000000..b3aceb121 --- /dev/null +++ b/packages/nodes-base/credentials/GitlabApi.credentials.ts @@ -0,0 +1,24 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + + +export class GitlabApi implements ICredentialType { + name = 'gitlabApi'; + displayName = 'Gitlab API'; + properties = [ + { + displayName: 'Gitlab Server', + name: 'server', + type: 'string' as NodePropertyTypes, + default: 'https://gitlab.com' + }, + { + displayName: 'Access Token', + name: 'accessToken', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts b/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts new file mode 100644 index 000000000..1c665dbf4 --- /dev/null +++ b/packages/nodes-base/nodes/Gitlab/GenericFunctions.ts @@ -0,0 +1,52 @@ +import { + IExecuteFunctions, + IHookFunctions, +} from 'n8n-core'; + +import { + IDataObject, +} from 'n8n-workflow'; + +/** + * Make an API request to Gitlab + * + * @param {IHookFunctions} this + * @param {string} method + * @param {string} url + * @param {object} body + * @returns {Promise} + */ +export async function gitlabApiRequest(this: IHookFunctions | IExecuteFunctions, method: string, endpoint: string, body: object, query?: object): Promise { // tslint:disable-line:no-any + const credentials = this.getCredentials('gitlabApi'); + if (credentials === undefined) { + throw new Error('No credentials got returned!'); + } + + const options = { + method, + headers: { + 'Private-Token': `token ${credentials.accessToken}`, + }, + body, + qs: query, + uri: `${(credentials.server as string).replace(/\/$/, '')}/api/v4/${endpoint}`, + json: true + }; + + try { + return await this.helpers.request(options); + } catch (error) { + if (error.statusCode === 401) { + // Return a clear error + throw new Error('The Gitlab credentials are not valid!'); + } + + if (error.response && error.response.body && error.response.body.message) { + // Try to return the error prettier + throw new Error(`Gitlab error response [${error.statusCode}]: ${error.response.body.message}`); + } + + // If that data does not exist for some reason return the actual error + throw error; + } +} diff --git a/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts new file mode 100644 index 000000000..307e7441d --- /dev/null +++ b/packages/nodes-base/nodes/Gitlab/GitlabTrigger.node.ts @@ -0,0 +1,252 @@ +import { + IHookFunctions, + IWebhookFunctions, +} from 'n8n-core'; + +import { + IDataObject, + INodeTypeDescription, + INodeType, + IWebhookResponseData, +} from 'n8n-workflow'; + +import { + gitlabApiRequest, +} from './GenericFunctions'; + + +export class GitlabTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Gitlab Trigger', + name: 'gitlabTrigger', + icon: 'file:gitlab.png', + group: ['trigger'], + version: 1, + subtitle: '={{$parameter["owner"] + "/" + $parameter["repository"] + ": " + $parameter["events"].join(", ")}}', + description: 'Starts the workflow when a Gitlab events occurs.', + defaults: { + name: 'Gitlab Trigger', + color: '#FC6D27', + }, + inputs: [], + outputs: ['main'], + credentials: [ + { + name: 'gitlabApi', + required: true, + } + ], + webhooks: [ + { + name: 'default', + httpMethod: 'POST', + responseMode: 'onReceived', + path: 'webhook', + }, + ], + properties: [ + { + displayName: 'Repository Owner', + name: 'owner', + type: 'string', + default: '', + required: true, + placeholder: 'n8n-io', + description: 'Owner of the repsitory.', + }, + { + displayName: 'Repository Name', + name: 'repository', + type: 'string', + default: '', + required: true, + placeholder: 'n8n', + description: 'The name of the repsitory.', + }, + { + displayName: 'Events', + name: 'events', + type: 'multiOptions', + options: [ + { + name: '*', + value: '*', + description: 'Any time any event is triggered (Wildcard Event).', + }, + { + name: 'Comment', + value: 'note', + description: 'Triggered when a new comment is made on commits, merge requests, issues, and code snippets.' + }, + { + name: 'Issue', + value: 'issues', + description: 'Triggered when a new issue is created or an existing issue was updated/closed/reopened.' + }, + { + name: 'Job', + value: 'job', + description: 'Triggered on status change of a job.' + }, + { + name: 'Merge Request', + value: 'merge_requests', + description: 'Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.' + }, + { + name: 'Pipeline', + value: 'pipeline', + description: 'Triggered on status change of Pipeline.' + }, + { + name: 'Push', + value: 'push', + description: 'Triggered when you push to the repository except when pushing tags.' + }, + { + name: 'Tag', + value: 'tag_push', + description: 'Triggered when you create (or delete) tags to the repository.' + }, + { + name: 'Wiki Page', + value: 'wiki_page', + description: 'Triggered when a wiki page is created, updated or deleted.' + } + ], + required: true, + default: [], + description: 'The events to listen to.', + }, + ], + }; + + // @ts-ignore (because of request) + webhookMethods = { + default: { + async checkExists(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId === undefined) { + // No webhook id is set so no webhook can exist + return false; + } + + // Webhook got created before so check if it still exists + const owner = this.getNodeParameter('owner') as string; + const repository = this.getNodeParameter('repository') as string; + const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`; + + try { + await gitlabApiRequest.call(this, 'GET', endpoint, {}); + } catch (e) { + if (e.message.includes('[404]:')) { + // Webhook does not exist + delete webhookData.webhookId; + delete webhookData.webhookEvents; + + return false; + } + + // Some error occured + throw e; + } + + // If it did not error then the webhook exists + return true; + }, + /** + * Gitlab API - Add project hook: + * https://docs.gitlab.com/ee/api/projects.html#add-project-hook + */ + async create(this: IHookFunctions): Promise { + const webhookUrl = this.getNodeWebhookUrl('default'); + + const owner = this.getNodeParameter('owner') as string; + const repository = this.getNodeParameter('repository') as string; + + let eventsArray = this.getNodeParameter('events', []) as string[]; + if (eventsArray.includes('*')) { + eventsArray = ['note', 'issues', 'job', 'merge_requests', 'pipeline', 'push', 'tag_push', 'wiki_page']; + } + + const events: { [key: string]: boolean } = { }; + for (const e of eventsArray) { + events[`${e}_events`] = true + } + + const endpoint = `/projects/${owner}%2F${repository}/hooks`; + + const body = { + url: webhookUrl, + events, + enable_ssl_verification: false, + }; + + + let responseData; + try { + responseData = await gitlabApiRequest.call(this, 'POST', endpoint, body); + } catch (e) { + throw e; + } + + if (responseData.id === undefined) { + // Required data is missing so was not successful + throw new Error('Gitlab webhook creation response did not contain the expected data.'); + } + + const webhookData = this.getWorkflowStaticData('node'); + webhookData.webhookId = responseData.id as string; + webhookData.webhookEvents = eventsArray as string[]; + + return true; + }, + async delete(this: IHookFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + + if (webhookData.webhookId !== undefined) { + const owner = this.getNodeParameter('owner') as string; + const repository = this.getNodeParameter('repository') as string; + const endpoint = `/projects/${owner}%2F${repository}/hooks/${webhookData.webhookId}`; + const body = {}; + + try { + await gitlabApiRequest.call(this, 'DELETE', endpoint, body); + } catch (e) { + return false; + } + + // Remove from the static workflow data so that it is clear + // that no webhooks are registred anymore + delete webhookData.webhookId; + delete webhookData.webhookEvents; + } + + return true; + }, + }, + }; + + + + async webhook(this: IWebhookFunctions): Promise { + const bodyData = this.getBodyData(); + + const returnData: IDataObject[] = []; + + returnData.push( + { + body: bodyData, + headers: this.getHeaderData(), + query: this.getQueryData(), + } + ); + + return { + workflowData: [ + this.helpers.returnJsonArray(returnData) + ], + }; + } +} diff --git a/packages/nodes-base/nodes/Gitlab/gitlab.png b/packages/nodes-base/nodes/Gitlab/gitlab.png new file mode 100644 index 0000000000000000000000000000000000000000..38ddea94d8506486893d5d20ce3ad820d23a2a46 GIT binary patch literal 2234 zcmV;r2u1gaP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 zVoOIv0RI600RN!9r;`8x00(qQO+^Re3K0M`4!f8HW&i*OzDYzuRA}Dqnph(cfTP_+#yJ_6UX)s;vA?G<5`?%q4%9r3lijo;USEN0Te}7o&2C_ zU#6K!`erZF8ILgQ-52G|E7>VopSjVLtD3ktJcK~X<>Wb+lgF=(`CR(S(48ebdc2p( z3jrvHRCx-OeJ+CWgXwuEtWHpsjjDWGm2Y+}&Gopbd4Ek2c9GD#HoH`nZ2$_pZJ^0) zrs)bPi_81cxCl^o0A0Y{s&bFI=HJttq#dZT85oQo@Oc+W|6BqohcGADKvN34@{c-D zc7|DDbZ>LAZkX_;J!z(Hc%bY8Qu)U#P`0`V#tzJ%tknt5$q>rtW}YdJ_(``Y$~EV+ zVZtsH+PcCXjb(N#IN0qcGzh2XC(D=jrEwt;mrZZc&<0i6*sPiwCY)~~n}FLjrQ8>V zeA>;Etn+eiQ`lWHvCW4L<+E3Vd(_Einvr!w zgbRBj%7*d=JP?I)*IZ@oLcs=_C?n?r7NhKN5e;|ix(Q#}lP1auC_9Pw#Bx}T@&$D; z`B;DRgq`3bS&y=%l8wSHFVOZzyLH`!T`1Uu>r~iT&7Z=spHwe%%{E1L{NNxO%lTp8 zQxy%BP3k1wsI2QIoR{+kl+RW-c{52=H(1A`3{c20;wvkv9{TDJKkZCkE^y}vHqsS1gtYY#)&RflU2;mdwP zPeh?{m-aN+RIG;Klx#&*fz@RlIBv@uv>$Ye@-U|;7V81mocfz@!4yJe*Yz3Ci46MknZj~DI0`S|Ensrz_$FDI*i7%vU0KaINgkf%K zK%#d3s#-{wKt_{D$q~;ZmUc&F(qbS{D_bRGf;PqaRcw5q4`?fSuW&zstgEL5S6>n< zV5J`^IpQgh%)dNQnLr|?n0jR``2{kS$5{z1D!a%l5x$6p@TKaqHZ@0u2Z=>Iix8c_ zs>)ux0D@$Fvi59T_Z4k>i5pd=NQE7G)fu{~ORAoeRu%{=j#dezZdq$wyPjWutk%Nj zF%`G=>ZtA_1R_qLyZ{xB90jR$Gdnv9K?;mlBZ|D7#mt^xvveRtIf3H^>GoW7*)>nt zK5-h*>tyvA98aRWU)Ink?wqOFq-~)1HDqmUT<^1NYB$F#IDSmT69^+Zp(K9zVXd{T zjque0WSK#)>kx+L5DrqjF%&#q189+VBvDP)<>XxbfHRHq7LG@; zoWxN#S!m*8TKL8Qn!`w&Y)5z=;ep!>9B>YpD2m^~nN`gUng#PEU7#3ASidX&Ua^PDOa}Xl61hOCDCkPt| ziMOi%#bVlwd#V6`WI3>&AI5WtT5Ok3zOA5O1OaV*MpGKgkF?GnSaW66RY*4P3w!AlW`qY z{O>J1**i)YAUZw(SxoMIS#CO+6vMRt{pL