1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce competition for listing active helm repositories in preferences

Co-authored-by: Mikko Aspiala <mikko.aspiala@gmail.com>

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-06-03 15:35:56 +03:00
parent fadbca0c7c
commit ccc0d539eb
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
9 changed files with 3979 additions and 0 deletions

View File

@ -0,0 +1,378 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { ReadYamlFile } from "../../common/fs/read-yaml-file.injectable";
import readYamlFileInjectable from "../../common/fs/read-yaml-file.injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { HelmRepositoriesFromYaml } from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import loggerInjectable from "../../common/logger.injectable";
import type { Logger } from "../../common/logger";
describe("listing active helm repositories in preferences", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
let readYamlFileMock: AsyncFnMock<ReadYamlFile>;
let execFileMock: AsyncFnMock<ReturnType<typeof execFileInjectable["instantiate"]>>;
let loggerStub: Logger;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
readYamlFileMock = asyncFn();
execFileMock = asyncFn();
loggerStub = { warn: jest.fn() } as unknown as Logger;
applicationBuilder.beforeApplicationStart(({ mainDi }) => {
mainDi.override(readYamlFileInjectable, () => readYamlFileMock);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
mainDi.override(loggerInjectable, () => loggerStub);
});
rendered = await applicationBuilder.render();
});
describe("when navigating to preferences containing helm repositories", () => {
beforeEach(async () => {
applicationBuilder.preferences.navigate();
applicationBuilder.preferences.navigation.click("kubernetes");
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows loader for repositories", () => {
expect(
rendered.getByTestId("helm-repositories-are-loading"),
).toBeInTheDocument();
});
it("calls for helm configuration", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["env"],
);
});
it("does not call for updating of repositories yet", () => {
expect(execFileMock).not.toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "update"],
);
});
describe("when configuration resolves without path to repository config file", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["env"]],
"HELM_REPOSITORY_CACHE=some-helm-repository-cache-path",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("logs error", () => {
expect(loggerStub.warn).toHaveBeenCalledWith(
"Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`. Behaving as if there were no repositories.",
);
});
it("shows message about no repositories found", () => {
expect(
rendered.getByTestId("no-helm-repositories"),
).toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("does not call for updating of repositories", () => {
expect(execFileMock).not.toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "update"],
);
});
});
describe("when configuration resolves without path to repository cache directory", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["env"]],
"HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("logs error", () => {
expect(loggerStub.warn).toHaveBeenCalledWith(
"Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`. Behaving as if there were no repositories.",
);
});
it("shows message about no repositories found", () => {
expect(
rendered.getByTestId("no-helm-repositories"),
).toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("does not call for updating of repositories", () => {
expect(execFileMock).not.toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "update"],
);
});
});
describe("when configuration resolves", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["env"]],
[
"HELM_REPOSITORY_CONFIG=some-helm-repository-config-file.yaml",
"HELM_REPOSITORY_CACHE=some-helm-repository-cache-path",
].join("\n"),
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("calls for update of repositories", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "update"],
);
});
it("does not call for repositories yet", () => {
expect(readYamlFileMock).not.toHaveBeenCalled();
});
describe("when updating repositories resolve", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["repo", "update"]],
"",
);
});
describe("when loading repositories resolves with existing repositories", () => {
beforeEach(async () => {
execFileMock.mockClear();
await readYamlFileMock.resolveSpecific(
["some-helm-repository-config-file.yaml"],
repositoryConfigStub,
);
});
it("does not add default repository", () => {
expect(execFileMock).not.toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("shows repositories in use", () => {
const actual = rendered.getAllByTestId(
/^helm-repository-(some-repository|some-other-repository)$/,
);
expect(actual).toHaveLength(2);
});
});
describe("when loading repositories resolves with no existing repositories", () => {
beforeEach(async () => {
execFileMock.mockClear();
await readYamlFileMock.resolveSpecific(
["some-helm-repository-config-file.yaml"],
{ repositories: [] },
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the loader for repositories", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).toBeInTheDocument();
});
it("does not show message about no repositories", () => {
expect(
rendered.queryByTestId("no-helm-repositories"),
).not.toBeInTheDocument();
});
it('adds "bitnami" as default repository', () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "bitnami", "https://charts.bitnami.com/bitnami"],
);
});
describe("when adding of default repository resolves", () => {
beforeEach(async () => {
readYamlFileMock.mockClear();
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
[
"repo",
"add",
"bitnami",
"https://charts.bitnami.com/bitnami",
],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the loader for repositories", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).toBeInTheDocument();
});
it("does not show message about no repositories", () => {
expect(
rendered.queryByTestId("no-helm-repositories"),
).not.toBeInTheDocument();
});
it("calls for repositories again", () => {
expect(readYamlFileMock).toHaveBeenCalledWith(
"some-helm-repository-config-file.yaml",
);
});
describe("when another call for repositories resolve", () => {
beforeEach(async () => {
await readYamlFileMock.resolveSpecific(
["some-helm-repository-config-file.yaml"],
{
repositories: [
{
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
caFile: "irrelevant",
certFile: "irrelevant",
insecure_skip_tls_verify: false,
keyFile: "irrelevant",
pass_credentials_all: false,
password: "irrelevant",
username: "irrelevant",
},
],
},
);
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("shows the added repository", () => {
const actual = rendered.getByTestId("helm-repository-bitnami");
expect(actual).toBeInTheDocument();
});
it("does not show message about no repositories", () => {
expect(
rendered.queryByTestId("no-helm-repositories"),
).not.toBeInTheDocument();
});
});
});
});
});
});
});
});
const repositoryConfigStub: HelmRepositoriesFromYaml = {
repositories: [
{
name: "some-repository",
url: "some-repository-url",
caFile: "irrelevant",
certFile: "irrelevant",
insecure_skip_tls_verify: false,
keyFile: "irrelevant",
pass_credentials_all: false,
password: "irrelevant",
username: "irrelevant",
},
{
name: "some-other-repository",
url: "some-other-repository-url",
caFile: "irrelevant",
certFile: "irrelevant",
insecure_skip_tls_verify: false,
keyFile: "irrelevant",
pass_credentials_all: false,
password: "irrelevant",
username: "irrelevant",
},
],
};

View File

@ -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 type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import type { HelmRepo } from "../helm-repo";
import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token";
export type GetHelmRepositoriesChannel = RequestChannel<void, HelmRepo[]>;
const getActiveHelmRepositoriesChannelInjectable = getInjectable({
id: "get-active-helm-repositories-channel",
instantiate: (): GetHelmRepositoriesChannel => ({
id: "get-helm-active-list-repositories",
}),
injectionToken: requestChannelInjectionToken,
});
export default getActiveHelmRepositoriesChannelInjectable;

View File

@ -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 { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
import getActiveHelmRepositoriesChannelInjectable from "../../../../common/helm/get-active-helm-repositories-channel.injectable";
import getActiveHelmRepositoriesInjectable from "./get-active-helm-repositories.injectable";
const getActiveHelmRepositoriesChannelListenerInjectable = getInjectable({
id: "get-active-helm-repositories-channel-listener",
instantiate: (di) => {
const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable);
return {
channel: di.inject(getActiveHelmRepositoriesChannelInjectable),
handler: getActiveHelmRepositories,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default getActiveHelmRepositoriesChannelListenerInjectable;

View File

@ -0,0 +1,101 @@
/**
* 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 { HelmRepo } from "../../../../common/helm-repo";
import type { ReadYamlFile } from "../../../../common/fs/read-yaml-file.injectable";
import readYamlFileInjectable from "../../../../common/fs/read-yaml-file.injectable";
import getHelmEnvInjectable from "../../get-helm-env/get-helm-env.injectable";
import execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import { isEmpty } from "lodash/fp";
import loggerInjectable from "../../../../common/logger.injectable";
interface HelmRepositoryFromYaml {
name: string;
url: string;
caFile: string;
certFile: string;
insecure_skip_tls_verify: boolean;
keyFile: string;
pass_credentials_all: boolean;
password: string;
username: string;
}
export interface HelmRepositoriesFromYaml {
repositories: HelmRepositoryFromYaml[];
}
const getActiveHelmRepositoriesInjectable = getInjectable({
id: "get-helm-repositories",
instantiate: (di) => {
const readYamlFile = di.inject(readYamlFileInjectable);
const execHelm = di.inject(execHelmInjectable);
const getHelmEnv = di.inject(getHelmEnvInjectable);
const logger = di.inject(loggerInjectable);
const getRepositoriesFor = getRepositoriesForFor(readYamlFile);
return async (): Promise<HelmRepo[]> => {
const { HELM_REPOSITORY_CONFIG: repositoryConfigFilePath, HELM_REPOSITORY_CACHE: helmRepositoryCacheDirPath } = await getHelmEnv();
if (!repositoryConfigFilePath) {
logger.warn("Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`. Behaving as if there were no repositories.");
return [];
}
if (!helmRepositoryCacheDirPath) {
logger.warn("Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`. Behaving as if there were no repositories.");
return [];
}
if (!repositoryConfigFilePath || !helmRepositoryCacheDirPath) {
return [];
}
const getRepositories = getRepositoriesFor(
repositoryConfigFilePath,
helmRepositoryCacheDirPath,
);
await execHelm("repo", "update");
const repositories = await getRepositories();
if (isEmpty(repositories)) {
await execHelm("repo", "add", "bitnami", "https://charts.bitnami.com/bitnami");
return await getRepositories();
}
return repositories;
};
},
});
export default getActiveHelmRepositoriesInjectable;
const getRepositoriesForFor =
(readYamlFile: ReadYamlFile) =>
(repositoryConfigFilePath: string, helmRepositoryCacheDirPath: string) =>
async () => {
const { repositories } = (await readYamlFile(
repositoryConfigFilePath,
)) as HelmRepositoriesFromYaml;
return repositories.map((repository) => ({
name: repository.name,
url: repository.url,
caFile: repository.caFile,
certFile: repository.certFile,
insecureSkipTlsVerify: repository.insecure_skip_tls_verify,
keyFile: repository.keyFile,
username: repository.username,
password: repository.password,
cacheFilePath: `${helmRepositoryCacheDirPath}/${repository.name}-index.yaml`,
}));
};

View File

@ -6,6 +6,7 @@ import { observer } from "mobx-react";
import React from "react";
import { HelmCharts } from "./helm-charts";
import { HelmCharts as HelmCharts2 } from "./kubernetes/helm-charts/helm-charts-2";
import { KubeconfigSyncs } from "./kubeconfig-syncs";
import { KubectlBinaries } from "./kubectl-binaries";
import { Preferences } from "./preferences";
@ -26,6 +27,7 @@ export const Kubernetes = observer(() => (
<section id="helm">
<h2>Helm Charts</h2>
<HelmCharts />
<HelmCharts2 />
</section>
</section>
</Preferences>

View File

@ -0,0 +1,24 @@
/**
* 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 { asyncComputed } from "@ogre-tools/injectable-react";
import requestFromChannelInjectable from "../../../../utils/channel/request-from-channel.injectable";
import getActiveHelmRepositoriesChannelInjectable from "../../../../../common/helm/get-active-helm-repositories-channel.injectable";
const activeHelmRepositoriesInjectable = getInjectable({
id: "active-helm-repositories",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectable);
const getHelmRepositoriesChannel = di.inject(getActiveHelmRepositoriesChannelInjectable);
return asyncComputed(
async () => await requestFromChannel(getHelmRepositoriesChannel),
[],
);
},
});
export default activeHelmRepositoriesInjectable;

View File

@ -0,0 +1,18 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import React from "react";
import { HelmRepositories } from "./helm-repositories";
export const HelmCharts = () => (
<div>
<div className="flex gaps">
<HelmRepositories />
</div>
</div>
);

View File

@ -0,0 +1,65 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import styles from "../../helm-charts.module.scss";
import React from "react";
import { observer } from "mobx-react";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import { Spinner } from "../../../spinner";
import type { HelmRepo } from "../../../../../common/helm-repo";
import { Notice } from "../../../+extensions/notice";
import { isEmpty } from "lodash/fp";
interface Dependencies {
activeHelmRepositories: IAsyncComputed<HelmRepo[]>;
}
const NonInjectedActiveHelmRepositories = observer(({ activeHelmRepositories }: Dependencies) => {
if (activeHelmRepositories.pending.get()) {
return <Spinner data-testid="helm-repositories-are-loading" />;
}
const repositories = activeHelmRepositories.value.get();
if (isEmpty(repositories)) {
return (
<Notice>
<div className="flex-grow text-center" data-testid="no-helm-repositories">
The repositories have not been added yet
</div>
</Notice>
);
}
return (
<div className={styles.repos}>
{repositories.map((repository) => (
<div key={repository.name}>
<div data-testid={`helm-repository-${repository.name}`} className={styles.repoName}>
{repository.name}
</div>
<div className={styles.repoUrl}>{repository.url}</div>
</div>
))}
</div>
);
});
export const HelmRepositories = withInjectables<Dependencies>(
NonInjectedActiveHelmRepositories,
{
getProps: (di) => ({
activeHelmRepositories: di.inject(activeHelmRepositoriesInjectable),
}),
},
);