mirror of
https://github.com/prymitive/karma
synced 2026-05-15 04:06:41 +00:00
Merge pull request #974 from prymitive/fetch-headers
fix(ui): pass configured alertmanager headers to in-browser fetch calls
This commit is contained in:
@@ -13,6 +13,7 @@ import (
|
||||
"github.com/prymitive/karma/internal/filters"
|
||||
"github.com/prymitive/karma/internal/models"
|
||||
"github.com/prymitive/karma/internal/slices"
|
||||
"github.com/prymitive/karma/internal/uri"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
@@ -111,11 +112,20 @@ func getUpstreams() models.AlertmanagerAPISummary {
|
||||
Name: upstream.Name,
|
||||
URI: upstream.SanitizedURI(),
|
||||
PublicURI: upstream.PublicURI(),
|
||||
Headers: map[string]string{},
|
||||
Error: upstream.Error(),
|
||||
Version: upstream.Version(),
|
||||
Cluster: upstream.ClusterID(),
|
||||
ClusterMembers: members,
|
||||
}
|
||||
if !upstream.ProxyRequests {
|
||||
for k, v := range uri.HeadersForBasicAuth(u.PublicURI) {
|
||||
u.Headers[k] = v
|
||||
}
|
||||
for k, v := range upstream.HTTPHeaders {
|
||||
u.Headers[k] = v
|
||||
}
|
||||
}
|
||||
summary.Instances = append(summary.Instances, u)
|
||||
|
||||
summary.Counters.Total++
|
||||
|
||||
@@ -69,10 +69,10 @@ alertmanager:
|
||||
(`https://user:password@alertmanager.example.com`) and you don't want it to
|
||||
be visible to users then ensure `proxy: true` is also set in order to avoid
|
||||
leaking auth information to the browser.
|
||||
Without proxy mode full URI needs to be passed to karma web UI code.
|
||||
With proxy mode all requests will be routed via karma HTTP server and since
|
||||
karma has full URI in the config it only needs Alertmanager name in that
|
||||
request.
|
||||
Note: if URI contains username and password and proxy option is NOT enabled
|
||||
(see below), then the username & password information will be stripped from
|
||||
the URI and `Authorization` header using Basic Auth will be set for all
|
||||
in browser requests.
|
||||
To set a different URI for all browser requests (can be any valid URI) see
|
||||
`external_uri` option below.
|
||||
- `external_uri` - base URI of this Alertmanager server used for all browser
|
||||
|
||||
@@ -35,12 +35,12 @@ var uriTests = []uriTest{
|
||||
{
|
||||
rawURI: "http://user:pass@alertmanager.example.com",
|
||||
proxy: false,
|
||||
publicURI: "http://user:pass@alertmanager.example.com",
|
||||
publicURI: "http://alertmanager.example.com",
|
||||
},
|
||||
{
|
||||
rawURI: "https://user:pass@alertmanager.example.com/foo",
|
||||
proxy: false,
|
||||
publicURI: "https://user:pass@alertmanager.example.com/foo",
|
||||
publicURI: "https://alertmanager.example.com/foo",
|
||||
},
|
||||
{
|
||||
rawURI: "http://user:pass@alertmanager.example.com",
|
||||
|
||||
@@ -211,7 +211,7 @@ func (am *Alertmanager) PublicURI() string {
|
||||
if am.ExternalURI != "" {
|
||||
return am.ExternalURI
|
||||
}
|
||||
return am.URI
|
||||
return uri.WithoutUserinfo(am.URI)
|
||||
}
|
||||
|
||||
func (am *Alertmanager) pullAlerts(version string) error {
|
||||
|
||||
@@ -29,11 +29,12 @@ type AlertmanagerAPIStatus struct {
|
||||
URI string `json:"uri"`
|
||||
// this is URI client should use to talk to this Alertmanager, it might be
|
||||
// same as real or proxied URI
|
||||
PublicURI string `json:"publicURI"`
|
||||
Error string `json:"error"`
|
||||
Version string `json:"version"`
|
||||
Cluster string `json:"cluster"`
|
||||
ClusterMembers []string `json:"clusterMembers"`
|
||||
PublicURI string `json:"publicURI"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
Error string `json:"error"`
|
||||
Version string `json:"version"`
|
||||
Cluster string `json:"cluster"`
|
||||
ClusterMembers []string `json:"clusterMembers"`
|
||||
}
|
||||
|
||||
// AlertmanagerAPICounters returns number of Alertmanager instances in each
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package uri
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/url"
|
||||
"path"
|
||||
)
|
||||
@@ -30,3 +31,35 @@ func SanitizeURI(s string) string {
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// HeadersForBasicAuth checks if the passed uri contains user & password
|
||||
// (http://user:pass@example.com) and if so generates headers for Basic Auth
|
||||
// based on
|
||||
func HeadersForBasicAuth(s string) map[string]string {
|
||||
headers := map[string]string{}
|
||||
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return headers
|
||||
}
|
||||
|
||||
if u.User != nil {
|
||||
if password, pwdSet := u.User.Password(); pwdSet {
|
||||
auth := u.User.Username() + ":" + password
|
||||
headers["Authorization"] = "Basic " + base64.StdEncoding.EncodeToString([]byte(auth))
|
||||
}
|
||||
}
|
||||
|
||||
return headers
|
||||
}
|
||||
|
||||
// WithoutUserinfo takes an URL and returns a copy of it with basic auth
|
||||
// stripped
|
||||
func WithoutUserinfo(s string) string {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return s
|
||||
}
|
||||
u.User = nil
|
||||
return u.String()
|
||||
}
|
||||
|
||||
@@ -7,35 +7,48 @@ import (
|
||||
)
|
||||
|
||||
type joinURLTest struct {
|
||||
base string
|
||||
sub string
|
||||
url string
|
||||
base string
|
||||
sub string
|
||||
url string
|
||||
isValid bool
|
||||
}
|
||||
|
||||
var joinURLTests = []joinURLTest{
|
||||
{
|
||||
base: "http://localhost",
|
||||
sub: "/sub",
|
||||
url: "http://localhost/sub",
|
||||
base: "http://localhost",
|
||||
sub: "/sub",
|
||||
url: "http://localhost/sub",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
base: "http://localhost",
|
||||
sub: "/sub/",
|
||||
url: "http://localhost/sub",
|
||||
base: "http://localhost",
|
||||
sub: "/sub/",
|
||||
url: "http://localhost/sub",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
base: "http://am.example.com",
|
||||
sub: "/api/v1/alerts",
|
||||
url: "http://am.example.com/api/v1/alerts",
|
||||
base: "http://am.example.com",
|
||||
sub: "/api/v1/alerts",
|
||||
url: "http://am.example.com/api/v1/alerts",
|
||||
isValid: true,
|
||||
},
|
||||
{
|
||||
base: "%gh&%ij",
|
||||
sub: "/a + b",
|
||||
url: "",
|
||||
isValid: false,
|
||||
},
|
||||
}
|
||||
|
||||
func TestJoinURL(t *testing.T) {
|
||||
for _, testCase := range joinURLTests {
|
||||
url, err := uri.JoinURL(testCase.base, testCase.sub)
|
||||
if err != nil {
|
||||
if err != nil && testCase.isValid {
|
||||
t.Errorf("joinURL(%v, %v) failed: %s", testCase.base, testCase.sub, err.Error())
|
||||
}
|
||||
if err == nil && !testCase.isValid {
|
||||
t.Errorf("expected error for '%s' and '%s' but got '%s'", testCase.base, testCase.sub, url)
|
||||
}
|
||||
if url != testCase.url {
|
||||
t.Errorf("Invalid joined url from '%s' + '%s', expected '%s', got '%s'", testCase.base, testCase.sub, testCase.url, url)
|
||||
}
|
||||
@@ -80,6 +93,10 @@ var sanitizeURITests = []sanitizeURITest{
|
||||
raw: "https://user:pass@alertmanager.example.com/foo",
|
||||
sanitized: "https://user:xxx@alertmanager.example.com/foo",
|
||||
},
|
||||
{
|
||||
raw: "%gh&%ij",
|
||||
sanitized: "%gh&%ij",
|
||||
},
|
||||
}
|
||||
|
||||
func TestSanitizedURI(t *testing.T) {
|
||||
@@ -91,3 +108,62 @@ func TestSanitizedURI(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestHeadersForBasicAuth(t *testing.T) {
|
||||
type headersTest struct {
|
||||
uri string
|
||||
isSet bool
|
||||
value string
|
||||
}
|
||||
testCases := []headersTest{
|
||||
{
|
||||
uri: "http://localhost.com",
|
||||
isSet: false,
|
||||
},
|
||||
{
|
||||
uri: "http://user@localhost.com",
|
||||
isSet: false,
|
||||
},
|
||||
{
|
||||
uri: "http://user:pass@localhost.com",
|
||||
isSet: true,
|
||||
value: "Basic dXNlcjpwYXNz",
|
||||
},
|
||||
{
|
||||
uri: "%gh&%ij",
|
||||
isSet: false,
|
||||
},
|
||||
}
|
||||
for _, test := range testCases {
|
||||
headers := uri.HeadersForBasicAuth(test.uri)
|
||||
value, isSet := headers["Authorization"]
|
||||
if isSet != test.isSet {
|
||||
t.Errorf("[%s] expected Authorization header: %v, was set: %v", test.uri, test.isSet, isSet)
|
||||
}
|
||||
if value != test.value {
|
||||
t.Errorf("[%s] expected Authorization value: %s, value: %s", test.uri, test.value, value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestURIWithoutUserinfo(t *testing.T) {
|
||||
type userinfoTest struct {
|
||||
uri string
|
||||
parsed string
|
||||
}
|
||||
testCases := []userinfoTest{
|
||||
{uri: "http://localhost", parsed: "http://localhost"},
|
||||
{uri: "http://localhost?foo=bar", parsed: "http://localhost?foo=bar"},
|
||||
{uri: "http://user@localhost", parsed: "http://localhost"},
|
||||
{uri: "http://user:pass@localhost", parsed: "http://localhost"},
|
||||
{uri: "http://user:pass@localhost?foo=bar#1", parsed: "http://localhost?foo=bar#1"},
|
||||
{uri: "%gh&%ij", parsed: "%gh&%ij"},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
parsed := uri.WithoutUserinfo(test.uri)
|
||||
if parsed != test.parsed {
|
||||
t.Errorf("'%s' got parsed as '%s', expected: '%s'", test.uri, parsed, test.parsed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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