diff --git a/lerna.json b/lerna.json index a94255ddf1..45e3fafbe4 100644 --- a/lerna.json +++ b/lerna.json @@ -4,7 +4,7 @@ "packages": [ "packages/*" ], - "version": "6.4.12", + "version": "6.4.13", "npmClient": "yarn", "npmClientArgs": [ "--network-timeout=100000" diff --git a/packages/core/package.json b/packages/core/package.json index d6ee80e3b3..c307242141 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -3,7 +3,7 @@ "productName": "", "description": "Lens Desktop Core", "homepage": "https://github.com/lensapp/lens", - "version": "6.4.12", + "version": "6.4.13", "repository": { "type": "git", "url": "git+https://github.com/lensapp/lens.git" diff --git a/packages/core/src/features/extensions/stopping/main/stop-all.injectable.ts b/packages/core/src/features/extensions/stopping/main/stop-all.injectable.ts new file mode 100644 index 0000000000..ce3d69d723 --- /dev/null +++ b/packages/core/src/features/extensions/stopping/main/stop-all.injectable.ts @@ -0,0 +1,25 @@ +/** + * 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 extensionInjectable from "../../../../extensions/extension-loader/extension/extension.injectable"; +import extensionsInjectable from "../../../../extensions/extensions.injectable"; + +const stopAllExtensionsInjectable = getInjectable({ + id: "stop-all-extensions", + instantiate: (di) => { + const extensionInstances = di.inject(extensionsInjectable); + + return async () => { + for (const instance of extensionInstances.get()) { + const extension = di.inject(extensionInjectable, instance); + + await instance.disable(); + extension.deregister(); + } + }; + }, +}); + +export default stopAllExtensionsInjectable; diff --git a/packages/core/src/main/lens-proxy/close-on-quit.injectable.ts b/packages/core/src/main/lens-proxy/close-on-quit.injectable.ts new file mode 100644 index 0000000000..eda96a47e2 --- /dev/null +++ b/packages/core/src/main/lens-proxy/close-on-quit.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 { getInjectable } from "@ogre-tools/injectable"; +import { beforeQuitOfBackEndInjectionToken } from "../start-main-application/runnable-tokens/before-quit-of-back-end-injection-token"; +import lensProxyInjectable from "./lens-proxy.injectable"; + +const closeLensProxyOnQuitInjectable = getInjectable({ + id: "close-lens-proxy-on-quit", + instantiate: (di) => ({ + id: "close-lens-proxy-on-quit", + run: async () => { + const lensProxy = di.inject(lensProxyInjectable); + + await lensProxy.close(); + }, + }), + injectionToken: beforeQuitOfBackEndInjectionToken, +}); + +export default closeLensProxyOnQuitInjectable; diff --git a/packages/core/src/main/lens-proxy/lens-proxy.ts b/packages/core/src/main/lens-proxy/lens-proxy.ts index 1a5acdd9f2..50b8549bd0 100644 --- a/packages/core/src/main/lens-proxy/lens-proxy.ts +++ b/packages/core/src/main/lens-proxy/lens-proxy.ts @@ -47,7 +47,7 @@ export function isLongRunningRequest(reqUrl: string) { /** * This is the list of ports that chrome considers unsafe to allow HTTP - * conntections to. Because they are the standard ports for processes that are + * connections to. Because they are the standard ports for processes that are * too forgiving in the connection types they accept. * * If we get one of these ports, the easiest thing to do is to just try again. @@ -165,10 +165,17 @@ export class LensProxy { } close() { + if (this.closed) { + return; + } + + // mark as closed immediately + this.closed = true; this.dependencies.logger.info("[LENS-PROXY]: Closing server"); - this.proxyServer.close(); - this.closed = true; + return new Promise((resolve) => { + this.proxyServer.close(() => resolve()); + }); } protected configureProxy(proxy: httpProxy): httpProxy { diff --git a/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts b/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts index 084aa2e17c..2544912bd1 100644 --- a/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts +++ b/packages/core/src/main/shell-session/local-shell-session/open.injectable.ts @@ -24,6 +24,8 @@ import appNameInjectable from "../../../common/vars/app-name.injectable"; import buildVersionInjectable from "../../vars/build-version/build-version.injectable"; import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import statInjectable from "../../../common/fs/stat.injectable"; +import shellSessionEnvsInjectable from "../shell-envs.injectable"; +import shellSessionProcessesInjectable from "../processes.injectable"; export interface OpenLocalShellSessionArgs { websocket: WebSocket; @@ -47,6 +49,8 @@ const openLocalShellSessionInjectable = getInjectable({ userShellSetting: di.inject(userShellSettingInjectable), appName: di.inject(appNameInjectable), buildVersion: di.inject(buildVersionInjectable), + shellSessionEnvs: di.inject(shellSessionEnvsInjectable), + shellSessionProcesses: di.inject(shellSessionProcessesInjectable), modifyTerminalShellEnv: di.inject(modifyTerminalShellEnvInjectable), emitAppEvent: di.inject(emitAppEventInjectable), getDirnameOfPath: di.inject(getDirnameOfPathInjectable), diff --git a/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts b/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts index cce3fa5f36..4cfc3eaf43 100644 --- a/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts +++ b/packages/core/src/main/shell-session/node-shell-session/open.injectable.ts @@ -20,6 +20,8 @@ import buildVersionInjectable from "../../vars/build-version/build-version.injec import emitAppEventInjectable from "../../../common/app-event-bus/emit-event.injectable"; import statInjectable from "../../../common/fs/stat.injectable"; import createKubeApiInjectable from "../../../common/k8s-api/create-kube-api.injectable"; +import shellSessionEnvsInjectable from "../shell-envs.injectable"; +import shellSessionProcessesInjectable from "../processes.injectable"; export interface NodeShellSessionArgs { websocket: WebSocket; @@ -41,6 +43,8 @@ const openNodeShellSessionInjectable = getInjectable({ userShellSetting: di.inject(userShellSettingInjectable), appName: di.inject(appNameInjectable), buildVersion: di.inject(buildVersionInjectable), + shellSessionEnvs: di.inject(shellSessionEnvsInjectable), + shellSessionProcesses: di.inject(shellSessionProcessesInjectable), createKubeJsonApiForCluster: di.inject(createKubeJsonApiForClusterInjectable), computeShellEnvironment: di.inject(computeShellEnvironmentInjectable), spawnPty: di.inject(spawnPtyInjectable), diff --git a/packages/core/src/main/shell-session/processes.injectable.ts b/packages/core/src/main/shell-session/processes.injectable.ts new file mode 100644 index 0000000000..ffe9934401 --- /dev/null +++ b/packages/core/src/main/shell-session/processes.injectable.ts @@ -0,0 +1,15 @@ +/** + * 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 { IPty } from "node-pty"; + +export type ShellSessionProcesses = Map; + +const shellSessionProcessesInjectable = getInjectable({ + id: "shell-session-processes", + instantiate: (): ShellSessionProcesses => new Map(), +}); + +export default shellSessionProcessesInjectable; diff --git a/packages/core/src/main/shell-session/shell-envs.injectable.ts b/packages/core/src/main/shell-session/shell-envs.injectable.ts new file mode 100644 index 0000000000..4b866c825b --- /dev/null +++ b/packages/core/src/main/shell-session/shell-envs.injectable.ts @@ -0,0 +1,14 @@ +/** + * 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"; + +export type ShellSessionEnvs = Map>; + +const shellSessionEnvsInjectable = getInjectable({ + id: "shell-session-envs", + instantiate: (): ShellSessionEnvs => new Map(), +}); + +export default shellSessionEnvsInjectable; diff --git a/packages/core/src/main/shell-session/shell-session.ts b/packages/core/src/main/shell-session/shell-session.ts index 196625f42a..77e5e37a65 100644 --- a/packages/core/src/main/shell-session/shell-session.ts +++ b/packages/core/src/main/shell-session/shell-session.ts @@ -19,6 +19,8 @@ import type { InitializableState } from "../../common/initializable-state/create import type { EmitAppEvent } from "../../common/app-event-bus/emit-event.injectable"; import type { Stat } from "../../common/fs/stat.injectable"; import type { IComputedValue } from "mobx"; +import type { ShellSessionEnvs } from "./shell-envs.injectable"; +import type { ShellSessionProcesses } from "./processes.injectable"; export class ShellOpenError extends Error { constructor(message: string, options?: ErrorOptions) { @@ -111,6 +113,8 @@ export interface ShellSessionDependencies { readonly userShellSetting: IComputedValue; readonly appName: string; readonly buildVersion: InitializableState; + readonly shellSessionEnvs: ShellSessionEnvs; + readonly shellSessionProcesses: ShellSessionProcesses; computeShellEnvironment: ComputeShellEnvironment; spawnPty: SpawnPty; emitAppEvent: EmitAppEvent; @@ -127,25 +131,6 @@ export interface ShellSessionArgs { export abstract class ShellSession { abstract readonly ShellType: string; - private static readonly shellEnvs = new Map>(); - private static readonly processes = new Map(); - - /** - * Kill all remaining shell backing processes. Should be called when about to - * quit - */ - public static cleanup(): void { - for (const shellProcess of this.processes.values()) { - try { - process.kill(shellProcess.pid); - } catch { - // ignore error - } - } - - this.processes.clear(); - } - protected running = false; protected readonly kubectlBinDirP: Promise; protected readonly kubeconfigPathP: Promise; @@ -157,8 +142,8 @@ export abstract class ShellSession { protected abstract get cwd(): string | undefined; protected ensureShellProcess(shell: string, args: string[], env: Partial>, cwd: string): { shellProcess: pty.IPty; resume: boolean } { - const resume = ShellSession.processes.has(this.terminalId); - const shellProcess = getOrInsertWith(ShellSession.processes, this.terminalId, () => ( + const resume = this.dependencies.shellSessionProcesses.has(this.terminalId); + const shellProcess = getOrInsertWith(this.dependencies.shellSessionProcesses, this.terminalId, () => ( this.dependencies.spawnPty(shell, args, { rows: 30, cols: 80, @@ -306,7 +291,7 @@ export abstract class ShellSession { try { this.dependencies.logger.info(`[SHELL-SESSION]: Killing shell process (pid=${shellProcess.pid}) for ${this.terminalId}`); shellProcess.kill(); - ShellSession.processes.delete(this.terminalId); + this.dependencies.shellSessionProcesses.delete(this.terminalId); } catch (error) { this.dependencies.logger.warn(`[SHELL-SESSION]: failed to kill shell process (pid=${shellProcess.pid}) for ${this.terminalId}`, error); } @@ -323,15 +308,15 @@ export abstract class ShellSession { protected async getCachedShellEnv() { const { id: clusterId } = this.cluster; - let env = ShellSession.shellEnvs.get(clusterId); + let env = this.dependencies.shellSessionEnvs.get(clusterId); if (!env) { env = await this.getShellEnv(); - ShellSession.shellEnvs.set(clusterId, env); + this.dependencies.shellSessionEnvs.set(clusterId, env); } else { // refresh env in the background this.getShellEnv().then((shellEnv: any) => { - ShellSession.shellEnvs.set(clusterId, shellEnv); + this.dependencies.shellSessionEnvs.set(clusterId, shellEnv); }); } diff --git a/packages/core/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts b/packages/core/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts index 46afa5e692..11587f7d02 100644 --- a/packages/core/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts @@ -4,14 +4,30 @@ */ import { getInjectable } from "@ogre-tools/injectable"; import { beforeQuitOfBackEndInjectionToken } from "../runnable-tokens/before-quit-of-back-end-injection-token"; -import { ShellSession } from "../../shell-session/shell-session"; +import shellSessionProcessesInjectable from "../../shell-session/processes.injectable"; +import prefixedLoggerInjectable from "../../../common/logger/prefixed-logger.injectable"; const cleanUpShellSessionsInjectable = getInjectable({ id: "clean-up-shell-sessions", - instantiate: () => ({ + instantiate: (di) => ({ id: "clean-up-shell-sessions", - run: () => void ShellSession.cleanup(), + run: () => { + const shellSessionProcesses = di.inject(shellSessionProcessesInjectable); + const logger = di.inject(prefixedLoggerInjectable, "SHELL-SESSIONS"); + + logger.info("Killing all remaining shell sessions"); + + for (const { pid } of shellSessionProcesses.values()) { + try { + process.kill(pid); + } catch { + // ignore error + } + } + + shellSessionProcesses.clear(); + }, }), injectionToken: beforeQuitOfBackEndInjectionToken, diff --git a/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts b/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts index 8765721d90..b098f7a868 100644 --- a/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts +++ b/packages/core/src/main/start-main-application/runnables/initialize-extensions.injectable.ts @@ -10,6 +10,7 @@ import extensionDiscoveryInjectable from "../../../extensions/extension-discover import extensionLoaderInjectable from "../../../extensions/extension-loader/extension-loader.injectable"; import showErrorPopupInjectable from "../../electron-app/features/show-error-popup.injectable"; import { onLoadOfApplicationInjectionToken } from "../runnable-tokens/on-load-of-application-injection-token"; +import setupShellInjectable from "../../../features/shell-sync/main/setup-shell.injectable"; const initializeExtensionsInjectable = getInjectable({ id: "initialize-extensions", @@ -57,6 +58,7 @@ const initializeExtensionsInjectable = getInjectable({ console.trace(); } }, + runAfter: di.inject(setupShellInjectable), }; }, diff --git a/packages/core/src/main/stop-services-and-exit-app.injectable.ts b/packages/core/src/main/stop-services-and-exit-app.injectable.ts index 402862b462..f9bd436818 100644 --- a/packages/core/src/main/stop-services-and-exit-app.injectable.ts +++ b/packages/core/src/main/stop-services-and-exit-app.injectable.ts @@ -8,6 +8,7 @@ import clusterManagerInjectable from "./cluster/manager.injectable"; import loggerInjectable from "../common/logger.injectable"; import closeAllWindowsInjectable from "./start-main-application/lens-window/hide-all-windows/close-all-windows.injectable"; import emitAppEventInjectable from "../common/app-event-bus/emit-event.injectable"; +import stopAllExtensionsInjectable from "../features/extensions/stopping/main/stop-all.injectable"; const stopServicesAndExitAppInjectable = getInjectable({ id: "stop-services-and-exit-app", @@ -18,11 +19,13 @@ const stopServicesAndExitAppInjectable = getInjectable({ const logger = di.inject(loggerInjectable); const closeAllWindows = di.inject(closeAllWindowsInjectable); const emitAppEvent = di.inject(emitAppEventInjectable); + const stopAllExtensions = di.inject(stopAllExtensionsInjectable); - return () => { + return async () => { emitAppEvent({ name: "service", action: "close" }); closeAllWindows(); clusterManager.stop(); + await stopAllExtensions(); logger.info("SERVICE:QUIT"); setTimeout(exitApp, 1000); }; diff --git a/packages/core/src/renderer/components/+workloads-pods/__snapshots__/secret-key.test.tsx.snap b/packages/core/src/renderer/components/+workloads-pods/__snapshots__/secret-key.test.tsx.snap new file mode 100644 index 0000000000..3d1c8368ad --- /dev/null +++ b/packages/core/src/renderer/components/+workloads-pods/__snapshots__/secret-key.test.tsx.snap @@ -0,0 +1,92 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SecretKey technical tests renders 1`] = ` + +
+ secret(some-secret-name)[some-key] +   + + + visibility + + +
+ Show +
+
+ +`; + +exports[`SecretKey technical tests when the show secret button is clicked renders 1`] = ` + +
+ secret(some-secret-name)[some-key] +   + + + visibility + + +
+ Show +
+
+ +`; + +exports[`SecretKey technical tests when the show secret button is clicked when the secret fails to load with a primitive renders 1`] = ` + +
+ Error: some-other-error +
+ +`; + +exports[`SecretKey technical tests when the show secret button is clicked when the secret fails to load with an error renders 1`] = ` + +
+ Error: some-error +
+ +`; + +exports[`SecretKey technical tests when the show secret button is clicked when the secret fails to load with an object renders 1`] = ` + +
+ Error: {"message":"some-error"} +
+ +`; + +exports[`SecretKey technical tests when the show secret button is clicked when the secret loads with base64 encoded data renders 1`] = ` + +
+ some-data-for-some-key +
+ +`; + +exports[`SecretKey technical tests when the show secret button is clicked when the secret loads with non base64 encoded data renders 1`] = ` + +
+ some-data-for-some-key +
+ +`; diff --git a/packages/core/src/renderer/components/+workloads-pods/__tests__/__snapshots__/pod-container-env.test.tsx.snap b/packages/core/src/renderer/components/+workloads-pods/__tests__/__snapshots__/pod-container-env.test.tsx.snap index 1183ce4309..1a243d6fa8 100644 --- a/packages/core/src/renderer/components/+workloads-pods/__tests__/__snapshots__/pod-container-env.test.tsx.snap +++ b/packages/core/src/renderer/components/+workloads-pods/__tests__/__snapshots__/pod-container-env.test.tsx.snap @@ -158,10 +158,11 @@ exports[` renders envFrom when given a secretRef 1`] = ` bar : - secretKeyRef(my-secret.bar) + secret(my-secret)[bar]   renders envFrom when given a secretRef 1`] = ` visibility -
+
Show
diff --git a/packages/core/src/renderer/components/+workloads-pods/pod-container-env.tsx b/packages/core/src/renderer/components/+workloads-pods/pod-container-env.tsx index 88a7667e6b..7aa29c877e 100644 --- a/packages/core/src/renderer/components/+workloads-pods/pod-container-env.tsx +++ b/packages/core/src/renderer/components/+workloads-pods/pod-container-env.tsx @@ -5,19 +5,19 @@ import "./pod-container-env.scss"; -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { observer } from "mobx-react"; -import type { Container, EnvVarKeySelector, Secret } from "../../../common/k8s-api/endpoints"; +import type { Container } from "../../../common/k8s-api/endpoints"; import { DrawerItem } from "../drawer"; import { autorun } from "mobx"; -import { Icon } from "../icon"; -import { base64, cssNames, object } from "../../utils"; +import { object } from "../../utils"; import _ from "lodash"; import { withInjectables } from "@ogre-tools/injectable-react"; import type { ConfigMapStore } from "../+config-maps/store"; import type { SecretStore } from "../+config-secrets/store"; import configMapStoreInjectable from "../+config-maps/store.injectable"; import secretStoreInjectable from "../+config-secrets/store.injectable"; +import { SecretKey } from "./secret-key"; export interface ContainerEnvironmentProps { container: Container; @@ -74,9 +74,11 @@ const NonInjectedContainerEnvironment = observer((props: Dependencies & Containe } else if (secretKeyRef?.name) { secretValue = ( ); } else if (configMapKeyRef?.name) { @@ -151,7 +153,6 @@ const NonInjectedContainerEnvironment = observer((props: Dependencies & Containe key, }} namespace={namespace} - secretStore={secretStore} /> )); @@ -172,52 +173,3 @@ export const ContainerEnvironment = withInjectables { - const { - reference: { name, key }, - namespace, - secretStore, - } = props; - - const [loading, setLoading] = useState(false); - const [secret, setSecret] = useState(); - - if (!name) { - return null; - } - - const showKey = async (evt: React.MouseEvent) => { - evt.preventDefault(); - setLoading(true); - const secret = await secretStore.load({ name, namespace }); - - setLoading(false); - setSecret(secret); - }; - - const value = secret?.data?.[key]; - - if (value) { - return <>{base64.decode(value)}; - } - - return ( - <> - {`secretKeyRef(${name}.${key})`} -   - - - ); -}; diff --git a/packages/core/src/renderer/components/+workloads-pods/secret-key.test.tsx b/packages/core/src/renderer/components/+workloads-pods/secret-key.test.tsx new file mode 100644 index 0000000000..6b70f28ed0 --- /dev/null +++ b/packages/core/src/renderer/components/+workloads-pods/secret-key.test.tsx @@ -0,0 +1,205 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { base64 } from "../../utils"; +import type { RenderResult } from "@testing-library/react"; +import { act } from "@testing-library/react"; +import React from "react"; +import type { SecretStore } from "../+config-secrets/store"; +import secretStoreInjectable from "../+config-secrets/store.injectable"; +import { Secret, SecretType } from "../../../common/k8s-api/endpoints"; +import { getDiForUnitTesting } from "../../getDiForUnitTesting"; +import { renderFor } from "../test-utils/renderFor"; +import { SecretKey } from "./secret-key"; + +describe("SecretKey technical tests", () => { + let loadSecretMock: AsyncFnMock; + let result: RenderResult; + + beforeEach(() => { + const di = getDiForUnitTesting(); + const render = renderFor(di); + + loadSecretMock = asyncFn(); + di.override(secretStoreInjectable, () => ({ + load: loadSecretMock, + } as Partial as SecretStore)); + + result = render(( + + )); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should not try to load secret", () => { + expect(loadSecretMock).not.toBeCalled(); + }); + + it("should show the 'show secret' button", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).toBeInTheDocument(); + }); + + describe("when the show secret button is clicked", () => { + beforeEach(() => { + result + .getByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key") + .click(); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should try to load secret", () => { + expect(loadSecretMock).toBeCalledWith({ + name: "some-secret-name", + namespace: "some-namespace", + }); + }); + + it("should mark icon as disabled", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).toHaveClass("disabled"); + }); + + describe("when the secret loads with base64 encoded data", () => { + beforeEach(async () => { + await act(async () => { + await loadSecretMock.resolve(new Secret({ + apiVersion: Secret.apiBase, + kind: Secret.kind, + metadata: { + name: "some-secret-name", + namespace: "some-namespace", + resourceVersion: "some-resource-version", + selfLink: "some-self-link", + uid: "some-uid", + }, + type: SecretType.Opaque, + data: { + "some-key": base64.encode("some-data-for-some-key"), + }, + })); + }); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should not show the 'show secret' button", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).not.toBeInTheDocument(); + }); + + it("should show the decoded secret data", () => { + expect(result.queryByText("some-data-for-some-key")).toBeInTheDocument(); + }); + }); + + describe("when the secret loads with non base64 encoded data", () => { + beforeEach(async () => { + await act(async () => { + await loadSecretMock.resolve(new Secret({ + apiVersion: Secret.apiBase, + kind: Secret.kind, + metadata: { + name: "some-secret-name", + namespace: "some-namespace", + resourceVersion: "some-resource-version", + selfLink: "some-self-link", + uid: "some-uid", + }, + type: SecretType.Opaque, + data: { + "some-key": "some-data-for-some-key", + }, + })); + }); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should not show the 'show secret' button", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).not.toBeInTheDocument(); + }); + + it("should show the non decoded secret data", () => { + expect(result.queryByText("some-data-for-some-key")).toBeInTheDocument(); + }); + }); + + describe("when the secret fails to load with an error", () => { + beforeEach(async () => { + await act(async () => { + await loadSecretMock.reject(new Error("some-error")); + }); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should not show the 'show secret' button", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).not.toBeInTheDocument(); + }); + + it("should show the loading error", () => { + expect(result.queryByText("Error: some-error")).toBeInTheDocument(); + }); + }); + + describe("when the secret fails to load with an object", () => { + beforeEach(async () => { + await act(async () => { + await loadSecretMock.reject({ message: "some-error" }); + }); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should not show the 'show secret' button", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).not.toBeInTheDocument(); + }); + + it("should show the loading error as JSON", () => { + expect(result.queryByText(`Error: {"message":"some-error"}`)).toBeInTheDocument(); + }); + }); + + describe("when the secret fails to load with a primitive", () => { + beforeEach(async () => { + await act(async () => { + await loadSecretMock.reject("some-other-error"); + }); + }); + + it("renders", () => { + expect(result.baseElement).toMatchSnapshot(); + }); + + it("should not show the 'show secret' button", () => { + expect(result.queryByTestId("show-secret-button-for-some-namespace/some-secret-name:some-key")).not.toBeInTheDocument(); + }); + + it("should show the loading error as JSON", () => { + expect(result.queryByText("Error: some-other-error")).toBeInTheDocument(); + }); + }); + }); +}); diff --git a/packages/core/src/renderer/components/+workloads-pods/secret-key.tsx b/packages/core/src/renderer/components/+workloads-pods/secret-key.tsx new file mode 100644 index 0000000000..0cf4b407a0 --- /dev/null +++ b/packages/core/src/renderer/components/+workloads-pods/secret-key.tsx @@ -0,0 +1,85 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ +import React, { useState } from "react"; +import type { EnvVarKeySelector } from "../../../common/k8s-api/endpoints"; +import { Icon } from "../icon"; +import { base64, cssNames, isObject } from "../../utils"; +import type { SecretStore } from "../+config-secrets/store"; +import { withInjectables } from "@ogre-tools/injectable-react"; +import secretStoreInjectable from "../+config-secrets/store.injectable"; +import type { SetRequired } from "type-fest"; + +export interface SecretKeyProps { + reference: SetRequired; + namespace: string; +} + +interface Dependencies { + secretStore: SecretStore; +} + +const NonInjectedSecretKey = (props: SecretKeyProps & Dependencies) => { + const { + reference: { name, key }, namespace, secretStore, + } = props; + + const [loading, setLoading] = useState(false); + const [secretData, setSecretData] = useState(); + + if (!name) { + return null; + } + + const showKey = async (evt: React.MouseEvent) => { + evt.preventDefault(); + setLoading(true); + + try { + const secret = await secretStore.load({ name, namespace }); + + try { + setSecretData(base64.decode(secret.data[key] ?? "")); + } catch { + setSecretData(secret.data[key]); + } + } catch (error) { + if (error instanceof Error) { + setSecretData(`${error}`); + } else if (isObject(error)) { + setSecretData(`Error: ${JSON.stringify(error)}`); + } else { + setSecretData(`Error: ${error}`); + } + } finally { + setLoading(false); + } + }; + + if (secretData) { + return <>{secretData}; + } + + return ( + <> + {`secret(${name})[${key}]`} +   + + + ); +}; + +export const SecretKey = withInjectables(NonInjectedSecretKey, { + getProps: (di, props) => ({ + ...props, + secretStore: di.inject(secretStoreInjectable), + }), +}); diff --git a/packages/extension-api/package.json b/packages/extension-api/package.json index 0cddab40c9..e44f19068e 100644 --- a/packages/extension-api/package.json +++ b/packages/extension-api/package.json @@ -2,7 +2,7 @@ "name": "@k8slens/extensions", "productName": "OpenLens extensions", "description": "OpenLens - Open Source Kubernetes IDE: extensions", - "version": "6.4.12", + "version": "6.4.13", "copyright": "© 2022 OpenLens Authors", "license": "MIT", "main": "dist/extension-api.js", @@ -26,7 +26,7 @@ "prepare:dev": "yarn run build" }, "dependencies": { - "@k8slens/core": "^6.4.12" + "@k8slens/core": "^6.4.13" }, "devDependencies": { "@types/node": "^16.18.6", diff --git a/packages/open-lens/package.json b/packages/open-lens/package.json index 2e56444c0a..b2796e62a8 100644 --- a/packages/open-lens/package.json +++ b/packages/open-lens/package.json @@ -4,7 +4,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.4.12", + "version": "6.4.13", "repository": { "type": "git", "url": "git+https://github.com/lensapp/lens.git" @@ -192,7 +192,7 @@ } }, "dependencies": { - "@k8slens/core": "^6.4.12", + "@k8slens/core": "^6.4.13", "@k8slens/ensure-binaries": "^6.4.0-beta.16", "@k8slens/generate-tray-icons": "^6.4.0-beta.16", "@ogre-tools/fp": "^12.0.1",