feat(ui): use api/v2 silence endpoints for alertmanager 0.16+

This commit is contained in:
Łukasz Mierzwa
2019-04-22 23:38:02 +01:00
parent 7b13499f2b
commit 809c6c6fab
5 changed files with 142 additions and 16 deletions

View File

@@ -50,6 +50,7 @@
"react-tippy": "1.2.3",
"react-transition-group": "4.0.0",
"react-truncate": "2.4.0",
"semver": "6.0.0",
"whatwg-fetch": "3.0.0"
},
"scripts": {

View File

@@ -4,6 +4,8 @@ import PropTypes from "prop-types";
import { observable, action } from "mobx";
import { observer } from "mobx-react";
import semver from "semver";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faTrash } from "@fortawesome/free-solid-svg-icons/faTrash";
import { faExclamationCircle } from "@fortawesome/free-solid-svg-icons/faExclamationCircle";
@@ -119,9 +121,7 @@ const DeleteSilenceModalContent = observer(
]);
this.previewState.fetch = fetch(alertsURI, { credentials: "include" })
.then(result => {
return result.json();
})
.then(result => result.json())
.then(result => {
this.previewState.groupsToUniqueLabels(Object.values(result.groups));
this.previewState.setError(null);
@@ -140,13 +140,29 @@ const DeleteSilenceModalContent = observer(
// if it's already deleted then do nothing
if (this.deleteState.done && this.deleteState.error === null) return;
const uri = `${alertmanager.publicURI}/api/v1/silence/${silenceID}`;
const isOpenAPI = semver.satisfies(alertmanager.version, ">=0.16.0");
const uri = isOpenAPI
? `${alertmanager.publicURI}/api/v2/silence/${silenceID}`
: `${alertmanager.publicURI}/api/v1/silence/${silenceID}`;
this.deleteState.fetch = fetch(uri, {
method: "DELETE",
credentials: "include"
})
.then(result => result.json())
.then(result => this.parseAlertmanagerResponse(result))
.then(result => {
if (isOpenAPI) {
if (result.ok) {
this.deleteState.setError(null);
this.deleteState.setDone();
} else {
result.text().then(this.deleteState.setError);
this.deleteState.setDone();
}
} else {
result.json().then(this.parseAlertmanagerResponse);
}
})
.catch(err => {
console.trace(err);
this.deleteState.setDone();

View File

@@ -13,6 +13,7 @@ let alertStore;
beforeEach(() => {
alertmanager = MockAlertmanager();
alertStore = new AlertStore([]);
alertStore.data.upstreams.instances[0] = alertmanager;
fetch.mockResponseOnce(JSON.stringify(MockAPIResponse()));
jest.restoreAllMocks();
@@ -121,7 +122,7 @@ describe("<DeleteSilenceModalContent />", () => {
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("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"
@@ -129,6 +130,15 @@ describe("<DeleteSilenceModalContent />", () => {
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
});
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(
"http://am.example.com/api/v2/silence/123456789"
);
expect(fetch.mock.calls[1][1]).toMatchObject({ method: "DELETE" });
});
it("'Confirm' button is no-op after successful DELETE", async () => {
const tree = await VerifyResponse({ status: "success" });
expect(fetch.mock.calls[1][0]).toBe(
@@ -165,7 +175,7 @@ describe("<DeleteSilenceModalContent />", () => {
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("renders ErrorMessage on failed fetch request", async () => {
it("[v1] renders ErrorMessage on failed fetch request", async () => {
const tree = MountedDeleteSilenceModalContent();
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
@@ -179,4 +189,20 @@ describe("<DeleteSilenceModalContent />", () => {
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
it("[v2] renders ErrorMessage on failed fetch request", async () => {
alertmanager.version = "0.16.2";
const tree = MountedDeleteSilenceModalContent();
await expect(tree.instance().previewState.fetch).resolves.toBeUndefined();
jest.spyOn(console, "trace").mockImplementation(() => {});
fetch.resetMocks();
fetch.mockResponseOnce("500 Internal Server Error", { status: 500 });
tree.find(".btn-outline-danger").simulate("click");
await expect(tree.instance().deleteState.fetch).resolves.toBeUndefined();
tree.update();
expect(tree.find("ErrorMessage")).toHaveLength(1);
});
});

View File

@@ -4,6 +4,8 @@ import PropTypes from "prop-types";
import { action, observable } from "mobx";
import { observer } from "mobx-react";
import semver from "semver";
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
import { faCircleNotch } from "@fortawesome/free-solid-svg-icons/faCircleNotch";
import { faCheckCircle } from "@fortawesome/free-regular-svg-icons/faCheckCircle";
@@ -54,7 +56,8 @@ const SilenceSubmitProgress = observer(
startsAt: PropTypes.string.isRequired,
endsAt: PropTypes.string.isRequired,
createdBy: PropTypes.string.isRequired,
comment: PropTypes.string.isRequired
comment: PropTypes.string.isRequired,
id: PropTypes.string
}).isRequired,
alertStore: PropTypes.instanceOf(AlertStore).isRequired
};
@@ -99,7 +102,13 @@ const SilenceSubmitProgress = observer(
return;
}
this.submitState.fetch = fetch(`${am.publicURI}/api/v1/silences`, {
const isOpenAPI = semver.satisfies(am.version, ">=0.16.0");
const uri = isOpenAPI
? `${am.publicURI}/api/v2/silences`
: `${am.publicURI}/api/v1/silences`;
this.submitState.fetch = fetch(uri, {
method: "POST",
body: JSON.stringify(payload),
headers: {
@@ -107,9 +116,34 @@ const SilenceSubmitProgress = observer(
},
credentials: "include"
})
.then(result => result.json())
.then(result => this.parseAlertmanagerResponse(am.uri, result))
.catch(err => this.maybeTryAgainAfterError(err));
.then(result => {
if (isOpenAPI) {
if (result.ok) {
return result
.json()
.then(r => this.parseOpenAPIResponse(am.uri, r));
} else {
return result.text().then(text => {
this.submitState.markFailed(text);
return text;
});
}
} else {
return result
.json()
.then(r => this.parseAlertmanagerResponse(am.uri, r));
}
})
.catch(err => {
this.maybeTryAgainAfterError(err);
});
};
parseOpenAPIResponse = (uri, response) => {
const link = <SilenceLink uri={uri} silenceId={response.silenceID} />;
this.submitState.markDone(link);
// return silenceId so we can assert it in tests
return response.silenceID;
};
parseAlertmanagerResponse = (uri, response) => {

View File

@@ -48,7 +48,15 @@ describe("<SilenceSubmitProgress />", () => {
expect(fetch.mock.calls).toHaveLength(1);
});
it("appends /api/v1/silences to the passed URI", async () => {
it("[v1] appends /api/v1/silences to the passed URI", async () => {
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
const uri = fetch.mock.calls[0][0];
expect(uri).toBe("http://example.com/api/v1/silences");
});
it("[v2] appends /api/v2/silences to the passed URI", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBeUndefined();
const uri = fetch.mock.calls[0][0];
@@ -188,7 +196,7 @@ describe("<SilenceSubmitProgress />", () => {
expect(tree.text()).toBe("mockAlertmanagermock error message");
});
it("renders success icon on successful fetch", async () => {
it("[v1] renders success icon on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123" } })
);
@@ -199,7 +207,36 @@ describe("<SilenceSubmitProgress />", () => {
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
});
it("renders error icon on failed fetch", async () => {
it("[v1] renders silence link on successful fetch", async () => {
fetch.mockResponseOnce(
JSON.stringify({ status: "success", data: { silenceId: "123" } })
);
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("success");
tree.update();
expect(tree.find("a").getDOMNode().getAttribute("href")).toBe("file:///mock/#/silences/123");
});
it("[v2] renders success icon on successful fetch", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("123");
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(1);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(0);
});
it("[v2] renders silence link on successful fetch", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
fetch.mockResponseOnce(JSON.stringify({ silenceID: "123" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("123");
tree.update();
expect(tree.find("a").getDOMNode().getAttribute("href")).toBe("file:///mock/#/silences/123");
});
it("[v1] renders error icon on failed fetch", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "error" }));
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe("error");
@@ -208,6 +245,18 @@ describe("<SilenceSubmitProgress />", () => {
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
});
it("[v2] renders error icon on failed fetch", async () => {
alertStore.data.upstreams.instances[0].version = "0.16.2";
fetch.mockResponseOnce("error message", { status: 500 });
const tree = MountedSilenceSubmitProgress();
await expect(tree.instance().submitState.fetch).resolves.toBe(
"error message"
);
tree.update();
expect(tree.find("FontAwesomeIcon.text-success")).toHaveLength(0);
expect(tree.find("FontAwesomeIcon.text-danger")).toHaveLength(1);
});
it("renders unhandled 'status' values in the response as error", async () => {
fetch.mockResponseOnce(JSON.stringify({ status: "unhandled" }));
const tree = MountedSilenceSubmitProgress();