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

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

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

No files matched your search

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