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 977c945f18..00d13fa833 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 "@k8slens/utilities"; +import { object } from "@k8slens/utilities"; 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..035bf158df --- /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 "@k8slens/utilities"; +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..ada6e9c3a2 --- /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 "@k8slens/utilities"; +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), + }), +});