1
0
mirror of https://github.com/lensapp/lens.git synced 2024-09-19 05:17:22 +03:00

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 2023-05-08 15:17:40 -04:00
parent b7ac14df73
commit 40af0d31c5
30 changed files with 664 additions and 417 deletions

View File

@ -4,14 +4,14 @@
*/
import type { ItemObject } from "@k8slens/list-layout";
import type { HelmReleaseDetails } from "./helm-releases.api/request-details.injectable";
import type { HelmReleaseData } from "../../../features/helm-releases/common/channels";
export interface HelmReleaseUpdateDetails {
log: string;
release: HelmReleaseDetails;
release: HelmReleaseData;
}
export interface HelmReleaseDto {
export interface HelmRelease extends ItemObject {
appVersion: string;
name: string;
namespace: string;
@ -19,14 +19,10 @@ export interface HelmReleaseDto {
status: string;
updated: string;
revision: string;
}
export interface HelmRelease extends HelmReleaseDto, ItemObject {
getNs: () => string;
getChart: (withVersion?: boolean) => string;
getRevision: () => number;
getStatus: () => string;
getVersion: () => string;
getUpdated: (humanize?: boolean, compact?: boolean) => string | number;
getRepo: () => Promise<string>;
}

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeJsonApiData } from "@k8slens/kube-object";
import { urlBuilderFor } from "@k8slens/utilities";
import apiBaseInjectable from "../../api-base.injectable";
export interface HelmReleaseDetails {
resources: KubeJsonApiData[];
name: string;
namespace: string;
version: string;
config: string; // release values
manifest: string;
info: {
deleted: string;
description: string;
first_deployed: string;
last_deployed: string;
notes: string;
status: string;
};
}
export type CallForHelmReleaseDetails = (name: string, namespace: string) => Promise<HelmReleaseDetails>;
const requestDetailsEndpoint = urlBuilderFor("/v2/releases/:namespace/:name");
const requestHelmReleaseDetailsInjectable = getInjectable({
id: "call-for-helm-release-details",
instantiate: (di): CallForHelmReleaseDetails => {
const apiBase = di.inject(apiBaseInjectable);
return (name, namespace) => apiBase.get(requestDetailsEndpoint.compile({ name, namespace }));
},
});
export default requestHelmReleaseDetailsInjectable;

View File

@ -1,24 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { urlBuilderFor } from "@k8slens/utilities";
import apiBaseInjectable from "../../api-base.injectable";
import type { HelmReleaseDto } from "../helm-releases.api";
export type RequestHelmReleases = (namespace?: string) => Promise<HelmReleaseDto[]>;
const requestHelmReleasesEndpoint = urlBuilderFor("/v2/releases/:namespace?");
const requestHelmReleasesInjectable = getInjectable({
id: "request-helm-releases",
instantiate: (di): RequestHelmReleases => {
const apiBase = di.inject(apiBaseInjectable);
return (namespace) => apiBase.get(requestHelmReleasesEndpoint.compile({ namespace }));
},
});
export default requestHelmReleasesInjectable;

View File

@ -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", () => {

View File

@ -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", () => {

View File

@ -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");

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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({

View File

@ -0,0 +1,67 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncResult } from "@k8slens/utilities";
import { isObject, json } from "@k8slens/utilities";
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmReleaseData } from "../../../features/helm-releases/common/channels";
import execHelmInjectable from "../exec-helm/exec-helm.injectable";
export type GetHelmReleaseData = (
name: string,
namespace: string,
kubeconfigPath: string,
) => AsyncResult<HelmReleaseData, string>;
const getHelmReleaseDataInjectable = getInjectable({
id: "get-helm-release-data",
instantiate: (di): GetHelmReleaseData => {
const execHelm = di.inject(execHelmInjectable);
return async (releaseName, namespace, proxyKubeconfigPath) => {
const result = await execHelm([
"status",
releaseName,
"--namespace",
namespace,
"--kubeconfig",
proxyKubeconfigPath,
"--output",
"json",
]);
if (!result.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Failed to execute helm: ${result.error}`,
};
}
const parseResult = json.parse(result.response);
if (!parseResult.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Failed to parse helm response: ${parseResult.error}`,
};
}
const release = parseResult.response;
if (!isObject(release) || Array.isArray(release)) {
return {
callWasSuccessful: false,
error: `Helm response is not an object: ${JSON.stringify(release)}`,
};
}
return {
callWasSuccessful: true,
response: release as unknown as HelmReleaseData,
};
};
},
});
export default getHelmReleaseDataInjectable;

View File

@ -4,12 +4,13 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AsyncResult } from "@k8slens/utilities";
import { isObject } from "@k8slens/utilities";
import execHelmInjectable from "../../../exec-helm/exec-helm.injectable";
import yaml from "js-yaml";
import type { KubeJsonApiData, KubeJsonApiDataList } from "@k8slens/kube-object";
const callForHelmManifestInjectable = getInjectable({
id: "call-for-helm-manifest",
const requestHelmManifestInjectable = getInjectable({
id: "request-helm-manifest",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
@ -37,11 +38,11 @@ const callForHelmManifestInjectable = getInjectable({
callWasSuccessful: true,
response: yaml
.loadAll(result.response)
.filter((manifest) => !!manifest) as KubeJsonApiData[],
.filter(isObject) as unknown as KubeJsonApiData[],
};
};
},
});
export default callForHelmManifestInjectable;
export default requestHelmManifestInjectable;

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import callForHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable";
import requestHelmManifestInjectable from "./call-for-helm-manifest/call-for-helm-manifest.injectable";
import type { KubeJsonApiData, KubeJsonApiDataList } from "@k8slens/kube-object";
import type { AsyncResult } from "@k8slens/utilities";
@ -17,10 +17,10 @@ const getHelmReleaseResourcesInjectable = getInjectable({
id: "get-helm-release-resources",
instantiate: (di): GetHelmReleaseResources => {
const callForHelmManifest = di.inject(callForHelmManifestInjectable);
const requestHelmManifest = di.inject(requestHelmManifestInjectable);
return async (name, namespace, kubeconfigPath) => {
const result = await callForHelmManifest(name, namespace, kubeconfigPath);
const result = await requestHelmManifest(name, namespace, kubeconfigPath);
if (!result.callWasSuccessful) {
return result;

View File

@ -4,47 +4,42 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import { loggerInjectionToken } from "@k8slens/logger";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import { isObject, json } from "@k8slens/utilities";
import execHelmInjectable from "../exec-helm/exec-helm.injectable";
import type { AsyncResult } from "@k8slens/utilities";
import getHelmReleaseResourcesInjectable from "./get-helm-release-resources/get-helm-release-resources.injectable";
import type { HelmReleaseDataWithResources } from "../../../features/helm-releases/common/channels";
import getHelmReleaseDataInjectable from "./get-helm-release-data.injectable";
export interface GetHelmReleaseArgs {
cluster: Cluster;
releaseName: string;
namespace: string;
}
export type GetHelmRelease = (args: GetHelmReleaseArgs) => AsyncResult<HelmReleaseDataWithResources, string>;
const getHelmReleaseInjectable = getInjectable({
id: "get-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectionToken);
const execHelm = di.inject(execHelmInjectable);
instantiate: (di): GetHelmRelease => {
const getHelmReleaseData = di.inject(getHelmReleaseDataInjectable);
const getHelmReleaseResources = di.inject(getHelmReleaseResourcesInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string) => {
return async ({ cluster, namespace, releaseName }) => {
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();
logger.debug("Fetch release");
const result = await execHelm([
"status",
const releaseResult = await getHelmReleaseData(
releaseName,
"--namespace",
namespace,
"--kubeconfig",
proxyKubeconfigPath,
"--output",
"json",
]);
);
if (!result.callWasSuccessful) {
logger.warn(`Failed to exectute helm: ${result.error}`);
return undefined;
}
const release = json.parse(result.response);
if (!isObject(release) || Array.isArray(release)) {
return undefined;
if (!releaseResult.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Failed to get helm release data: ${releaseResult.error}`,
};
}
const resourcesResult = await getHelmReleaseResources(
@ -54,14 +49,18 @@ const getHelmReleaseInjectable = getInjectable({
);
if (!resourcesResult.callWasSuccessful) {
logger.warn(`Failed to get helm release resources: ${resourcesResult.error}`);
return undefined;
return {
callWasSuccessful: false,
error: `Failed to get helm release resources: ${resourcesResult.error}`,
};
}
return {
...release,
resources: resourcesResult.response,
callWasSuccessful: true,
response: {
...releaseResult.response,
resources: resourcesResult.response,
},
};
};
},

View File

@ -7,15 +7,19 @@ import type { Cluster } from "../../../common/cluster/cluster";
import { loggerInjectionToken } from "@k8slens/logger";
import kubeconfigManagerInjectable from "../../kubeconfig-manager/kubeconfig-manager.injectable";
import listHelmReleasesInjectable from "../list-helm-releases.injectable";
import type { AsyncResult } from "@k8slens/utilities";
import type { ListedHelmRelease } from "../../../features/helm-releases/common/channels";
export type ListClusterHelmReleases = (cluster: Cluster, namespace?: string) => AsyncResult<ListedHelmRelease[], string>;
const listClusterHelmReleasesInjectable = getInjectable({
id: "list-cluster-helm-releases",
instantiate: (di) => {
instantiate: (di): ListClusterHelmReleases => {
const logger = di.inject(loggerInjectionToken);
const listHelmReleases = di.inject(listHelmReleasesInjectable);
return async (cluster: Cluster, namespace?: string) => {
return async (cluster, namespace) => {
const proxyKubeconfigManager = di.inject(kubeconfigManagerInjectable, cluster);
const proxyKubeconfigPath = await proxyKubeconfigManager.ensurePath();

View File

@ -52,9 +52,15 @@ const updateHelmReleaseInjectable = getInjectable({
throw result.error; // keep the same interface
}
const releaseResult = await getHelmRelease({ cluster, releaseName, namespace });
if (!releaseResult.callWasSuccessful) {
throw releaseResult.error; // keep the same interface
}
return {
log: result.response,
release: await getHelmRelease(cluster, releaseName, namespace),
release: releaseResult.response,
};
} finally {
await removePath(valuesFilePath);

View File

@ -4,9 +4,11 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import execHelmInjectable from "./exec-helm/exec-helm.injectable";
import { toCamelCase, isObject } from "@k8slens/utilities";
import type { AsyncResult } from "@k8slens/utilities";
import { isObject } from "@k8slens/utilities";
import type { ListedHelmRelease } from "../../features/helm-releases/common/channels";
export type ListHelmReleases = (pathToKubeconfig: string, namespace?: string) => Promise<Record<string, any>[]>;
export type ListHelmReleases = (pathToKubeconfig: string, namespace?: string) => AsyncResult<ListedHelmRelease[], string>;
const listHelmReleasesInjectable = getInjectable({
id: "list-helm-releases",
@ -33,16 +35,21 @@ const listHelmReleasesInjectable = getInjectable({
const result = await execHelm(args);
if (!result.callWasSuccessful) {
throw result.error;
return {
callWasSuccessful: false,
error: `Failed to list helm releases: ${result.error}`,
};
}
const output = JSON.parse(result.response);
const rawOutput = JSON.parse(result.response);
const output = Array.isArray(rawOutput)
? rawOutput.filter(isObject)
: [];
if (!Array.isArray(output) || output.length == 0) {
return [];
}
return output.filter(isObject).map(toCamelCase);
return {
callWasSuccessful: true,
response: output as unknown as ListedHelmRelease[],
};
};
},
});

View File

@ -1,29 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiPrefix } from "../../../../common/vars";
import { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route";
import getHelmReleaseInjectable from "../../../helm/helm-service/get-helm-release.injectable";
const getReleaseRouteInjectable = getRouteInjectable({
id: "get-release-route",
instantiate: (di) => {
const getHelmRelease = di.inject(getHelmReleaseInjectable);
return clusterRoute({
method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
})(async ({ cluster, params }) => ({
response: await getHelmRelease(
cluster,
params.release,
params.namespace,
),
}));
},
});
export default getReleaseRouteInjectable;

View File

@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { apiPrefix } from "../../../../common/vars";
import { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route";
import listClusterHelmReleasesInjectable from "../../../helm/helm-service/list-helm-releases.injectable";
const listReleasesRouteInjectable = getRouteInjectable({
id: "list-releases-route",
instantiate: (di) => {
const listHelmReleases = di.inject(listClusterHelmReleasesInjectable);
return clusterRoute({
method: "get",
path: `${apiPrefix}/v2/releases/{namespace?}`,
})(async ({ cluster, params }) => ({
response: await listHelmReleases(cluster, params.namespace),
}));
},
});
export default listReleasesRouteInjectable;

View File

@ -26,7 +26,7 @@ const helmChartVersionsInjectable = getInjectable({
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, release: HelmRelease) => release.getName(),
getInstanceKey: (di, release: HelmRelease) => `${release.namespace}/${release.name}}`,
}),
});

View File

@ -0,0 +1,33 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import { when } from "mobx";
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import helmChartVersionsInjectable from "../helm-charts/helm-charts/versions.injectable";
const helmChartRepoInjectable = getInjectable({
id: "helm-chart-repo",
instantiate: (di, release) => {
const chartVersions = di.inject(helmChartVersionsInjectable, release);
return asyncComputed({
getValueFromObservedPromise: async () => {
await when(() => !chartVersions.pending.get());
const version = release.getVersion();
return chartVersions.value
.get()
.find((chartVersion) => chartVersion.version === version)?.repo;
},
});
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, release: HelmRelease) => `${release.namespace}/${release.name}`,
}),
});
export default helmChartRepoInjectable;

View File

@ -29,17 +29,24 @@ import type { NavigateToHelmReleases } from "../../../../../common/front-end-rou
import navigateToHelmReleasesInjectable from "../../../../../common/front-end-routing/routes/cluster/helm/releases/navigate-to-helm-releases.injectable";
import assert from "assert";
import activeThemeInjectable from "../../../../themes/active.injectable";
import type { ToHelmRelease } from "../../to-helm-release.injectable";
import toHelmReleaseInjectable from "../../to-helm-release.injectable";
import hostedClusterIdInjectable from "../../../../cluster-frame-context/hosted-cluster-id.injectable";
import helmChartRepoInjectable from "../../helm-chart-repo.injectable";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { waitUntilDefined } from "@k8slens/utilities";
const releaseDetailsModelInjectable = getInjectable({
id: "release-details-model",
instantiate: async (di, targetRelease: TargetHelmRelease) => {
const clusterId = di.inject(hostedClusterIdInjectable);
assert(clusterId, "Cluster id is required");
const model = new ReleaseDetailsModel({
requestDetailedHelmRelease: di.inject(requestDetailedHelmReleaseInjectable),
targetRelease,
activeTheme: di.inject(activeThemeInjectable),
clusterId,
requestHelmReleaseConfiguration: di.inject(requestHelmReleaseConfigurationInjectable),
getResourceDetailsUrl: di.inject(getResourceDetailsUrlInjectable),
updateRelease: di.inject(updateReleaseInjectable),
@ -47,7 +54,7 @@ const releaseDetailsModelInjectable = getInjectable({
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
navigateToHelmReleases: di.inject(navigateToHelmReleasesInjectable),
toHelmRelease: di.inject(toHelmReleaseInjectable),
helmChartRepo: di.injectFactory(helmChartRepoInjectable),
});
await model.load();
@ -78,6 +85,7 @@ export interface ConfigurationInput {
interface Dependencies {
readonly targetRelease: TargetHelmRelease;
readonly activeTheme: IComputedValue<LensTheme>;
readonly clusterId: string;
requestDetailedHelmRelease: RequestDetailedHelmRelease;
requestHelmReleaseConfiguration: RequestHelmReleaseConfiguration;
getResourceDetailsUrl: GetResourceDetailsUrl;
@ -86,7 +94,7 @@ interface Dependencies {
showSuccessNotification: ShowNotification;
createUpgradeChartTab: (release: HelmRelease) => string;
navigateToHelmReleases: NavigateToHelmReleases;
toHelmRelease: ToHelmRelease;
helmChartRepo: (release: HelmRelease) => IAsyncComputed<string | undefined>;
}
export class ReleaseDetailsModel {
@ -114,10 +122,12 @@ export class ReleaseDetailsModel {
const name = this.release.getName();
const namespace = this.release.getNs();
const helmChartRepo = this.dependencies.helmChartRepo(this.release);
const repo = await waitUntilDefined(helmChartRepo.value);
const data = {
chart: this.release.getChart(),
repo: await this.release.getRepo(),
repo,
version: this.release.getVersion(),
values: this.configuration.nonSavedValue.get(),
};
@ -165,10 +175,11 @@ export class ReleaseDetailsModel {
load = async () => {
const { name, namespace } = this.dependencies.targetRelease;
const result = await this.dependencies.requestDetailedHelmRelease(
name,
const result = await this.dependencies.requestDetailedHelmRelease({
releaseName: name,
namespace,
);
clusterId: this.dependencies.clusterId,
});
if (!result.callWasSuccessful) {
runInAction(() => {
@ -210,7 +221,7 @@ export class ReleaseDetailsModel {
assert(detailedRelease, "Tried to access release before load");
return this.dependencies.toHelmRelease(detailedRelease.release);
return detailedRelease.release;
}
@computed private get details() {
@ -227,7 +238,7 @@ export class ReleaseDetailsModel {
@computed get groupedResources(): MinimalResourceGroup[] {
return pipeline(
this.details?.resources ?? [],
this.details.resources ?? [],
groupBy((resource) => resource.kind),
(grouped) => Object.entries(grouped),

View File

@ -3,47 +3,57 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { HelmReleaseDto } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
import requestHelmReleasesInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import type { HelmReleaseDetails } from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-details.injectable";
import requestHelmReleaseDetailsInjectable from "../../../../../common/k8s-api/endpoints/helm-releases.api/request-details.injectable";
import type { AsyncResult } from "@k8slens/utilities";
import requestHelmReleaseInjectable from "../../../../../features/helm-releases/renderer/requesthelm-release.injectable";
import type { GetHelmReleaseArgs, HelmReleaseDataWithResources } from "../../../../../features/helm-releases/common/channels";
import requestListHelmReleasesInjectable from "../../../../../features/helm-releases/renderer/request-list-helm-releases.injectable";
import type { HelmRelease } from "../../../../../common/k8s-api/endpoints/helm-releases.api";
import { toHelmRelease } from "../../to-helm-release";
export interface DetailedHelmRelease {
release: HelmReleaseDto;
details?: HelmReleaseDetails;
release: HelmRelease;
details: HelmReleaseDataWithResources;
}
export type RequestDetailedHelmRelease = (
name: string,
namespace: string
) => AsyncResult<DetailedHelmRelease>;
export type RequestDetailedHelmRelease = (args: GetHelmReleaseArgs) => AsyncResult<DetailedHelmRelease>;
const requestDetailedHelmReleaseInjectable = getInjectable({
id: "request-detailed-helm-release",
instantiate: (di): RequestDetailedHelmRelease => {
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
const requestHelmReleaseDetails = di.inject(requestHelmReleaseDetailsInjectable);
const requestListHelmReleases = di.inject(requestListHelmReleasesInjectable);
const requestHelmRelease = di.inject(requestHelmReleaseInjectable);
return async (name, namespace) => {
const [releases, details] = await Promise.all([
requestHelmReleases(namespace),
requestHelmReleaseDetails(name, namespace),
]);
return async ({ clusterId, namespace, releaseName }) => {
const listReleasesResult = await requestListHelmReleases({ clusterId, namespace });
const detailsResult = await requestHelmRelease({ clusterId, releaseName, namespace });
const release = releases.find(
(rel) => rel.name === name && rel.namespace === namespace,
if (!listReleasesResult.callWasSuccessful) {
return listReleasesResult;
}
const release = listReleasesResult.response.find(
(rel) => rel.name === releaseName && rel.namespace === namespace,
);
if (!release) {
return {
callWasSuccessful: false,
error: `Release ${name} didn't exist in ${namespace} namespace.`,
error: `Release ${releaseName} didn't exist in ${namespace} namespace.`,
};
}
return { callWasSuccessful: true, response: { release, details }};
if (!detailsResult.callWasSuccessful) {
return detailsResult;
}
return {
callWasSuccessful: true,
response: {
release: toHelmRelease(release),
details: detailsResult.response,
},
};
};
},
});

View File

@ -6,31 +6,49 @@ import { getInjectable } from "@ogre-tools/injectable";
import { asyncComputed } from "@ogre-tools/injectable-react";
import clusterFrameContextForNamespacedResourcesInjectable from "../../cluster-frame-context/for-namespaced-resources.injectable";
import releaseSecretsInjectable from "./release-secrets.injectable";
import requestHelmReleasesInjectable from "../../../common/k8s-api/endpoints/helm-releases.api/request-releases.injectable";
import toHelmReleaseInjectable from "./to-helm-release.injectable";
import requestListHelmReleasesInjectable from "../../../features/helm-releases/renderer/request-list-helm-releases.injectable";
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
import assert from "assert";
import { iter } from "@k8slens/utilities";
import { prefixedLoggerInjectable } from "@k8slens/logger";
import { toHelmRelease } from "./to-helm-release";
const releasesInjectable = getInjectable({
id: "releases",
instantiate: (di) => {
const clusterContext = di.inject(clusterFrameContextForNamespacedResourcesInjectable);
const hostedClusterId = di.inject(hostedClusterIdInjectable);
const releaseSecrets = di.inject(releaseSecretsInjectable);
const requestHelmReleases = di.inject(requestHelmReleasesInjectable);
const toHelmRelease = di.inject(toHelmReleaseInjectable);
const requestListHelmReleases = di.inject(requestListHelmReleasesInjectable);
const logger = di.inject(prefixedLoggerInjectable, "HELM-RELEASES");
assert(hostedClusterId, "hostedClusterId is required");
return asyncComputed({
getValueFromObservedPromise: async () => {
void releaseSecrets.get();
const releaseArrays = await (
const releaseResults = await (
clusterContext.hasSelectedAll
? requestHelmReleases()
: Promise.all(clusterContext.contextNamespaces.map((namespace) => requestHelmReleases(namespace)))
? requestListHelmReleases({ clusterId: hostedClusterId })
: Promise.all(clusterContext.contextNamespaces.map((namespace) => requestListHelmReleases({ clusterId: hostedClusterId, namespace })))
);
return releaseArrays.flat().map(toHelmRelease);
},
return iter.chain([releaseResults].flat().values())
.filterMap((result) => {
if (result.callWasSuccessful) {
return result.response;
}
logger.warn("Failed to list helm releases", { error: result.error });
return undefined;
})
.flatMap((releases) => releases)
.map(toHelmRelease)
.toArray();
},
valueWhenPending: [],
});
},

View File

@ -1,90 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { capitalize } from "lodash";
import { when } from "mobx";
import helmChartVersionsInjectable from "../helm-charts/helm-charts/versions.injectable";
import type { HelmRelease, HelmReleaseDto } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { getMillisecondsFromUnixEpoch } from "../../../common/utils/date/get-current-date-time";
import { formatDuration } from "@k8slens/utilities";
export type ToHelmRelease = (release: HelmReleaseDto) => HelmRelease;
const toHelmReleaseInjectable = getInjectable({
id: "to-helm-release",
instantiate: (di): ToHelmRelease => {
const helmChartVersions = (release: HelmRelease) => di.inject(helmChartVersionsInjectable, release);
return (release) => ({
...release,
getId() {
return `${this.namespace}/${this.name}`;
},
getName() {
return this.name;
},
getNs() {
return this.namespace;
},
getChart(withVersion = false) {
let chart = this.chart;
if (!withVersion && this.getVersion() != "") {
const search = new RegExp(`-${this.getVersion()}`);
chart = chart.replace(search, "");
}
return chart;
},
getRevision() {
return parseInt(this.revision, 10);
},
getStatus() {
return capitalize(this.status);
},
getVersion() {
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
return versions?.[0] ?? "";
},
getUpdated(humanize = true, compact = true) {
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updatedDate = new Date(updated).getTime();
const diff = getMillisecondsFromUnixEpoch() - updatedDate;
if (humanize) {
return formatDuration(diff, compact);
}
return diff;
},
// Helm does not store from what repository the release is installed,
// so we have to try to guess it by searching charts
async getRepo() {
const versionsComputed = helmChartVersions(this);
const version = this.getVersion();
await when(() => !versionsComputed.pending.get());
return versionsComputed.value
.get()
.find((chartVersion) => chartVersion.version === version)?.repo
?? "";
},
});
},
});
export default toHelmReleaseInjectable;

View File

@ -0,0 +1,69 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { capitalize } from "lodash";
import type { HelmRelease } from "../../../common/k8s-api/endpoints/helm-releases.api";
import { getMillisecondsFromUnixEpoch } from "../../../common/utils/date/get-current-date-time";
import { formatDuration } from "@k8slens/utilities";
import type { ListedHelmRelease } from "../../../features/helm-releases/common/channels";
export const toHelmRelease = (release: ListedHelmRelease): HelmRelease => ({
appVersion: release.app_version,
chart: release.chart,
namespace: release.namespace,
revision: release.revision,
status: release.status,
name: release.name,
updated: release.updated,
getId() {
return `${this.namespace}/${this.name}`;
},
getName() {
return this.name;
},
getNs() {
return this.namespace;
},
getChart(withVersion = false) {
let chart = this.chart;
if (!withVersion && this.getVersion() != "") {
const search = new RegExp(`-${this.getVersion()}`);
chart = chart.replace(search, "");
}
return chart;
},
getRevision() {
return parseInt(this.revision, 10);
},
getStatus() {
return capitalize(this.status);
},
getVersion() {
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
return versions?.[0] ?? "";
},
getUpdated(humanize = true, compact = true) {
const updated = this.updated.replace(/\s\w*$/, ""); // 2019-11-26 10:58:09 +0300 MSK -> 2019-11-26 10:58:09 +0300 to pass into Date()
const updatedDate = new Date(updated).getTime();
const diff = getMillisecondsFromUnixEpoch() - updatedDate;
if (humanize) {
return formatDuration(diff, compact);
}
return diff;
},
});

View File

@ -71,6 +71,7 @@ import { testUsingFakeTime } from "../../../test-utils/use-fake-time";
import { sendMessageToChannelInjectionToken } from "@k8slens/messaging";
import { getMessageBridgeFake } from "@k8slens/messaging-fake-bridge";
import { historyInjectionToken } from "@k8slens/routing";
import writeJsonSyncInjectable from "../../../common/fs/write-json-sync.injectable";
type MainDiCallback = (container: { mainDi: DiContainer }) => void | Promise<void>;
type WindowDiCallback = (container: { windowDi: DiContainer }) => void | Promise<void>;
@ -240,7 +241,7 @@ export const getApplicationBuilder = () => {
},
}));
const windowHelpers = new Map<string, { di: DiContainer; getRendered: () => RenderResult }>();
const windowHelpers: ApplicationWindowHelpers = new Map();
const createElectronWindowFake: CreateElectronWindow = (configuration) => {
const windowId = configuration.id;
@ -273,7 +274,10 @@ export const getApplicationBuilder = () => {
let rendered: RenderResult;
windowHelpers.set(windowId, { di: windowDi, getRendered: () => rendered });
windowHelpers.set(windowId, {
di: windowDi,
getRendered: () => rendered,
});
return {
show: () => {},
@ -321,9 +325,10 @@ export const getApplicationBuilder = () => {
const namespaceItems = observable.array<Namespace>();
const selectedNamespaces = observable.set<string>();
const startApplication = mainDi.inject(startApplicationInjectionToken);
const startApp = async ({ shouldStartHidden }: { shouldStartHidden: boolean }) => {
const startApplication = mainDi.inject(startApplicationInjectionToken);
mainDi.inject(lensProxyPortInjectable).set(42);
for (const callback of beforeApplicationStartCallbacks) {
@ -545,19 +550,17 @@ export const getApplicationBuilder = () => {
setEnvironmentToClusterFrame: () => {
environment = environments.clusterFrame;
const cluster = new Cluster({
id: "some-cluster-id",
contextName: "some-context-name",
kubeConfigPath: "/some-path-to-kube-config",
});
builder.beforeWindowStart(({ windowDi }) => {
const cluster = new Cluster({
id: "some-cluster-id",
contextName: "some-context-name",
kubeConfigPath: "/some-path-to-kube-config",
});
windowDi.override(activeKubernetesClusterInjectable, () =>
computed(() => catalogEntityFromCluster(cluster)),
);
windowDi.override(hostedClusterIdInjectable, () => cluster.id);
windowDi.override(hostedClusterInjectable, () => cluster);
// TODO: Remove this once we moved catalog to new IPC injectables
windowDi.override(activeKubernetesClusterInjectable, () => computed(() => catalogEntityFromCluster(cluster)));
// TODO: Figure out a way to remove this stub.
windowDi.override(namespaceStoreInjectable, () => ({
@ -581,6 +584,43 @@ export const getApplicationBuilder = () => {
} as Partial<NamespaceStore> as NamespaceStore));
});
builder.beforeApplicationStart(({ mainDi }) => {
const writeJsonSync = mainDi.inject(writeJsonSyncInjectable);
writeJsonSync("/some-path-to-kube-config", {
clusters: [
{
name: cluster.contextName,
},
],
users: [
{
name: "some-user-name",
},
],
contexts: [
{
name: cluster.contextName,
context: {
cluster: cluster.contextName,
user: "some-user-name",
},
},
],
"current-context": cluster.contextName,
});
writeJsonSync("/some-directory-for-app-data/some-product-name/lens-cluster-store.json", {
clusters: [
{
id: cluster.id,
kubeConfigPath: "/some-path-to-kube-config",
contextName: cluster.contextName,
},
],
__internal__: { migrations: { version: "6.4.0" }},
});
});
return builder;
},
@ -774,17 +814,13 @@ const findExtensionInstance = <T extends LensExtension> (di: DiContainer, inject
type ApplicationWindowHelpers = Map<string, { di: DiContainer; getRendered: () => RenderResult }>;
const toWindowWithHelpersFor =
(windowHelpers: ApplicationWindowHelpers) => (applicationWindow: LensWindow) => ({
(windowHelpers: ApplicationWindowHelpers) => (applicationWindow: LensWindow): LensWindowWithHelpers => ({
...applicationWindow,
get rendered() {
const helpers = windowHelpers.get(applicationWindow.id);
if (!helpers) {
throw new Error(
`Tried to get rendered for application window "${applicationWindow.id}" before it was started.`,
);
}
assert(helpers, `Tried to get rendered for application window "${applicationWindow.id}" before it was started.`);
return helpers.getRendered();
},
@ -792,11 +828,7 @@ const toWindowWithHelpersFor =
get di() {
const helpers = windowHelpers.get(applicationWindow.id);
if (!helpers) {
throw new Error(
`Tried to get di for application window "${applicationWindow.id}" before it was started.`,
);
}
assert(helpers, `Tried to get rendered for application window "${applicationWindow.id}" before it was started.`);
return helpers.di;
},

View File

@ -49,7 +49,7 @@ const webpackLensMain = (): webpack.Configuration => {
use: "node-loader",
},
{
test: /\.ts$/,
test: (modulePath) => modulePath.endsWith(".ts") && !modulePath.endsWith(".test.ts"),
exclude: /node_modules/,
use: {
loader: "ts-loader",

View File

@ -62,7 +62,10 @@ export function webpackLensRenderer(): webpack.Configuration {
use: "node-loader",
},
{
test: /\.tsx?$/,
test: (modulePath) => (
(modulePath.endsWith(".ts") && !modulePath.endsWith(".test.ts"))
|| (modulePath.endsWith(".tsx") && !modulePath.endsWith(".test.tsx"))
),
exclude: /node_modules/,
use: {
loader: "ts-loader",