mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix crash due to non base64 encoded secrets (#7448)
* Fix crash due to non base64 encoded secrets Signed-off-by: Sebastian Malton <sebastian@malton.name> * Update snapshot Signed-off-by: Sebastian Malton <sebastian@malton.name> --------- Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
908a3cabe1
commit
9b8d6e4e44
@ -0,0 +1,92 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SecretKey technical tests renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
secret(some-secret-name)[some-key]
|
||||
|
||||
<i
|
||||
class="Icon secret-button material interactive focusable"
|
||||
data-testid="show-secret-button-for-some-namespace/some-secret-name:some-key"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="visibility"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</i>
|
||||
<div
|
||||
data-testid="tooltip-content-for-show-secret-button-for-some-namespace/some-secret-name:some-key"
|
||||
>
|
||||
Show
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`SecretKey technical tests when the show secret button is clicked renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
secret(some-secret-name)[some-key]
|
||||
|
||||
<i
|
||||
class="Icon secret-button loading material interactive disabled focusable"
|
||||
data-testid="show-secret-button-for-some-namespace/some-secret-name:some-key"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="visibility"
|
||||
>
|
||||
visibility
|
||||
</span>
|
||||
</i>
|
||||
<div
|
||||
data-testid="tooltip-content-for-show-secret-button-for-some-namespace/some-secret-name:some-key"
|
||||
>
|
||||
Show
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`SecretKey technical tests when the show secret button is clicked when the secret fails to load with a primitive renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
Error: some-other-error
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`SecretKey technical tests when the show secret button is clicked when the secret fails to load with an error renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
Error: some-error
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`SecretKey technical tests when the show secret button is clicked when the secret fails to load with an object renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
Error: {"message":"some-error"}
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`SecretKey technical tests when the show secret button is clicked when the secret loads with base64 encoded data renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
some-data-for-some-key
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
|
||||
exports[`SecretKey technical tests when the show secret button is clicked when the secret loads with non base64 encoded data renders 1`] = `
|
||||
<body>
|
||||
<div>
|
||||
some-data-for-some-key
|
||||
</div>
|
||||
</body>
|
||||
`;
|
||||
@ -158,10 +158,11 @@ exports[`<ContainerEnv /> renders envFrom when given a secretRef 1`] = `
|
||||
bar
|
||||
</span>
|
||||
:
|
||||
secretKeyRef(my-secret.bar)
|
||||
secret(my-secret)[bar]
|
||||
|
||||
<i
|
||||
class="Icon secret-button material interactive focusable"
|
||||
data-testid="show-secret-button-for-default/my-secret:bar"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
@ -171,7 +172,9 @@ exports[`<ContainerEnv /> renders envFrom when given a secretRef 1`] = `
|
||||
visibility
|
||||
</span>
|
||||
</i>
|
||||
<div>
|
||||
<div
|
||||
data-testid="tooltip-content-for-show-secret-button-for-default/my-secret:bar"
|
||||
>
|
||||
Show
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -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 = (
|
||||
<SecretKey
|
||||
reference={secretKeyRef}
|
||||
reference={{
|
||||
...secretKeyRef,
|
||||
name: secretKeyRef.name,
|
||||
}}
|
||||
namespace={namespace}
|
||||
secretStore={secretStore}
|
||||
/>
|
||||
);
|
||||
} else if (configMapKeyRef?.name) {
|
||||
@ -151,7 +153,6 @@ const NonInjectedContainerEnvironment = observer((props: Dependencies & Containe
|
||||
key,
|
||||
}}
|
||||
namespace={namespace}
|
||||
secretStore={secretStore}
|
||||
/>
|
||||
</div>
|
||||
));
|
||||
@ -172,52 +173,3 @@ export const ContainerEnvironment = withInjectables<Dependencies, ContainerEnvir
|
||||
secretStore: di.inject(secretStoreInjectable),
|
||||
}),
|
||||
});
|
||||
|
||||
interface SecretKeyProps {
|
||||
reference: EnvVarKeySelector;
|
||||
namespace: string;
|
||||
secretStore: SecretStore;
|
||||
}
|
||||
|
||||
const SecretKey = (props: SecretKeyProps) => {
|
||||
const {
|
||||
reference: { name, key },
|
||||
namespace,
|
||||
secretStore,
|
||||
} = props;
|
||||
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [secret, setSecret] = useState<Secret>();
|
||||
|
||||
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})`}
|
||||
|
||||
<Icon
|
||||
className={cssNames("secret-button", { loading })}
|
||||
material="visibility"
|
||||
tooltip="Show"
|
||||
onClick={showKey}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<SecretStore["load"]>;
|
||||
let result: RenderResult;
|
||||
|
||||
beforeEach(() => {
|
||||
const di = getDiForUnitTesting();
|
||||
const render = renderFor(di);
|
||||
|
||||
loadSecretMock = asyncFn();
|
||||
di.override(secretStoreInjectable, () => ({
|
||||
load: loadSecretMock,
|
||||
} as Partial<SecretStore> as SecretStore));
|
||||
|
||||
result = render((
|
||||
<SecretKey
|
||||
namespace="some-namespace"
|
||||
reference={{
|
||||
key: "some-key",
|
||||
name: "some-secret-name",
|
||||
}}
|
||||
/>
|
||||
));
|
||||
});
|
||||
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -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<EnvVarKeySelector, "name">;
|
||||
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<string>();
|
||||
|
||||
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}]`}
|
||||
|
||||
<Icon
|
||||
className={cssNames("secret-button", { loading })}
|
||||
material="visibility"
|
||||
tooltip="Show"
|
||||
disabled={loading}
|
||||
onClick={showKey}
|
||||
data-testid={`show-secret-button-for-${namespace}/${name}:${key}`}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const SecretKey = withInjectables<Dependencies, SecretKeyProps>(NonInjectedSecretKey, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
secretStore: di.inject(secretStoreInjectable),
|
||||
}),
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user