fix: Do not crash when opening details of a helm release

- Switch to using RequestChannel instead of LensProxy for better
  type safety

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton committed 2023-05-09 09:28:18 -04:00
1 parent b7ac14df73
commit 40af0d31c5
30 files changed
+664 -417

No files matched your search

@@ -28,9 +28,9 @@ import requestHelmChartReadmeInjectable from "../../../common/k8s-api/endpoints/
import requestHelmChartValuesInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import type { RequestDetailedHelmRelease } from "../../../renderer/components/helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable";
import requestDetailedHelmReleaseInjectable from "../../../renderer/components/helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable";
import type { RequestHelmReleases } from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import { flushPromises } from "@k8slens/test-utils";
import type { ListClusterHelmReleases } from "../../../main/helm/helm-service/list-helm-releases.injectable";
import listClusterHelmReleasesInjectable from "../../../main/helm/helm-service/list-helm-releases.injectable";
describe("installing helm chart from new tab", () => {
let builder: ApplicationBuilder;
@@ -40,7 +40,7 @@ describe("installing helm chart from new tab", () => {
let requestHelmChartReadmeMock: AsyncFnMock<RequestHelmChartReadme>;
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
let requestCreateHelmReleaseMock: AsyncFnMock<RequestCreateHelmRelease>;
let requestHelmReleasesMock: AsyncFnMock<RequestHelmReleases>;
let listClusterHelmReleasesMock: AsyncFnMock<ListClusterHelmReleases>;
beforeEach(() => {
builder = getApplicationBuilder();
@@ -53,7 +53,6 @@ describe("installing helm chart from new tab", () => {
requestHelmChartReadmeMock = asyncFn();
requestHelmChartValuesMock = asyncFn();
requestCreateHelmReleaseMock = asyncFn();
requestHelmReleasesMock = asyncFn();
builder.beforeWindowStart(({ windowDi }) => {
windowDi.override(directoryForLensLocalStorageInjectable, () => "/some-directory-for-lens-local-storage");
@@ -63,7 +62,6 @@ describe("installing helm chart from new tab", () => {
windowDi.override(requestHelmChartReadmeInjectable, () => requestHelmChartReadmeMock);
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
windowDi.override(requestCreateHelmReleaseInjectable, () => requestCreateHelmReleaseMock);
windowDi.override(requestHelmReleasesInjectable, () => requestHelmReleasesMock);
windowDi.override(getRandomInstallChartTabIdInjectable, () =>
jest
@@ -73,6 +71,11 @@ describe("installing helm chart from new tab", () => {
);
});
builder.beforeApplicationStart(({ mainDi }) => {
listClusterHelmReleasesMock = asyncFn();
mainDi.override(listClusterHelmReleasesInjectable, () => listClusterHelmReleasesMock);
});
builder.namespaces.add("default");
builder.namespaces.add("some-other-namespace");
});
@@ -360,11 +363,10 @@ describe("installing helm chart from new tab", () => {
log: "some-execution-output",
release: {
resources: [],
name: "some-release",
namespace: "default",
version: "some-version",
config: "some-config",
version: 1,
config: {},
manifest: "some-manifest",
info: {
@@ -400,7 +402,10 @@ describe("installing helm chart from new tab", () => {
fireEvent.click(releaseButton);
await flushPromises();
await requestHelmReleasesMock.resolve([]);
await listClusterHelmReleasesMock.resolve({
callWasSuccessful: true,
response: [],
});
});
it("renders", () => {
@@ -6,6 +6,7 @@
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RenderResult } from "@testing-library/react";
import { anyObject } from "jest-mock-extended";
import type { NavigateToHelmReleases } from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import navigateToHelmReleasesInjectable from "../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import { HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
@@ -15,8 +16,8 @@ import type { RequestHelmChartVersions } from "../../../common/k8s-api/endpoints
import requestHelmChartVersionsInjectable from "../../../common/k8s-api/endpoints/helm-charts.api/request-versions.injectable";
import type { RequestHelmReleaseConfiguration } from "../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import requestHelmReleaseConfigurationInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import type { RequestHelmReleases } from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import type { ListClusterHelmReleases } from "../../../main/helm/helm-service/list-helm-releases.injectable";
import listClusterHelmReleasesInjectable from "../../../main/helm/helm-service/list-helm-releases.injectable";
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import type { ApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
@@ -26,9 +27,9 @@ describe("New Upgrade Helm Chart Dock Tab", () => {
let builder: ApplicationBuilder;
let renderResult: RenderResult;
let requestHelmReleaseConfigurationMock: AsyncFnMock<RequestHelmReleaseConfiguration>;
let requestHelmReleasesMock: AsyncFnMock<RequestHelmReleases>;
let requestHelmChartsMock: AsyncFnMock<RequestHelmCharts>;
let requestHelmChartVersionsMock: AsyncFnMock<RequestHelmChartVersions>;
let listClusterHelmReleasesMock: AsyncFnMock<ListClusterHelmReleases>;
let navigateToHelmReleases: NavigateToHelmReleases;
beforeEach(async () => {
@@ -39,9 +40,6 @@ describe("New Upgrade Helm Chart Dock Tab", () => {
requestHelmReleaseConfigurationMock = asyncFn();
windowDi.override(requestHelmReleaseConfigurationInjectable, () => requestHelmReleaseConfigurationMock);
requestHelmReleasesMock = asyncFn();
windowDi.override(requestHelmReleasesInjectable, () => requestHelmReleasesMock);
requestHelmChartsMock = asyncFn();
windowDi.override(requestHelmChartsInjectable, () => requestHelmChartsMock);
@@ -51,6 +49,11 @@ describe("New Upgrade Helm Chart Dock Tab", () => {
navigateToHelmReleases = windowDi.inject(navigateToHelmReleasesInjectable);
});
builder.beforeApplicationStart(({ mainDi }) => {
listClusterHelmReleasesMock = asyncFn();
mainDi.override(listClusterHelmReleasesInjectable, () => listClusterHelmReleasesMock);
});
testUsingFakeTime("2020-01-12 12:00:00");
builder.namespaces.add("my-first-namespace");
@@ -79,22 +82,25 @@ describe("New Upgrade Helm Chart Dock Tab", () => {
});
it("requests helm releases for the selected namespace", () => {
expect(requestHelmReleasesMock).toBeCalledWith("my-second-namespace");
expect(listClusterHelmReleasesMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }), "my-second-namespace");
});
describe("when helm releases resolves", () => {
beforeEach(async () => {
await requestHelmReleasesMock.resolve([
{
appVersion: "some-app-version",
name: "some-name",
namespace: "my-second-namespace",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
},
]);
await listClusterHelmReleasesMock.resolve({
callWasSuccessful: true,
response: [
{
app_version: "some-app-version",
name: "some-name",
namespace: "my-second-namespace",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
},
],
});
});
it("renders", () => {
@@ -0,0 +1,59 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRequestChannel } from "@k8slens/messaging";
import type { Result } from "@k8slens/utilities";
import type { KubeJsonApiData } from "@k8slens/kube-object";
export interface GetHelmReleaseArgs {
clusterId: string;
releaseName: string;
namespace: string;
}
export interface HelmReleaseInfo {
first_deployed: string;
last_deployed: string;
deleted: string;
description: string;
status: string;
notes: string;
}
export interface HelmReleaseDataWithResources extends HelmReleaseData {
resources: KubeJsonApiData[];
}
export interface HelmReleaseData {
name: string;
info: HelmReleaseInfo;
config: Record<string, unknown>;
manifest: string;
version: number;
namespace: string;
}
export type GetHelmReleaseResponse = Result<HelmReleaseDataWithResources, string>;
export const getHelmReleaseChannel = getRequestChannel<GetHelmReleaseArgs, GetHelmReleaseResponse>("get-helm-release");
export interface ListedHelmRelease {
name: string;
namespace: string;
revision: string;
updated: string;
status: string;
chart: string;
app_version: string;
}
export interface ListHelmReleasesArgs {
namespace?: string;
clusterId: string;
}
export type ListHelmReleasesResponse = Result<ListedHelmRelease[], string>;
export const listHelmReleasesChannel = getRequestChannel<ListHelmReleasesArgs, ListHelmReleasesResponse>("list-helm-releases");
@@ -0,0 +1,35 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import getHelmReleaseInjectable from "../../../main/helm/helm-service/get-helm-release.injectable";
import getClusterByIdInjectable from "../../cluster/storage/common/get-by-id.injectable";
import { getHelmReleaseChannel } from "../common/channels";
const handleGetHelmReleaseInjectable = getRequestChannelListenerInjectable({
channel: getHelmReleaseChannel,
getHandler: (di) => {
const getHelmRelease = di.inject(getHelmReleaseInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
return async ({ clusterId, ...args }) => {
const cluster = getClusterById(clusterId);
if (!cluster) {
return {
callWasSuccessful: false,
error: `Cluster with id "${clusterId}" not found`,
};
}
return getHelmRelease({
cluster,
...args,
});
};
},
id: "handle-get-helm-release",
});
export default handleGetHelmReleaseInjectable;
@@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getRequestChannelListenerInjectable } from "@k8slens/messaging";
import listClusterHelmReleasesInjectable from "../../../main/helm/helm-service/list-helm-releases.injectable";
import getClusterByIdInjectable from "../../cluster/storage/common/get-by-id.injectable";
import { listHelmReleasesChannel } from "../common/channels";
const handleListHelmReleasesInjectable = getRequestChannelListenerInjectable({
channel: listHelmReleasesChannel,
id: "handle-list-helm-releases",
getHandler: (di) => {
const listClusterHelmReleases = di.inject(listClusterHelmReleasesInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
return async ({ clusterId, namespace }) => {
const cluster = getClusterById(clusterId);
if (!cluster) {
return {
callWasSuccessful: false,
error: `Cluster with id "${clusterId}" not found`,
};
}
return listClusterHelmReleases(cluster, namespace);
};
},
});
export default handleListHelmReleasesInjectable;
@@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ChannelRequester } from "@k8slens/messaging";
import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { getInjectable } from "@ogre-tools/injectable";
import { listHelmReleasesChannel } from "../common/channels";
export type RequestListHelmReleases = ChannelRequester<typeof listHelmReleasesChannel>;
const requestListHelmReleasesInjectable = getInjectable({
id: "request-list-helm-releases",
instantiate: (di): RequestListHelmReleases => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
return (args) => requestFromChannel(listHelmReleasesChannel, args);
},
});
export default requestListHelmReleasesInjectable;
@@ -0,0 +1,22 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ChannelRequester } from "@k8slens/messaging";
import { requestFromChannelInjectionToken } from "@k8slens/messaging";
import { getInjectable } from "@ogre-tools/injectable";
import { getHelmReleaseChannel } from "../common/channels";
export type RequestHelmRelease = ChannelRequester<typeof getHelmReleaseChannel>;
const requestHelmReleaseInjectable = getInjectable({
id: "request-helm-release",
instantiate: (di): RequestHelmRelease => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
return (args) => requestFromChannel(getHelmReleaseChannel, args);
},
});
export default requestHelmReleaseInjectable;
@@ -9,8 +9,6 @@ import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { RequestHelmReleases } from "../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import requestHelmReleasesInjectable from "../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import type { RequestHelmReleaseConfiguration } from "../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import requestHelmReleaseConfigurationInjectable from "../../common/k8s-api/endpoints/helm-releases.api/request-configuration.injectable";
import type { RequestHelmReleaseUpdate } from "../../common/k8s-api/endpoints/helm-releases.api/request-update.injectable";
@@ -30,10 +28,13 @@ import requestHelmChartReadmeInjectable from "../../common/k8s-api/endpoints/hel
import requestHelmChartValuesInjectable from "../../common/k8s-api/endpoints/helm-charts.api/request-values.injectable";
import { HelmChart } from "../../common/k8s-api/endpoints/helm-charts.api";
import { testUsingFakeTime } from "../../test-utils/use-fake-time";
import type { ListClusterHelmReleases } from "../../main/helm/helm-service/list-helm-releases.injectable";
import listClusterHelmReleasesInjectable from "../../main/helm/helm-service/list-helm-releases.injectable";
import { anyObject } from "jest-mock-extended";
import { toHelmRelease } from "../../renderer/components/helm-releases/to-helm-release";
describe("showing details for helm release", () => {
let builder: ApplicationBuilder;
let requestHelmReleasesMock: AsyncFnMock<RequestHelmReleases>;
let requestDetailedHelmReleaseMock: AsyncFnMock<RequestDetailedHelmRelease>;
let requestHelmReleaseConfigurationMock: AsyncFnMock<RequestHelmReleaseConfiguration>;
let requestHelmReleaseUpdateMock: AsyncFnMock<RequestHelmReleaseUpdate>;
@@ -43,6 +44,7 @@ describe("showing details for helm release", () => {
let requestHelmChartValuesMock: AsyncFnMock<RequestHelmChartValues>;
let showSuccessNotificationMock: jest.Mock;
let showCheckedErrorNotificationMock: jest.Mock;
let listClusterHelmReleasesMock: AsyncFnMock<ListClusterHelmReleases>;
beforeEach(() => {
testUsingFakeTime("2015-10-21T07:28:00Z");
@@ -51,7 +53,6 @@ describe("showing details for helm release", () => {
builder.setEnvironmentToClusterFrame();
requestHelmReleasesMock = asyncFn();
requestDetailedHelmReleaseMock = asyncFn();
requestHelmReleaseConfigurationMock = asyncFn();
requestHelmReleaseUpdateMock = asyncFn();
@@ -67,7 +68,6 @@ describe("showing details for helm release", () => {
windowDi.override(getRandomUpgradeChartTabIdInjectable, () => () => "some-tab-id");
windowDi.override(showSuccessNotificationInjectable, () => showSuccessNotificationMock);
windowDi.override(showCheckedErrorInjectable, () => showCheckedErrorNotificationMock);
windowDi.override(requestHelmReleasesInjectable, () => requestHelmReleasesMock);
windowDi.override(requestDetailedHelmReleaseInjectable, () => requestDetailedHelmReleaseMock);
windowDi.override(requestHelmReleaseConfigurationInjectable, () => requestHelmReleaseConfigurationMock);
windowDi.override(requestHelmReleaseUpdateInjectable, () => requestHelmReleaseUpdateMock);
@@ -77,6 +77,11 @@ describe("showing details for helm release", () => {
windowDi.override(requestHelmChartValuesInjectable, () => requestHelmChartValuesMock);
});
builder.beforeApplicationStart(({ mainDi }) => {
listClusterHelmReleasesMock = asyncFn();
mainDi.override(listClusterHelmReleasesInjectable, () => listClusterHelmReleasesMock);
});
builder.namespaces.add("some-namespace");
builder.namespaces.add("some-other-namespace");
builder.namespaces.add("some-third-namespace");
@@ -110,9 +115,9 @@ describe("showing details for helm release", () => {
});
it("calls for releases for each selected namespace", () => {
expect(requestHelmReleasesMock).toBeCalledTimes(2);
expect(requestHelmReleasesMock).toBeCalledWith("some-namespace");
expect(requestHelmReleasesMock).toBeCalledWith("some-other-namespace");
expect(listClusterHelmReleasesMock).toBeCalledTimes(2);
expect(listClusterHelmReleasesMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }), "some-namespace");
expect(listClusterHelmReleasesMock).toBeCalledWith(anyObject({ id: "some-cluster-id" }), "some-other-namespace");
});
it("shows spinner", () => {
@@ -122,42 +127,54 @@ describe("showing details for helm release", () => {
});
it("when releases resolve but there is none, renders", async () => {
await requestHelmReleasesMock.resolve([]);
await requestHelmReleasesMock.resolve([]);
await listClusterHelmReleasesMock.resolve({
callWasSuccessful: true,
response: [],
});
await listClusterHelmReleasesMock.resolve({
callWasSuccessful: true,
response: [],
});
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when releases resolve", () => {
beforeEach(async () => {
await requestHelmReleasesMock.resolveSpecific(
([namespace]) => namespace === "some-namespace",
[
{
appVersion: "some-app-version",
name: "some-name",
namespace: "some-namespace",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
},
],
await listClusterHelmReleasesMock.resolveSpecific(
([, namespace]) => namespace === "some-namespace",
{
callWasSuccessful: true,
response: [
{
app_version: "some-app-version",
name: "some-name",
namespace: "some-namespace",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
},
],
},
);
await requestHelmReleasesMock.resolveSpecific(
([namespace]) => namespace === "some-other-namespace",
[
{
appVersion: "some-other-app-version",
name: "some-other-name",
namespace: "some-other-namespace",
chart: "some-other-chart-2.0.0",
status: "some-other-status",
updated: "some-other-updated",
revision: "some-other-revision",
},
],
await listClusterHelmReleasesMock.resolveSpecific(
([, namespace]) => namespace === "some-other-namespace",
{
callWasSuccessful: true,
response: [
{
app_version: "some-other-app-version",
name: "some-other-name",
namespace: "some-other-namespace",
chart: "some-other-chart-2.0.0",
status: "some-other-status",
updated: "some-other-updated",
revision: "some-other-revision",
},
],
},
);
});
@@ -191,10 +208,11 @@ describe("showing details for helm release", () => {
});
it("calls for release", () => {
expect(requestDetailedHelmReleaseMock).toHaveBeenCalledWith(
"some-name",
"some-namespace",
);
expect(requestDetailedHelmReleaseMock).toHaveBeenCalledWith({
clusterId: "some-cluster-id",
namespace: "some-namespace",
releaseName: "some-name",
});
});
it("shows spinner", () => {
@@ -219,10 +237,11 @@ describe("showing details for helm release", () => {
});
it("calls for another release", () => {
expect(requestDetailedHelmReleaseMock).toHaveBeenCalledWith(
"some-other-name",
"some-other-namespace",
);
expect(requestDetailedHelmReleaseMock).toHaveBeenCalledWith({
clusterId: "some-cluster-id",
namespace: "some-other-namespace",
releaseName: "some-other-name",
});
});
it("closes details for first release", () => {
@@ -252,21 +271,21 @@ describe("showing details for helm release", () => {
await requestDetailedHelmReleaseMock.resolve({
callWasSuccessful: true,
response: {
release: {
appVersion: "some-app-version",
release: toHelmRelease({
app_version: "some-app-version",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
name: "some-other-name",
namespace: "some-other-namespace",
},
}),
details: {
name: "some-other-name",
namespace: "some-other-namespace",
version: "some-version",
config: "some-config",
version: 1,
config: {},
manifest: "some-manifest",
info: {
@@ -393,21 +412,21 @@ describe("showing details for helm release", () => {
await requestDetailedHelmReleaseMock.resolve({
callWasSuccessful: true,
response: {
release: {
appVersion: "some-app-version",
release: toHelmRelease({
app_version: "some-app-version",
chart: "some-chart-1.0.0",
status: "some-status",
updated: "some-updated",
revision: "some-revision",
name: "some-name",
namespace: "some-namespace",
},
}),
details: {
name: "some-name",
namespace: "some-namespace",
version: "some-version",
config: "some-config",
version: 1,
config: {},
manifest: "some-manifest",
info: {
@@ -632,7 +651,7 @@ describe("showing details for helm release", () => {
describe("when update resolves with success", () => {
beforeEach(async () => {
requestHelmReleasesMock.mockClear();
listClusterHelmReleasesMock.mockClear();
requestHelmReleaseConfigurationMock.mockClear();
await requestHelmReleaseUpdateMock.resolve({
@@ -671,7 +690,7 @@ describe("showing details for helm release", () => {
describe("when update resolves with failure", () => {
beforeEach(async () => {
requestHelmReleasesMock.mockClear();
listClusterHelmReleasesMock.mockClear();
requestHelmReleaseConfigurationMock.mockClear();
await requestHelmReleaseUpdateMock.resolve({