mirror of
https://github.com/prymitive/karma
synced 2026-05-17 04:16:42 +00:00
fix(ui): pass configured alertmanager headers when making requests from the browser
Right now configured headers are only set on requests made from the backend to alertmanager API. With this change fetch() calls in the browser will use those headers if proxy mode is not enabled.
This commit is contained in:
@@ -19,6 +19,7 @@
|
||||
"favico.js": "0.3.10",
|
||||
"fontfaceobserver": "2.1.0",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.throttle": "4.1.1",
|
||||
"lodash.uniqueid": "4.0.1",
|
||||
"mobx": "5.13.0",
|
||||
|
||||
6
ui/src/Common/Fetch.js
Normal file
6
ui/src/Common/Fetch.js
Normal file
@@ -0,0 +1,6 @@
|
||||
import merge from "lodash.merge";
|
||||
|
||||
const FetchWithCredentials = async (uri, options) =>
|
||||
await fetch(uri, merge({}, { credentials: "include" }, options));
|
||||
|
||||
export { FetchWithCredentials };
|
||||
40
ui/src/Common/Fetch.test.js
Normal file
40
ui/src/Common/Fetch.test.js
Normal file
@@ -0,0 +1,40 @@
|
||||
import { FetchWithCredentials } from "./Fetch";
|
||||
|
||||
beforeEach(() => {
|
||||
fetch.resetMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe("FetchWithCredentials", () => {
|
||||
it("fetch passes '{credentials: include}' to all requests", async () => {
|
||||
const request = FetchWithCredentials("http://example.com", {});
|
||||
await expect(request).resolves.toBeUndefined();
|
||||
expect(fetch).toHaveBeenCalledWith("http://example.com", {
|
||||
credentials: "include"
|
||||
});
|
||||
});
|
||||
|
||||
it("custom keys are merged with defaults", async () => {
|
||||
const request = FetchWithCredentials("http://example.com", {
|
||||
foo: "bar"
|
||||
});
|
||||
await expect(request).resolves.toBeUndefined();
|
||||
expect(fetch).toHaveBeenCalledWith("http://example.com", {
|
||||
credentials: "include",
|
||||
foo: "bar"
|
||||
});
|
||||
});
|
||||
|
||||
it("custom credentials are used when passed", async () => {
|
||||
const request = FetchWithCredentials("http://example.com", {
|
||||
credentials: "none"
|
||||
});
|
||||
await expect(request).resolves.toBeUndefined();
|
||||
expect(fetch).toHaveBeenCalledWith("http://example.com", {
|
||||
credentials: "none"
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,7 @@ import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
|
||||
import { APIAlertmanagerUpstream } from "Models/API";
|
||||
import { AlertStore, FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore";
|
||||
import { FormatQuery, QueryOperators, StaticLabels } from "Common/Query";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
import { Modal } from "Components/Modal";
|
||||
import {
|
||||
LabelSetList,
|
||||
@@ -136,7 +137,7 @@ const DeleteSilenceModalContent = observer(
|
||||
FormatQuery(StaticLabels.SilenceID, QueryOperators.Equal, silenceID)
|
||||
]);
|
||||
|
||||
this.previewState.fetch = fetch(alertsURI, { credentials: "include" })
|
||||
this.previewState.fetch = FetchWithCredentials(alertsURI, {})
|
||||
.then(result => result.json())
|
||||
.then(result => {
|
||||
this.previewState.groupsToUniqueLabels(Object.values(result.groups));
|
||||
@@ -165,9 +166,9 @@ const DeleteSilenceModalContent = observer(
|
||||
? `${alertmanager.publicURI}/api/v2/silence/${silenceID}`
|
||||
: `${alertmanager.publicURI}/api/v1/silence/${silenceID}`;
|
||||
|
||||
this.deleteState.fetch = fetch(uri, {
|
||||
this.deleteState.fetch = FetchWithCredentials(uri, {
|
||||
method: "DELETE",
|
||||
credentials: "include"
|
||||
headers: alertmanager.headers
|
||||
})
|
||||
.then(result => {
|
||||
if (isOpenAPI) {
|
||||
|
||||
@@ -122,7 +122,7 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(tree.find("ErrorMessage")).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("[v1] sends a DELETE request after clicking 'Confirm' button ", async () => {
|
||||
it("[v1] sends a DELETE request after clicking 'Confirm' button", async () => {
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://am.example.com/api/v1/silence/123456789"
|
||||
@@ -130,7 +130,7 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
|
||||
});
|
||||
|
||||
it("[v2] sends a DELETE request after clicking 'Confirm' button ", async () => {
|
||||
it("[v2] sends a DELETE request after clicking 'Confirm' button", async () => {
|
||||
alertmanager.version = "0.16.2";
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
@@ -139,6 +139,33 @@ describe("<DeleteSilenceModalContent />", () => {
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
|
||||
});
|
||||
|
||||
it("[v1] sends headers from alertmanager config", async () => {
|
||||
alertmanager.headers = { Authorization: "Basic ***" };
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://am.example.com/api/v1/silence/123456789"
|
||||
);
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({
|
||||
credentials: "include",
|
||||
method: "DELETE",
|
||||
headers: { Authorization: "Basic ***" }
|
||||
});
|
||||
});
|
||||
|
||||
it("[v1] sends headers from alertmanager config", async () => {
|
||||
alertmanager.headers = { Authorization: "Basic ***" };
|
||||
alertmanager.version = "0.16.2";
|
||||
await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
"http://am.example.com/api/v2/silence/123456789"
|
||||
);
|
||||
expect(fetch.mock.calls[1][1]).toMatchObject({
|
||||
credentials: "include",
|
||||
method: "DELETE",
|
||||
headers: { Authorization: "Basic ***" }
|
||||
});
|
||||
});
|
||||
|
||||
it("'Confirm' button is no-op after successful DELETE", async () => {
|
||||
const tree = await VerifyResponse({ status: "success" });
|
||||
expect(fetch.mock.calls[1][0]).toBe(
|
||||
|
||||
@@ -67,6 +67,7 @@ beforeEach(() => {
|
||||
cluster: "default",
|
||||
uri: "file:///mock",
|
||||
publicURI: "http://example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
clusterMembers: ["default"]
|
||||
@@ -203,6 +204,7 @@ describe("<Silence />", () => {
|
||||
cluster: "default",
|
||||
uri: "file:///mock",
|
||||
publicURI: "http://example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
clusterMembers: ["default"]
|
||||
|
||||
@@ -7,6 +7,7 @@ import { observer } from "mobx-react";
|
||||
import Creatable from "react-select/creatable";
|
||||
|
||||
import { StaticLabels } from "Common/Query";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
import { FormatBackendURI } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { ReactSelectStyles } from "Components/MultiSelect";
|
||||
@@ -33,9 +34,10 @@ const SortLabelName = observer(
|
||||
});
|
||||
|
||||
populateNameSuggestions = action(() => {
|
||||
this.nameSuggestionsFetch = fetch(FormatBackendURI(`labelNames.json`), {
|
||||
credentials: "include"
|
||||
})
|
||||
this.nameSuggestionsFetch = FetchWithCredentials(
|
||||
FormatBackendURI(`labelNames.json`),
|
||||
{}
|
||||
)
|
||||
.then(
|
||||
result => result.json(),
|
||||
err => {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { faSearch } from "@fortawesome/free-solid-svg-icons/faSearch";
|
||||
import { AlertStore, FormatBackendURI } from "Stores/AlertStore";
|
||||
import { Settings } from "Stores/Settings";
|
||||
import { IsMobile } from "Common/Device";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
import { FilterInputLabel } from "Components/Labels/FilterInputLabel";
|
||||
import { AutosuggestTheme } from "./Constants";
|
||||
import { History } from "./History";
|
||||
@@ -72,9 +73,9 @@ const FilterInput = observer(
|
||||
onSuggestionsFetchRequested = debounce(
|
||||
action(({ value }) => {
|
||||
if (value !== "") {
|
||||
this.inputStore.suggestionsFetch = fetch(
|
||||
this.inputStore.suggestionsFetch = FetchWithCredentials(
|
||||
FormatBackendURI(`autocomplete.json?term=${value}`),
|
||||
{ credentials: "include" }
|
||||
{}
|
||||
)
|
||||
.then(
|
||||
result => result.json(),
|
||||
|
||||
@@ -22,6 +22,7 @@ beforeEach(() => {
|
||||
name: "am1",
|
||||
uri: "http://am1.example.com",
|
||||
publicURI: "http://am1.example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
@@ -31,6 +32,7 @@ beforeEach(() => {
|
||||
name: "am2",
|
||||
uri: "http://am2.example.com",
|
||||
publicURI: "http://am2.example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
@@ -40,6 +42,7 @@ beforeEach(() => {
|
||||
name: "am3",
|
||||
uri: "http://am3.example.com",
|
||||
publicURI: "http://am3.example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "am3",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { SilenceFormMatcher } from "Models/SilenceForm";
|
||||
import { MultiSelect } from "Components/MultiSelect";
|
||||
import { ValidationError } from "Components/MultiSelect/ValidationError";
|
||||
import { FormatBackendURI } from "Stores/AlertStore";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
|
||||
const LabelNameInput = observer(
|
||||
class LabelNameInput extends MultiSelect {
|
||||
@@ -19,9 +20,10 @@ const LabelNameInput = observer(
|
||||
populateNameSuggestions = action(() => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
this.nameSuggestionsFetch = fetch(FormatBackendURI(`labelNames.json`), {
|
||||
credentials: "include"
|
||||
})
|
||||
this.nameSuggestionsFetch = FetchWithCredentials(
|
||||
FormatBackendURI(`labelNames.json`),
|
||||
{}
|
||||
)
|
||||
.then(
|
||||
result => result.json(),
|
||||
err => {
|
||||
@@ -43,11 +45,9 @@ const LabelNameInput = observer(
|
||||
populateValueSuggestions = action(() => {
|
||||
const { matcher } = this.props;
|
||||
|
||||
this.valueSuggestionsFetch = fetch(
|
||||
this.valueSuggestionsFetch = FetchWithCredentials(
|
||||
FormatBackendURI(`labelValues.json?name=${matcher.name}`),
|
||||
{
|
||||
credentials: "include"
|
||||
}
|
||||
{}
|
||||
)
|
||||
.then(
|
||||
result => result.json(),
|
||||
|
||||
@@ -14,6 +14,7 @@ import { FormatBackendURI, FormatAlertsQ } from "Stores/AlertStore";
|
||||
import { SilenceFormStore } from "Stores/SilenceFormStore";
|
||||
import { SilenceFormMatcher } from "Models/SilenceForm";
|
||||
import { TooltipWrapper } from "Components/TooltipWrapper";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
import { MatcherToFilter, AlertManagersToFilter } from "../Matchers";
|
||||
|
||||
const MatchCounter = observer(
|
||||
@@ -54,7 +55,7 @@ const MatchCounter = observer(
|
||||
const alertsURI =
|
||||
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters);
|
||||
|
||||
this.matchedAlerts.fetch = fetch(alertsURI, { credentials: "include" })
|
||||
this.matchedAlerts.fetch = FetchWithCredentials(alertsURI, {})
|
||||
.then(result => {
|
||||
return result.json();
|
||||
})
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
LabelSetList,
|
||||
GroupListToUniqueLabelsList
|
||||
} from "Components/LabelSetList";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
import { MatcherToFilter, AlertManagersToFilter } from "../Matchers";
|
||||
|
||||
const FetchError = ({ message }) => (
|
||||
@@ -67,7 +68,7 @@ const SilencePreview = observer(
|
||||
const alertsURI =
|
||||
FormatBackendURI("alerts.json?") + FormatAlertsQ(filters);
|
||||
|
||||
this.matchedAlerts.fetch = fetch(alertsURI, { credentials: "include" })
|
||||
this.matchedAlerts.fetch = FetchWithCredentials(alertsURI, {})
|
||||
.then(result => {
|
||||
return result.json();
|
||||
})
|
||||
|
||||
@@ -13,6 +13,7 @@ import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclama
|
||||
|
||||
import { APISilenceMatcher } from "Models/API";
|
||||
import { AlertStore } from "Stores/AlertStore";
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
|
||||
const SubmitState = Object.freeze({
|
||||
InProgress: "InProgress",
|
||||
@@ -108,13 +109,13 @@ const SilenceSubmitProgress = observer(
|
||||
? `${am.publicURI}/api/v2/silences`
|
||||
: `${am.publicURI}/api/v1/silences`;
|
||||
|
||||
this.submitState.fetch = fetch(uri, {
|
||||
this.submitState.fetch = FetchWithCredentials(uri, {
|
||||
method: "POST",
|
||||
body: JSON.stringify(payload),
|
||||
headers: {
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
credentials: "include"
|
||||
"Content-Type": "application/json",
|
||||
...am.headers
|
||||
}
|
||||
})
|
||||
.then(result => {
|
||||
if (isOpenAPI) {
|
||||
|
||||
@@ -15,6 +15,7 @@ beforeEach(() => {
|
||||
name: "mockAlertmanager",
|
||||
uri: "file:///mock",
|
||||
publicURI: "http://example.com",
|
||||
headers: { foo: "bar" },
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "mockAlertmanager",
|
||||
@@ -68,7 +69,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
const payload = fetch.mock.calls[0][1];
|
||||
expect(payload).toMatchObject({
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
headers: { "Content-Type": "application/json", foo: "bar" },
|
||||
body: JSON.stringify({
|
||||
matchers: [],
|
||||
startsAt: "now",
|
||||
@@ -93,6 +94,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
name: "am1",
|
||||
uri: "file:///mock",
|
||||
publicURI: "http://am1.example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
@@ -102,6 +104,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
name: "am2",
|
||||
uri: "file:///mock",
|
||||
publicURI: "http://am2.example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
@@ -147,6 +150,7 @@ describe("<SilenceSubmitProgress />", () => {
|
||||
name: "am1",
|
||||
uri: "file:///mock",
|
||||
publicURI: "http://am1.example.com",
|
||||
headers: {},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
cluster: "ha",
|
||||
|
||||
@@ -70,6 +70,7 @@ const APIAlertmanagerUpstream = PropTypes.exact({
|
||||
cluster: PropTypes.string.isRequired,
|
||||
uri: PropTypes.string.isRequired,
|
||||
publicURI: PropTypes.string.isRequired,
|
||||
headers: PropTypes.object.isRequired,
|
||||
error: PropTypes.string.isRequired,
|
||||
version: PropTypes.string.isRequired,
|
||||
clusterMembers: PropTypes.arrayOf(PropTypes.string).isRequired
|
||||
|
||||
@@ -6,6 +6,8 @@ import equal from "fast-deep-equal";
|
||||
|
||||
import qs from "qs";
|
||||
|
||||
import { FetchWithCredentials } from "Common/Fetch";
|
||||
|
||||
const QueryStringEncodeOptions = {
|
||||
encodeValuesOnly: true, // don't encode q[]
|
||||
indices: false // go-gin doesn't support parsing q[0]=foo&q[1]=bar
|
||||
@@ -247,7 +249,7 @@ class AlertStore {
|
||||
`alerts.json?sortOrder=${sortOrder}&sortLabel=${sortLabel}&sortReverse=${sortReverse}&`
|
||||
) + FormatAPIFilterQuery(this.filters.values.map(f => f.raw));
|
||||
|
||||
return fetch(alertsURI, { credentials: "include" })
|
||||
return FetchWithCredentials(alertsURI, {})
|
||||
.then(result => {
|
||||
this.status.setProcessing();
|
||||
return result.json();
|
||||
|
||||
@@ -71,6 +71,9 @@ const MockAlertmanager = () => ({
|
||||
cluster: "default",
|
||||
uri: "http://localhost",
|
||||
publicURI: "http://am.example.com",
|
||||
headers: {
|
||||
Authorization: "Basic foo bar"
|
||||
},
|
||||
error: "",
|
||||
version: "0.15.0",
|
||||
clusterMembers: ["default"]
|
||||
|
||||
Reference in New Issue
Block a user