diff --git a/extensions/metrics-cluster-feature/src/metrics-settings.tsx b/extensions/metrics-cluster-feature/src/metrics-settings.tsx index bfbeb3e10b2e..4ae42357807f 100644 --- a/extensions/metrics-cluster-feature/src/metrics-settings.tsx +++ b/extensions/metrics-cluster-feature/src/metrics-settings.tsx @@ -190,7 +190,7 @@ export class MetricsSettings extends React.Component { render() { return ( - <> +
{ this.props.cluster.status.phase !== "connected" && (

@@ -270,7 +270,7 @@ export class MetricsSettings extends React.Component { )}

- +
); } } diff --git a/integration/__tests__/cluster-pages.tests.ts b/integration/__tests__/cluster-pages.tests.ts index e1474a43323a..c2896309f41f 100644 --- a/integration/__tests__/cluster-pages.tests.ts +++ b/integration/__tests__/cluster-pages.tests.ts @@ -390,12 +390,6 @@ const scenarios = [ sidebarItemTestId: "sidebar-item-link-for-service-accounts", }, - { - expectedSelector: "h5.title", - parentSidebarItemTestId: "sidebar-item-link-for-user-management", - sidebarItemTestId: "sidebar-item-link-for-roles", - }, - { expectedSelector: "h5.title", parentSidebarItemTestId: "sidebar-item-link-for-user-management", @@ -405,7 +399,7 @@ const scenarios = [ { expectedSelector: "h5.title", parentSidebarItemTestId: "sidebar-item-link-for-user-management", - sidebarItemTestId: "sidebar-item-link-for-role-bindings", + sidebarItemTestId: "sidebar-item-link-for-roles", }, { @@ -417,7 +411,7 @@ const scenarios = [ { expectedSelector: "h5.title", parentSidebarItemTestId: "sidebar-item-link-for-user-management", - sidebarItemTestId: "sidebar-item-link-for-pod-security-policies", + sidebarItemTestId: "sidebar-item-link-for-role-bindings", }, { diff --git a/package.json b/package.json index e662f129dc1b..f4daef57ac4b 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.2.1", + "version": "6.2.2", "main": "static/build/main.js", "copyright": "© 2022 OpenLens Authors", "license": "MIT", @@ -375,7 +375,7 @@ "css-loader": "^6.7.1", "deepdash": "^5.3.9", "dompurify": "^2.4.1", - "electron": "^19.1.6", + "electron": "^19.1.7", "electron-builder": "^23.6.0", "electron-notarize": "^0.3.0", "esbuild": "^0.15.14", diff --git a/src/common/cluster-types.ts b/src/common/cluster-types.ts index faf6debbabad..0cd447f0e207 100644 --- a/src/common/cluster-types.ts +++ b/src/common/cluster-types.ts @@ -195,13 +195,6 @@ export enum ClusterMetricsResourceType { */ export const initialNodeShellImage = "docker.io/alpine:3.13"; -/** - * The arguments for requesting to refresh a cluster's metadata - */ -export interface ClusterRefreshOptions { - refreshMetadata?: boolean; -} - /** * The data representing a cluster's state, for passing between main and renderer */ diff --git a/src/common/cluster/authorization-namespace-review.injectable.ts b/src/common/cluster/authorization-namespace-review.injectable.ts new file mode 100644 index 000000000000..aa7845356967 --- /dev/null +++ b/src/common/cluster/authorization-namespace-review.injectable.ts @@ -0,0 +1,87 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { KubeConfig } from "@kubernetes/client-node"; +import { AuthorizationV1Api } from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { Logger } from "../logger"; +import loggerInjectable from "../logger.injectable"; +import type { KubeApiResource } from "../rbac"; + +/** + * Requests the permissions for actions on the kube cluster + * @param namespace The namespace of the resources + * @param availableResources List of available resources in the cluster to resolve glob values fir api groups + * @returns list of allowed resources names + */ +export type RequestNamespaceResources = (namespace: string, availableResources: KubeApiResource[]) => Promise; + +/** + * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster + */ +export type AuthorizationNamespaceReview = (proxyConfig: KubeConfig) => RequestNamespaceResources; + +interface Dependencies { + logger: Logger; +} + +const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNamespaceReview => { + return (proxyConfig) => { + + const api = proxyConfig.makeApiClient(AuthorizationV1Api); + + return async (namespace, availableResources) => { + try { + const { body } = await api.createSelfSubjectRulesReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectRulesReview", + spec: { namespace }, + }); + + const resources = new Set(); + + body.status?.resourceRules.forEach(resourceRule => { + if (!resourceRule.verbs.some(verb => ["*", "list"].includes(verb)) || !resourceRule.resources) { + return; + } + + const apiGroups = resourceRule.apiGroups; + + if (resourceRule.resources.length === 1 && resourceRule.resources[0] === "*" && apiGroups) { + if (apiGroups[0] === "*") { + availableResources.forEach(resource => resources.add(resource.apiName)); + } else { + availableResources.forEach((apiResource)=> { + if (apiGroups.includes(apiResource.group || "")) { + resources.add(apiResource.apiName); + } + }); + } + } else { + resourceRule.resources.forEach(resource => resources.add(resource)); + } + + }); + + return [...resources]; + } catch (error) { + logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review: ${error}`, { namespace }); + + return []; + } + }; + }; +}; + +const authorizationNamespaceReviewInjectable = getInjectable({ + id: "authorization-namespace-review", + instantiate: (di) => { + const logger = di.inject(loggerInjectable); + + return authorizationNamespaceReview({ logger }); + }, +}); + +export default authorizationNamespaceReviewInjectable; diff --git a/src/common/cluster/authorization-review.injectable.ts b/src/common/cluster/authorization-review.injectable.ts index c622893b63f7..4c9b83330d8b 100644 --- a/src/common/cluster/authorization-review.injectable.ts +++ b/src/common/cluster/authorization-review.injectable.ts @@ -5,42 +5,55 @@ import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node"; import { AuthorizationV1Api } from "@kubernetes/client-node"; -import logger from "../logger"; import { getInjectable } from "@ogre-tools/injectable"; +import type { Logger } from "../logger"; +import loggerInjectable from "../logger.injectable"; +/** + * Requests the permissions for actions on the kube cluster + * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed + * @returns `true` if the actions described are allowed + */ export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise; /** * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster - */ -export function authorizationReview(proxyConfig: KubeConfig): CanI { - const api = proxyConfig.makeApiClient(AuthorizationV1Api); - - /** - * Requests the permissions for actions on the kube cluster - * @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed - * @returns `true` if the actions described are allowed - */ - return async (resourceAttributes: V1ResourceAttributes): Promise => { - try { - const { body } = await api.createSelfSubjectAccessReview({ - apiVersion: "authorization.k8s.io/v1", - kind: "SelfSubjectAccessReview", - spec: { resourceAttributes }, - }); - - return body.status?.allowed ?? false; - } catch (error) { - logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); - - return false; - } - }; + */ +export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI; + +interface Dependencies { + logger: Logger; } +const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => { + return (proxyConfig) => { + const api = proxyConfig.makeApiClient(AuthorizationV1Api); + + return async (resourceAttributes: V1ResourceAttributes): Promise => { + try { + const { body } = await api.createSelfSubjectAccessReview({ + apiVersion: "authorization.k8s.io/v1", + kind: "SelfSubjectAccessReview", + spec: { resourceAttributes }, + }); + + return body.status?.allowed ?? false; + } catch (error) { + logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes }); + + return false; + } + }; + }; +}; + const authorizationReviewInjectable = getInjectable({ id: "authorization-review", - instantiate: () => authorizationReview, + instantiate: (di) => { + const logger = di.inject(loggerInjectable); + + return authorizationReview({ logger }); + }, }); export default authorizationReviewInjectable; diff --git a/src/common/cluster/cluster.ts b/src/common/cluster/cluster.ts index b0d7fbfbc3f3..7f2702519026 100644 --- a/src/common/cluster/cluster.ts +++ b/src/common/cluster/cluster.ts @@ -14,7 +14,7 @@ import { apiResourceRecord, apiResources } from "../rbac"; import type { VersionDetector } from "../../main/cluster-detectors/version-detector"; import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry"; import plimit from "p-limit"; -import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types"; +import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types"; import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types"; import { disposer, isDefined, isRequestError, toJS } from "../utils"; import type { Response } from "request"; @@ -25,6 +25,8 @@ import assert from "assert"; import type { Logger } from "../logger"; import type { BroadcastMessage } from "../ipc/broadcast-message.injectable"; import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable"; +import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable"; +import type { RequestListApiResources } from "./list-api-resources.injectable"; export interface ClusterDependencies { readonly directoryForKubeConfigs: string; @@ -34,6 +36,8 @@ export interface ClusterDependencies { createContextHandler: (cluster: Cluster) => ClusterContextHandler; createKubectl: (clusterVersion: string) => Kubectl; createAuthorizationReview: (config: KubeConfig) => CanI; + createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources; + createListApiResources: (cluster: Cluster) => RequestListApiResources; createListNamespaces: (config: KubeConfig) => ListNamespaces; createVersionDetector: (cluster: Cluster) => VersionDetector; broadcastMessage: BroadcastMessage; @@ -309,7 +313,7 @@ export class Cluster implements ClusterModel, ClusterState { protected bindEvents() { this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta()); const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s - const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes + const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes this.eventsDisposer.push( reaction(() => this.getState(), state => this.pushState(state)), @@ -439,66 +443,68 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal - * @param opts refresh options */ @action - async refresh(opts: ClusterRefreshOptions = {}) { + async refresh() { this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta()); await this.refreshConnectionStatus(); - - if (this.accessible) { - await this.refreshAccessibility(); - - if (opts.refreshMetadata) { - this.refreshMetadata(); - } - } this.pushState(); } /** * @internal */ - @action - async refreshMetadata() { - this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); - const metadata = await this.dependencies.detectorRegistry.detectForCluster(this); - const existingMetadata = this.metadata; - - this.metadata = Object.assign(existingMetadata, metadata); + @action + async refreshAccessibilityAndMetadata() { + await this.refreshAccessibility(); + await this.refreshMetadata(); } - /** + /** * @internal */ - private async refreshAccessibility(): Promise { - const proxyConfig = await this.getProxyKubeconfig(); - const canI = this.dependencies.createAuthorizationReview(proxyConfig); + async refreshMetadata() { + this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta()); + const metadata = await this.dependencies.detectorRegistry.detectForCluster(this); + const existingMetadata = this.metadata; - this.isAdmin = await canI({ - namespace: "kube-system", - resource: "*", - verb: "create", - }); - this.isGlobalWatchEnabled = await canI({ - verb: "watch", - resource: "*", - }); - this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig); - this.allowedResources = await this.getAllowedResources(canI); - this.ready = true; - } + this.metadata = Object.assign(existingMetadata, metadata); + } + + /** + * @internal + */ + private async refreshAccessibility(): Promise { + this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta()); + const proxyConfig = await this.getProxyKubeconfig(); + const canI = this.dependencies.createAuthorizationReview(proxyConfig); + const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig); + const listApiResources = this.dependencies.createListApiResources(this); + + this.isAdmin = await canI({ + namespace: "kube-system", + resource: "*", + verb: "create", + }); + this.isGlobalWatchEnabled = await canI({ + verb: "watch", + resource: "*", + }); + this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig); + this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources); + this.ready = true; + } /** * @internal */ @action - async refreshConnectionStatus() { - const connectionStatus = await this.getConnectionStatus(); + async refreshConnectionStatus() { + const connectionStatus = await this.getConnectionStatus(); - this.online = connectionStatus > ClusterStatus.Offline; - this.accessible = connectionStatus == ClusterStatus.AccessGranted; - } + this.online = connectionStatus > ClusterStatus.Offline; + this.accessible = connectionStatus == ClusterStatus.AccessGranted; + } async getKubeconfig(): Promise { const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath); @@ -667,32 +673,48 @@ export class Cluster implements ClusterModel, ClusterState { } } - protected async getAllowedResources(canI: CanI) { + protected async getAllowedResources(listApiResources:RequestListApiResources, requestNamespaceResources: RequestNamespaceResources) { try { if (!this.allowedNamespaces.length) { return []; } - const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined); - const apiLimit = plimit(5); // 5 concurrent api requests - const requests = []; - - for (const apiResource of resources) { - requests.push(apiLimit(async () => { - for (const namespace of this.allowedNamespaces.slice(0, 10)) { - if (!this.resourceAccessStatuses.get(apiResource)) { - const result = await canI({ - resource: apiResource.apiName, - group: apiResource.group, - verb: "list", - namespace, - }); - - this.resourceAccessStatuses.set(apiResource, result); + + const unknownResources = new Map(apiResources.map(resource => ([resource.apiName, resource]))); + + const availableResources = await listApiResources(); + const availableResourcesNames = new Set(availableResources.map(apiResource => apiResource.apiName)); + + [...unknownResources.values()].map(unknownResource => { + if (!availableResourcesNames.has(unknownResource.apiName)) { + this.resourceAccessStatuses.set(unknownResource, false); + unknownResources.delete(unknownResource.apiName); + } + }); + + if (unknownResources.size > 0) { + const apiLimit = plimit(5); // 5 concurrent api requests + + await Promise.all(this.allowedNamespaces.map(namespace => apiLimit(async () => { + if (unknownResources.size === 0) { + return; + } + + const namespaceResources = await requestNamespaceResources(namespace, availableResources); + + for (const resourceName of namespaceResources) { + const unknownResource = unknownResources.get(resourceName); + + if (unknownResource) { + this.resourceAccessStatuses.set(unknownResource, true); + unknownResources.delete(resourceName); } } - })); + }))); + + for (const forbiddenResource of unknownResources.values()) { + this.resourceAccessStatuses.set(forbiddenResource, false); + } } - await Promise.all(requests); return apiResources .filter((resource) => this.resourceAccessStatuses.get(resource)) diff --git a/src/common/cluster/list-api-resources.injectable.ts b/src/common/cluster/list-api-resources.injectable.ts new file mode 100644 index 000000000000..ed9d5c9c39c7 --- /dev/null +++ b/src/common/cluster/list-api-resources.injectable.ts @@ -0,0 +1,91 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { + V1APIGroupList, + V1APIResourceList, + V1APIVersions, +} from "@kubernetes/client-node"; +import { getInjectable } from "@ogre-tools/injectable"; +import type { K8sRequest } from "../../main/k8s-request.injectable"; +import k8SRequestInjectable from "../../main/k8s-request.injectable"; +import type { Logger } from "../logger"; +import loggerInjectable from "../logger.injectable"; +import type { KubeApiResource, KubeResource } from "../rbac"; +import type { Cluster } from "./cluster"; +import plimit from "p-limit"; + +export type RequestListApiResources = () => Promise; + +/** + * @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster + */ +export type ListApiResources = (cluster: Cluster) => RequestListApiResources; + +interface Dependencies { + logger: Logger; + k8sRequest: K8sRequest; +} + +const listApiResources = ({ k8sRequest, logger }: Dependencies): ListApiResources => { + return (cluster) => { + const clusterRequest = (path: string) => k8sRequest(cluster, path); + const apiLimit = plimit(5); + + return async () => { + const resources: KubeApiResource[] = []; + + try { + const resourceListGroups:{ group:string;path:string }[] = []; + + await Promise.all( + [ + clusterRequest("/api").then((response:V1APIVersions)=>response.versions.forEach(version => resourceListGroups.push({ group:version, path:`/api/${version}` }))), + clusterRequest("/apis").then((response:V1APIGroupList) => response.groups.forEach(group => { + const preferredVersion = group.preferredVersion?.groupVersion; + + if (preferredVersion) { + resourceListGroups.push({ group:group.name, path:`/apis/${preferredVersion}` }); + } + })), + ], + ); + + await Promise.all( + resourceListGroups.map(({ group, path }) => apiLimit(async () => { + const apiResources:V1APIResourceList = await clusterRequest(path); + + if (apiResources.resources) { + resources.push( + ...apiResources.resources.filter(resource => resource.verbs.includes("list")).map((resource) => ({ + apiName: resource.name as KubeResource, + kind: resource.kind, + group, + })), + ); + } + }), + ), + ); + } catch (error) { + logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`); + } + + return resources; + }; + }; +}; + +const listApiResourcesInjectable = getInjectable({ + id: "list-api-resources", + instantiate: (di) => { + const k8sRequest = di.inject(k8SRequestInjectable); + const logger = di.inject(loggerInjectable); + + return listApiResources({ k8sRequest, logger }); + }, +}); + +export default listApiResourcesInjectable; diff --git a/src/common/ipc/cluster.ts b/src/common/ipc/cluster.ts index c5bec1f59fa3..803069c00e4e 100644 --- a/src/common/ipc/cluster.ts +++ b/src/common/ipc/cluster.ts @@ -6,7 +6,6 @@ export const clusterActivateHandler = "cluster:activate"; export const clusterSetFrameIdHandler = "cluster:set-frame-id"; export const clusterVisibilityHandler = "cluster:visibility"; -export const clusterRefreshHandler = "cluster:refresh"; export const clusterDisconnectHandler = "cluster:disconnect"; export const clusterKubectlApplyAllHandler = "cluster:kubectl-apply-all"; export const clusterKubectlDeleteAllHandler = "cluster:kubectl-delete-all"; diff --git a/src/common/k8s-api/__tests__/kube-api.test.ts b/src/common/k8s-api/__tests__/kube-api.test.ts index 9244d5e6ab7f..4f67ed1401de 100644 --- a/src/common/k8s-api/__tests__/kube-api.test.ts +++ b/src/common/k8s-api/__tests__/kube-api.test.ts @@ -592,7 +592,7 @@ describe("KubeApi", () => { it("requests the watch", () => { expect(fetchMock.mock.lastCall).toMatchObject([ - "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=600", { headers: { "content-type": "application/json", @@ -606,7 +606,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ([url, init]) => { - const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion="; + const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=600"; if (isMatch) { init?.signal?.addEventListener("abort", () => { @@ -616,7 +616,7 @@ describe("KubeApi", () => { return isMatch; }, - createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", stream), + createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=600", stream), ); }); @@ -688,7 +688,7 @@ describe("KubeApi", () => { it("requests the watch", () => { expect(fetchMock.mock.lastCall).toMatchObject([ - "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", + "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=600", { headers: { "content-type": "application/json", @@ -702,7 +702,7 @@ describe("KubeApi", () => { beforeEach(async () => { await fetchMock.resolveSpecific( ([url, init]) => { - const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion="; + const isMatch = url === "http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=600"; if (isMatch) { init?.signal?.addEventListener("abort", () => { @@ -712,7 +712,7 @@ describe("KubeApi", () => { return isMatch; }, - createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=", stream), + createMockResponseFromStream("http://127.0.0.1:9999/api-kube/api/v1/namespaces/kube-system/pods?watch=1&resourceVersion=&timeoutSeconds=600", stream), ); }); diff --git a/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts b/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts index 2703ea2c5602..8f46a3a2103f 100644 --- a/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts +++ b/src/common/k8s-api/endpoints/helm-releases.api/request-update.injectable.ts @@ -3,9 +3,9 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ import { getInjectable } from "@ogre-tools/injectable"; -import yaml from "js-yaml"; import { apiBaseInjectionToken } from "../../api-base"; import { urlBuilderFor } from "../../../utils/buildUrl"; +import type { AsyncResult } from "../../../utils/async-result"; interface HelmReleaseUpdatePayload { repo: string; @@ -18,7 +18,7 @@ export type RequestHelmReleaseUpdate = ( name: string, namespace: string, payload: HelmReleaseUpdatePayload -) => Promise<{ updateWasSuccessful: true } | { updateWasSuccessful: false; error: unknown }>; +) => Promise>; const requestUpdateEndpoint = urlBuilderFor("/v2/releases/:namespace/:name"); @@ -28,20 +28,20 @@ const requestHelmReleaseUpdateInjectable = getInjectable({ instantiate: (di): RequestHelmReleaseUpdate => { const apiBase = di.inject(apiBaseInjectionToken); - return async (name, namespace, { repo, chart, values, ...data }) => { + return async (name, namespace, { repo, chart, values, version }) => { try { await apiBase.put(requestUpdateEndpoint.compile({ name, namespace }), { data: { chart: `${repo}/${chart}`, - values: yaml.load(values), - ...data, + values, + version, }, }); } catch (e) { - return { updateWasSuccessful: false, error: e }; + return { callWasSuccessful: false, error: e }; } - return { updateWasSuccessful: true }; + return { callWasSuccessful: true }; }; }, }); diff --git a/src/common/k8s-api/kube-api.ts b/src/common/k8s-api/kube-api.ts index 593a1de38c99..7ca29959064f 100644 --- a/src/common/k8s-api/kube-api.ts +++ b/src/common/k8s-api/kube-api.ts @@ -14,7 +14,7 @@ import byline from "byline"; import type { IKubeWatchEvent } from "./kube-watch-event"; import type { KubeJsonApiData, KubeJsonApi } from "./kube-json-api"; import type { Disposer } from "../utils"; -import { setTimeoutFor, isDefined, noop, WrappedAbortController } from "../utils"; +import { isDefined, noop, WrappedAbortController } from "../utils"; import type { RequestInit, Response } from "node-fetch"; import type { Patch } from "rfc6902"; import assert from "assert"; @@ -639,7 +639,7 @@ export class KubeApi< namespace, callback = noop as KubeApiWatchCallback, retry = false, - timeout, + timeout = 600, watchId = `${this.kind.toLowerCase()}-${this.watchId++}`, } = opts ?? {}; @@ -651,8 +651,6 @@ export class KubeApi< clearTimeout(timedRetry); }); - setTimeoutFor(abortController, 600 * 1000); - const requestParams = timeout ? { query: { timeoutSeconds: timeout }} : {}; const watchUrl = this.getWatchUrl(namespace); const responsePromise = this.request.getResponse(watchUrl, requestParams, { @@ -695,8 +693,10 @@ export class KubeApi< }, timeout * 1000 * 1.1); } - if (!response.body) { - this.dependencies.logger.error(`[KUBE-API]: watch (${watchId}) did not return a body`); + if (!response.body || !response.body.readable) { + if (!response.body) { + this.dependencies.logger.warn(`[KUBE-API]: watch (${watchId}) did not return a body`); + } requestRetried = true; clearTimeout(timedRetry); diff --git a/src/features/helm-charts/add-custom-helm-repository-in-preferences.test.ts b/src/features/helm-charts/add-custom-helm-repository-in-preferences.test.ts index e7d2b00bbcee..a1b215c40cde 100644 --- a/src/features/helm-charts/add-custom-helm-repository-in-preferences.test.ts +++ b/src/features/helm-charts/add-custom-helm-repository-in-preferences.test.ts @@ -169,7 +169,10 @@ describe("add custom helm repository in preferences", () => { expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "add", "some-custom-repository", "http://some.url"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); @@ -373,7 +376,10 @@ describe("add custom helm repository in preferences", () => { "--cert-file", "some-cert-file", ], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); }); diff --git a/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts b/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts index eab408f337c7..29e6ea890d51 100644 --- a/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts +++ b/src/features/helm-charts/add-helm-repository-from-list-in-preferences.test.ts @@ -118,7 +118,10 @@ describe("add helm repository from list in preferences", () => { expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "add", "Some to be added repository", "some-other-url"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); @@ -232,7 +235,10 @@ describe("add helm repository from list in preferences", () => { expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "remove", "Some already active repository"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); diff --git a/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts b/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts index b3cf629e1d43..df92796e7687 100644 --- a/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts +++ b/src/features/helm-charts/listing-active-helm-repositories-in-preferences.test.ts @@ -69,7 +69,10 @@ describe("listing active helm repositories in preferences", () => { expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["env"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); @@ -77,7 +80,10 @@ describe("listing active helm repositories in preferences", () => { expect(execFileMock).not.toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "update"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); @@ -222,7 +228,10 @@ describe("listing active helm repositories in preferences", () => { expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "update"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); @@ -289,7 +298,10 @@ describe("listing active helm repositories in preferences", () => { expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); @@ -434,7 +446,10 @@ describe("listing active helm repositories in preferences", () => { expect(execFileMock).not.toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); diff --git a/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts b/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts index 87a83c8eb3dd..f2a23f040962 100644 --- a/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts +++ b/src/features/helm-charts/remove-helm-repository-from-list-of-active-repository-in-preferences.test.ts @@ -85,7 +85,10 @@ describe("remove helm repository from list of active repositories in preferences expect(execFileMock).toHaveBeenCalledWith( "some-helm-binary-path", ["repo", "remove", "some-active-repository"], - { "maxBuffer": 34359738368 }, + { + maxBuffer: 34359738368, + env: {}, + }, ); }); diff --git a/src/features/helm-releases/showing-details-for-helm-release.test.ts b/src/features/helm-releases/showing-details-for-helm-release.test.ts index 57bf1014147e..b743ce013afa 100644 --- a/src/features/helm-releases/showing-details-for-helm-release.test.ts +++ b/src/features/helm-releases/showing-details-for-helm-release.test.ts @@ -552,7 +552,7 @@ describe("showing details for helm release", () => { requestHelmReleaseConfigurationMock.mockClear(); await requestHelmReleaseUpdateMock.resolve({ - updateWasSuccessful: true, + callWasSuccessful: true, }); }); @@ -591,7 +591,7 @@ describe("showing details for helm release", () => { requestHelmReleaseConfigurationMock.mockClear(); await requestHelmReleaseUpdateMock.resolve({ - updateWasSuccessful: false, + callWasSuccessful: false, error: "some-error", }); }); diff --git a/src/main/__test__/cluster.test.ts b/src/main/__test__/cluster.test.ts index 0244141ab387..980b63c767bd 100644 --- a/src/main/__test__/cluster.test.ts +++ b/src/main/__test__/cluster.test.ts @@ -10,6 +10,7 @@ import { getDiForUnitTesting } from "../getDiForUnitTesting"; import type { CreateCluster } from "../../common/cluster/create-cluster-injection-token"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; +import authorizationNamespaceReviewInjectable from "../../common/cluster/authorization-namespace-review.injectable"; import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import type { ClusterContextHandler } from "../context-handler/context-handler"; @@ -19,6 +20,8 @@ import directoryForTempInjectable from "../../common/app-paths/directory-for-tem import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import kubectlBinaryNameInjectable from "../kubectl/binary-name.injectable"; import kubectlDownloadingNormalizedArchInjectable from "../kubectl/normalized-arch.injectable"; +import { apiResourceRecord, apiResources } from "../../common/rbac"; +import listApiResourcesInjectable from "../../common/cluster/list-api-resources.injectable"; console = new Console(process.stdout, process.stderr); // fix mockFS @@ -39,6 +42,8 @@ describe("create clusters", () => { di.override(normalizedPlatformInjectable, () => "darwin"); di.override(broadcastMessageInjectable, () => async () => {}); di.override(authorizationReviewInjectable, () => () => () => Promise.resolve(true)); + di.override(authorizationNamespaceReviewInjectable, () => () => () => Promise.resolve(Object.keys(apiResourceRecord))); + di.override(listApiResourcesInjectable, () => () => () => Promise.resolve(apiResources)); di.override(listNamespacesInjectable, () => () => () => Promise.resolve([ "default" ])); di.override(createContextHandlerInjectable, () => (cluster) => ({ restartServer: jest.fn(), diff --git a/src/main/create-cluster/create-cluster.injectable.ts b/src/main/create-cluster/create-cluster.injectable.ts index 5290e92db3ec..e1782ec6c9c4 100644 --- a/src/main/create-cluster/create-cluster.injectable.ts +++ b/src/main/create-cluster/create-cluster.injectable.ts @@ -11,7 +11,9 @@ import createKubectlInjectable from "../kubectl/create-kubectl.injectable"; import createContextHandlerInjectable from "../context-handler/create-context-handler.injectable"; import { createClusterInjectionToken } from "../../common/cluster/create-cluster-injection-token"; import authorizationReviewInjectable from "../../common/cluster/authorization-review.injectable"; +import createAuthorizationNamespaceReview from "../../common/cluster/authorization-namespace-review.injectable"; import listNamespacesInjectable from "../../common/cluster/list-namespaces.injectable"; +import createListApiResourcesInjectable from "../../common/cluster/list-api-resources.injectable"; import loggerInjectable from "../../common/logger.injectable"; import detectorRegistryInjectable from "../cluster-detectors/detector-registry.injectable"; import createVersionDetectorInjectable from "../cluster-detectors/create-version-detector.injectable"; @@ -28,6 +30,8 @@ const createClusterInjectable = getInjectable({ createKubectl: di.inject(createKubectlInjectable), createContextHandler: di.inject(createContextHandlerInjectable), createAuthorizationReview: di.inject(authorizationReviewInjectable), + createAuthorizationNamespaceReview: di.inject(createAuthorizationNamespaceReview), + createListApiResources: di.inject(createListApiResourcesInjectable), createListNamespaces: di.inject(listNamespacesInjectable), logger: di.inject(loggerInjectable), detectorRegistry: di.inject(detectorRegistryInjectable), diff --git a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts index 5685b5a201eb..a78ae3a588bb 100644 --- a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts +++ b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts @@ -5,7 +5,7 @@ import type { IpcMainInvokeEvent } from "electron"; import { BrowserWindow, Menu } from "electron"; import { clusterFrameMap } from "../../../../common/cluster-frames"; -import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterRefreshHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../../../common/ipc/cluster"; +import { clusterActivateHandler, clusterSetFrameIdHandler, clusterVisibilityHandler, clusterDisconnectHandler, clusterKubectlApplyAllHandler, clusterKubectlDeleteAllHandler } from "../../../../common/ipc/cluster"; import type { ClusterId } from "../../../../common/cluster-types"; import { ClusterStore } from "../../../../common/cluster-store/cluster-store"; import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../../common/ipc"; @@ -68,12 +68,6 @@ export const setupIpcMainHandlers = ({ clusterManager.visibleCluster = clusterId; }); - ipcMainHandle(clusterRefreshHandler, (event, clusterId: ClusterId) => { - return ClusterStore.getInstance() - .getById(clusterId) - ?.refresh({ refreshMetadata: true }); - }); - ipcMainHandle(clusterDisconnectHandler, (event, clusterId: ClusterId) => { emitAppEvent({ name: "cluster", action: "stop" }); const cluster = ClusterStore.getInstance().getById(clusterId); diff --git a/src/main/helm/exec-helm/exec-env.global-override-for-injectable.ts b/src/main/helm/exec-helm/exec-env.global-override-for-injectable.ts new file mode 100644 index 000000000000..59c376cffd65 --- /dev/null +++ b/src/main/helm/exec-helm/exec-env.global-override-for-injectable.ts @@ -0,0 +1,10 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { computed } from "mobx"; +import { getGlobalOverride } from "../../../common/test-utils/get-global-override"; +import execHelmEnvInjectable from "./exec-env.injectable"; + +export default getGlobalOverride(execHelmEnvInjectable, () => computed(() => ({}))); diff --git a/src/main/helm/exec-helm/exec-env.injectable.ts b/src/main/helm/exec-helm/exec-env.injectable.ts new file mode 100644 index 000000000000..f562dc2d4b1b --- /dev/null +++ b/src/main/helm/exec-helm/exec-env.injectable.ts @@ -0,0 +1,26 @@ +/** + * 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 { computed } from "mobx"; +import userStoreInjectable from "../../../common/user-store/user-store.injectable"; + +const execHelmEnvInjectable = getInjectable({ + id: "exec-helm-env", + instantiate: (di) => { + const userStore = di.inject(userStoreInjectable); + + return computed(() => { + const { + HTTPS_PROXY = userStore.httpsProxy, + ...env + } = process.env; + + return { HTTPS_PROXY, ...env } as Partial>; + }); + }, + causesSideEffects: true, +}); + +export default execHelmEnvInjectable; diff --git a/src/main/helm/exec-helm/exec-helm.injectable.ts b/src/main/helm/exec-helm/exec-helm.injectable.ts index 39f45a3f7db4..034ab43248fb 100644 --- a/src/main/helm/exec-helm/exec-helm.injectable.ts +++ b/src/main/helm/exec-helm/exec-helm.injectable.ts @@ -7,6 +7,7 @@ import type { ExecFileException } from "child_process"; import execFileInjectable from "../../../common/fs/exec-file.injectable"; import type { AsyncResult } from "../../../common/utils/async-result"; import helmBinaryPathInjectable from "../helm-binary-path.injectable"; +import execHelmEnvInjectable from "./exec-env.injectable"; export type ExecHelm = (args: string[]) => Promise>; @@ -15,10 +16,12 @@ const execHelmInjectable = getInjectable({ instantiate: (di): ExecHelm => { const execFile = di.inject(execFileInjectable); + const execHelmEnv = di.inject(execHelmEnvInjectable); const helmBinaryPath = di.inject(helmBinaryPathInjectable); return async (args) => execFile(helmBinaryPath, args, { maxBuffer: 32 * 1024 * 1024 * 1024, // 32 MiB + env: execHelmEnv.get(), }); }, }); diff --git a/src/main/helm/helm-service/update-helm-release.injectable.ts b/src/main/helm/helm-service/update-helm-release.injectable.ts index e07269ce6a79..f428e7a782ca 100644 --- a/src/main/helm/helm-service/update-helm-release.injectable.ts +++ b/src/main/helm/helm-service/update-helm-release.injectable.ts @@ -5,16 +5,15 @@ import { getInjectable } from "@ogre-tools/injectable"; import type { Cluster } from "../../../common/cluster/cluster"; import loggerInjectable from "../../../common/logger.injectable"; -import type { JsonObject } from "type-fest"; -import { execHelm } from "../exec"; import tempy from "tempy"; -import fse from "fs-extra"; -import yaml from "js-yaml"; import getHelmReleaseInjectable from "./get-helm-release.injectable"; +import writeFileInjectable from "../../../common/fs/write-file.injectable"; +import removePathInjectable from "../../../common/fs/remove-path.injectable"; +import execHelmInjectable from "../exec-helm/exec-helm.injectable"; export interface UpdateChartArgs { chart: string; - values: JsonObject; + values: string; version: string; } @@ -24,40 +23,42 @@ const updateHelmReleaseInjectable = getInjectable({ instantiate: (di) => { const logger = di.inject(loggerInjectable); const getHelmRelease = di.inject(getHelmReleaseInjectable); + const writeFile = di.inject(writeFileInjectable); + const removePath = di.inject(removePathInjectable); + const execHelm = di.inject(execHelmInjectable); return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); - - logger.debug("Upgrade release"); - const valuesFilePath = tempy.file({ name: "values.yaml" }); - await fse.writeFile(valuesFilePath, yaml.dump(data.values)); - - const args = [ - "upgrade", - releaseName, - data.chart, - "--version", data.version, - "--values", valuesFilePath, - "--namespace", namespace, - "--kubeconfig", proxyKubeconfig, - ]; + logger.debug(`[HELM]: upgrading "${releaseName}" in "${namespace}" to ${data.version}`); try { - const output = await execHelm(args); + await writeFile(valuesFilePath, data.values); + + const result = await execHelm([ + "upgrade", + releaseName, + data.chart, + "--version", data.version, + "--values", valuesFilePath, + "--namespace", namespace, + "--kubeconfig", proxyKubeconfig, + ]); + + if (result.callWasSuccessful === false) { + throw result.error; // keep the same interface + } return { - log: output, + log: result.response, release: await getHelmRelease(cluster, releaseName, namespace), }; } finally { - await fse.unlink(valuesFilePath); + await removePath(valuesFilePath); } }; }, - - causesSideEffects: true, }); export default updateHelmReleaseInjectable; diff --git a/src/main/routes/helm/releases/update-release-route.injectable.ts b/src/main/routes/helm/releases/update-release-route.injectable.ts index 573c85d9625f..5993d7922d4a 100644 --- a/src/main/routes/helm/releases/update-release-route.injectable.ts +++ b/src/main/routes/helm/releases/update-release-route.injectable.ts @@ -17,8 +17,8 @@ const updateChartArgsValidator = Joi.object { return ( <> -
+
{ const helmCharts = di.inject(helmChartsInjectable); - - const requestVersionsOfHelmChart = di.inject( - requestVersionsOfHelmChartInjectable, - ); + const requestVersionsOfHelmChart = di.inject(requestVersionsOfHelmChartInjectable); return asyncComputed({ getValueFromObservedPromise: async () => { @@ -25,8 +22,6 @@ const helmChartVersionsInjectable = getInjectable({ return requestVersionsOfHelmChart(release, helmCharts.value.get()); }, - - valueWhenPending: [], }); }, diff --git a/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx b/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx index fc9a1ee9a9bb..acd65374b94f 100644 --- a/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx +++ b/src/renderer/components/+helm-releases/release-details/release-details-model/release-details-model.injectable.tsx @@ -128,7 +128,7 @@ export class ReleaseDetailsModel { this.configuration.isSaving.set(false); }); - if (!result.updateWasSuccessful) { + if (!result.callWasSuccessful) { this.dependencies.showCheckedErrorNotification( result.error, "Unknown error occured while updating release", diff --git a/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts b/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts index 8cdcbfafe190..309c19444bb0 100644 --- a/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts +++ b/src/renderer/components/+helm-releases/update-release/update-release.injectable.ts @@ -12,14 +12,14 @@ const updateReleaseInjectable = getInjectable({ instantiate: (di): RequestHelmReleaseUpdate => { const releases = di.inject(releasesInjectable); - const callForHelmReleaseUpdate = di.inject(requestHelmReleaseUpdateInjectable); + const requestHelmReleaseUpdate = di.inject(requestHelmReleaseUpdateInjectable); return async ( name, namespace, payload, ) => { - const result = await callForHelmReleaseUpdate(name, namespace, payload); + const result = await requestHelmReleaseUpdate(name, namespace, payload); releases.invalidate(); diff --git a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx index 228b9caeeccb..639623e08f2c 100644 --- a/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx +++ b/src/renderer/components/cluster-settings/components/cluster-local-terminal-settings.tsx @@ -18,6 +18,7 @@ import type { ValidateDirectory } from "../../../../common/fs/validate-directory import validateDirectoryInjectable from "../../../../common/fs/validate-directory.injectable"; import type { ResolveTilde } from "../../../../common/path/resolve-tilde.injectable"; import resolveTildeInjectable from "../../../../common/path/resolve-tilde.injectable"; +import Gutter from "../../gutter/gutter"; export interface ClusterLocalTerminalSettingProps { cluster: Cluster; @@ -139,6 +140,7 @@ const NonInjectedClusterLocalTerminalSetting = observer(({ this is used as the current working directory (cwd) for the shell process. +
+
; set: (value: SingleValue>) => void; }; - submit: () => Promise; -} - -export interface UpgradeChartSubmitResult { - completedSuccessfully: boolean; + submit: () => Promise>; } const upgradeChartModelInjectable = getInjectable({ @@ -105,13 +102,23 @@ const upgradeChartModelInjectable = getInjectable({ submit: async () => { const selectedVersion = version.value.get(); - if (!selectedVersion || configrationEditError.get()) { + if (!selectedVersion) { + return { + callWasSuccessful: false, + error: "No selected version", + }; + } + + const editError = configrationEditError.get(); + + if (editError) { return { - completedSuccessfully: false, + callWasSuccessful: false, + error: editError, }; } - await updateRelease( + const result = await updateRelease( release.getName(), release.getNs(), { @@ -120,10 +127,16 @@ const upgradeChartModelInjectable = getInjectable({ ...selectedVersion, }, ); - storedConfiguration.invalidate(); + + if (result.callWasSuccessful === true) { + storedConfiguration.invalidate(); + + return { callWasSuccessful: true }; + } return { - completedSuccessfully: true, + callWasSuccessful: false, + error: String(result.error), }; }, }; diff --git a/src/renderer/components/dock/upgrade-chart/view.tsx b/src/renderer/components/dock/upgrade-chart/view.tsx index 2a3fe59996af..b25f3f24fccf 100644 --- a/src/renderer/components/dock/upgrade-chart/view.tsx +++ b/src/renderer/components/dock/upgrade-chart/view.tsx @@ -33,20 +33,20 @@ interface Dependencies { export class NonInjectedUpgradeChart extends React.Component { upgrade = async () => { const { model } = this.props; - const { completedSuccessfully } = await model.submit(); + const result = await model.submit(); - if (completedSuccessfully) { + if (result.callWasSuccessful) { return (

{"Release "} {model.release.getName()} {" successfully upgraded to version "} - {model.version.value.get()} + {model.version.value.get()?.version}

); } - return null; + throw result.error; }; render() { diff --git a/src/renderer/create-cluster/create-cluster.injectable.ts b/src/renderer/create-cluster/create-cluster.injectable.ts index 0c73eb82fa81..e0a9f5165671 100644 --- a/src/renderer/create-cluster/create-cluster.injectable.ts +++ b/src/renderer/create-cluster/create-cluster.injectable.ts @@ -27,7 +27,9 @@ const createClusterInjectable = getInjectable({ createKubectl: () => { throw new Error("Tried to access back-end feature in front-end.");}, createContextHandler: () => undefined as never, createAuthorizationReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, + createAuthorizationNamespaceReview: () => { throw new Error("Tried to access back-end feature in front-end."); }, createListNamespaces: () => { throw new Error("Tried to access back-end feature in front-end."); }, + createListApiResources: ()=> { throw new Error("Tried to access back-end feature in front-end."); }, detectorRegistry: undefined as never, createVersionDetector: () => { throw new Error("Tried to access back-end feature in front-end."); }, }; diff --git a/yarn.lock b/yarn.lock index 687457dbc83a..b49e980a7c82 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5100,10 +5100,10 @@ electron-window-state@^5.0.3: jsonfile "^4.0.0" mkdirp "^0.5.1" -electron@^19.1.6: - version "19.1.6" - resolved "https://registry.yarnpkg.com/electron/-/electron-19.1.6.tgz#32443cd293d3d877cd3d224e45880e3fbf264e49" - integrity sha512-bT6Mr7JbHbONpr/U7R47lwTkMUvuAyOfnoLlbDqvGocQyZCCN3JB436wtf2+r3/IpMEz3T+dHLweFDY5i2wuxw== +electron@^19.1.7: + version "19.1.7" + resolved "https://registry.yarnpkg.com/electron/-/electron-19.1.7.tgz#35036a510d9ca943d271e1d1a12463547ed5cd12" + integrity sha512-U5rCktIm/EeRjfg/9QFo29jzvZVV2z8Xw7r2NdGTpljmjd+7kySHvUHthO2hk8HETILJivL4+R5lF9zxcJ2J9w== dependencies: "@electron/get" "^1.14.1" "@types/node" "^16.11.26"