From 1bed7fc063648e7e07cf5bec6405e04352c6b0ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gast=C3=B3n=20Fournier?= Date: Wed, 12 Jul 2023 20:09:46 +0200 Subject: [PATCH] chore: prepare 5.2 patch (#4224) ## About the changes Early release of https://github.com/Unleash/unleash/pull/4139 and https://github.com/Unleash/unleash/pull/4196 as 5.2 patch --------- Co-authored-by: Christopher Kolstad --- .../addons/__snapshots__/datadog.test.ts.snap | 19 +++++ .../addons/__snapshots__/slack.test.ts.snap | 17 +++-- .../addons/__snapshots__/teams.test.ts.snap | 9 +++ src/lib/addons/datadog-definition.ts | 21 ++++++ src/lib/addons/datadog.test.ts | 69 +++++++++++++++++++ src/lib/addons/datadog.ts | 45 ++++++++++-- src/lib/addons/slack-definition.ts | 12 ++++ src/lib/addons/slack.test.ts | 36 ++++++++++ src/lib/addons/slack.ts | 33 +++++++-- src/lib/addons/teams-definition.ts | 12 ++++ src/lib/addons/teams.test.ts | 31 +++++++++ src/lib/addons/teams.ts | 24 ++++++- src/lib/addons/webhook-definition.ts | 12 ++++ src/lib/addons/webhook.test.ts | 32 +++++++++ src/lib/addons/webhook.ts | 15 +++- .../__snapshots__/openapi.e2e.test.ts.snap | 58 +++++++++------- 16 files changed, 402 insertions(+), 43 deletions(-) diff --git a/src/lib/addons/__snapshots__/datadog.test.ts.snap b/src/lib/addons/__snapshots__/datadog.test.ts.snap index 35432af758..c2031adf15 100644 --- a/src/lib/addons/__snapshots__/datadog.test.ts.snap +++ b/src/lib/addons/__snapshots__/datadog.test.ts.snap @@ -7,3 +7,22 @@ exports[`Should call datadog webhook for archived toggle with project info 1`] exports[`Should call datadog webhook 1`] = `"{"text":"%%% \\n some@user.com created feature toggle [some-toggle](http://some-url.com/projects//features/some-toggle) in project *undefined* \\n %%% ","title":"Unleash notification update"}"`; exports[`Should call datadog webhook for toggled environment 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`; + +exports[`Should include customHeaders in headers when calling service 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update"}"`; + +exports[`Should include customHeaders in headers when calling service 2`] = ` +{ + "Content-Type": "application/json", + "DD-API-KEY": "fakeKey", + "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE", +} +`; + +exports[`Should not include source_type_name when included in the config 1`] = `"{"text":"%%% \\n some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default* \\n %%% ","title":"Unleash notification update","source_type_name":"my-custom-source-type"}"`; + +exports[`Should not include source_type_name when included in the config 2`] = ` +{ + "Content-Type": "application/json", + "DD-API-KEY": "fakeKey", +} +`; diff --git a/src/lib/addons/__snapshots__/slack.test.ts.snap b/src/lib/addons/__snapshots__/slack.test.ts.snap index 40817e1d1a..ef1dfa2ef3 100644 --- a/src/lib/addons/__snapshots__/slack.test.ts.snap +++ b/src/lib/addons/__snapshots__/slack.test.ts.snap @@ -1,9 +1,18 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Should call slack webhook 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com created feature toggle in project *default*","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Should call slack webhook 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com created feature toggle in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; -exports[`Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle **","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/archive"}]}]}"`; +exports[`Should call slack webhook for archived toggle 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/archive"}]}]}"`; -exports[`Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle **","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`; +exports[`Should call slack webhook for archived toggle with project info 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":" some@user.com just archived feature toggle **","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/some-project/archive"}]}]}"`; -exports[`Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* in *development* environment in project *default*","channel":"#undefined","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; +exports[`Should call webhook for toggled environment 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* in *development* environment in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; + +exports[`Should include custom headers from parameters in call to service 1`] = `"{"username":"Unleash","icon_emoji":":unleash:","text":"some@user.com *disabled* in *development* environment in project *default*","channel":"#general","attachments":[{"actions":[{"name":"featureToggle","text":"Open in Unleash","type":"button","value":"featureToggle","style":"primary","url":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; + +exports[`Should include custom headers from parameters in call to service 2`] = ` +{ + "Content-Type": "application/json", + "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE", +} +`; diff --git a/src/lib/addons/__snapshots__/teams.test.ts.snap b/src/lib/addons/__snapshots__/teams.test.ts.snap index feb53a5d90..76877e1d36 100644 --- a/src/lib/addons/__snapshots__/teams.test.ts.snap +++ b/src/lib/addons/__snapshots__/teams.test.ts.snap @@ -7,3 +7,12 @@ exports[`Should call teams webhook for archived toggle 1`] = `"{"themeColor":"00 exports[`Should call teams webhook for archived toggle with project info 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":" some@user.com just archived feature toggle *[some-toggle](http://some-url.com/projects/some-project/archive)*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-archived"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/some-project/archive"}]}]}"`; exports[`Should call teams webhook for toggled environment 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; + +exports[`Should include custom headers in call to teams 1`] = `"{"themeColor":"0076D7","summary":"Message","sections":[{"activityTitle":"some@user.com *disabled* [some-toggle](http://some-url.com/projects/default/features/some-toggle) in *development* environment in project *default*","activitySubtitle":"Unleash notification update","facts":[{"name":"User","value":"some@user.com"},{"name":"Action","value":"feature-environment-disabled"}]}],"potentialAction":[{"@type":"OpenUri","name":"Go to feature","targets":[{"os":"default","uri":"http://some-url.com/projects/default/features/some-toggle"}]}]}"`; + +exports[`Should include custom headers in call to teams 2`] = ` +{ + "Content-Type": "application/json", + "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE", +} +`; diff --git a/src/lib/addons/datadog-definition.ts b/src/lib/addons/datadog-definition.ts index 16ef412164..b6db754d9a 100644 --- a/src/lib/addons/datadog-definition.ts +++ b/src/lib/addons/datadog-definition.ts @@ -40,6 +40,27 @@ const dataDogDefinition: IAddonDefinition = { required: true, sensitive: true, }, + { + name: 'sourceTypeName', + displayName: 'Datadog Source Type Name', + description: + '(Optional) source_type_name parameter to be included in Datadog events.', + type: 'text', + required: false, + sensitive: false, + }, + { + name: 'customHeaders', + displayName: 'Extra HTTP Headers', + placeholder: `{ + "ISTIO_USER_KEY": "hunter2", + "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" + }`, + description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, + required: false, + sensitive: true, + type: 'textfield', + }, ], events: [ FEATURE_CREATED, diff --git a/src/lib/addons/datadog.test.ts b/src/lib/addons/datadog.test.ts index 2cd70ae595..43857afa33 100644 --- a/src/lib/addons/datadog.test.ts +++ b/src/lib/addons/datadog.test.ts @@ -55,6 +55,7 @@ test('Should call datadog webhook', async () => { const parameters = { url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', }; await addon.handleEvent(event, parameters); @@ -81,6 +82,7 @@ test('Should call datadog webhook for archived toggle', async () => { const parameters = { url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', }; await addon.handleEvent(event, parameters); @@ -108,6 +110,7 @@ test('Should call datadog webhook for archived toggle with project info', async const parameters = { url: 'http://api.datadoghq.com/api/v1/events', + apiKey: 'fakeKey', }; await addon.handleEvent(event, parameters); @@ -136,6 +139,7 @@ test(`Should call datadog webhook for toggled environment`, async () => { const parameters = { url: 'http://hooks.slack.com', + apiKey: 'fakeKey', }; await addon.handleEvent(event, parameters); @@ -144,3 +148,68 @@ test(`Should call datadog webhook for toggled environment`, async () => { expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); + +test(`Should include customHeaders in headers when calling service`, async () => { + const addon = new DatadogAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + apiKey: 'fakeKey', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); +}); + +test(`Should not include source_type_name when included in the config`, async () => { + const addon = new DatadogAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + apiKey: 'fakeKey', + sourceTypeName: 'my-custom-source-type', + }; + + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch( + /"source_type_name":"my-custom-source-type"/, + ); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); +}); diff --git a/src/lib/addons/datadog.ts b/src/lib/addons/datadog.ts index f8b15b60b1..ee03c93d34 100644 --- a/src/lib/addons/datadog.ts +++ b/src/lib/addons/datadog.ts @@ -9,6 +9,20 @@ import { } from './feature-event-formatter-md'; import { IEvent } from '../types/events'; +interface IDatadogParameters { + url: string; + apiKey: string; + sourceTypeName?: string; + customHeaders?: string; +} + +interface DDRequestBody { + text: string; + title: string; + tags?: string[]; + source_type_name?: string; +} + export default class DatadogAddon extends Addon { private msgFormatter: FeatureEventFormatter; @@ -20,27 +34,46 @@ export default class DatadogAddon extends Addon { ); } - // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async handleEvent(event: IEvent, parameters: any): Promise { - const { url = 'https://api.datadoghq.com/api/v1/events', apiKey } = - parameters; + async handleEvent( + event: IEvent, + parameters: IDatadogParameters, + ): Promise { + const { + url = 'https://api.datadoghq.com/api/v1/events', + apiKey, + sourceTypeName, + customHeaders, + } = parameters; const text = this.msgFormatter.format(event); const { tags: eventTags } = event; const tags = eventTags && eventTags.map((tag) => `${tag.type}:${tag.value}`); - const body = { + const body: DDRequestBody = { text: `%%% \n ${text} \n %%% `, title: 'Unleash notification update', tags, }; - + if (sourceTypeName) { + body.source_type_name = sourceTypeName; + } + let extraHeaders = {}; + if (typeof customHeaders === 'string' && customHeaders.length > 1) { + try { + extraHeaders = JSON.parse(customHeaders); + } catch (e) { + this.logger.warn( + `Could not parse the json in the customHeaders parameter. [${customHeaders}]`, + ); + } + } const requestOpts = { method: 'POST', headers: { 'Content-Type': 'application/json', 'DD-API-KEY': apiKey, + ...extraHeaders, }, body: JSON.stringify(body), }; diff --git a/src/lib/addons/slack-definition.ts b/src/lib/addons/slack-definition.ts index f8f3839755..d6b79211ec 100644 --- a/src/lib/addons/slack-definition.ts +++ b/src/lib/addons/slack-definition.ts @@ -59,6 +59,18 @@ const slackDefinition: IAddonDefinition = { required: true, sensitive: false, }, + { + name: 'customHeaders', + displayName: 'Extra HTTP Headers', + placeholder: `{ + "ISTIO_USER_KEY": "hunter2", + "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" + }`, + description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, + required: false, + sensitive: true, + type: 'textfield', + }, ], events: [ FEATURE_CREATED, diff --git a/src/lib/addons/slack.test.ts b/src/lib/addons/slack.test.ts index 14bc626d4c..416ffc1d70 100644 --- a/src/lib/addons/slack.test.ts +++ b/src/lib/addons/slack.test.ts @@ -57,6 +57,7 @@ test('Should call slack webhook', async () => { const parameters = { url: 'http://hooks.slack.com', + defaultChannel: 'general', }; await addon.handleEvent(event, parameters); @@ -83,6 +84,7 @@ test('Should call slack webhook for archived toggle', async () => { const parameters = { url: 'http://hooks.slack.com', + defaultChannel: 'general', }; await addon.handleEvent(event, parameters); @@ -110,6 +112,7 @@ test('Should call slack webhook for archived toggle with project info', async () const parameters = { url: 'http://hooks.slack.com', + defaultChannel: 'general', }; await addon.handleEvent(event, parameters); @@ -138,6 +141,7 @@ test(`Should call webhook for toggled environment`, async () => { const parameters = { url: 'http://hooks.slack.com', + defaultChannel: 'general', }; await addon.handleEvent(event, parameters); @@ -255,3 +259,35 @@ test('Should post to all channels in tags', async () => { expect(req1.channel).toBe('#another-channel-1'); expect(req2.channel).toBe('#another-channel-2'); }); + +test('Should include custom headers from parameters in call to service', async () => { + const addon = new SlackAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + defaultChannel: 'general', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); +}); diff --git a/src/lib/addons/slack.ts b/src/lib/addons/slack.ts index 45795d2548..4400b704e2 100644 --- a/src/lib/addons/slack.ts +++ b/src/lib/addons/slack.ts @@ -10,6 +10,13 @@ import { } from './feature-event-formatter-md'; import { IEvent } from '../types/events'; +interface ISlackAddonParameters { + url: string; + username?: string; + defaultChannel: string; + emojiIcon?: string; + customHeaders?: string; +} export default class SlackAddon extends Addon { private msgFormatter: FeatureEventFormatter; @@ -22,12 +29,16 @@ export default class SlackAddon extends Addon { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async handleEvent(event: IEvent, parameters: any): Promise { + async handleEvent( + event: IEvent, + parameters: ISlackAddonParameters, + ): Promise { const { url, defaultChannel, username = 'Unleash', - iconEmoji = ':unleash:', + emojiIcon = ':unleash:', + customHeaders, } = parameters; const slackChannels = this.findSlackChannels(event); @@ -42,7 +53,7 @@ export default class SlackAddon extends Addon { const requests = slackChannels.map((channel) => { const body = { username, - icon_emoji: iconEmoji, // eslint-disable-line camelcase + icon_emoji: emojiIcon, // eslint-disable-line camelcase text, channel: `#${channel}`, attachments: [ @@ -60,10 +71,22 @@ export default class SlackAddon extends Addon { }, ], }; - + let extraHeaders = {}; + if (typeof customHeaders === 'string' && customHeaders.length > 1) { + try { + extraHeaders = JSON.parse(customHeaders); + } catch (e) { + this.logger.warn( + `Could not parse the json in the customHeaders parameter. [${customHeaders}]`, + ); + } + } const requestOpts = { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + ...extraHeaders, + }, body: JSON.stringify(body), }; diff --git a/src/lib/addons/teams-definition.ts b/src/lib/addons/teams-definition.ts index 67d7f4c2d0..b37c3a1c8b 100644 --- a/src/lib/addons/teams-definition.ts +++ b/src/lib/addons/teams-definition.ts @@ -30,6 +30,18 @@ const teamsDefinition: IAddonDefinition = { required: true, sensitive: true, }, + { + name: 'customHeaders', + displayName: 'Extra HTTP Headers', + placeholder: `{ + "ISTIO_USER_KEY": "hunter2", + "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" + }`, + description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, + required: false, + sensitive: true, + type: 'textfield', + }, ], events: [ FEATURE_CREATED, diff --git a/src/lib/addons/teams.test.ts b/src/lib/addons/teams.test.ts index 9a7f167684..61f43f8e0f 100644 --- a/src/lib/addons/teams.test.ts +++ b/src/lib/addons/teams.test.ts @@ -145,3 +145,34 @@ test(`Should call teams webhook for toggled environment`, async () => { expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); }); + +test('Should include custom headers in call to teams', async () => { + const addon = new TeamsAddon({ + getLogger: noLogger, + unleashUrl: 'http://some-url.com', + }); + const event: IEvent = { + id: 2, + createdAt: new Date(), + type: FEATURE_ENVIRONMENT_DISABLED, + createdBy: 'some@user.com', + environment: 'development', + project: 'default', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + }, + }; + + const parameters = { + url: 'http://hooks.slack.com', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + + await addon.handleEvent(event, parameters); + expect(fetchRetryCalls).toHaveLength(1); + expect(fetchRetryCalls[0].url).toBe(parameters.url); + expect(fetchRetryCalls[0].options.body).toMatch(/disabled/); + expect(fetchRetryCalls[0].options.body).toMatchSnapshot(); + expect(fetchRetryCalls[0].options.headers).toMatchSnapshot(); +}); diff --git a/src/lib/addons/teams.ts b/src/lib/addons/teams.ts index c1a2aad781..44c284ee3f 100644 --- a/src/lib/addons/teams.ts +++ b/src/lib/addons/teams.ts @@ -8,6 +8,10 @@ import { } from './feature-event-formatter-md'; import { IEvent } from '../types/events'; +interface ITeamsParameters { + url: string; + customHeaders?: string; +} export default class TeamsAddon extends Addon { private msgFormatter: FeatureEventFormatter; @@ -17,8 +21,11 @@ export default class TeamsAddon extends Addon { } // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types - async handleEvent(event: IEvent, parameters: any): Promise { - const { url } = parameters; + async handleEvent( + event: IEvent, + parameters: ITeamsParameters, + ): Promise { + const { url, customHeaders } = parameters; const { createdBy } = event; const text = this.msgFormatter.format(event); const featureLink = this.msgFormatter.featureLink(event); @@ -56,9 +63,20 @@ export default class TeamsAddon extends Addon { ], }; + let extraHeaders = {}; + if (typeof customHeaders === 'string' && customHeaders.length > 1) { + try { + extraHeaders = JSON.parse(customHeaders); + } catch (e) { + this.logger.warn( + `Could not parse the json in the customHeaders parameter. [${customHeaders}]`, + ); + } + } + const requestOpts = { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { 'Content-Type': 'application/json', ...extraHeaders }, body: JSON.stringify(body), }; const res = await this.fetchRetry(url, requestOpts); diff --git a/src/lib/addons/webhook-definition.ts b/src/lib/addons/webhook-definition.ts index 75bf8415d1..f85a1f0a0a 100644 --- a/src/lib/addons/webhook-definition.ts +++ b/src/lib/addons/webhook-definition.ts @@ -78,6 +78,18 @@ const webhookDefinition: IAddonDefinition = { required: false, sensitive: false, }, + { + name: 'customHeaders', + displayName: 'Extra HTTP Headers', + placeholder: `{ + "ISTIO_USER_KEY": "hunter2", + "SOME_OTHER_CUSTOM_HTTP_HEADER": "SOMEVALUE" + }`, + description: `(Optional) Used to add extra HTTP Headers to the request the plugin fires off. Format here needs to be a valid json object of key value pairs where both key and value are strings`, + required: false, + sensitive: true, + type: 'textfield', + }, ], events: [ FEATURE_CREATED, diff --git a/src/lib/addons/webhook.test.ts b/src/lib/addons/webhook.test.ts index 197e52381e..43174b95df 100644 --- a/src/lib/addons/webhook.test.ts +++ b/src/lib/addons/webhook.test.ts @@ -114,3 +114,35 @@ test('Should format event with "authorization"', () => { expect(call.options.headers.Authorization).toBe(parameters.authorization); expect(call.options.body).toBe('feature-created on toggle some-toggle'); }); + +test('Should handle custom headers', async () => { + const addon = new WebhookAddon({ getLogger: noLogger }); + const event: IEvent = { + id: 1, + createdAt: new Date(), + type: FEATURE_CREATED, + createdBy: 'some@user.com', + featureName: 'some-toggle', + data: { + name: 'some-toggle', + enabled: false, + strategies: [{ name: 'default' }], + }, + }; + + const parameters = { + url: 'http://test.webhook.com/plain', + bodyTemplate: '{{event.type}} on toggle {{event.data.name}}', + contentType: 'text/plain', + authorization: 'API KEY 123abc', + customHeaders: `{ "MY_CUSTOM_HEADER": "MY_CUSTOM_VALUE" }`, + }; + + addon.handleEvent(event, parameters); + const call = fetchRetryCalls[0]; + expect(fetchRetryCalls.length).toBe(1); + expect(call.url).toBe(parameters.url); + expect(call.options.headers.Authorization).toBe(parameters.authorization); + expect(call.options.headers.MY_CUSTOM_HEADER).toBe('MY_CUSTOM_VALUE'); + expect(call.options.body).toBe('feature-created on toggle some-toggle'); +}); diff --git a/src/lib/addons/webhook.ts b/src/lib/addons/webhook.ts index 5241610c98..ae4000a57b 100644 --- a/src/lib/addons/webhook.ts +++ b/src/lib/addons/webhook.ts @@ -9,6 +9,7 @@ interface IParameters { bodyTemplate?: string; contentType?: string; authorization?: string; + customHeaders?: string; } export default class Webhook extends Addon { @@ -17,7 +18,8 @@ export default class Webhook extends Addon { } async handleEvent(event: IEvent, parameters: IParameters): Promise { - const { url, bodyTemplate, contentType, authorization } = parameters; + const { url, bodyTemplate, contentType, authorization, customHeaders } = + parameters; const context = { event, }; @@ -30,11 +32,22 @@ export default class Webhook extends Addon { body = JSON.stringify(event); } + let extraHeaders = {}; + if (typeof customHeaders === 'string' && customHeaders.length > 1) { + try { + extraHeaders = JSON.parse(customHeaders); + } catch (e) { + this.logger.warn( + `Could not parse the json in the customHeaders parameter. [${customHeaders}]`, + ); + } + } const requestOpts = { method: 'POST', headers: { 'Content-Type': contentType || 'application/json', Authorization: authorization || undefined, + ...extraHeaders, }, body, }; diff --git a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap index 69352dc914..99f7b3f047 100644 --- a/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap +++ b/src/test/e2e/api/openapi/__snapshots__/openapi.e2e.test.ts.snap @@ -12299,6 +12299,40 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 ], }, }, + "/api/admin/projects/{projectId}/tags": { + "put": { + "operationId": "addTagToFeatures", + "parameters": [ + { + "in": "path", + "name": "projectId", + "required": true, + "schema": { + "type": "string", + }, + }, + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/tagsBulkAddSchema", + }, + }, + }, + "description": "tagsBulkAddSchema", + "required": true, + }, + "responses": { + "200": { + "description": "This response has no body.", + }, + }, + "tags": [ + "Tags", + ], + }, + }, "/api/admin/splash/{id}": { "post": { "operationId": "updateSplashSettings", @@ -12933,30 +12967,6 @@ true,false,"[{""range"":""allTime"",""count"":15},{""range"":""30d"",""count"":9 ], }, }, - "/api/admin/tags/features": { - "put": { - "operationId": "addTagToFeatures", - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/tagsBulkAddSchema", - }, - }, - }, - "description": "tagsBulkAddSchema", - "required": true, - }, - "responses": { - "200": { - "description": "This response has no body.", - }, - }, - "tags": [ - "Tags", - ], - }, - }, "/api/admin/tags/{type}": { "get": { "operationId": "getTagsByType",