diff --git a/packages/core/src/common/k8s-api/endpoints/helm-releases.api.ts b/packages/core/src/common/k8s-api/endpoints/helm-releases.api.ts index a5807edb6882..a3a1c0f8660d 100644 --- a/packages/core/src/common/k8s-api/endpoints/helm-releases.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/helm-releases.api.ts @@ -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; } diff --git a/packages/core/src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts b/packages/core/src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts deleted file mode 100644 index 8c711a019e9c..000000000000 --- a/packages/core/src/common/k8s-api/endpoints/helm-releases.api/request-details.injectable.ts +++ /dev/null @@ -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; - -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; diff --git a/packages/core/src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts b/packages/core/src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts deleted file mode 100644 index ffd3a92deef5..000000000000 --- a/packages/core/src/common/k8s-api/endpoints/helm-releases.api/request-releases.injectable.ts +++ /dev/null @@ -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; - -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; diff --git a/packages/core/src/features/cluster/workloads/pods.test.tsx b/packages/core/src/features/cluster/workloads/pods.test.tsx index e5aa1190f1e4..948ab73c2eca 100644 --- a/packages/core/src/features/cluster/workloads/pods.test.tsx +++ b/packages/core/src/features/cluster/workloads/pods.test.tsx @@ -13,7 +13,6 @@ import type { PodMetricsApi } from "../../../common/k8s-api/endpoints/pod-metric import podMetricsApiInjectable from "../../../common/k8s-api/endpoints/pod-metrics.api.injectable"; import type { RequestMetrics } from "../../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable"; import requestMetricsInjectable from "../../../common/k8s-api/endpoints/metrics.api/request-metrics.injectable"; -import apiManagerInjectable from "../../../common/k8s-api/api-manager/manager.injectable"; describe("workloads / pods", () => { let rendered: RenderResult; @@ -24,19 +23,15 @@ describe("workloads / pods", () => { applicationBuilder = getApplicationBuilder().setEnvironmentToClusterFrame(); applicationBuilder.namespaces.add("default"); applicationBuilder.beforeWindowStart(({ windowDi }) => { + windowDi.override(podMetricsApiInjectable, () => ({ + list: async () => Promise.resolve(podMetrics), + } as PodMetricsApi)); + }); + applicationBuilder.afterWindowStart(() => { applicationBuilder.allowKubeResource({ apiName: "pods", group: "", }); - - windowDi.override(podMetricsApiInjectable, () => ({ - list: async () => Promise.resolve(podMetrics), - } as PodMetricsApi)); - - const apiManager = windowDi.inject(apiManagerInjectable); - const podStore = windowDi.inject(podStoreInjectable); - - apiManager.registerStore(podStore); }); }); diff --git a/packages/core/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts b/packages/core/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts index b5f2e08d7174..76345d0315da 100644 --- a/packages/core/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts +++ b/packages/core/src/features/helm-charts/installing-chart/installing-helm-chart-from-new-tab.test.ts @@ -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; let requestHelmChartValuesMock: AsyncFnMock; let requestCreateHelmReleaseMock: AsyncFnMock; - let requestHelmReleasesMock: AsyncFnMock; + let listClusterHelmReleasesMock: AsyncFnMock; 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", () => { diff --git a/packages/core/src/features/helm-charts/upgrade-chart/upgrade-chart-new-tab.test.ts b/packages/core/src/features/helm-charts/upgrade-chart/upgrade-chart-new-tab.test.ts index a10c18aa10e5..195396006b44 100644 --- a/packages/core/src/features/helm-charts/upgrade-chart/upgrade-chart-new-tab.test.ts +++ b/packages/core/src/features/helm-charts/upgrade-chart/upgrade-chart-new-tab.test.ts @@ -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; - let requestHelmReleasesMock: AsyncFnMock; let requestHelmChartsMock: AsyncFnMock; let requestHelmChartVersionsMock: AsyncFnMock; + let listClusterHelmReleasesMock: AsyncFnMock; 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", () => { diff --git a/packages/core/src/features/helm-releases/common/channels.ts b/packages/core/src/features/helm-releases/common/channels.ts new file mode 100644 index 000000000000..9b451ccf250f --- /dev/null +++ b/packages/core/src/features/helm-releases/common/channels.ts @@ -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; + manifest: string; + version: number; + namespace: string; +} + +export type GetHelmReleaseResponse = Result; + +export const getHelmReleaseChannel = getRequestChannel("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; + +export const listHelmReleasesChannel = getRequestChannel("list-helm-releases"); diff --git a/packages/core/src/features/helm-releases/main/handle-get-helm-release.injectable.ts b/packages/core/src/features/helm-releases/main/handle-get-helm-release.injectable.ts new file mode 100644 index 000000000000..2925e0640f61 --- /dev/null +++ b/packages/core/src/features/helm-releases/main/handle-get-helm-release.injectable.ts @@ -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; diff --git a/packages/core/src/features/helm-releases/main/handle-list-helm-releases.injectable.ts b/packages/core/src/features/helm-releases/main/handle-list-helm-releases.injectable.ts new file mode 100644 index 000000000000..6fbff5fb1b3a --- /dev/null +++ b/packages/core/src/features/helm-releases/main/handle-list-helm-releases.injectable.ts @@ -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; diff --git a/packages/core/src/features/helm-releases/renderer/request-list-helm-releases.injectable.ts b/packages/core/src/features/helm-releases/renderer/request-list-helm-releases.injectable.ts new file mode 100644 index 000000000000..d502eedac6b7 --- /dev/null +++ b/packages/core/src/features/helm-releases/renderer/request-list-helm-releases.injectable.ts @@ -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; + +const requestListHelmReleasesInjectable = getInjectable({ + id: "request-list-helm-releases", + instantiate: (di): RequestListHelmReleases => { + const requestFromChannel = di.inject(requestFromChannelInjectionToken); + + return (args) => requestFromChannel(listHelmReleasesChannel, args); + }, +}); + +export default requestListHelmReleasesInjectable; diff --git "a/packages/core/src/features/helm-releases/renderer/request\342\200\223helm-release.injectable.ts" "b/packages/core/src/features/helm-releases/renderer/request\342\200\223helm-release.injectable.ts" new file mode 100644 index 000000000000..106fa1b0e4fd --- /dev/null +++ "b/packages/core/src/features/helm-releases/renderer/request\342\200\223helm-release.injectable.ts" @@ -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; + +const requestHelmReleaseInjectable = getInjectable({ + id: "request-helm-release", + instantiate: (di): RequestHelmRelease => { + const requestFromChannel = di.inject(requestFromChannelInjectionToken); + + return (args) => requestFromChannel(getHelmReleaseChannel, args); + }, +}); + +export default requestHelmReleaseInjectable; diff --git a/packages/core/src/features/helm-releases/showing-details-for-helm-release.test.ts b/packages/core/src/features/helm-releases/showing-details-for-helm-release.test.ts index 7b6dcbe06d97..98a9e54c4c71 100644 --- a/packages/core/src/features/helm-releases/showing-details-for-helm-release.test.ts +++ b/packages/core/src/features/helm-releases/showing-details-for-helm-release.test.ts @@ -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; let requestDetailedHelmReleaseMock: AsyncFnMock; let requestHelmReleaseConfigurationMock: AsyncFnMock; let requestHelmReleaseUpdateMock: AsyncFnMock; @@ -43,6 +44,7 @@ describe("showing details for helm release", () => { let requestHelmChartValuesMock: AsyncFnMock; let showSuccessNotificationMock: jest.Mock; let showCheckedErrorNotificationMock: jest.Mock; + let listClusterHelmReleasesMock: AsyncFnMock; 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({ diff --git a/packages/core/src/features/namespaces/route-with-sub-namespaces.test.tsx b/packages/core/src/features/namespaces/route-with-sub-namespaces.test.tsx index 33dfba5c1b9c..095b21fa1212 100644 --- a/packages/core/src/features/namespaces/route-with-sub-namespaces.test.tsx +++ b/packages/core/src/features/namespaces/route-with-sub-namespaces.test.tsx @@ -33,11 +33,14 @@ describe("namespaces route when viewed with some subNamespaces", () => { requestDeleteSubNamespaceAnchorMock = asyncFn(); builder.beforeWindowStart(({ windowDi }) => { - builder.allowKubeResource({ group: "", apiName: "namespaces" }); windowDi.override(requestDeleteNormalNamespaceInjectable, () => requestDeleteNormalNamespaceMock); windowDi.override(requestDeleteSubNamespaceAnchorInjectable, () => requestDeleteSubNamespaceAnchorMock); }); + builder.afterWindowStart(() => { + builder.allowKubeResource({ group: "", apiName: "namespaces" }); + }); + result = await builder.render(); }); diff --git a/packages/core/src/main/helm/helm-service/get-helm-release-data.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release-data.injectable.ts new file mode 100644 index 000000000000..3317046e42fa --- /dev/null +++ b/packages/core/src/main/helm/helm-service/get-helm-release-data.injectable.ts @@ -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; + +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; diff --git a/packages/core/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts index b5ad835c2573..92a888867b9e 100644 --- a/packages/core/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts +++ b/packages/core/src/main/helm/helm-service/get-helm-release-resources/call-for-helm-manifest/call-for-helm-manifest.injectable.ts @@ -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; diff --git a/packages/core/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts index 02e6f0f08c88..4499591e3cc8 100644 --- a/packages/core/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts +++ b/packages/core/src/main/helm/helm-service/get-helm-release-resources/get-helm-release-resources.injectable.ts @@ -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; diff --git a/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts b/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts index da7a42d9937d..9247c5415346 100644 --- a/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts +++ b/packages/core/src/main/helm/helm-service/get-helm-release.injectable.ts @@ -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; 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, + }, }; }; }, diff --git a/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts b/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts index 30239046b31e..8c1d41892fd7 100644 --- a/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts +++ b/packages/core/src/main/helm/helm-service/list-helm-releases.injectable.ts @@ -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; 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(); diff --git a/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts b/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts index 03aae067633d..a8cfd32181d7 100644 --- a/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts +++ b/packages/core/src/main/helm/helm-service/update-helm-release.injectable.ts @@ -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); diff --git a/packages/core/src/main/helm/list-helm-releases.injectable.ts b/packages/core/src/main/helm/list-helm-releases.injectable.ts index 5ee02698ba1b..7a22ab85e4e3 100644 --- a/packages/core/src/main/helm/list-helm-releases.injectable.ts +++ b/packages/core/src/main/helm/list-helm-releases.injectable.ts @@ -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[]>; +export type ListHelmReleases = (pathToKubeconfig: string, namespace?: string) => AsyncResult; 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[], + }; }; }, }); diff --git a/packages/core/src/main/routes/helm/releases/get-release-route.injectable.ts b/packages/core/src/main/routes/helm/releases/get-release-route.injectable.ts deleted file mode 100644 index 7b264b4fe96d..000000000000 --- a/packages/core/src/main/routes/helm/releases/get-release-route.injectable.ts +++ /dev/null @@ -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; diff --git a/packages/core/src/main/routes/helm/releases/list-releases-route.injectable.ts b/packages/core/src/main/routes/helm/releases/list-releases-route.injectable.ts deleted file mode 100644 index 53a4dcb179d2..000000000000 --- a/packages/core/src/main/routes/helm/releases/list-releases-route.injectable.ts +++ /dev/null @@ -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; diff --git a/packages/core/src/renderer/components/helm-charts/helm-charts/versions.injectable.ts b/packages/core/src/renderer/components/helm-charts/helm-charts/versions.injectable.ts index 867052b8542d..2ad2aa7bfb43 100644 --- a/packages/core/src/renderer/components/helm-charts/helm-charts/versions.injectable.ts +++ b/packages/core/src/renderer/components/helm-charts/helm-charts/versions.injectable.ts @@ -26,7 +26,7 @@ const helmChartVersionsInjectable = getInjectable({ }, lifecycle: lifecycleEnum.keyedSingleton({ - getInstanceKey: (di, release: HelmRelease) => release.getName(), + getInstanceKey: (di, release: HelmRelease) => `${release.namespace}/${release.name}}`, }), }); diff --git a/packages/core/src/renderer/components/helm-releases/helm-chart-repo.injectable.ts b/packages/core/src/renderer/components/helm-releases/helm-chart-repo.injectable.ts new file mode 100644 index 000000000000..70c38a2843a9 --- /dev/null +++ b/packages/core/src/renderer/components/helm-releases/helm-chart-repo.injectable.ts @@ -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; diff --git a/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/release-details-model.injectable.tsx b/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/release-details-model.injectable.tsx index 55795f5a5575..728ad4bbcbfd 100644 --- a/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/release-details-model.injectable.tsx +++ b/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/release-details-model.injectable.tsx @@ -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; + 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; } 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), diff --git a/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts b/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts index 43125a87b05d..d1f02f4edd19 100644 --- a/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts +++ b/packages/core/src/renderer/components/helm-releases/release-details/release-details-model/request-detailed-helm-release.injectable.ts @@ -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/request–helm-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; +export type RequestDetailedHelmRelease = (args: GetHelmReleaseArgs) => AsyncResult; 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, + }, + }; }; }, }); diff --git a/packages/core/src/renderer/components/helm-releases/releases.injectable.ts b/packages/core/src/renderer/components/helm-releases/releases.injectable.ts index ae90a67e18a4..468890bac66b 100644 --- a/packages/core/src/renderer/components/helm-releases/releases.injectable.ts +++ b/packages/core/src/renderer/components/helm-releases/releases.injectable.ts @@ -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: [], }); }, diff --git a/packages/core/src/renderer/components/helm-releases/to-helm-release.injectable.ts b/packages/core/src/renderer/components/helm-releases/to-helm-release.injectable.ts deleted file mode 100644 index e70ae7587bac..000000000000 --- a/packages/core/src/renderer/components/helm-releases/to-helm-release.injectable.ts +++ /dev/null @@ -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; diff --git a/packages/core/src/renderer/components/helm-releases/to-helm-release.ts b/packages/core/src/renderer/components/helm-releases/to-helm-release.ts new file mode 100644 index 000000000000..ed534613152e --- /dev/null +++ b/packages/core/src/renderer/components/helm-releases/to-helm-release.ts @@ -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; + }, +}); diff --git a/packages/core/src/renderer/components/test-utils/get-application-builder.tsx b/packages/core/src/renderer/components/test-utils/get-application-builder.tsx index d215cc206b03..c2822c4fb569 100644 --- a/packages/core/src/renderer/components/test-utils/get-application-builder.tsx +++ b/packages/core/src/renderer/components/test-utils/get-application-builder.tsx @@ -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; type WindowDiCallback = (container: { windowDi: DiContainer }) => void | Promise; @@ -240,7 +241,7 @@ export const getApplicationBuilder = () => { }, })); - const windowHelpers = new Map 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(); const selectedNamespaces = observable.set(); - 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; - 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)), - ); + const cluster = new Cluster({ + id: "some-cluster-id", + contextName: "some-context-name", + kubeConfigPath: "/some-path-to-kube-config", + }); + builder.beforeWindowStart(({ windowDi }) => { 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 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; }, @@ -647,8 +687,10 @@ export const getApplicationBuilder = () => { const windowDi = builder.applicationWindow.only.di; const cluster = windowDi.inject(hostedClusterInjectable); + assert(cluster, "For some reason the hosted cluster is not yet available, are you running in an 'afterWindowStart' callback?"); + runInAction(() => { - cluster?.resourcesToShow.add(formatKubeApiResource(resource)); + cluster.resourcesToShow.add(formatKubeApiResource(resource)); }); return builder; @@ -774,17 +816,13 @@ const findExtensionInstance = (di: DiContainer, inject type ApplicationWindowHelpers = Map 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 +830,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; }, diff --git a/packages/core/webpack/main.ts b/packages/core/webpack/main.ts index 3e8c3ed4d739..ed7ec425d132 100755 --- a/packages/core/webpack/main.ts +++ b/packages/core/webpack/main.ts @@ -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", diff --git a/packages/core/webpack/renderer.ts b/packages/core/webpack/renderer.ts index 6d581af8aa3a..ea18352ed19b 100755 --- a/packages/core/webpack/renderer.ts +++ b/packages/core/webpack/renderer.ts @@ -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",