mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix sending secure settings when using K8S API for contact points (#93498)
This commit is contained in:
parent
bcab60d9e6
commit
cc68f1b673
@ -4,7 +4,7 @@
|
||||
*/
|
||||
|
||||
import { produce } from 'immer';
|
||||
import { remove } from 'lodash';
|
||||
import { merge, remove, set } from 'lodash';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { alertingApi } from 'app/features/alerting/unified/api/alertingApi';
|
||||
@ -349,9 +349,9 @@ export function useDeleteContactPoint({ alertmanager }: BaseAlertmanagerArgs) {
|
||||
* so we should not tell the API that we want to preserve it. Those values will instead be sent within `settings`
|
||||
*/
|
||||
const mapIntegrationSettingsForK8s = (integration: GrafanaManagedReceiverConfig): GrafanaManagedReceiverConfig => {
|
||||
const { secureSettings, ...restOfIntegration } = integration;
|
||||
|
||||
const { secureSettings, settings, ...restOfIntegration } = integration;
|
||||
const secureFields = Object.entries(secureSettings || {}).reduce((acc, [key, value]) => {
|
||||
// If a secure field has no (changed) value, then we tell the backend to persist it
|
||||
if (value === undefined) {
|
||||
return {
|
||||
...acc,
|
||||
@ -361,10 +361,24 @@ const mapIntegrationSettingsForK8s = (integration: GrafanaManagedReceiverConfig)
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const mappedSecureSettings = Object.entries(secureSettings || {}).reduce((acc, [key, value]) => {
|
||||
// If the value is an empty string/falsy value, then we need to omit it from the payload
|
||||
// so the backend knows to remove it
|
||||
if (!value) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
// Otherwise, we send the value of the secure field
|
||||
return set(acc, key, value);
|
||||
}, {});
|
||||
|
||||
// Merge settings properly with lodash so we don't lose any information from nested keys/secure settings
|
||||
const mergedSettings = merge({}, settings, mappedSecureSettings);
|
||||
|
||||
return {
|
||||
...restOfIntegration,
|
||||
secureFields,
|
||||
settings: { ...restOfIntegration.settings, ...secureSettings },
|
||||
settings: mergedSettings,
|
||||
};
|
||||
};
|
||||
const grafanaContactPointToK8sReceiver = (
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { clickSelectOption } from 'test/helpers/selectOptionInTest';
|
||||
import { render, waitFor } from 'test/test-utils';
|
||||
import { render, waitFor, screen } from 'test/test-utils';
|
||||
import { byLabelText, byRole, byTestId, byText } from 'testing-library-selector';
|
||||
|
||||
import { config } from '@grafana/runtime';
|
||||
import { disablePlugin } from 'app/features/alerting/unified/mocks/server/configure';
|
||||
import { captureRequests } from 'app/features/alerting/unified/mocks/server/events';
|
||||
import {
|
||||
setOnCallFeatures,
|
||||
setOnCallIntegrations,
|
||||
@ -33,6 +35,51 @@ const ui = {
|
||||
};
|
||||
|
||||
describe('GrafanaReceiverForm', () => {
|
||||
describe('alertingApiServer', () => {
|
||||
beforeEach(() => {
|
||||
config.featureToggles.alertingApiServer = true;
|
||||
});
|
||||
afterEach(() => {
|
||||
config.featureToggles.alertingApiServer = false;
|
||||
});
|
||||
|
||||
it('handles nested secure fields correctly', async () => {
|
||||
const capturedRequests = captureRequests(
|
||||
(req) => req.url.includes('/v0alpha1/namespaces/default/receivers') && req.method === 'POST'
|
||||
);
|
||||
const { user } = render(<GrafanaReceiverForm />);
|
||||
const { type, click } = user;
|
||||
|
||||
await waitFor(() => expect(ui.loadingIndicator.query()).not.toBeInTheDocument());
|
||||
|
||||
// Select MQTT receiver and fill out basic required fields for contact point
|
||||
await clickSelectOption(await byTestId('items.0.type').find(), 'MQTT');
|
||||
await type(screen.getByLabelText(/^name/i), 'mqtt contact point');
|
||||
await type(screen.getByLabelText(/broker url/i), 'broker url');
|
||||
await type(screen.getByLabelText(/topic/i), 'topic');
|
||||
|
||||
// Fill out fields that we know will be nested secure fields
|
||||
await click(screen.getByText(/optional mqtt settings/i));
|
||||
await click(screen.getByRole('button', { name: /^Add$/i }));
|
||||
await type(screen.getByLabelText(/ca certificate/i), 'some cert');
|
||||
|
||||
await click(screen.getByRole('button', { name: /save contact point/i }));
|
||||
|
||||
const [request] = await capturedRequests;
|
||||
const postRequestbody = await request.clone().json();
|
||||
|
||||
const integrationPayload = postRequestbody.spec.integrations[0];
|
||||
expect(integrationPayload.settings.tlsConfig).toEqual({
|
||||
// Expect the payload to have included the value of a secret field
|
||||
caCertificate: 'some cert',
|
||||
// And to not have removed other values (which would happen if we incorrectly merged settings together)
|
||||
insecureSkipVerify: false,
|
||||
});
|
||||
|
||||
expect(postRequestbody).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('OnCall contact point', () => {
|
||||
it('OnCall contact point should be disabled if OnCall integration is not enabled', async () => {
|
||||
disablePlugin(SupportedPlugin.OnCall);
|
||||
|
@ -0,0 +1,31 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`GrafanaReceiverForm alertingApiServer handles nested secure fields correctly 1`] = `
|
||||
{
|
||||
"metadata": {},
|
||||
"spec": {
|
||||
"integrations": [
|
||||
{
|
||||
"disableResolveMessage": false,
|
||||
"name": "mqtt contact point",
|
||||
"secureFields": {
|
||||
"password": true,
|
||||
"tlsConfig.clientCertificate": true,
|
||||
"tlsConfig.clientKey": true,
|
||||
},
|
||||
"settings": {
|
||||
"brokerUrl": "broker url",
|
||||
"retain": false,
|
||||
"tlsConfig": {
|
||||
"caCertificate": "some cert",
|
||||
"insecureSkipVerify": false,
|
||||
},
|
||||
"topic": "topic",
|
||||
},
|
||||
"type": "mqtt",
|
||||
},
|
||||
],
|
||||
"title": "mqtt contact point",
|
||||
},
|
||||
}
|
||||
`;
|
@ -2461,6 +2461,278 @@ export const grafanaAlertNotifiersMock: NotifierDTO[] = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'mqtt',
|
||||
name: 'MQTT',
|
||||
heading: 'MQTT settings',
|
||||
description: 'Sends notifications to an MQTT broker',
|
||||
info: 'The MQTT notifier sends messages to an MQTT broker. The message is sent to the topic specified in the configuration. ',
|
||||
options: [
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Broker URL',
|
||||
description: 'The URL of the MQTT broker.',
|
||||
placeholder: 'tcp://localhost:1883',
|
||||
propertyName: 'brokerUrl',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: true,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Topic',
|
||||
description: 'The topic to which the message will be sent.',
|
||||
placeholder: 'grafana/alerts',
|
||||
propertyName: 'topic',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: true,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'select',
|
||||
inputType: 'text',
|
||||
label: 'Message format',
|
||||
description:
|
||||
"The format of the message to be sent. If set to 'json', the message will be sent as a JSON object. If set to 'text', the message will be sent as a plain text string. By default json is used.",
|
||||
placeholder: 'json',
|
||||
propertyName: 'messageFormat',
|
||||
selectOptions: [
|
||||
{
|
||||
value: 'json',
|
||||
label: 'json',
|
||||
},
|
||||
{
|
||||
value: 'text',
|
||||
label: 'text',
|
||||
},
|
||||
],
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Client ID',
|
||||
description: 'The client ID to use when connecting to the MQTT broker. If blank, a random client ID is used.',
|
||||
placeholder: '',
|
||||
propertyName: 'clientId',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'textarea',
|
||||
inputType: '',
|
||||
label: 'Message',
|
||||
description: '',
|
||||
placeholder: '{{ template "default.message" . }}',
|
||||
propertyName: 'message',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Username',
|
||||
description: 'The username to use when connecting to the MQTT broker.',
|
||||
placeholder: '',
|
||||
propertyName: 'username',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'input',
|
||||
inputType: 'text',
|
||||
label: 'Password',
|
||||
description: 'The password to use when connecting to the MQTT broker.',
|
||||
placeholder: '',
|
||||
propertyName: 'password',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: true,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'select',
|
||||
inputType: '',
|
||||
label: 'QoS',
|
||||
description: 'The quality of service to use when sending the message.',
|
||||
placeholder: '',
|
||||
propertyName: 'qos',
|
||||
selectOptions: [
|
||||
{
|
||||
value: '0',
|
||||
label: 'At most once (0)',
|
||||
},
|
||||
{
|
||||
value: '1',
|
||||
label: 'At least once (1)',
|
||||
},
|
||||
{
|
||||
value: '2',
|
||||
label: 'Exactly once (2)',
|
||||
},
|
||||
],
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'checkbox',
|
||||
inputType: '',
|
||||
label: 'Retain',
|
||||
description: 'If set to true, the message will be retained by the broker.',
|
||||
placeholder: '',
|
||||
propertyName: 'retain',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'subform',
|
||||
inputType: '',
|
||||
label: 'TLS',
|
||||
description: 'TLS configuration options',
|
||||
placeholder: '',
|
||||
propertyName: 'tlsConfig',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
subformOptions: [
|
||||
{
|
||||
element: 'checkbox',
|
||||
inputType: '',
|
||||
label: 'Disable certificate verification',
|
||||
description: "Do not verify the broker's certificate chain and host name.",
|
||||
placeholder: '',
|
||||
propertyName: 'insecureSkipVerify',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: false,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'textarea',
|
||||
inputType: 'text',
|
||||
label: 'CA Certificate',
|
||||
description: "Certificate in PEM format to use when verifying the broker's certificate chain.",
|
||||
placeholder: '',
|
||||
propertyName: 'caCertificate',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: true,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'textarea',
|
||||
inputType: 'text',
|
||||
label: 'Client Certificate',
|
||||
description: 'Client certificate in PEM format to use when connecting to the broker.',
|
||||
placeholder: '',
|
||||
propertyName: 'clientCertificate',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: true,
|
||||
dependsOn: '',
|
||||
},
|
||||
{
|
||||
element: 'textarea',
|
||||
inputType: 'text',
|
||||
label: 'Client Key',
|
||||
description: 'Client key in PEM format to use when connecting to the broker.',
|
||||
placeholder: '',
|
||||
propertyName: 'clientKey',
|
||||
selectOptions: null,
|
||||
showWhen: {
|
||||
field: '',
|
||||
is: '',
|
||||
},
|
||||
required: false,
|
||||
validationRule: '',
|
||||
secure: true,
|
||||
dependsOn: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
type: 'opsgenie',
|
||||
name: 'OpsGenie',
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
getPluginMissingHandler,
|
||||
} from 'app/features/alerting/unified/mocks/server/handlers/plugins';
|
||||
import { SupportedPlugin } from 'app/features/alerting/unified/types/pluginBridges';
|
||||
import { clearPluginSettingsCache } from 'app/features/plugins/pluginSettings';
|
||||
import { AlertManagerCortexConfig, AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
|
||||
import { FolderDTO } from 'app/types';
|
||||
|
||||
@ -126,6 +127,7 @@ export const removePlugin = (pluginId: string) => {
|
||||
|
||||
/** Make a plugin respond with `enabled: false`, as if its installed but disabled */
|
||||
export const disablePlugin = (pluginId: SupportedPlugin) => {
|
||||
clearPluginSettingsCache(pluginId);
|
||||
server.use(getDisabledPluginHandler(pluginId));
|
||||
};
|
||||
|
||||
|
@ -43,7 +43,7 @@ const createNamespacedReceiverHandler = () =>
|
||||
http.post<{ namespace: string }>(
|
||||
`${ALERTING_API_SERVER_BASE_URL}/namespaces/:namespace/receivers`,
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
const body = await request.clone().json();
|
||||
return HttpResponse.json(body);
|
||||
}
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user