From ce4a9c3e67772f2d479adb5e4e661bf60fbd4bcd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Mierzwa?= Date: Tue, 11 Jan 2022 22:35:53 +0000 Subject: [PATCH] fix(ui): escape label values in silence form Fixes #3866 --- CHANGELOG.md | 1 + demo/generator.py | 21 ++ ui/src/Common/Select.ts | 2 + .../Grid/AlertGrid/GridLabelSelect.tsx | 8 +- .../AlertGroupCollapseConfiguration.tsx | 1 + .../AlertGroupSortConfiguration.tsx | 6 +- .../MainModal/Configuration/GridLabelName.tsx | 13 +- .../Configuration/ThemeConfiguration.tsx | 1 + ui/src/Components/SilenceModal/Matchers.ts | 19 +- .../SilenceModal/SilenceForm.test.tsx | 36 +++- .../SilenceMatch/LabelValueInput.test.tsx | 19 +- .../SilenceMatch/LabelValueInput.tsx | 5 +- .../SilenceMatch/MatchCounter.test.tsx | 44 ++++ ui/src/Stores/Settings.ts | 34 ++- ui/src/Stores/SilenceFormStore.test.ts | 198 ++++++++++++++---- ui/src/Stores/SilenceFormStore.ts | 12 +- .../SilenceFormStore.test.ts.snap | 83 ++++++++ ui/src/__fixtures__/useFetchGet.ts | 4 + 18 files changed, 434 insertions(+), 73 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 50888efed..7f80f2e6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Messages are now logged correctly when both `--log.format=json` and `--log.timestamp=true` flags are set #3822. +- Escape label values in silence form #3866. ### Changed diff --git a/demo/generator.py b/demo/generator.py index 07b0802d5..ec22afe31 100755 --- a/demo/generator.py +++ b/demo/generator.py @@ -557,6 +557,26 @@ class RichAnnotations(AlertGenerator): ] +class RegexEscapeValue(AlertGenerator): + name = "Labels with rich values" + comment = "This alert will have rich labels" + + def alerts(self): + return [ + newAlert( + self._labels( + instance="server{}".format(i), + cluster="staging", + job="textfile_exporter", + region="SA", + device="Device {} (main)".format(i % 2), + regex="^device{}(.+)bar\\$".format(i), + ), + ) + for i in range(0, 10) + ] + + if __name__ == "__main__": generators = [ AlwaysOnAlert(MAX_INTERVAL), @@ -573,6 +593,7 @@ if __name__ == "__main__": SilencedAlertWithJiraLink(MAX_INTERVAL), PaginationTest(MAX_INTERVAL), RichAnnotations(MAX_INTERVAL), + RegexEscapeValue(MAX_INTERVAL), ] while True: for g in generators: diff --git a/ui/src/Common/Select.ts b/ui/src/Common/Select.ts index d66bd1a93..0bba00709 100644 --- a/ui/src/Common/Select.ts +++ b/ui/src/Common/Select.ts @@ -5,6 +5,7 @@ export const NewLabelValue = (v: string): string => `New value: ${v}`; export interface OptionT { label: string; value: string; + wasCreated: boolean; } export interface MultiValueOptionT { @@ -15,4 +16,5 @@ export interface MultiValueOptionT { export const StringToOption = (value: string): OptionT => ({ label: value, value: value, + wasCreated: false, }); diff --git a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx index 4634a8416..4975e4bf5 100644 --- a/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx +++ b/ui/src/Components/Grid/AlertGrid/GridLabelSelect.tsx @@ -27,10 +27,10 @@ import { ThemeContext } from "Components/Theme"; import { useOnClickOutside } from "Hooks/useOnClickOutside"; const specialLabels: OptionT[] = [ - { label: "Automatic selection", value: "@auto" }, - { label: "@alertmanager", value: "@alertmanager" }, - { label: "@cluster", value: "@cluster" }, - { label: "@receiver", value: "@receiver" }, + { label: "Automatic selection", value: "@auto", wasCreated: false }, + { label: "@alertmanager", value: "@alertmanager", wasCreated: false }, + { label: "@cluster", value: "@cluster", wasCreated: false }, + { label: "@receiver", value: "@receiver", wasCreated: false }, ]; const NullContainer: FC = () => null; diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupCollapseConfiguration.tsx b/ui/src/Components/MainModal/Configuration/AlertGroupCollapseConfiguration.tsx index 147ba93bb..74426a3a8 100644 --- a/ui/src/Components/MainModal/Configuration/AlertGroupCollapseConfiguration.tsx +++ b/ui/src/Components/MainModal/Configuration/AlertGroupCollapseConfiguration.tsx @@ -24,6 +24,7 @@ const AlertGroupCollapseConfiguration: FC<{ return { label: settingsStore.alertGroupConfig.options[val].label, value: val, + wasCreated: false, }; }; diff --git a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.tsx b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.tsx index c858067f8..95bb86b65 100644 --- a/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.tsx +++ b/ui/src/Components/MainModal/Configuration/AlertGroupSortConfiguration.tsx @@ -30,7 +30,11 @@ const AlertGroupSortConfiguration: FC<{ }; const valueToOption = (val: SortOrderT): OptionT => { - return { label: settingsStore.gridConfig.options[val].label, value: val }; + return { + label: settingsStore.gridConfig.options[val].label, + value: val, + wasCreated: false, + }; }; const hideReverse = diff --git a/ui/src/Components/MainModal/Configuration/GridLabelName.tsx b/ui/src/Components/MainModal/Configuration/GridLabelName.tsx index b05cbcf15..3d8649075 100644 --- a/ui/src/Components/MainModal/Configuration/GridLabelName.tsx +++ b/ui/src/Components/MainModal/Configuration/GridLabelName.tsx @@ -14,14 +14,15 @@ const disabledLabel = "Disable multi-grid"; const valueToOption = (v: string) => ({ label: v ? v : disabledLabel, value: v, + wasCreated: false, }); const staticValues = [ - { label: disabledLabel, value: "" }, - { label: "Automatic selection", value: "@auto" }, - { label: "@alertmanager", value: "@alertmanager" }, - { label: "@cluster", value: "@cluster" }, - { label: "@receiver", value: "@receiver" }, + { label: disabledLabel, value: "", wasCreated: false }, + { label: "Automatic selection", value: "@auto", wasCreated: false }, + { label: "@alertmanager", value: "@alertmanager", wasCreated: false }, + { label: "@cluster", value: "@cluster", wasCreated: false }, + { label: "@receiver", value: "@receiver", wasCreated: false }, ]; const GridLabelName: FC<{ @@ -35,7 +36,7 @@ const GridLabelName: FC<{ const defaultValue = settingsStore.multiGridConfig.config.gridLabel === "@auto" - ? { label: "Automatic selection", value: "@auto" } + ? { label: "Automatic selection", value: "@auto", wasCreated: false } : valueToOption(settingsStore.multiGridConfig.config.gridLabel); return ( diff --git a/ui/src/Components/MainModal/Configuration/ThemeConfiguration.tsx b/ui/src/Components/MainModal/Configuration/ThemeConfiguration.tsx index 3b4f9e958..4d4afe093 100644 --- a/ui/src/Components/MainModal/Configuration/ThemeConfiguration.tsx +++ b/ui/src/Components/MainModal/Configuration/ThemeConfiguration.tsx @@ -24,6 +24,7 @@ const ThemeConfiguration: FC<{ return { label: settingsStore.themeConfig.options[val].label, value: val, + wasCreated: false, }; }; diff --git a/ui/src/Components/SilenceModal/Matchers.ts b/ui/src/Components/SilenceModal/Matchers.ts index b30c2ecf9..00a6af3cb 100644 --- a/ui/src/Components/SilenceModal/Matchers.ts +++ b/ui/src/Components/SilenceModal/Matchers.ts @@ -1,12 +1,23 @@ import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query"; import type { MultiValueOptionT } from "Common/Select"; -import { MatcherT, MatcherToOperator } from "Stores/SilenceFormStore"; +import { + MatcherT, + MatcherToOperator, + EscapeRegex, +} from "Stores/SilenceFormStore"; const MatcherToFilter = (matcher: MatcherT): string => { + const values = matcher.values.map((v) => + v.wasCreated + ? v + : matcher.isRegex + ? { ...v, value: EscapeRegex(v.value) } + : v + ); const value = - matcher.values.length > 1 - ? `(${matcher.values.map((v) => v.value).join("|")})` - : matcher.values[0].value; + values.length > 1 + ? `(${values.map((v) => v.value).join("|")})` + : values[0].value; return FormatQuery( matcher.name, MatcherToOperator(matcher), diff --git a/ui/src/Components/SilenceModal/SilenceForm.test.tsx b/ui/src/Components/SilenceModal/SilenceForm.test.tsx index ae1ee71c4..8935dba7a 100644 --- a/ui/src/Components/SilenceModal/SilenceForm.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceForm.test.tsx @@ -110,6 +110,7 @@ describe(" matchers", () => { { label: "alertnameEqual", value: "alertnameEqual", + wasCreated: false, }, ], }, @@ -122,6 +123,7 @@ describe(" matchers", () => { { label: "alertnameNotEqual", value: "alertnameNotEqual", + wasCreated: false, }, ], }, @@ -134,6 +136,7 @@ describe(" matchers", () => { { label: ".*alertnameRegex.*", value: ".*alertnameRegex.*", + wasCreated: false, }, ], }, @@ -146,6 +149,7 @@ describe(" matchers", () => { { label: ".*alertnameNegativeRegex.*", value: ".*alertnameNegativeRegex.*", + wasCreated: false, }, ], }, @@ -158,6 +162,7 @@ describe(" matchers", () => { { label: "clusterEqual", value: "clusterEqual", + wasCreated: false, }, ], }, @@ -170,6 +175,7 @@ describe(" matchers", () => { { label: "clusterNotEqual", value: "clusterNotEqual", + wasCreated: false, }, ], }, @@ -182,6 +188,7 @@ describe(" matchers", () => { { label: ".*clusterRegex.*", value: ".*clusterRegex.*", + wasCreated: false, }, ], }, @@ -194,6 +201,7 @@ describe(" matchers", () => { { label: ".*clusterNegativeRegex.*", value: ".*clusterNegativeRegex.*", + wasCreated: false, }, ], }, @@ -206,6 +214,7 @@ describe(" matchers", () => { { label: "fooEqual", value: "fooEqual", + wasCreated: false, }, ], }, @@ -218,6 +227,7 @@ describe(" matchers", () => { { label: "fooNotEqual", value: "fooNotEqual", + wasCreated: false, }, ], }, @@ -230,6 +240,7 @@ describe(" matchers", () => { { label: ".*fooRegex.*", value: ".*fooRegex.*", + wasCreated: false, }, ], }, @@ -242,6 +253,7 @@ describe(" matchers", () => { { label: ".*fooNegativeRegex.*", value: ".*fooNegativeRegex.*", + wasCreated: false, }, ], }, @@ -288,6 +300,7 @@ describe(" matchers", () => { { label: "alertnameEqual", value: "alertnameEqual", + wasCreated: false, }, ], }, @@ -300,6 +313,7 @@ describe(" matchers", () => { { label: "alertnameNotEqual", value: "alertnameNotEqual", + wasCreated: false, }, ], }, @@ -312,6 +326,7 @@ describe(" matchers", () => { { label: ".*alertnameRegex.*", value: ".*alertnameRegex.*", + wasCreated: false, }, ], }, @@ -324,6 +339,7 @@ describe(" matchers", () => { { label: ".*alertnameNegativeRegex.*", value: ".*alertnameNegativeRegex.*", + wasCreated: false, }, ], }, @@ -336,6 +352,7 @@ describe(" matchers", () => { { label: "clusterEqual", value: "clusterEqual", + wasCreated: false, }, ], }, @@ -348,6 +365,7 @@ describe(" matchers", () => { { label: "clusterNotEqual", value: "clusterNotEqual", + wasCreated: false, }, ], }, @@ -360,6 +378,7 @@ describe(" matchers", () => { { label: ".*clusterRegex.*", value: ".*clusterRegex.*", + wasCreated: false, }, ], }, @@ -372,6 +391,7 @@ describe(" matchers", () => { { label: ".*clusterNegativeRegex.*", value: ".*clusterNegativeRegex.*", + wasCreated: false, }, ], }, @@ -384,6 +404,7 @@ describe(" matchers", () => { { label: "fooEqual", value: "fooEqual", + wasCreated: false, }, ], }, @@ -396,6 +417,7 @@ describe(" matchers", () => { { label: "fooNotEqual", value: "fooNotEqual", + wasCreated: false, }, ], }, @@ -408,6 +430,7 @@ describe(" matchers", () => { { label: ".*fooRegex.*", value: ".*fooRegex.*", + wasCreated: false, }, ], }, @@ -420,6 +443,7 @@ describe(" matchers", () => { { label: ".*fooNegativeRegex.*", value: ".*fooNegativeRegex.*", + wasCreated: false, }, ], }, @@ -531,7 +555,9 @@ describe(" preview", () => { it("clicking on the copy button copies form link to the clipboard", () => { const matcher = NewEmptyMatcher(); matcher.name = "job"; - matcher.values = [{ label: "node_exporter", value: "node_exporter" }]; + matcher.values = [ + { label: "node_exporter", value: "node_exporter", wasCreated: false }, + ]; silenceFormStore.data.setMatchers([matcher]); silenceFormStore.data.setAlertmanagers([{ label: "am1", value: ["am1"] }]); silenceFormStore.data.setAuthor("me@example.com"); @@ -550,7 +576,9 @@ describe(" preview", () => { it("silence form share link doesn't change on new input", () => { const matcher = NewEmptyMatcher(); matcher.name = "job"; - matcher.values = [{ label: "node_exporter", value: "node_exporter" }]; + matcher.values = [ + { label: "node_exporter", value: "node_exporter", wasCreated: false }, + ]; silenceFormStore.data.setMatchers([matcher]); silenceFormStore.data.setAlertmanagers([{ label: "am1", value: ["am1"] }]); silenceFormStore.data.setAuthor("me@example.com"); @@ -620,7 +648,9 @@ describe("", () => { it("calling submit move form to the 'Preview' stage when form is valid", () => { const matcher = NewEmptyMatcher(); matcher.name = "job"; - matcher.values = [{ label: "node_exporter", value: "node_exporter" }]; + matcher.values = [ + { label: "node_exporter", value: "node_exporter", wasCreated: false }, + ]; silenceFormStore.data.setMatchers([matcher]); silenceFormStore.data.setAlertmanagers([{ label: "am1", value: ["am1"] }]); silenceFormStore.data.setAuthor("me@example.com"); diff --git a/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.test.tsx b/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.test.tsx index 9831dbdd0..40876ebbd 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.test.tsx @@ -10,8 +10,9 @@ import { MatcherWithIDT, } from "Stores/SilenceFormStore"; import { ThemeContext } from "Components/Theme"; -import { StringToOption } from "Common/Select"; +import { OptionT, StringToOption } from "Common/Select"; import { LabelValueInput } from "./LabelValueInput"; +import { act } from "react-dom/test-utils"; let silenceFormStore: SilenceFormStore; let matcher: MatcherWithIDT; @@ -123,6 +124,22 @@ describe("", () => { expect(matcher.isRegex).toBe(true); }); + it("creating a manual option sets wasCreated=true", () => { + const tree = MountedLabelValueInput(true); + const input = tree.find("Select").instance(); + const options: OptionT[] = [ + { label: "foo", value: "foo", wasCreated: false }, + ]; + act(() => { + (input.props as any).onChange(options, { action: "create-option" }); + }); + expect(matcher.values[0]).toStrictEqual({ + label: "foo", + value: "foo", + wasCreated: true, + }); + }); + it("removing last value sets matcher.values to []", () => { matcher.values = [StringToOption("dev"), StringToOption("staging")]; const tree = MountedLabelValueInput(true); diff --git a/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.tsx b/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.tsx index 1290f92b5..3d6abce64 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/LabelValueInput.tsx @@ -90,13 +90,16 @@ const LabelValueInput: FC<{ placeholder={isValid ? "Label value" : } onChange={( newValue: OnChangeValue, - _: ActionMeta + meta: ActionMeta ) => { matcher.values = newValue as OptionT[]; // force regex if we have multiple values if (matcher.values.length > 1 && matcher.isRegex === false) { matcher.isRegex = true; } + if (meta.action === "create-option") { + matcher.values[matcher.values.length - 1].wasCreated = true; + } }} hideSelectedOptions isMulti diff --git a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx index 164c01863..50edd2dc1 100644 --- a/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx +++ b/ui/src/Components/SilenceModal/SilenceMatch/MatchCounter.test.tsx @@ -145,6 +145,50 @@ describe("", () => { ).toBe("./alertList.json?q=foo%3D~%5Ebar%24"); }); + it("sends correct query string for a 'foo=(x)' matcher with wasCreated=true & isRegex=false", () => { + const v = StringToOption("(x)"); + v.wasCreated = true; + matcher.values = [v]; + matcher.isRegex = false; + MountedMatchCounter(); + expect( + (useFetchGet as jest.MockedFunction).mock.calls[0][0] + ).toBe("./alertList.json?q=foo%3D%28x%29"); + }); + + it("sends correct query string for a 'foo=(x)' matcher with wasCreated=true & isRegex=true", () => { + const v = StringToOption("(x)"); + v.wasCreated = true; + matcher.values = [v]; + matcher.isRegex = true; + MountedMatchCounter(); + expect( + (useFetchGet as jest.MockedFunction).mock.calls[0][0] + ).toBe("./alertList.json?q=foo%3D~%5E%28x%29%24"); + }); + + it("sends correct query string for a 'foo=(x)' matcher with wasCreated=false & isRegex=false", () => { + const v = StringToOption("(x)"); + v.wasCreated = false; + matcher.values = [v]; + matcher.isRegex = false; + MountedMatchCounter(); + expect( + (useFetchGet as jest.MockedFunction).mock.calls[0][0] + ).toBe("./alertList.json?q=foo%3D%28x%29"); + }); + + it("sends correct query string for a 'foo=(x)' matcher with wasCreated=false & isRegex=true", () => { + const v = StringToOption("(x)"); + v.wasCreated = false; + matcher.values = [v]; + matcher.isRegex = true; + MountedMatchCounter(); + expect( + (useFetchGet as jest.MockedFunction).mock.calls[0][0] + ).toBe("./alertList.json?q=foo%3D~%5E%5C%28x%5C%29%24"); + }); + it("sends correct query string for a 'foo=~(bar|baz)' matcher", () => { matcher.values = [StringToOption("bar"), StringToOption("baz")]; matcher.isRegex = true; diff --git a/ui/src/Stores/Settings.ts b/ui/src/Stores/Settings.ts index 208ea13b1..79cb3482c 100644 --- a/ui/src/Stores/Settings.ts +++ b/ui/src/Stores/Settings.ts @@ -59,12 +59,21 @@ interface AlertGroupConfigStorage { } class AlertGroupConfig { options = Object.freeze({ - expanded: { label: "Always expanded", value: "expanded" }, + expanded: { + label: "Always expanded", + value: "expanded", + wasCreated: false, + }, collapsedOnMobile: { label: "Collapse on mobile", value: "collapsedOnMobile", + wasCreated: false, + }, + collapsed: { + label: "Always collapsed", + value: "collapsed", + wasCreated: false, }, - collapsed: { label: "Always collapsed", value: "collapsed" }, }); config: AlertGroupConfigStorage; @@ -128,10 +137,18 @@ interface GridConfigStorage { } class GridConfig { options = Object.freeze({ - default: { label: "Use defaults from karma config file", value: "default" }, - disabled: { label: "No sorting", value: "disabled" }, - startsAt: { label: "Sort by alert timestamp", value: "startsAt" }, - label: { label: "Sort by alert label", value: "label" }, + default: { + label: "Use defaults from karma config file", + value: "default", + wasCreated: false, + }, + disabled: { label: "No sorting", value: "disabled", wasCreated: false }, + startsAt: { + label: "Sort by alert timestamp", + value: "startsAt", + wasCreated: false, + }, + label: { label: "Sort by alert label", value: "label", wasCreated: false }, }); config: GridConfigStorage; @@ -206,9 +223,10 @@ class ThemeConfig { auto: { label: "Automatic theme, follow browser preference", value: "auto", + wasCreated: false, }, - light: { label: "Light theme", value: "light" }, - dark: { label: "Dark theme", value: "dark" }, + light: { label: "Light theme", value: "light", wasCreated: false }, + dark: { label: "Dark theme", value: "dark", wasCreated: false }, }); this.config = localStored( diff --git a/ui/src/Stores/SilenceFormStore.test.ts b/ui/src/Stores/SilenceFormStore.test.ts index 7a87e356e..1a8f0388b 100644 --- a/ui/src/Stores/SilenceFormStore.test.ts +++ b/ui/src/Stores/SilenceFormStore.test.ts @@ -174,14 +174,14 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "job", - values: [{ label: "mock", value: "mock" }], + values: [{ label: "mock", value: "mock", wasCreated: false }], isRegex: false, }) ); @@ -189,12 +189,12 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "instance", values: [ - { label: "dev1", value: "dev1" }, - { label: "prod1", value: "prod1" }, - { label: "prod2", value: "prod2" }, - { label: "dev2", value: "dev2" }, - { label: "dev3", value: "dev3" }, - { label: "dev4", value: "dev4" }, + { label: "dev1", value: "dev1", wasCreated: false }, + { label: "prod1", value: "prod1", wasCreated: false }, + { label: "prod2", value: "prod2", wasCreated: false }, + { label: "dev2", value: "dev2", wasCreated: false }, + { label: "dev3", value: "dev3", wasCreated: false }, + { label: "dev4", value: "dev4", wasCreated: false }, ], isRegex: true, }) @@ -203,8 +203,8 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "cluster", values: [ - { label: "dev", value: "dev" }, - { label: "prod", value: "prod" }, + { label: "dev", value: "dev", wasCreated: false }, + { label: "prod", value: "prod", wasCreated: false }, ], isRegex: true, }) @@ -232,7 +232,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); @@ -240,8 +240,8 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "instance", values: [ - { label: "prod1", value: "prod1" }, - { label: "prod2", value: "prod2" }, + { label: "prod1", value: "prod1", wasCreated: false }, + { label: "prod2", value: "prod2", wasCreated: false }, ], isRegex: true, }) @@ -249,7 +249,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "cluster", - values: [{ label: "prod", value: "prod" }], + values: [{ label: "prod", value: "prod", wasCreated: false }], isRegex: false, }) ); @@ -277,7 +277,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); @@ -285,8 +285,8 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "instance", values: [ - { label: "dev1", value: "dev1" }, - { label: "prod1", value: "prod1" }, + { label: "dev1", value: "dev1", wasCreated: false }, + { label: "prod1", value: "prod1", wasCreated: false }, ], isRegex: true, }) @@ -295,8 +295,8 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "cluster", values: [ - { label: "dev", value: "dev" }, - { label: "prod", value: "prod" }, + { label: "dev", value: "dev", wasCreated: false }, + { label: "prod", value: "prod", wasCreated: false }, ], isRegex: true, }) @@ -348,7 +348,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); @@ -356,8 +356,8 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "instance", values: [ - { label: "1", value: "1" }, - { label: "3", value: "3" }, + { label: "1", value: "1", wasCreated: false }, + { label: "3", value: "3", wasCreated: false }, ], isRegex: true, }) @@ -394,7 +394,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); @@ -415,28 +415,28 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "job", - values: [{ label: "mock", value: "mock" }], + values: [{ label: "mock", value: "mock", wasCreated: false }], isRegex: false, }) ); expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "instance", - values: [{ label: "prod1", value: "prod1" }], + values: [{ label: "prod1", value: "prod1", wasCreated: false }], isRegex: false, }) ); expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "cluster", - values: [{ label: "prod", value: "prod" }], + values: [{ label: "prod", value: "prod", wasCreated: false }], isRegex: false, }) ); @@ -454,7 +454,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "alertname", - values: [{ label: "FakeAlert", value: "FakeAlert" }], + values: [{ label: "FakeAlert", value: "FakeAlert", wasCreated: false }], isRegex: false, }) ); @@ -505,7 +505,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "region", - values: [{ label: "AF", value: "AF" }], + values: [{ label: "AF", value: "AF", wasCreated: false }], isRegex: false, }) ); @@ -513,9 +513,9 @@ describe("SilenceFormStore.data", () => { expect.objectContaining({ name: "alertname", values: [ - { label: "Alert1", value: "Alert1" }, - { label: "Alert2", value: "Alert2" }, - { label: "Alert3", value: "Alert3" }, + { label: "Alert1", value: "Alert1", wasCreated: false }, + { label: "Alert2", value: "Alert2", wasCreated: false }, + { label: "Alert3", value: "Alert3", wasCreated: false }, ], isRegex: true, }) @@ -523,7 +523,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "job", - values: [{ label: "mock", value: "mock" }], + values: [{ label: "mock", value: "mock", wasCreated: false }], isRegex: false, }) ); @@ -558,7 +558,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "regex", - values: [{ label: "equal", value: "equal" }], + values: [{ label: "equal", value: "equal", wasCreated: false }], isRegex: true, isEqual: true, }) @@ -566,7 +566,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "regex", - values: [{ label: "notEqual", value: "notEqual" }], + values: [{ label: "notEqual", value: "notEqual", wasCreated: false }], isRegex: true, isEqual: false, }) @@ -574,7 +574,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "notRegex", - values: [{ label: "equal", value: "equal" }], + values: [{ label: "equal", value: "equal", wasCreated: false }], isRegex: false, isEqual: true, }) @@ -582,7 +582,7 @@ describe("SilenceFormStore.data", () => { expect(store.data.matchers).toContainEqual( expect.objectContaining({ name: "notRegex", - values: [{ label: "notEqual", value: "notEqual" }], + values: [{ label: "notEqual", value: "notEqual", wasCreated: false }], isRegex: false, isEqual: false, }) @@ -694,7 +694,11 @@ describe("SilenceFormStore.data", () => { expect(silenceFormStorestore.data.matchers).toContainEqual( expect.objectContaining({ name: t.result.name, - values: t.result.values.map((v) => ({ label: v, value: v })), + values: t.result.values.map((v) => ({ + label: v, + value: v, + wasCreated: false, + })), isRegex: t.matcher.isRegex, isEqual: t.matcher.isEqual, }) @@ -732,6 +736,112 @@ describe("SilenceFormStore.data", () => { expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); }); + it("toAlertmanagerPayload creates payload that matches snapshot with regex values", () => { + store.data.setMatchers([ + { + id: "1", + name: "notEqualRegexAuto", + values: [{ label: "foo", value: "^(.+)$", wasCreated: false }], + isEqual: false, + isRegex: true, + }, + { + id: "2", + name: "equalRegexAuto", + values: [{ label: "foo", value: "^(.+)$", wasCreated: false }], + isEqual: true, + isRegex: true, + }, + { + id: "3", + name: "equalNotRegexAuto", + values: [{ label: "foo", value: "^(.+)$", wasCreated: false }], + isEqual: true, + isRegex: false, + }, + { + id: "4", + name: "notEqualnotRegexAuto", + values: [{ label: "foo", value: "^(.+)$", wasCreated: false }], + isEqual: false, + isRegex: false, + }, + { + id: "5", + name: "notEqualRegexCreated", + values: [{ label: "foo", value: "^(.+)$", wasCreated: true }], + isEqual: false, + isRegex: true, + }, + { + id: "6", + name: "equalRegexCreated", + values: [{ label: "foo", value: "^(.+)$", wasCreated: true }], + isEqual: true, + isRegex: true, + }, + { + id: "7", + name: "equalNotRegexCreated", + values: [{ label: "foo", value: "^(.+)$", wasCreated: true }], + isEqual: true, + isRegex: false, + }, + { + id: "8", + name: "notEqualnotRegexAuto", + values: [{ label: "foo", value: "^(.+)$", wasCreated: true }], + isEqual: false, + isRegex: false, + }, + { + id: "9", + name: "notEqualRegexCreatedMulti", + values: [ + { label: "foo", value: "^(.+)$", wasCreated: true }, + { label: "bar", value: "\\", wasCreated: true }, + ], + isEqual: false, + isRegex: true, + }, + { + id: "10", + name: "equalRegexCreatedMulti", + values: [ + { label: "foo", value: "^(.+)$", wasCreated: true }, + { label: "bar", value: "\\", wasCreated: true }, + ], + isEqual: true, + isRegex: true, + }, + { + id: "11", + name: "notEqualRegexAuto", + values: [ + { label: "foo", value: "^(.+)$", wasCreated: false }, + { label: "bar", value: "\\", wasCreated: true }, + ], + isEqual: false, + isRegex: true, + }, + { + id: "12", + name: "equalRegexAuto", + values: [ + { label: "foo", value: "^(.+)$", wasCreated: false }, + { label: "bar", value: "\\", wasCreated: true }, + ], + isEqual: true, + isRegex: true, + }, + ]); + store.data.setStart(new Date(Date.UTC(2000, 1, 1, 0, 0, 0))); + store.data.setEnd(new Date(Date.UTC(2000, 1, 1, 1, 0, 0))); + store.data.setAuthor("me@example.com"); + store.data.setComment("toAlertmanagerPayload test"); + expect(store.data.toAlertmanagerPayload).toMatchSnapshot(); + }); + it("dumps to base64 and back", () => { store.data.setMatchers([ MockMatcher("foo", [StringToOption("bar")]), @@ -793,7 +903,7 @@ describe("SilenceFormStore.data", () => { describe("SilenceFormStore.data.isValid", () => { it("isValid returns 'false' if alertmanagers list is empty", () => { store.data.setMatchers([ - MockMatcher("foo", [{ label: "bar", value: "bar" }]), + MockMatcher("foo", [{ label: "bar", value: "bar", wasCreated: false }]), ]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); @@ -810,7 +920,9 @@ describe("SilenceFormStore.data.isValid", () => { it("isValid returns 'false' if matchers list is pupulated when a matcher without any name", () => { store.data.setAlertmanagers([MockAlertmanagerOption()]); - store.data.setMatchers([MockMatcher("", [{ label: "bar", value: "bar" }])]); + store.data.setMatchers([ + MockMatcher("", [{ label: "bar", value: "bar", wasCreated: false }]), + ]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); expect(store.data.isValid).toBe(false); @@ -835,7 +947,7 @@ describe("SilenceFormStore.data.isValid", () => { it("isValid returns 'false' if author is empty", () => { store.data.setAlertmanagers([MockAlertmanagerOption()]); store.data.setMatchers([ - MockMatcher("foo", [{ label: "bar", value: "bar" }]), + MockMatcher("foo", [{ label: "bar", value: "bar", wasCreated: false }]), ]); store.data.setAuthor(""); store.data.setComment("fake silence"); @@ -845,7 +957,7 @@ describe("SilenceFormStore.data.isValid", () => { it("isValid returns 'false' if comment is empty", () => { store.data.setAlertmanagers([MockAlertmanagerOption()]); store.data.setMatchers([ - MockMatcher("foo", [{ label: "bar", value: "bar" }]), + MockMatcher("foo", [{ label: "bar", value: "bar", wasCreated: false }]), ]); store.data.setAuthor("me@example.com"); store.data.setComment(""); @@ -855,7 +967,7 @@ describe("SilenceFormStore.data.isValid", () => { it("isValid returns 'true' if all fileds are set", () => { store.data.setAlertmanagers([MockAlertmanagerOption()]); store.data.setMatchers([ - MockMatcher("foo", [{ label: "bar", value: "bar" }]), + MockMatcher("foo", [{ label: "bar", value: "bar", wasCreated: false }]), ]); store.data.setAuthor("me@example.com"); store.data.setComment("fake silence"); diff --git a/ui/src/Stores/SilenceFormStore.ts b/ui/src/Stores/SilenceFormStore.ts index f319ec7ec..12ee14531 100644 --- a/ui/src/Stores/SilenceFormStore.ts +++ b/ui/src/Stores/SilenceFormStore.ts @@ -77,6 +77,10 @@ const AlertmanagerClustersToOption = (clusterDict: { value: clusterMembers, })); +export const EscapeRegex = (v: string): string => { + return v.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); +}; + const MatchersFromGroup = ( group: APIAlertGroupT, stripLabels: string[], @@ -218,9 +222,13 @@ const GenerateAlertmanagerSilenceData = ( name: m.name, value: m.values.length > 1 - ? `(${m.values.map((v) => v.value).join("|")})` + ? `(${m.values + .map((v) => (v.wasCreated ? v.value : EscapeRegex(v.value))) + .join("|")})` : m.values.length === 1 - ? m.values[0].value + ? m.values[0].wasCreated + ? m.values[0].value + : EscapeRegex(m.values[0].value) : "", isRegex: m.isRegex, isEqual: m.isEqual, diff --git a/ui/src/Stores/__snapshots__/SilenceFormStore.test.ts.snap b/ui/src/Stores/__snapshots__/SilenceFormStore.test.ts.snap index 199e8079e..c714589cc 100644 --- a/ui/src/Stores/__snapshots__/SilenceFormStore.test.ts.snap +++ b/ui/src/Stores/__snapshots__/SilenceFormStore.test.ts.snap @@ -40,3 +40,86 @@ Object { "startsAt": "2000-02-01T00:00:00.000Z", } `; + +exports[`SilenceFormStore.data toAlertmanagerPayload creates payload that matches snapshot with regex values 1`] = ` +Object { + "comment": "toAlertmanagerPayload test", + "createdBy": "me@example.com", + "endsAt": "2000-02-01T01:00:00.000Z", + "matchers": Array [ + Object { + "isEqual": false, + "isRegex": true, + "name": "notEqualRegexAuto", + "value": "\\\\^\\\\(\\\\.\\\\+\\\\)\\\\$", + }, + Object { + "isEqual": true, + "isRegex": true, + "name": "equalRegexAuto", + "value": "\\\\^\\\\(\\\\.\\\\+\\\\)\\\\$", + }, + Object { + "isEqual": true, + "isRegex": false, + "name": "equalNotRegexAuto", + "value": "\\\\^\\\\(\\\\.\\\\+\\\\)\\\\$", + }, + Object { + "isEqual": false, + "isRegex": false, + "name": "notEqualnotRegexAuto", + "value": "\\\\^\\\\(\\\\.\\\\+\\\\)\\\\$", + }, + Object { + "isEqual": false, + "isRegex": true, + "name": "notEqualRegexCreated", + "value": "^(.+)$", + }, + Object { + "isEqual": true, + "isRegex": true, + "name": "equalRegexCreated", + "value": "^(.+)$", + }, + Object { + "isEqual": true, + "isRegex": false, + "name": "equalNotRegexCreated", + "value": "^(.+)$", + }, + Object { + "isEqual": false, + "isRegex": false, + "name": "notEqualnotRegexAuto", + "value": "^(.+)$", + }, + Object { + "isEqual": false, + "isRegex": true, + "name": "notEqualRegexCreatedMulti", + "value": "(^(.+)$|\\\\)", + }, + Object { + "isEqual": true, + "isRegex": true, + "name": "equalRegexCreatedMulti", + "value": "(^(.+)$|\\\\)", + }, + Object { + "isEqual": false, + "isRegex": true, + "name": "notEqualRegexAuto", + "value": "(\\\\^\\\\(\\\\.\\\\+\\\\)\\\\$|\\\\)", + }, + Object { + "isEqual": true, + "isRegex": true, + "name": "equalRegexAuto", + "value": "(\\\\^\\\\(\\\\.\\\\+\\\\)\\\\$|\\\\)", + }, + ], + "startsAt": "2000-02-01T00:00:00.000Z", +} +`; diff --git a/ui/src/__fixtures__/useFetchGet.ts b/ui/src/__fixtures__/useFetchGet.ts index 6502c172d..0fa0691b3 100644 --- a/ui/src/__fixtures__/useFetchGet.ts +++ b/ui/src/__fixtures__/useFetchGet.ts @@ -98,6 +98,10 @@ const useFetchGetMock = ( uri: "./labelValues.json?name=cluster", response: ["dev", "staging", "prod"], }, + { + uri: "./labelValues.json?name=regex", + response: ["(dev)", "staging (.+)", "\\prod\\"], + }, // matcher value counters { re: /^\.\/alerts\.json\?q=/,