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

Stop using HelmCli from Renderer (#4861)

* Introduce way for execute file

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make typing of HelmRepo shared

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce way to get Helm environment values

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce function to read YAML file

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* 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>

* Make sense in name of injectable

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce helper for opening and selecting values of select

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for activating, deactivating public helm repositories in preferences

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for deactivating helm repository from list of active repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global overrides

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Make some tests more deterministic by mocking tooltips

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce competition for activating custom helm repository in preferences

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Update snapshots

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove old implementation made redundant with competition for preferences of helm repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add success notification for activating custom helm repository

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Introduce way to get single active helm repository

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Extract responsibilities from god-class and switch to getting helm repositories using competition instead of another god class

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove dead code

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add TODO

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Tweak position of spinner

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Start handling errors when accessing helm repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Handle error about no helm repositories when updating repositories

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove unwarranted function configuration

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Add missing global overrides

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove duplication how to acquire binary path for helm

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Remove redundant comment

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Consolidate naming to match Helm's internal

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

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>

* Relocate file closer to it's relatives

Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
This commit is contained in:
Janne Savolainen 2022-06-13 11:42:53 +03:00 committed by GitHub
parent 5420780ae0
commit 1393cc303d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 21409 additions and 1039 deletions

View File

@ -0,0 +1,387 @@
/**
* 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 { fireEvent } 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 { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import callForPublicHelmRepositoriesInjectable from "../../renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
import isPathInjectable from "../../renderer/components/input/validators/is-path.injectable";
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
// TODO: Make tooltips free of side effects by making it deterministic
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (target: any) => target,
}));
describe("add custom helm repository in preferences", () => {
let applicationBuilder: ApplicationBuilder;
let showSuccessNotificationMock: jest.Mock;
let showErrorNotificationMock: jest.Mock;
let rendered: RenderResult;
let execFileMock: AsyncFnMock<
ReturnType<typeof execFileInjectable["instantiate"]>
>;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => AsyncResult<HelmRepo[]>>;
beforeEach(async () => {
jest.useFakeTimers("modern");
applicationBuilder = getApplicationBuilder();
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
rendererDi.override(callForPublicHelmRepositoriesInjectable, () => async () => []);
showSuccessNotificationMock = jest.fn();
rendererDi.override(showSuccessNotificationInjectable, () => showSuccessNotificationMock);
showErrorNotificationMock = jest.fn();
rendererDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
// TODO: Figure out how to make async validators unit testable
rendererDi.override(isPathInjectable, () => ({ debounce: 0, validate: async () => {} }));
mainDi.override(
getActiveHelmRepositoriesInjectable,
() => getActiveHelmRepositoriesMock,
);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
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();
});
describe("when active repositories resolve", () => {
beforeEach(async () => {
await Promise.all([
getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [
{ name: "Some active repository", url: "some-url" },
],
}),
]);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when selecting to add custom repository", () => {
beforeEach(() => {
const button = rendered.getByTestId("add-custom-helm-repo-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows dialog", () => {
expect(
rendered.queryByTestId("add-custom-helm-repository-dialog"),
).toBeInTheDocument();
});
// TODO: Figure out how to close dialog by clicking outside of it
xdescribe("when closing the dialog by clicking outside", () => {
beforeEach(() => {});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show dialog anymore", () => {
expect(
rendered.queryByTestId("add-custom-helm-repository-dialog"),
).not.toBeInTheDocument();
});
});
describe("when closing the dialog by clicking cancel", () => {
beforeEach(() => {
const button = rendered.getByTestId("custom-helm-repository-cancel-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show dialog anymore", () => {
expect(
rendered.queryByTestId("add-custom-helm-repository-dialog"),
).not.toBeInTheDocument();
});
});
describe("when inputted minimal options for the repository", () => {
beforeEach(() => {
getActiveHelmRepositoriesMock.mockClear();
const nameInput = rendered.getByTestId("custom-helm-repository-name-input");
fireEvent.change(nameInput, { target: { value: "some-custom-repository" }});
const urlInput = rendered.getByTestId("custom-helm-repository-url-input");
fireEvent.change(urlInput, { target: { value: "http://some.url" }});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when submitted and some time passes", () => {
beforeEach(() => {
const submitButton = rendered.getByTestId("custom-helm-repository-submit-button");
fireEvent.click(submitButton);
// TODO: Remove when debounce is removed from WizardStep.submit
jest.runOnlyPendingTimers();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("adds the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "some-custom-repository", "http://some.url"],
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
it("does not show notification yet", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
describe("when activation rejects", () => {
beforeEach(async () => {
await execFileMock.reject(
"Some error",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Some error",
);
});
it("does not show success notification", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
it("does not show dialog anymore", () => {
expect(rendered.queryByTestId("add-custom-helm-repository-dialog")).not.toBeInTheDocument();
});
it("does not reload active repositories", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
});
describe("when activation resolves with success", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "add", "some-custom-repository", "http://some.url"],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show dialog anymore", () => {
expect(rendered.queryByTestId("add-custom-helm-repository-dialog")).not.toBeInTheDocument();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
it("shows success notification", () => {
expect(showSuccessNotificationMock).toHaveBeenCalledWith(
"Helm repository some-custom-repository has been added.",
);
});
describe("when adding custom repository again", () => {
beforeEach(() => {
const button = rendered.getByTestId("add-custom-helm-repo-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("repository name is empty", () => {
const input = rendered.getByTestId("custom-helm-repository-name-input") as HTMLInputElement;
expect(input.value).toBe("");
});
it("repository url is empty", () => {
const input = rendered.getByTestId("custom-helm-repository-url-input") as HTMLInputElement;
expect(input.value).toBe("");
});
});
});
});
describe("when showing the maximal options", () => {
beforeEach(() => {
const button = rendered.getByTestId("toggle-maximal-options-for-custom-helm-repository-button");
fireEvent.click(button);
});
it("shows maximal options", () => {
const maximalOptions = rendered.getByTestId("maximal-options-for-custom-helm-repository-dialog");
expect(maximalOptions).toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("given closing the dialog, when reopening the dialog, still shows maximal options", () => {
const cancelButton = rendered.getByTestId("custom-helm-repository-cancel-button");
fireEvent.click(cancelButton);
const openButton = rendered.getByTestId("add-custom-helm-repo-button");
fireEvent.click(openButton);
const maximalOptions = rendered.getByTestId("maximal-options-for-custom-helm-repository-dialog");
expect(maximalOptions).toBeInTheDocument();
});
describe("when hiding maximal options", () => {
beforeEach(() => {
const button = rendered.getByTestId("toggle-maximal-options-for-custom-helm-repository-button");
fireEvent.click(button);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not show maximal options anymore", () => {
const maximalOptions = rendered.queryByTestId("maximal-options-for-custom-helm-repository-dialog");
expect(maximalOptions).not.toBeInTheDocument();
});
});
describe("when inputted maximal options", () => {
beforeEach(async () => {
[
{ selector: "username-input", value: "some-username" },
{ selector: "password-input", value: "some-password" },
{ selector: "ca-cert-file-input", value: "some-ca-cert-file" },
{ selector: "cert-file-input", value: "some-cert-file" },
{ selector: "key-file-input", value: "some-key-file" },
].forEach(({ selector, value }) => {
const input = rendered.getByTestId(`custom-helm-repository-${selector}`);
fireEvent.change(input, { target: { value }});
});
const checkbox = rendered.getByTestId(`custom-helm-repository-verify-tls-input`);
fireEvent.click(checkbox);
jest.runOnlyPendingTimers();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("when submitted and some time passes, adds the repository with maximal options", () => {
const submitButton = rendered.getByTestId("custom-helm-repository-submit-button");
fireEvent.click(submitButton);
// TODO: Remove when debounce is removed from WizardStep.submit
jest.runOnlyPendingTimers();
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
[
"repo",
"add",
"some-custom-repository",
"http://some.url",
"--insecure-skip-tls-verify",
"--username",
"some-username",
"--password",
"some-password",
"--ca-file",
"some-ca-cert-file",
"--key-file",
"some-key-file",
"--cert-file",
"some-cert-file",
],
);
});
});
});
});
});
});
});
});

View File

@ -0,0 +1,278 @@
/**
* 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 { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import callForPublicHelmRepositoriesInjectable from "../../renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
import showSuccessNotificationInjectable from "../../renderer/components/notifications/show-success-notification.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
// TODO: Make tooltips free of side effects by making it deterministic
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (target: any) => target,
}));
describe("add helm repository from list in preferences", () => {
let applicationBuilder: ApplicationBuilder;
let showSuccessNotificationMock: jest.Mock;
let showErrorNotificationMock: jest.Mock;
let rendered: RenderResult;
let execFileMock: AsyncFnMock<
ReturnType<typeof execFileInjectable["instantiate"]>
>;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => AsyncResult<HelmRepo[]>>;
let callForPublicHelmRepositoriesMock: AsyncFnMock<() => Promise<HelmRepo[]>>;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
callForPublicHelmRepositoriesMock = asyncFn();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
showSuccessNotificationMock = jest.fn();
rendererDi.override(showSuccessNotificationInjectable, () => showSuccessNotificationMock);
showErrorNotificationMock = jest.fn();
rendererDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
rendererDi.override(
callForPublicHelmRepositoriesInjectable,
() => callForPublicHelmRepositoriesMock,
);
mainDi.override(
getActiveHelmRepositoriesInjectable,
() => getActiveHelmRepositoriesMock,
);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
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("calls for public repositories", () => {
expect(callForPublicHelmRepositoriesMock).toHaveBeenCalled();
});
it("calls for active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
describe("when both active and public repositories resolve", () => {
beforeEach(async () => {
await Promise.all([
callForPublicHelmRepositoriesMock.resolve([
{ name: "Some already active repository", url: "some-url" },
{ name: "Some to be added repository", url: "some-other-url" },
]),
getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [
{ name: "Some already active repository", url: "some-url" },
],
}),
]);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when select for adding public repositories is clicked", () => {
beforeEach(() => {
applicationBuilder.select.openMenu(
"selection-of-active-public-helm-repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when deactive public repository is selected", () => {
beforeEach(async () => {
getActiveHelmRepositoriesMock.mockClear();
applicationBuilder.select.selectOption(
"selection-of-active-public-helm-repository",
"Some to be added repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("adds the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "add", "Some to be added repository", "some-other-url"],
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when adding rejects", () => {
beforeEach(async () => {
await execFileMock.reject(
"Some error",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Some error",
);
});
it("does not show success notification", () => {
expect(showSuccessNotificationMock).not.toHaveBeenCalled();
});
it("does not show dialog anymore", () => {
expect(rendered.queryByTestId("add-custom-helm-repository-dialog")).not.toBeInTheDocument();
});
it("does not reload active repositories", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
});
describe("when adding resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "add", "Some to be added repository", "some-other-url"],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
it("shows success notification", () => {
expect(showSuccessNotificationMock).toHaveBeenCalledWith(
"Helm repository Some to be added repository has been added.",
);
});
describe("when active repositories resolve again", () => {
beforeEach(async () => {
await getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [
{ name: "Some already active repository", url: "some-url" },
{ name: "Some to be added repository", url: "some-other-url" },
],
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when select for selecting active repositories is clicked", () => {
beforeEach(() => {
applicationBuilder.select.openMenu(
"selection-of-active-public-helm-repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when active repository is selected", () => {
beforeEach(() => {
execFileMock.mockClear();
getActiveHelmRepositoriesMock.mockClear();
applicationBuilder.select.selectOption(
"selection-of-active-public-helm-repository",
"Some already active repository",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("removes the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "remove", "Some already active repository"],
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when removing resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "remove", "Some already active repository"],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
});
});
});
});
});
});
});
});
});
});

View File

@ -0,0 +1,458 @@
/**
* 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";
import callForPublicHelmRepositoriesInjectable from "../../renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
import showErrorNotificationInjectable from "../../renderer/components/notifications/show-error-notification.injectable";
// TODO: Make tooltips free of side effects by making it deterministic
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (target: any) => target,
}));
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;
let showErrorNotificationMock: jest.Mock;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
readYamlFileMock = asyncFn();
execFileMock = asyncFn();
loggerStub = { error: jest.fn() } as unknown as Logger;
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
showErrorNotificationMock = jest.fn();
rendererDi.override(showErrorNotificationInjectable, () => showErrorNotificationMock);
rendererDi.override(callForPublicHelmRepositoriesInjectable, () => async () => []);
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 getting configuration rejects", () => {
beforeEach(async () => {
await execFileMock.reject("some-error");
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error getting Helm configuration: some-error",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
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("logs error", () => {
expect(loggerStub.error).toHaveBeenCalledWith(
"Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`.",
);
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error getting Helm configuration: Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`.",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
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("logs error", () => {
expect(loggerStub.error).toHaveBeenCalledWith(
"Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`.",
);
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error getting Helm configuration: Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`.",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
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 reject with any other error", () => {
beforeEach(async () => {
await execFileMock.reject("Some error");
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error updating Helm repositories: Some error",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
describe("when updating repositories reject with error about no existing repositories", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.reject(
"Error: no repositories found. You must add one before updating",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the loader for repositories", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).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 default repository reject", () => {
beforeEach(async () => {
await execFileMock.reject("Some error");
});
it("shows error notification", () => {
expect(showErrorNotificationMock).toHaveBeenCalledWith(
"Error when adding default Helm repository: Some error",
);
});
it("removes all helm controls", () => {
expect(
rendered.queryByTestId("helm-controls"),
).not.toBeInTheDocument();
});
it("does not show loader for repositories anymore", () => {
expect(
rendered.queryByTestId("helm-repositories-are-loading"),
).not.toBeInTheDocument();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
});
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("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();
});
});
});
});
describe("when updating repositories resolve", () => {
beforeEach(async () => {
execFileMock.mockClear();
await execFileMock.resolveSpecific(
["some-helm-binary-path", ["repo", "update"]],
"",
);
});
it("loads repositories from file system", () => {
expect(readYamlFileMock).toHaveBeenCalledWith(
"some-helm-repository-config-file.yaml",
);
});
describe("when repositories resolves", () => {
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);
});
});
});
});
});
});
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,126 @@
/**
* 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 { fireEvent } 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 { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import execFileInjectable from "../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../../main/helm/helm-binary-path.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
import type { HelmRepo } from "../../common/helm/helm-repo";
import callForPublicHelmRepositoriesInjectable from "../../renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
import type { AsyncResult } from "../../common/utils/async-result";
// TODO: Make tooltips free of side effects by making it deterministic
jest.mock("../../renderer/components/tooltip/withTooltip", () => ({
withTooltip: (target: any) => target,
}));
describe("remove helm repository from list of active repositories in preferences", () => {
let applicationBuilder: ApplicationBuilder;
let rendered: RenderResult;
let getActiveHelmRepositoriesMock: AsyncFnMock<() => AsyncResult<HelmRepo[]>>;
let execFileMock: AsyncFnMock<
ReturnType<typeof execFileInjectable["instantiate"]>
>;
beforeEach(async () => {
applicationBuilder = getApplicationBuilder();
execFileMock = asyncFn();
getActiveHelmRepositoriesMock = asyncFn();
applicationBuilder.beforeApplicationStart(({ mainDi, rendererDi }) => {
rendererDi.override(callForPublicHelmRepositoriesInjectable, () => async () => []);
mainDi.override(
getActiveHelmRepositoriesInjectable,
() => getActiveHelmRepositoriesMock,
);
mainDi.override(execFileInjectable, () => execFileMock);
mainDi.override(helmBinaryPathInjectable, () => "some-helm-binary-path");
});
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();
});
describe("when active repositories resolve", () => {
beforeEach(async () => {
getActiveHelmRepositoriesMock.resolve({
callWasSuccessful: true,
response: [
{ name: "some-active-repository", url: "some-url" },
],
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when removing repository", () => {
beforeEach(() => {
execFileMock.mockClear();
getActiveHelmRepositoriesMock.mockClear();
const removeButton = rendered.getByTestId(
"remove-helm-repository-some-active-repository",
);
fireEvent.click(removeButton);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("removes the repository", () => {
expect(execFileMock).toHaveBeenCalledWith(
"some-helm-binary-path",
["repo", "remove", "some-active-repository"],
);
});
it("does not reload active repositories yet", () => {
expect(getActiveHelmRepositoriesMock).not.toHaveBeenCalled();
});
describe("when removing resolves", () => {
beforeEach(async () => {
await execFileMock.resolveSpecific(
[
"some-helm-binary-path",
["repo", "remove", "some-active-repository"],
],
"",
);
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("reloads active repositories", () => {
expect(getActiveHelmRepositoriesMock).toHaveBeenCalled();
});
});
});
});
});
});

View File

@ -833,6 +833,9 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
Helm Charts Helm Charts
</h2> </h2>
<div> <div>
<div
data-testid="helm-controls"
>
<div <div
class="flex gaps" class="flex gaps"
> >
@ -841,7 +844,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<span <span
class="css-1f43avz-a11yText-A11yText" class="css-1f43avz-a11yText-A11yText"
id="react-select-HelmRepoSelect-live-region" id="react-select-selection-of-active-public-helm-repository-live-region"
/> />
<span <span
aria-atomic="false" aria-atomic="false"
@ -857,7 +860,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Select__placeholder css-14el2xx-placeholder" class="Select__placeholder css-14el2xx-placeholder"
id="react-select-HelmRepoSelect-placeholder" id="react-select-selection-of-active-public-helm-repository-placeholder"
> >
Repositories Repositories
</div> </div>
@ -867,7 +870,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<input <input
aria-autocomplete="list" aria-autocomplete="list"
aria-describedby="react-select-HelmRepoSelect-placeholder" aria-describedby="react-select-selection-of-active-public-helm-repository-placeholder"
aria-expanded="false" aria-expanded="false"
aria-haspopup="true" aria-haspopup="true"
autocapitalize="none" autocapitalize="none"
@ -875,7 +878,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
autocorrect="off" autocorrect="off"
class="Select__input" class="Select__input"
disabled="" disabled=""
id="HelmRepoSelect" id="selection-of-active-public-helm-repository"
role="combobox" role="combobox"
spellcheck="false" spellcheck="false"
style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;" style="opacity: 1; width: 100%; grid-area: 1 / 2; min-width: 2px; border: 0px; margin: 0px; outline: 0; padding: 0px;"
@ -927,6 +930,7 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
</div> </div>
<button <button
class="Button primary" class="Button primary"
data-testid="add-custom-helm-repo-button"
type="button" type="button"
> >
Add Custom Helm Repo Add Custom Helm Repo
@ -940,9 +944,12 @@ exports[`preferences - navigation to kubernetes preferences given in preferences
> >
<div <div
class="Spinner singleColor center" class="Spinner singleColor center"
data-testid="helm-repositories-are-loading"
/> />
</div> </div>
</div> </div>
<div />
</div>
</div> </div>
</section> </section>
</section> </section>

View File

@ -5,6 +5,8 @@
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import callForPublicHelmRepositoriesInjectable from "../../renderer/components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
import getActiveHelmRepositoriesInjectable from "../../main/helm/repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
describe("preferences - navigation to kubernetes preferences", () => { describe("preferences - navigation to kubernetes preferences", () => {
let applicationBuilder: ApplicationBuilder; let applicationBuilder: ApplicationBuilder;
@ -17,6 +19,15 @@ describe("preferences - navigation to kubernetes preferences", () => {
let rendered: RenderResult; let rendered: RenderResult;
beforeEach(async () => { beforeEach(async () => {
applicationBuilder.beforeApplicationStart(({ rendererDi, mainDi }) => {
rendererDi.override(callForPublicHelmRepositoriesInjectable, () => async () => []);
mainDi.override(
getActiveHelmRepositoriesInjectable,
() => async () => ({ callWasSuccessful: true, response: [] }),
);
});
applicationBuilder.beforeRender(() => { applicationBuilder.beforeRender(() => {
applicationBuilder.preferences.navigate(); applicationBuilder.preferences.navigate();
}); });

View File

@ -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 { execFile } from "child_process";
import { promisify } from "util";
export type ExecFile = (filePath: string, args: string[]) => Promise<string>;
const execFileInjectable = getInjectable({
id: "exec-file",
instantiate: (): ExecFile => async (filePath, args) => {
const asyncExecFile = promisify(execFile);
const result = await asyncExecFile(filePath, args);
return result.stdout;
},
causesSideEffects: true,
});
export default execFileInjectable;

View File

@ -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 readFileInjectable from "./read-file.injectable";
import yaml from "js-yaml";
export type ReadYamlFile = (filePath: string) => Promise<unknown>;
const readYamlFileInjectable = getInjectable({
id: "read-yaml-file",
instantiate: (di): ReadYamlFile => {
const readFile = di.inject(readFileInjectable);
return async (filePath: string) => {
const contents = await readFile(filePath);
return yaml.load(contents);
};
},
});
export default readYamlFileInjectable;

View File

@ -0,0 +1,23 @@
/**
* 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 "./helm-repo";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token";
import type { AsyncResult } from "../utils/async-result";
export type AddHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<string>>;
const addHelmRepositoryChannelInjectable = getInjectable({
id: "add-helm-repository-channel",
instantiate: (): AddHelmRepositoryChannel => ({
id: "add-helm-repository-channel",
}),
injectionToken: requestChannelInjectionToken,
});
export default addHelmRepositoryChannelInjectable;

View File

@ -0,0 +1,23 @@
/**
* 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";
import type { AsyncResult } from "../utils/async-result";
export type GetHelmRepositoriesChannel = RequestChannel<void, AsyncResult<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,16 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
export type HelmRepo = {
name: string;
url: string;
cacheFilePath?: string;
caFile?: string;
certFile?: string;
insecureSkipTlsVerify?: boolean;
keyFile?: string;
username?: string;
password?: string;
};

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 { HelmRepo } from "./helm-repo";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { requestChannelInjectionToken } from "../utils/channel/request-channel-injection-token";
export type RemoveHelmRepositoryChannel = RequestChannel<HelmRepo>;
const removeHelmRepositoryChannelInjectable = getInjectable({
id: "remove-helm-repository-channel",
instantiate: (): RemoveHelmRepositoryChannel => ({
id: "remove-helm-repository-channel",
}),
injectionToken: requestChannelInjectionToken,
});
export default removeHelmRepositoryChannelInjectable;

View File

@ -0,0 +1,7 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export type AsyncResult<Response, Error = string> =
| { callWasSuccessful: true; response: Response }
| { callWasSuccessful: false; error: Error };

View File

@ -0,0 +1,15 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
export const getErrorMessage = (error: unknown): string => {
if (typeof error === "string") {
return error;
}
if (error instanceof Error) {
return error.message;
}
return JSON.stringify(error);
};

View File

@ -3,16 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import bundledBinariesNormalizedArchInjectable from "./bundled-binaries-normalized-arch.injectable";
import bundledResourcesDirectoryInjectable from "./bundled-resources-dir.injectable"; import bundledResourcesDirectoryInjectable from "./bundled-resources-dir.injectable";
import getAbsolutePathInjectable from "../path/get-absolute-path.injectable";
import normalizedPlatformArchitectureInjectable from "./normalized-platform-architecture.injectable";
const baseBundeledBinariesDirectoryInjectable = getInjectable({ const baseBundledBinariesDirectoryInjectable = getInjectable({
id: "base-bundeled-binaries-directory", id: "base-bundled-binaries-directory",
instantiate: (di) => path.join( instantiate: (di) => {
di.inject(bundledResourcesDirectoryInjectable), const bundledResourcesDirectory = di.inject(bundledResourcesDirectoryInjectable);
di.inject(bundledBinariesNormalizedArchInjectable), const normalizedPlatformArchitecture = di.inject(normalizedPlatformArchitectureInjectable);
), const getAbsolutePath = di.inject(getAbsolutePathInjectable);
return getAbsolutePath(
bundledResourcesDirectory,
normalizedPlatformArchitecture,
);
},
}); });
export default baseBundeledBinariesDirectoryInjectable; export default baseBundledBinariesDirectoryInjectable;

View File

@ -3,19 +3,22 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import isProductionInjectable from "./is-production.injectable"; import isProductionInjectable from "./is-production.injectable";
import normalizedPlatformInjectable from "./normalized-platform.injectable"; import normalizedPlatformInjectable from "./normalized-platform.injectable";
import getAbsolutePathInjectable from "../path/get-absolute-path.injectable";
import lensResourcesDirInjectable from "./lens-resources-dir.injectable";
const bundledResourcesDirectoryInjectable = getInjectable({ const bundledResourcesDirectoryInjectable = getInjectable({
id: "bundled-resources-directory", id: "bundled-resources-directory",
instantiate: (di) => { instantiate: (di) => {
const isProduction = di.inject(isProductionInjectable); const isProduction = di.inject(isProductionInjectable);
const normalizedPlatform = di.inject(normalizedPlatformInjectable); const normalizedPlatform = di.inject(normalizedPlatformInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const lensResourcesDir = di.inject(lensResourcesDirInjectable);
return isProduction return isProduction
? process.resourcesPath ? lensResourcesDir
: path.join(process.cwd(), "binaries", "client", normalizedPlatform); : getAbsolutePath(lensResourcesDir, "binaries", "client", normalizedPlatform);
}, },
}); });

View File

@ -4,8 +4,8 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
const bundledBinariesNormalizedArchInjectable = getInjectable({ const normalizedPlatformArchitectureInjectable = getInjectable({
id: "bundled-binaries-normalized-arch", id: "normalized-platform-architecture",
instantiate: () => { instantiate: () => {
switch (process.arch) { switch (process.arch) {
case "arm64": case "arm64":
@ -24,4 +24,4 @@ const bundledBinariesNormalizedArchInjectable = getInjectable({
causesSideEffects: true, causesSideEffects: true,
}); });
export default bundledBinariesNormalizedArchInjectable; export default normalizedPlatformArchitectureInjectable;

View File

@ -3,11 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import platformInjectable from "./platform.injectable";
const normalizedPlatformInjectable = getInjectable({ const normalizedPlatformInjectable = getInjectable({
id: "normalized-platform", id: "normalized-platform",
instantiate: () => {
switch (process.platform) { instantiate: (di) => {
const platform = di.inject(platformInjectable);
switch (platform) {
case "darwin": case "darwin":
return "darwin"; return "darwin";
case "linux": case "linux":
@ -15,10 +19,9 @@ const normalizedPlatformInjectable = getInjectable({
case "win32": case "win32":
return "windows"; return "windows";
default: default:
throw new Error(`platform=${process.platform} is unsupported`); throw new Error(`platform=${platform} is unsupported`);
} }
}, },
causesSideEffects: true,
}); });
export default normalizedPlatformInjectable; export default normalizedPlatformInjectable;

View File

@ -5,7 +5,7 @@
import glob from "glob"; import glob from "glob";
import { kebabCase, memoize, noop } from "lodash/fp"; import { kebabCase, memoize, noop } from "lodash/fp";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer, Injectable } from "@ogre-tools/injectable";
import { createContainer } from "@ogre-tools/injectable"; import { createContainer } from "@ogre-tools/injectable";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import appNameInjectable from "./app-paths/app-name/app-name.injectable"; import appNameInjectable from "./app-paths/app-name/app-name.injectable";
@ -76,7 +76,7 @@ import quitAndInstallUpdateInjectable from "./electron-app/features/quit-and-ins
import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable"; import electronUpdaterIsActiveInjectable from "./electron-app/features/electron-updater-is-active.injectable";
import publishIsConfiguredInjectable from "./application-update/publish-is-configured.injectable"; import publishIsConfiguredInjectable from "./application-update/publish-is-configured.injectable";
import checkForPlatformUpdatesInjectable from "./application-update/check-for-platform-updates/check-for-platform-updates.injectable"; import checkForPlatformUpdatesInjectable from "./application-update/check-for-platform-updates/check-for-platform-updates.injectable";
import baseBundeledBinariesDirectoryInjectable from "../common/vars/base-bundled-binaries-dir.injectable"; import baseBundledBinariesDirectoryInjectable from "../common/vars/base-bundled-binaries-dir.injectable";
import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-quit.injectable"; import setUpdateOnQuitInjectable from "./electron-app/features/set-update-on-quit.injectable";
import downloadPlatformUpdateInjectable from "./application-update/download-platform-update/download-platform-update.injectable"; import downloadPlatformUpdateInjectable from "./application-update/download-platform-update/download-platform-update.injectable";
import startCatalogSyncInjectable from "./catalog-sync-to-renderer/start-catalog-sync.injectable"; import startCatalogSyncInjectable from "./catalog-sync-to-renderer/start-catalog-sync.injectable";
@ -84,6 +84,19 @@ import startKubeConfigSyncInjectable from "./start-main-application/runnables/ku
import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable"; import appVersionInjectable from "../common/get-configuration-file-model/app-version/app-version.injectable";
import getRandomIdInjectable from "../common/utils/get-random-id.injectable"; import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import periodicalCheckForUpdatesInjectable from "./application-update/periodical-check-for-updates/periodical-check-for-updates.injectable"; import periodicalCheckForUpdatesInjectable from "./application-update/periodical-check-for-updates/periodical-check-for-updates.injectable";
import execFileInjectable from "../common/fs/exec-file.injectable";
import normalizedPlatformArchitectureInjectable from "../common/vars/normalized-platform-architecture.injectable";
import getHelmChartInjectable from "./helm/helm-service/get-helm-chart.injectable";
import getHelmChartValuesInjectable from "./helm/helm-service/get-helm-chart-values.injectable";
import listHelmChartsInjectable from "./helm/helm-service/list-helm-charts.injectable";
import deleteHelmReleaseInjectable from "./helm/helm-service/delete-helm-release.injectable";
import getHelmReleaseHistoryInjectable from "./helm/helm-service/get-helm-release-history.injectable";
import getHelmReleaseInjectable from "./helm/helm-service/get-helm-release.injectable";
import getHelmReleaseValuesInjectable from "./helm/helm-service/get-helm-release-values.injectable";
import installHelmChartInjectable from "./helm/helm-service/install-helm-chart.injectable";
import listHelmReleasesInjectable from "./helm/helm-service/list-helm-releases.injectable";
import rollbackHelmReleaseInjectable from "./helm/helm-service/rollback-helm-release.injectable";
import updateHelmReleaseInjectable from "./helm/helm-service/update-helm-release.injectable";
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) { export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
const { const {
@ -134,6 +147,24 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false })); di.override(periodicalCheckForUpdatesInjectable, () => ({ start: () => {}, stop: () => {}, started: false }));
overrideFunctionalInjectables(di, [
getHelmChartInjectable,
getHelmChartValuesInjectable,
listHelmChartsInjectable,
deleteHelmReleaseInjectable,
getHelmReleaseHistoryInjectable,
getHelmReleaseInjectable,
getHelmReleaseValuesInjectable,
installHelmChartInjectable,
listHelmReleasesInjectable,
rollbackHelmReleaseInjectable,
updateHelmReleaseInjectable,
writeJsonFileInjectable,
readJsonFileInjectable,
readFileInjectable,
execFileInjectable,
]);
// TODO: Remove usages of globally exported appEventBus to get rid of this // TODO: Remove usages of globally exported appEventBus to get rid of this
di.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>()); di.override(appEventBusInjectable, () => new EventEmitter<[AppEvent]>());
@ -141,7 +172,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.override(broadcastMessageInjectable, () => (channel) => { di.override(broadcastMessageInjectable, () => (channel) => {
throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`); throw new Error(`Tried to broadcast message to channel "${channel}" over IPC without explicit override.`);
}); });
di.override(baseBundeledBinariesDirectoryInjectable, () => "some-bin-directory"); di.override(baseBundledBinariesDirectoryInjectable, () => "some-bin-directory");
di.override(spawnInjectable, () => () => { di.override(spawnInjectable, () => () => {
return { return {
stderr: { on: jest.fn(), removeAllListeners: jest.fn() }, stderr: { on: jest.fn(), removeAllListeners: jest.fn() },
@ -150,18 +181,6 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
} as never; } as never;
}); });
di.override(writeJsonFileInjectable, () => () => {
throw new Error("Tried to write JSON file to file system without specifying explicit override.");
});
di.override(readJsonFileInjectable, () => () => {
throw new Error("Tried to read JSON file from file system without specifying explicit override.");
});
di.override(readFileInjectable, () => () => {
throw new Error("Tried to read file from file system without specifying explicit override.");
});
di.override(loggerInjectable, () => ({ di.override(loggerInjectable, () => ({
warn: noop, warn: noop,
debug: noop, debug: noop,
@ -204,6 +223,7 @@ const overrideOperatingSystem = (di: DiContainer) => {
di.override(platformInjectable, () => "darwin"); di.override(platformInjectable, () => "darwin");
di.override(getAbsolutePathInjectable, () => getAbsolutePathFake); di.override(getAbsolutePathInjectable, () => getAbsolutePathFake);
di.override(joinPathsInjectable, () => joinPathsFake); di.override(joinPathsInjectable, () => joinPathsFake);
di.override(normalizedPlatformArchitectureInjectable, () => "arm64");
}; };
const overrideElectronFeatures = (di: DiContainer) => { const overrideElectronFeatures = (di: DiContainer) => {
@ -257,3 +277,11 @@ const overrideElectronFeatures = (di: DiContainer) => {
di.override(publishIsConfiguredInjectable, () => false); di.override(publishIsConfiguredInjectable, () => false);
di.override(electronUpdaterIsActiveInjectable, () => false); di.override(electronUpdaterIsActiveInjectable, () => false);
}; };
const overrideFunctionalInjectables = (di: DiContainer, injectables: Injectable<any, any, any>[]) => {
injectables.forEach(injectable => {
di.override(injectable, () => () => {
throw new Error(`Tried to run "${injectable.id}" without explicit override.`);
});
});
};

View File

@ -4,7 +4,7 @@
*/ */
import { sortCharts } from "../../../common/utils"; import { sortCharts } from "../../../common/utils";
import type { HelmRepo } from "../helm-repo-manager"; import type { HelmRepo } from "../../../common/helm/helm-repo";
const charts = new Map([ const charts = new Map([
["stable", { ["stable", {

View File

@ -3,27 +3,48 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { helmService } from "../helm-service"; import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { HelmRepoManager } from "../helm-repo-manager"; import listHelmChartsInjectable from "../helm-service/list-helm-charts.injectable";
import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
const mockHelmRepoManager = jest.spyOn(HelmRepoManager, "getInstance").mockImplementation(); import type { AsyncResult } from "../../../common/utils/async-result";
import type { HelmRepo } from "../../../common/helm/helm-repo";
jest.mock("../helm-chart-manager"); jest.mock("../helm-chart-manager");
describe("Helm Service tests", () => { describe("Helm Service tests", () => {
let listHelmCharts: () => Promise<any>;
let getActiveHelmRepositoriesMock: jest.Mock<Promise<AsyncResult<HelmRepo[]>>>;
beforeEach(() => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
getActiveHelmRepositoriesMock = jest.fn();
di.override(getActiveHelmRepositoriesInjectable, () => getActiveHelmRepositoriesMock);
di.unoverride(listHelmChartsInjectable);
di.permitSideEffects(listHelmChartsInjectable);
listHelmCharts = di.inject(listHelmChartsInjectable);
});
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
}); });
it("list charts with deprecated entries", async () => { it("list charts with deprecated entries", async () => {
mockHelmRepoManager.mockReturnValue({ getActiveHelmRepositoriesMock.mockReturnValue(
repositories: jest.fn().mockImplementation(async () => [ Promise.resolve({
callWasSuccessful: true,
response: [
{ name: "stable", url: "stableurl" }, { name: "stable", url: "stableurl" },
{ name: "experiment", url: "experimenturl" }, { name: "experiment", url: "experimenturl" },
]), ],
} as any); }),
);
const charts = await helmService.listCharts(); const charts = await listHelmCharts();
expect(charts).toEqual({ expect(charts).toEqual({
stable: { stable: {
@ -123,15 +144,14 @@ describe("Helm Service tests", () => {
}); });
it("list charts sorted by version in descending order", async () => { it("list charts sorted by version in descending order", async () => {
mockHelmRepoManager.mockReturnValue({ getActiveHelmRepositoriesMock.mockReturnValue(
repositories: jest.fn().mockImplementation(async () => { Promise.resolve({
return [ callWasSuccessful: true,
{ name: "bitnami", url: "bitnamiurl" }, response: [{ name: "bitnami", url: "bitnamiurl" }],
];
}), }),
} as any); );
const charts = await helmService.listCharts(); const charts = await listHelmCharts();
expect(charts).toEqual({ expect(charts).toEqual({
bitnami: { bitnami: {

View File

@ -0,0 +1,31 @@
/**
* 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 execFileInjectable from "../../../common/fs/exec-file.injectable";
import helmBinaryPathInjectable from "../helm-binary-path.injectable";
import type { AsyncResult } from "../../../common/utils/async-result";
import { getErrorMessage } from "../../../common/utils/get-error-message";
const execHelmInjectable = getInjectable({
id: "exec-helm",
instantiate: (di) => {
const execFile = di.inject(execFileInjectable);
const helmBinaryPath = di.inject(helmBinaryPathInjectable);
return async (...args: string[]): Promise<AsyncResult<string>> => {
try {
const response = await execFile(helmBinaryPath, args);
return { callWasSuccessful: true, response };
} catch (error) {
return { callWasSuccessful: false, error: getErrorMessage(error) };
}
};
},
});
export default execHelmInjectable;

View File

@ -0,0 +1,43 @@
/**
* 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 execHelmInjectable from "../exec-helm/exec-helm.injectable";
import type { AsyncResult } from "../../../common/utils/async-result";
export type HelmEnv = Record<string, string> & {
HELM_REPOSITORY_CACHE?: string;
HELM_REPOSITORY_CONFIG?: string;
};
const getHelmEnvInjectable = getInjectable({
id: "get-helm-env",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
return async (): Promise<AsyncResult<HelmEnv>> => {
const result = await execHelm("env");
if (!result.callWasSuccessful) {
return { callWasSuccessful: false, error: result.error };
}
const lines = result.response.split(/\r?\n/); // split by new line feed
const env: HelmEnv = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return { callWasSuccessful: true, response: env };
};
},
});
export default getHelmEnvInjectable;

View File

@ -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 { getBinaryName } from "../../common/vars";
import getAbsolutePathInjectable from "../../common/path/get-absolute-path.injectable";
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
const helmBinaryPathInjectable = getInjectable({
id: "helm-binary-path",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const normalizedPlatform = di.inject(normalizedPlatformInjectable);
const baseBundledBinariesDirectory = di.inject(baseBundledBinariesDirectoryInjectable);
const helmBinaryName = getBinaryName("helm", { forPlatform: normalizedPlatform });
return getAbsolutePath(baseBundledBinariesDirectory, helmBinaryName);
},
});
export default helmBinaryPathInjectable;

View File

@ -5,13 +5,13 @@
import fs from "fs"; import fs from "fs";
import * as yaml from "js-yaml"; import * as yaml from "js-yaml";
import type { HelmRepo } from "./helm-repo-manager";
import logger from "../logger"; import logger from "../logger";
import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api"; import type { RepoHelmChartList } from "../../common/k8s-api/endpoints/helm-charts.api";
import { iter, put, sortCharts } from "../../common/utils"; import { iter, put, sortCharts } from "../../common/utils";
import { execHelm } from "./exec"; import { execHelm } from "./exec";
import type { SetRequired } from "type-fest"; import type { SetRequired } from "type-fest";
import { assert } from "console"; import { assert } from "console";
import type { HelmRepo } from "../../common/helm/helm-repo";
interface ChartCacheEntry { interface ChartCacheEntry {
data: string; // serialized JSON data: string; // serialized JSON

View File

@ -1,172 +0,0 @@
/**
* 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 yaml from "js-yaml";
import { readFile } from "fs-extra";
import { customRequestPromise } from "../../common/request";
import orderBy from "lodash/orderBy";
import logger from "../logger";
import { execHelm } from "./exec";
import type { HelmEnv, HelmRepo, HelmRepoConfig } from "./helm-repo-manager";
interface EnsuredHelmRepoManagerData {
helmEnv: HelmEnv;
didUpdateOnce: boolean;
}
export class HelmRepoManager {
protected helmEnv?: HelmEnv;
protected didUpdateOnce?: boolean;
public async loadAvailableRepos(): Promise<HelmRepo[]> {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
return orderBy(res.body as HelmRepo[], repo => repo.name);
}
private async ensureInitialized(): Promise<EnsuredHelmRepoManagerData> {
this.helmEnv ??= await this.parseHelmEnv();
const repos = await this.list(this.helmEnv);
if (repos.length === 0) {
await this.addRepo({
name: "bitnami",
url: "https://charts.bitnami.com/bitnami",
});
}
if (!this.didUpdateOnce) {
await this.update();
this.didUpdateOnce = true;
}
return {
didUpdateOnce: this.didUpdateOnce,
helmEnv: this.helmEnv,
};
}
protected async parseHelmEnv() {
const output = await execHelm(["env"]);
const lines = output.split(/\r?\n/); // split by new line feed
const env: Partial<Record<string, string>> = {};
lines.forEach((line: string) => {
const [key, value] = line.split("=");
if (key && value) {
env[key] = value.replace(/"/g, ""); // strip quotas
}
});
return env as HelmEnv;
}
public async repo(name: string): Promise<HelmRepo | undefined> {
const repos = await this.repositories();
return repos.find(repo => repo.name === name);
}
private async list(helmEnv: HelmEnv): Promise<HelmRepo[]> {
try {
const rawConfig = await readFile(helmEnv.HELM_REPOSITORY_CONFIG, "utf8");
const parsedConfig = yaml.load(rawConfig) as HelmRepoConfig;
if (typeof parsedConfig === "object" && parsedConfig) {
return parsedConfig.repositories;
}
} catch {
// ignore error
}
return [];
}
public async repositories(): Promise<HelmRepo[]> {
try {
const { helmEnv } = await this.ensureInitialized();
const repos = await this.list(helmEnv);
return repos.map(repo => ({
...repo,
cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml`,
}));
} catch (error) {
logger.error(`[HELM]: repositories listing error`, error);
return [];
}
}
public async update() {
return execHelm([
"repo",
"update",
]);
}
public async addRepo({ name, url, insecureSkipTlsVerify, username, password, caFile, keyFile, certFile }: HelmRepo) {
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = [
"repo",
"add",
name,
url,
];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return execHelm(args);
}
public async removeRepo({ name, url }: HelmRepo): Promise<string> {
logger.info(`[HELM]: removing repo ${name} (${url})`);
return execHelm([
"repo",
"remove",
name,
]);
}
}
const helmRepoManagerInjectable = getInjectable({
id: "helm-repo-manager",
instantiate: () => new HelmRepoManager(),
});
export default helmRepoManagerInjectable;

View File

@ -1,33 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import {
asLegacyGlobalSingletonForExtensionApi,
} from "../../extensions/as-legacy-globals-for-extension-api/as-legacy-global-singleton-object-for-extension-api";
import helmRepoManagerInjectable from "./helm-repo-manager.injectable";
export type HelmEnv = Partial<Record<string, string>> & {
HELM_REPOSITORY_CACHE: string;
HELM_REPOSITORY_CONFIG: string;
};
export interface HelmRepoConfig {
repositories: HelmRepo[];
}
export interface HelmRepo {
name: string;
url: string;
cacheFilePath?: string;
caFile?: string;
certFile?: string;
insecureSkipTlsVerify?: boolean;
keyFile?: string;
username?: string;
password?: string;
}
export const HelmRepoManager = asLegacyGlobalSingletonForExtensionApi(helmRepoManagerInjectable);

View File

@ -1,134 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger";
import { HelmRepoManager } from "./helm-repo-manager";
import { HelmChartManager } from "./helm-chart-manager";
import { deleteRelease, getHistory, getRelease, getValues, installChart, listReleases, rollback, upgradeRelease } from "./helm-release-manager";
import type { JsonObject } from "type-fest";
import { object } from "../../common/utils";
interface GetReleaseValuesArgs {
cluster: Cluster;
namespace: string;
all: boolean;
}
export interface InstallChartArgs {
chart: string;
values: JsonObject;
name: string;
namespace: string;
version: string;
}
export interface UpdateChartArgs {
chart: string;
values: JsonObject;
version: string;
}
class HelmService {
public async installChart(cluster: Cluster, data: InstallChartArgs) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
return installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
}
public async listCharts() {
const repositories = await HelmRepoManager.getInstance().repositories();
return object.fromEntries(
await Promise.all(repositories.map(async repo => [repo.name, await HelmChartManager.forRepo(repo).charts()] as const)),
);
}
public async getChart(repoName: string, chartName: string, version = "") {
const repo = await HelmRepoManager.getInstance().repo(repoName);
if (!repo) {
return undefined;
}
const chartManager = HelmChartManager.forRepo(repo);
return {
readme: await chartManager.getReadme(chartName, version),
versions: await chartManager.chartVersions(chartName),
};
}
public async getChartValues(repoName: string, chartName: string, version = "") {
const repo = await HelmRepoManager.getInstance().repo(repoName);
if (!repo) {
return undefined;
}
return HelmChartManager.forRepo(repo).getValues(chartName, version);
}
public async listReleases(cluster: Cluster, namespace?: string) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("list releases");
return listReleases(proxyKubeconfig, namespace);
}
public async getRelease(cluster: Cluster, releaseName: string, namespace: string) {
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Fetch release");
return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
}
public async getReleaseValues(releaseName: string, { cluster, namespace, all }: GetReleaseValuesArgs) {
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release values");
return getValues(releaseName, { namespace, all, kubeconfigPath: pathToKubeconfig });
}
public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release history");
return getHistory(releaseName, namespace, proxyKubeconfig);
}
public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Delete release");
return deleteRelease(releaseName, namespace, proxyKubeconfig);
}
public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Upgrade release");
return upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, proxyKubeconfig, kubectlPath);
}
public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Rollback release");
await rollback(releaseName, namespace, revision, proxyKubeconfig);
}
}
export const helmService = new HelmService();

View File

@ -0,0 +1,28 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import { deleteRelease } from "../helm-release-manager";
import loggerInjectable from "../../../common/logger.injectable";
const deleteHelmReleaseInjectable = getInjectable({
id: "delete-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Delete release");
return deleteRelease(releaseName, namespace, proxyKubeconfig);
};
},
causesSideEffects: true,
});
export default deleteHelmReleaseInjectable;

View File

@ -0,0 +1,29 @@
/**
* 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 { HelmChartManager } from "../helm-chart-manager";
import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
const getHelmChartValuesInjectable = getInjectable({
id: "get-helm-chart-values",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
return async (repoName: string, chartName: string, version = "") => {
const repo = await getActiveHelmRepository(repoName);
if (!repo) {
return undefined;
}
return HelmChartManager.forRepo(repo).getValues(chartName, version);
};
},
causesSideEffects: true,
});
export default getHelmChartValuesInjectable;

View File

@ -0,0 +1,34 @@
/**
* 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 { HelmChartManager } from "../helm-chart-manager";
import getActiveHelmRepositoryInjectable from "../repositories/get-active-helm-repository.injectable";
const getHelmChartInjectable = getInjectable({
id: "get-helm-chart",
instantiate: (di) => {
const getActiveHelmRepository = di.inject(getActiveHelmRepositoryInjectable);
return async (repoName: string, chartName: string, version = "") => {
const repo = await getActiveHelmRepository(repoName);
if (!repo) {
return undefined;
}
const chartManager = HelmChartManager.forRepo(repo);
return {
readme: await chartManager.getReadme(chartName, version),
versions: await chartManager.chartVersions(chartName),
};
};
},
causesSideEffects: true,
});
export default getHelmChartInjectable;

View File

@ -0,0 +1,28 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import { getHistory } from "../helm-release-manager";
import loggerInjectable from "../../../common/logger.injectable";
const getHelmReleaseHistoryInjectable = getInjectable({
id: "get-helm-release-history",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release history");
return getHistory(releaseName, namespace, proxyKubeconfig);
};
},
causesSideEffects: true,
});
export default getHelmReleaseHistoryInjectable;

View File

@ -0,0 +1,41 @@
/**
* 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 { getValues } from "../helm-release-manager";
import loggerInjectable from "../../../common/logger.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
interface GetReleaseValuesArgs {
cluster: Cluster;
namespace: string;
all: boolean;
}
const getHelmReleaseValuesInjectable = getInjectable({
id: "get-helm-release-values",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (
releaseName: string,
{ cluster, namespace, all }: GetReleaseValuesArgs,
) => {
const pathToKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Fetch release values");
return getValues(releaseName, {
namespace,
all,
kubeconfigPath: pathToKubeconfig,
});
};
},
causesSideEffects: true,
});
export default getHelmReleaseValuesInjectable;

View File

@ -0,0 +1,30 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import { getRelease } from "../helm-release-manager";
import loggerInjectable from "../../../common/logger.injectable";
const getHelmReleaseInjectable = getInjectable({
id: "get-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string) => {
const kubeconfigPath = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Fetch release");
return getRelease(releaseName, namespace, kubeconfigPath, kubectlPath);
};
},
causesSideEffects: true,
});
export default getHelmReleaseInjectable;

View File

@ -0,0 +1,30 @@
/**
* 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 { JsonObject } from "type-fest";
import type { Cluster } from "../../../common/cluster/cluster";
import { installChart } from "../helm-release-manager";
export interface InstallChartArgs {
chart: string;
values: JsonObject;
name: string;
namespace: string;
version: string;
}
const installHelmChartInjectable = getInjectable({
id: "install-helm-chart",
instantiate: () => async (cluster: Cluster, data: InstallChartArgs) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
return installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig);
},
causesSideEffects: true,
});
export default installHelmChartInjectable;

View File

@ -0,0 +1,41 @@
/**
* 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 assert from "assert";
import { object } from "../../../common/utils";
import { HelmChartManager } from "../helm-chart-manager";
import getActiveHelmRepositoriesInjectable from "../repositories/get-active-helm-repositories/get-active-helm-repositories.injectable";
const listHelmChartsInjectable = getInjectable({
id: "list-helm-charts",
instantiate: (di) => {
const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable);
return async () => {
const result = await getActiveHelmRepositories();
assert(result.callWasSuccessful);
const repositories = result.response;
return object.fromEntries(
await Promise.all(
repositories.map(
async (repo) =>
[
repo.name,
await HelmChartManager.forRepo(repo).charts(),
] as const,
),
),
);
};
},
causesSideEffects: true,
});
export default listHelmChartsInjectable;

View File

@ -0,0 +1,28 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import { listReleases } from "../helm-release-manager";
const listHelmReleasesInjectable = getInjectable({
id: "list-helm-releases",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (cluster: Cluster, namespace?: string) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("list releases");
return listReleases(proxyKubeconfig, namespace);
};
},
causesSideEffects: true,
});
export default listHelmReleasesInjectable;

View File

@ -0,0 +1,32 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import loggerInjectable from "../../../common/logger.injectable";
import { rollback } from "../helm-release-manager";
const rollbackHelmReleaseInjectable = getInjectable({
id: "rollback-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (
cluster: Cluster,
releaseName: string,
namespace: string,
revision: number,
) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
logger.debug("Rollback release");
await rollback(releaseName, namespace, revision, proxyKubeconfig);
};
},
causesSideEffects: true,
});
export default rollbackHelmReleaseInjectable;

View File

@ -0,0 +1,45 @@
/**
* 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 { Cluster } from "../../../common/cluster/cluster";
import { upgradeRelease } from "../helm-release-manager";
import loggerInjectable from "../../../common/logger.injectable";
import type { JsonObject } from "type-fest";
export interface UpdateChartArgs {
chart: string;
values: JsonObject;
version: string;
}
const updateHelmReleaseInjectable = getInjectable({
id: "update-helm-release",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return async (cluster: Cluster, releaseName: string, namespace: string, data: UpdateChartArgs) => {
const proxyKubeconfig = await cluster.getProxyKubeconfigPath();
const kubectl = await cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
logger.debug("Upgrade release");
return upgradeRelease(
releaseName,
data.chart,
data.values,
namespace,
data.version,
proxyKubeconfig,
kubectlPath,
);
};
},
causesSideEffects: true,
});
export default updateHelmReleaseInjectable;

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 addHelmRepositoryChannelInjectable from "../../../../common/helm/add-helm-repository-channel.injectable";
import addHelmRepositoryInjectable from "./add-helm-repository.injectable";
import { requestChannelListenerInjectionToken } from "../../../../common/utils/channel/request-channel-listener-injection-token";
const addHelmRepositoryChannelListenerInjectable = getInjectable({
id: "add-helm-repository-channel-listener",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const channel = di.inject(addHelmRepositoryChannelInjectable);
return {
channel,
handler: addHelmRepository,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default addHelmRepositoryChannelListenerInjectable;

View File

@ -0,0 +1,62 @@
/**
* 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 execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import type { HelmRepo } from "../../../../common/helm/helm-repo";
import loggerInjectable from "../../../../common/logger.injectable";
const addHelmRepositoryInjectable = getInjectable({
id: "add-helm-repository",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
const logger = di.inject(loggerInjectable);
return async (repo: HelmRepo) => {
const {
name,
url,
insecureSkipTlsVerify,
username,
password,
caFile,
keyFile,
certFile,
} = repo;
logger.info(`[HELM]: adding repo ${name} from ${url}`);
const args = ["repo", "add", name, url];
if (insecureSkipTlsVerify) {
args.push("--insecure-skip-tls-verify");
}
if (username) {
args.push("--username", username);
}
if (password) {
args.push("--password", password);
}
if (caFile) {
args.push("--ca-file", caFile);
}
if (keyFile) {
args.push("--key-file", keyFile);
}
if (certFile) {
args.push("--cert-file", certFile);
}
return await execHelm(...args);
};
},
});
export default addHelmRepositoryInjectable;

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,131 @@
/**
* 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/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 loggerInjectable from "../../../../common/logger.injectable";
import type { AsyncResult } from "../../../../common/utils/async-result";
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 getRepositories = getRepositoriesFor(readYamlFile);
return async (): Promise<AsyncResult<HelmRepo[]>> => {
const envResult = await getHelmEnv();
if (!envResult.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Error getting Helm configuration: ${envResult.error}`,
};
}
const {
HELM_REPOSITORY_CONFIG: repositoryConfigFilePath,
HELM_REPOSITORY_CACHE: helmRepositoryCacheDirPath,
} = envResult.response;
if (!repositoryConfigFilePath) {
const errorMessage = "Tried to get Helm repositories, but HELM_REPOSITORY_CONFIG was not present in `$ helm env`.";
logger.error(errorMessage);
return {
callWasSuccessful: false,
error: `Error getting Helm configuration: ${errorMessage}`,
};
}
if (!helmRepositoryCacheDirPath) {
const errorMessage = "Tried to get Helm repositories, but HELM_REPOSITORY_CACHE was not present in `$ helm env`.";
logger.error(errorMessage);
return {
callWasSuccessful: false,
error: `Error getting Helm configuration: ${errorMessage}`,
};
}
const updateResult = await execHelm("repo", "update");
if (!updateResult.callWasSuccessful) {
if (!updateResult.error.includes(internalHelmErrorForNoRepositoriesFound)) {
return {
callWasSuccessful: false,
error: `Error updating Helm repositories: ${updateResult.error}`,
};
}
const resultOfAddingDefaultRepository = await execHelm("repo", "add", "bitnami", "https://charts.bitnami.com/bitnami");
if (!resultOfAddingDefaultRepository.callWasSuccessful) {
return {
callWasSuccessful: false,
error: `Error when adding default Helm repository: ${resultOfAddingDefaultRepository.error}`,
};
}
}
return {
callWasSuccessful: true,
response: await getRepositories(
repositoryConfigFilePath,
helmRepositoryCacheDirPath,
),
};
};
},
});
export default getActiveHelmRepositoriesInjectable;
const getRepositoriesFor =
(readYamlFile: ReadYamlFile) =>
async (repositoryConfigFilePath: string, helmRepositoryCacheDirPath: string): Promise<HelmRepo[]> => {
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`,
}));
};
const internalHelmErrorForNoRepositoriesFound = "no repositories found. You must add one before updating";

View File

@ -0,0 +1,27 @@
/**
* 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 assert from "assert";
import getActiveHelmRepositoriesInjectable from "./get-active-helm-repositories/get-active-helm-repositories.injectable";
const getActiveHelmRepositoryInjectable = getInjectable({
id: "get-active-helm-repository",
instantiate: (di) => {
const getActiveHelmRepositories = di.inject(getActiveHelmRepositoriesInjectable);
return async (name: string) => {
const activeHelmRepositories = await getActiveHelmRepositories();
assert(activeHelmRepositories.callWasSuccessful);
return activeHelmRepositories.response.find(
(repository) => repository.name === name,
);
};
},
});
export default getActiveHelmRepositoryInjectable;

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 removeHelmRepositoryInjectable from "./remove-helm-repository.injectable";
import removeHelmRepositoryChannelInjectable from "../../../../common/helm/remove-helm-repository-channel.injectable";
const removeHelmRepositoryChannelListenerInjectable = getInjectable({
id: "remove-helm-repository-channel-listener",
instantiate: (di) => {
const removeHelmRepository = di.inject(removeHelmRepositoryInjectable);
const channel = di.inject(removeHelmRepositoryChannelInjectable);
return {
channel,
handler: removeHelmRepository,
};
},
injectionToken: requestChannelListenerInjectionToken,
});
export default removeHelmRepositoryChannelListenerInjectable;

View File

@ -0,0 +1,29 @@
/**
* 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 execHelmInjectable from "../../exec-helm/exec-helm.injectable";
import type { HelmRepo } from "../../../../common/helm/helm-repo";
import loggerInjectable from "../../../../common/logger.injectable";
const removeHelmRepositoryInjectable = getInjectable({
id: "remove-helm-repository",
instantiate: (di) => {
const execHelm = di.inject(execHelmInjectable);
const logger = di.inject(loggerInjectable);
return async (repo: HelmRepo) => {
logger.info(`[HELM]: removing repo ${repo.name} (${repo.url})`);
return execHelm(
"repo",
"remove",
repo.name,
);
};
},
});
export default removeHelmRepositoryInjectable;

View File

@ -12,7 +12,7 @@ import { getBinaryName } from "../../common/vars";
import spawnInjectable from "../child-process/spawn.injectable"; import spawnInjectable from "../child-process/spawn.injectable";
import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate"; import { getKubeAuthProxyCertificate } from "./get-kube-auth-proxy-certificate";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import baseBundeledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy; export type CreateKubeAuthProxy = (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => KubeAuthProxy;
@ -25,7 +25,7 @@ const createKubeAuthProxyInjectable = getInjectable({
return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => { return (cluster: Cluster, environmentVariables: NodeJS.ProcessEnv) => {
const clusterUrl = new URL(cluster.apiUrl); const clusterUrl = new URL(cluster.apiUrl);
const dependencies: KubeAuthProxyDependencies = { const dependencies: KubeAuthProxyDependencies = {
proxyBinPath: path.join(di.inject(baseBundeledBinariesDirectoryInjectable), binaryName), proxyBinPath: path.join(di.inject(baseBundledBinariesDirectoryInjectable), binaryName),
proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate), proxyCert: getKubeAuthProxyCertificate(clusterUrl.hostname, selfsigned.generate),
spawn: di.inject(spawnInjectable), spawn: di.inject(spawnInjectable),
logger: di.inject(loggerInjectable), logger: di.inject(loggerInjectable),

View File

@ -4,13 +4,13 @@
*/ */
import { getInjectable } from "@ogre-tools/injectable"; import { getInjectable } from "@ogre-tools/injectable";
import path from "path"; import path from "path";
import baseBundeledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
import kubectlBinaryNameInjectable from "./binary-name.injectable"; import kubectlBinaryNameInjectable from "./binary-name.injectable";
const bundledKubectlBinaryPathInjectable = getInjectable({ const bundledKubectlBinaryPathInjectable = getInjectable({
id: "bundled-kubectl-binary-path", id: "bundled-kubectl-binary-path",
instantiate: (di) => path.join( instantiate: (di) => path.join(
di.inject(baseBundeledBinariesDirectoryInjectable), di.inject(baseBundledBinariesDirectoryInjectable),
di.inject(kubectlBinaryNameInjectable), di.inject(kubectlBinaryNameInjectable),
), ),
}); });

View File

@ -11,7 +11,7 @@ import kubectlDownloadingNormalizedArchInjectable from "./normalized-arch.inject
import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable"; import normalizedPlatformInjectable from "../../common/vars/normalized-platform.injectable";
import kubectlBinaryNameInjectable from "./binary-name.injectable"; import kubectlBinaryNameInjectable from "./binary-name.injectable";
import bundledKubectlBinaryPathInjectable from "./bundled-binary-path.injectable"; import bundledKubectlBinaryPathInjectable from "./bundled-binary-path.injectable";
import baseBundeledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable"; import baseBundledBinariesDirectoryInjectable from "../../common/vars/base-bundled-binaries-dir.injectable";
const createKubectlInjectable = getInjectable({ const createKubectlInjectable = getInjectable({
id: "create-kubectl", id: "create-kubectl",
@ -24,7 +24,7 @@ const createKubectlInjectable = getInjectable({
normalizedDownloadPlatform: di.inject(normalizedPlatformInjectable), normalizedDownloadPlatform: di.inject(normalizedPlatformInjectable),
kubectlBinaryName: di.inject(kubectlBinaryNameInjectable), kubectlBinaryName: di.inject(kubectlBinaryNameInjectable),
bundledKubectlBinaryPath: di.inject(bundledKubectlBinaryPathInjectable), bundledKubectlBinaryPath: di.inject(bundledKubectlBinaryPathInjectable),
baseBundeledBinariesDirectory: di.inject(baseBundeledBinariesDirectoryInjectable), baseBundeledBinariesDirectory: di.inject(baseBundledBinariesDirectoryInjectable),
}; };
return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion); return (clusterVersion: string) => new Kubectl(dependencies, clusterVersion);

View File

@ -5,25 +5,29 @@
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route"; import { route } from "../../../router/route";
import { helmService } from "../../../helm/helm-service"; import getHelmChartInjectable from "../../../helm/helm-service/get-helm-chart.injectable";
const getChartRouteInjectable = getRouteInjectable({ const getChartRouteInjectable = getRouteInjectable({
id: "get-chart-route", id: "get-chart-route",
instantiate: () => route({ instantiate: (di) => {
const getHelmChart = di.inject(getHelmChartInjectable);
return route({
method: "get", method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}`, path: `${apiPrefix}/v2/charts/{repo}/{chart}`,
})(async ({ params, query }) => { })(async ({ params, query }) => {
const { repo, chart } = params; const { repo, chart } = params;
return { return {
response: await helmService.getChart( response: await getHelmChart(
repo, repo,
chart, chart,
query.get("version") ?? undefined, query.get("version") ?? undefined,
), ),
}; };
}), });
},
}); });
export default getChartRouteInjectable; export default getChartRouteInjectable;

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { helmService } from "../../../helm/helm-service";
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route"; import { route } from "../../../router/route";
import getHelmChartValuesInjectable from "../../../helm/helm-service/get-helm-chart-values.injectable";
const getChartRouteValuesInjectable = getRouteInjectable({ const getChartRouteValuesInjectable = getRouteInjectable({
id: "get-chart-route-values", id: "get-chart-route-values",
instantiate: () => route({ instantiate: (di) => {
const getHelmChartValues = di.inject(getHelmChartValuesInjectable);
return route({
method: "get", method: "get",
path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`, path: `${apiPrefix}/v2/charts/{repo}/{chart}/values`,
})(async ({ params, query }) => ({ })(async ({ params, query }) => ({
response: await helmService.getChartValues( response: await getHelmChartValues(
params.repo, params.repo,
params.chart, params.chart,
query.get("version") ?? undefined, query.get("version") ?? undefined,
), ),
})), }));
},
}); });
export default getChartRouteValuesInjectable; export default getChartRouteValuesInjectable;

View File

@ -3,19 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { helmService } from "../../../helm/helm-service";
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { route } from "../../../router/route"; import { route } from "../../../router/route";
import listHelmChartsInjectable from "../../../helm/helm-service/list-helm-charts.injectable";
const listChartsRouteInjectable = getRouteInjectable({ const listChartsRouteInjectable = getRouteInjectable({
id: "list-charts-route", id: "list-charts-route",
instantiate: () => route({ instantiate: (di) => {
const listHelmCharts = di.inject(listHelmChartsInjectable);
return route({
method: "get", method: "get",
path: `${apiPrefix}/v2/charts`, path: `${apiPrefix}/v2/charts`,
})(async () => ({ })(async () => ({
response: await helmService.listCharts(), response: await listHelmCharts(),
})), }));
},
}); });
export default listChartsRouteInjectable; export default listChartsRouteInjectable;

View File

@ -3,23 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route"; import { clusterRoute } from "../../../router/route";
import deleteHelmReleaseInjectable from "../../../helm/helm-service/delete-helm-release.injectable";
const deleteReleaseRouteInjectable = getRouteInjectable({ const deleteReleaseRouteInjectable = getRouteInjectable({
id: "delete-release-route", id: "delete-release-route",
instantiate: () => clusterRoute({ instantiate: (di) => {
const deleteHelmRelease = di.inject(deleteHelmReleaseInjectable);
return clusterRoute({
method: "delete", method: "delete",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`, path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
})(async ({ cluster, params: { release, namespace }}) => ({ })(async ({ cluster, params: { release, namespace }}) => ({
response: await helmService.deleteRelease( response: await deleteHelmRelease(cluster, release, namespace),
cluster, }));
release, },
namespace,
),
})),
}); });
export default deleteReleaseRouteInjectable; export default deleteReleaseRouteInjectable;

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route"; import { clusterRoute } from "../../../router/route";
import getHelmReleaseHistoryInjectable from "../../../helm/helm-service/get-helm-release-history.injectable";
const getReleaseRouteHistoryInjectable = getRouteInjectable({ const getReleaseRouteHistoryInjectable = getRouteInjectable({
id: "get-release-history-route", id: "get-release-history-route",
instantiate: () => clusterRoute({ instantiate: (di) => {
const getHelmReleaseHistory = di.inject(getHelmReleaseHistoryInjectable);
return clusterRoute({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}/history`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/history`,
})(async ({ cluster, params }) => ({ })(async ({ cluster, params }) => ({
response: await helmService.getReleaseHistory( response: await getHelmReleaseHistory(
cluster, cluster,
params.release, params.release,
params.namespace, params.namespace,
), ),
})), }));
},
}); });
export default getReleaseRouteHistoryInjectable; export default getReleaseRouteHistoryInjectable;

View File

@ -3,23 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route"; import { clusterRoute } from "../../../router/route";
import getHelmReleaseInjectable from "../../../helm/helm-service/get-helm-release.injectable";
const getReleaseRouteInjectable = getRouteInjectable({ const getReleaseRouteInjectable = getRouteInjectable({
id: "get-release-route", id: "get-release-route",
instantiate: () => clusterRoute({ instantiate: (di) => {
const getHelmRelease = di.inject(getHelmReleaseInjectable);
return clusterRoute({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`, path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
})(async ({ cluster, params }) => ({ })(async ({ cluster, params }) => ({
response: await helmService.getRelease( response: await getHelmRelease(
cluster, cluster,
params.release, params.release,
params.namespace, params.namespace,
), ),
})), }));
},
}); });
export default getReleaseRouteInjectable; export default getReleaseRouteInjectable;

View File

@ -3,27 +3,31 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { getBoolean } from "../../../utils/parse-query"; import { getBoolean } from "../../../utils/parse-query";
import { contentTypes } from "../../../router/router-content-types"; import { contentTypes } from "../../../router/router-content-types";
import { clusterRoute } from "../../../router/route"; import { clusterRoute } from "../../../router/route";
import getHelmReleaseValuesInjectable from "../../../helm/helm-service/get-helm-release-values.injectable";
const getReleaseRouteValuesInjectable = getRouteInjectable({ const getReleaseRouteValuesInjectable = getRouteInjectable({
id: "get-release-values-route", id: "get-release-values-route",
instantiate: () => clusterRoute({ instantiate: (di) => {
const getHelmReleaseValues = di.inject(getHelmReleaseValuesInjectable);
return clusterRoute({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace}/{release}/values`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/values`,
})(async ({ cluster, params: { namespace, release }, query }) => ({ })(async ({ cluster, params: { namespace, release }, query }) => ({
response: await helmService.getReleaseValues(release, { response: await getHelmReleaseValues(release, {
cluster, cluster,
namespace, namespace,
all: getBoolean(query, "all"), all: getBoolean(query, "all"),
}), }),
contentType: contentTypes.txt, contentType: contentTypes.txt,
})), }));
},
}); });
export default getReleaseRouteValuesInjectable; export default getReleaseRouteValuesInjectable;

View File

@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { InstallChartArgs } from "../../../helm/helm-service";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import Joi from "joi"; import Joi from "joi";
import { payloadValidatedClusterRoute } from "../../../router/route"; import { payloadValidatedClusterRoute } from "../../../router/route";
import type { InstallChartArgs } from "../../../helm/helm-service/install-helm-chart.injectable";
import installHelmChartInjectable from "../../../helm/helm-service/install-helm-chart.injectable";
const installChartArgsValidator = Joi.object<InstallChartArgs, true, InstallChartArgs>({ const installChartArgsValidator = Joi.object<InstallChartArgs, true, InstallChartArgs>({
chart: Joi chart: Joi
@ -31,14 +31,18 @@ const installChartArgsValidator = Joi.object<InstallChartArgs, true, InstallChar
const installChartRouteInjectable = getRouteInjectable({ const installChartRouteInjectable = getRouteInjectable({
id: "install-chart-route", id: "install-chart-route",
instantiate: () => payloadValidatedClusterRoute({ instantiate: (di) => {
const installHelmChart = di.inject(installHelmChartInjectable);
return payloadValidatedClusterRoute({
method: "post", method: "post",
path: `${apiPrefix}/v2/releases`, path: `${apiPrefix}/v2/releases`,
payloadValidator: installChartArgsValidator, payloadValidator: installChartArgsValidator,
})(async ({ payload, cluster }) => ({ })(async ({ payload, cluster }) => ({
response: await helmService.installChart(cluster, payload), response: await installHelmChart(cluster, payload),
statusCode: 201, statusCode: 201,
})), }));
},
}); });
export default installChartRouteInjectable; export default installChartRouteInjectable;

View File

@ -3,19 +3,23 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { clusterRoute } from "../../../router/route"; import { clusterRoute } from "../../../router/route";
import listHelmReleasesInjectable from "../../../helm/helm-service/list-helm-releases.injectable";
const listReleasesRouteInjectable = getRouteInjectable({ const listReleasesRouteInjectable = getRouteInjectable({
id: "list-releases-route", id: "list-releases-route",
instantiate: () => clusterRoute({ instantiate: (di) => {
const listHelmReleases = di.inject(listHelmReleasesInjectable);
return clusterRoute({
method: "get", method: "get",
path: `${apiPrefix}/v2/releases/{namespace?}`, path: `${apiPrefix}/v2/releases/{namespace?}`,
})(async ({ cluster, params }) => ({ })(async ({ cluster, params }) => ({
response: await helmService.listReleases(cluster, params.namespace), response: await listHelmReleases(cluster, params.namespace),
})), }));
},
}); });
export default listReleasesRouteInjectable; export default listReleasesRouteInjectable;

View File

@ -3,10 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import Joi from "joi"; import Joi from "joi";
import { payloadValidatedClusterRoute } from "../../../router/route"; import { payloadValidatedClusterRoute } from "../../../router/route";
import rollbackHelmReleaseInjectable from "../../../helm/helm-service/rollback-helm-release.injectable";
interface RollbackReleasePayload { interface RollbackReleasePayload {
revision: number; revision: number;
@ -21,13 +21,17 @@ const rollbackReleasePayloadValidator = Joi.object<RollbackReleasePayload, true,
const rollbackReleaseRouteInjectable = getRouteInjectable({ const rollbackReleaseRouteInjectable = getRouteInjectable({
id: "rollback-release-route", id: "rollback-release-route",
instantiate: () => payloadValidatedClusterRoute({ instantiate: (di) => {
const rollbackRelease = di.inject(rollbackHelmReleaseInjectable);
return payloadValidatedClusterRoute({
method: "put", method: "put",
path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback`, path: `${apiPrefix}/v2/releases/{namespace}/{release}/rollback`,
payloadValidator: rollbackReleasePayloadValidator, payloadValidator: rollbackReleasePayloadValidator,
})(async ({ cluster, params: { release, namespace }, payload }) => { })(async ({ cluster, params: { release, namespace }, payload }) => {
await helmService.rollback(cluster, release, namespace, payload.revision); await rollbackRelease(cluster, release, namespace, payload.revision);
}), });
},
}); });
export default rollbackReleaseRouteInjectable; export default rollbackReleaseRouteInjectable;

View File

@ -3,11 +3,11 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { apiPrefix } from "../../../../common/vars"; import { apiPrefix } from "../../../../common/vars";
import type { UpdateChartArgs } from "../../../helm/helm-service";
import { helmService } from "../../../helm/helm-service";
import { getRouteInjectable } from "../../../router/router.injectable"; import { getRouteInjectable } from "../../../router/router.injectable";
import { payloadValidatedClusterRoute } from "../../../router/route"; import { payloadValidatedClusterRoute } from "../../../router/route";
import Joi from "joi"; import Joi from "joi";
import type { UpdateChartArgs } from "../../../helm/helm-service/update-helm-release.injectable";
import updateHelmReleaseInjectable from "../../../helm/helm-service/update-helm-release.injectable";
const updateChartArgsValidator = Joi.object<UpdateChartArgs, true, UpdateChartArgs>({ const updateChartArgsValidator = Joi.object<UpdateChartArgs, true, UpdateChartArgs>({
chart: Joi chart: Joi
@ -24,18 +24,22 @@ const updateChartArgsValidator = Joi.object<UpdateChartArgs, true, UpdateChartAr
const updateReleaseRouteInjectable = getRouteInjectable({ const updateReleaseRouteInjectable = getRouteInjectable({
id: "update-release-route", id: "update-release-route",
instantiate: () => payloadValidatedClusterRoute({ instantiate: (di) => {
const updateRelease = di.inject(updateHelmReleaseInjectable);
return payloadValidatedClusterRoute({
method: "put", method: "put",
path: `${apiPrefix}/v2/releases/{namespace}/{release}`, path: `${apiPrefix}/v2/releases/{namespace}/{release}`,
payloadValidator: updateChartArgsValidator, payloadValidator: updateChartArgsValidator,
})(async ({ cluster, params, payload }) => ({ })(async ({ cluster, params, payload }) => ({
response: await helmService.updateRelease( response: await updateRelease(
cluster, cluster,
params.release, params.release,
params.namespace, params.namespace,
payload, payload,
), ),
})), }));
},
}); });
export default updateReleaseRouteInjectable; export default updateReleaseRouteInjectable;

View File

@ -15,7 +15,6 @@ import * as LensExtensionsCommonApi from "../extensions/common-api";
import * as LensExtensionsRendererApi from "../extensions/renderer-api"; import * as LensExtensionsRendererApi from "../extensions/renderer-api";
import { delay } from "../common/utils"; import { delay } from "../common/utils";
import { isMac, isDevelopment } from "../common/vars"; import { isMac, isDevelopment } from "../common/vars";
import { HelmRepoManager } from "../main/helm/helm-repo-manager";
import { DefaultProps } from "./mui-base-theme"; import { DefaultProps } from "./mui-base-theme";
import configurePackages from "../common/configure-packages"; import configurePackages from "../common/configure-packages";
import * as initializers from "./initializers"; import * as initializers from "./initializers";
@ -151,8 +150,6 @@ export async function bootstrap(di: DiContainer) {
extensionInstallationStateStore.bindIpcListeners(); extensionInstallationStateStore.bindIpcListeners();
HelmRepoManager.createInstance(); // initialize the manager
// Register additional store listeners // Register additional store listeners
clusterStore.registerIpcListener(); clusterStore.registerIpcListener();

View File

@ -1,220 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import type { FileFilter } from "electron";
import { observable, makeObservable, action } from "mobx";
import { observer } from "mobx-react";
import type { DialogProps } from "../dialog";
import { Dialog } from "../dialog";
import { Wizard, WizardStep } from "../wizard";
import { Input } from "../input";
import { Checkbox } from "../checkbox";
import { Button } from "../button";
import { systemName, isUrl, isPath } from "../input/input_validators";
import { SubTitle } from "../layout/sub-title";
import { Icon } from "../icon";
import { Notifications } from "../notifications";
import { type HelmRepo, HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { requestOpenFilePickingDialog } from "../../ipc";
export interface AddHelmRepoDialogProps extends Partial<DialogProps> {
onAddRepo: () => void;
}
enum FileType {
CaFile = "caFile",
KeyFile = "keyFile",
CertFile = "certFile",
}
const dialogState = observable.object({
isOpen: false,
});
function getEmptyRepo(): HelmRepo {
return { name: "", url: "", username: "", password: "", insecureSkipTlsVerify: false, caFile: "", keyFile: "", certFile: "" };
}
@observer
export class AddHelmRepoDialog extends React.Component<AddHelmRepoDialogProps> {
private static keyExtensions = ["key", "keystore", "jks", "p12", "pfx", "pem"];
private static certExtensions = ["crt", "cer", "ca-bundle", "p7b", "p7c", "p7s", "p12", "pfx", "pem"];
constructor(props: AddHelmRepoDialogProps) {
super(props);
makeObservable(this);
}
static open() {
dialogState.isOpen = true;
}
static close() {
dialogState.isOpen = false;
}
@observable helmRepo = getEmptyRepo();
@observable showOptions = false;
@action
close = () => {
AddHelmRepoDialog.close();
this.helmRepo = getEmptyRepo();
this.showOptions = false;
};
setFilepath(type: FileType, value: string) {
this.helmRepo[type] = value;
}
getFilePath(type: FileType) {
return this.helmRepo[type];
}
async selectFileDialog(type: FileType, fileFilter: FileFilter) {
const { canceled, filePaths } = await requestOpenFilePickingDialog({
defaultPath: this.getFilePath(type),
properties: ["openFile", "showHiddenFiles"],
message: `Select file`,
buttonLabel: `Use file`,
filters: [
fileFilter,
{ name: "Any", extensions: ["*"] },
],
});
if (!canceled && filePaths.length) {
this.setFilepath(type, filePaths[0]);
}
}
async addCustomRepo() {
try {
await HelmRepoManager.getInstance().addRepo(this.helmRepo);
Notifications.ok((
<>
{"Helm repository "}
<b>{this.helmRepo.name}</b>
{" has been added."}
</>
));
this.props.onAddRepo();
this.close();
} catch (err) {
Notifications.error((
<>
{"Adding helm branch "}
<b>{this.helmRepo.name}</b>
{" has failed: "}
{String(err)}
</>
));
}
}
renderFileInput(placeholder:string, fileType:FileType, fileExtensions:string[]){
return (
<div className="flex gaps align-center">
<Input
placeholder={placeholder}
validators={isPath}
className="box grow"
value={this.getFilePath(fileType)}
onChange={v => this.setFilepath(fileType, v)}
/>
<Icon
material="folder"
onClick={() => this.selectFileDialog(fileType, { name: placeholder, extensions: fileExtensions })}
tooltip="Browse"
/>
</div>
);
}
renderOptions() {
return (
<>
<SubTitle title="Security settings" />
<Checkbox
label="Skip TLS certificate checks for the repository"
value={this.helmRepo.insecureSkipTlsVerify}
onChange={v => this.helmRepo.insecureSkipTlsVerify = v}
/>
{this.renderFileInput("Key file", FileType.KeyFile, AddHelmRepoDialog.keyExtensions)}
{this.renderFileInput("Ca file", FileType.CaFile, AddHelmRepoDialog.certExtensions)}
{this.renderFileInput("Certificate file", FileType.CertFile, AddHelmRepoDialog.certExtensions)}
<SubTitle title="Chart Repository Credentials" />
<Input
placeholder="Username"
value={this.helmRepo.username}
onChange= {v => this.helmRepo.username = v}
/>
<Input
type="password"
placeholder="Password"
value={this.helmRepo.password}
onChange={v => this.helmRepo.password = v}
/>
</>
);
}
render() {
const { ...dialogProps } = this.props;
const header = <h5>Add custom Helm Repo</h5>;
return (
<Dialog
{...dialogProps}
className="AddHelmRepoDialog"
isOpen={dialogState.isOpen}
close={this.close}
>
<Wizard header={header} done={this.close}>
<WizardStep
contentClass="flow column"
nextLabel="Add"
next={() => this.addCustomRepo()}
>
<div className="flex column gaps">
<Input
autoFocus
required
placeholder="Helm repo name"
trim
validators={systemName}
value={this.helmRepo.name}
onChange={v => this.helmRepo.name = v}
/>
<Input
required
placeholder="URL"
validators={isUrl}
value={this.helmRepo.url}
onChange={v => this.helmRepo.url = v}
/>
<Button
plain
className="accordion"
onClick={() => this.showOptions = !this.showOptions}
>
More
<Icon
small
tooltip="More"
material={this.showOptions ? "remove" : "add"}
/>
</Button>
{this.showOptions && this.renderOptions()}
</div>
</WizardStep>
</Wizard>
</Dialog>
);
}
}

View File

@ -1,202 +0,0 @@
/**
* 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 { observable, makeObservable, computed } from "mobx";
import type { HelmRepo } from "../../../main/helm/helm-repo-manager";
import { HelmRepoManager } from "../../../main/helm/helm-repo-manager";
import { Button } from "../button";
import { Icon } from "../icon";
import { Notifications } from "../notifications";
import type { SelectOption } from "../select";
import { Select } from "../select";
import { AddHelmRepoDialog } from "./add-helm-repo-dialog";
import { observer } from "mobx-react";
import { RemovableItem } from "./removable-item";
import { Notice } from "../+extensions/notice";
import { Spinner } from "../spinner";
import { noop } from "../../utils";
import type { SingleValue } from "react-select";
@observer
export class HelmCharts extends React.Component {
@observable loadingRepos = false;
@observable loadingAvailableRepos = false;
@observable repos: HelmRepo[] = [];
@observable addedRepos = observable.map<string, HelmRepo>();
constructor(props: {}) {
super(props);
makeObservable(this);
}
@computed get repoOptions() {
return this.repos.map(repo => ({
value: repo,
label: repo.name,
isSelected: this.addedRepos.has(repo.name),
}));
}
componentDidMount() {
this.loadAvailableRepos().catch(noop);
this.loadRepos().catch(noop);
}
async loadAvailableRepos() {
this.loadingAvailableRepos = true;
try {
if (!this.repos.length) {
this.repos = await HelmRepoManager.getInstance().loadAvailableRepos();
}
} catch (err) {
Notifications.error(String(err));
}
this.loadingAvailableRepos = false;
}
async loadRepos() {
this.loadingRepos = true;
try {
const repos = await HelmRepoManager.getInstance().repositories(); // via helm-cli
this.addedRepos.replace(repos.map(repo => [repo.name, repo]));
} catch (err) {
Notifications.error(String(err));
}
this.loadingRepos = false;
}
async addRepo(repo: HelmRepo) {
try {
await HelmRepoManager.getInstance().addRepo(repo);
this.addedRepos.set(repo.name, repo);
} catch (err) {
Notifications.error((
<>
{"Adding helm branch "}
<b>{repo.name}</b>
{" has failed: "}
{String(err)}
</>
));
}
}
async removeRepo(repo: HelmRepo) {
try {
await HelmRepoManager.getInstance().removeRepo(repo);
this.addedRepos.delete(repo.name);
} catch (err) {
Notifications.error(
<>
{"Removing helm branch "}
<b>{repo.name}</b>
{" has failed: "}
{String(err)}
</>,
);
}
}
onRepoSelect = async (option: SingleValue<{ value: HelmRepo }>): Promise<void> => {
if (!option) {
return;
}
if (this.addedRepos.has(option.value.name)) {
return void Notifications.ok((
<>
{"Helm repo "}
<b>{option.value.name}</b>
{" already in use."}
</>
));
}
await this.addRepo(option.value);
};
formatOptionLabel = ({ value, isSelected }: SelectOption<HelmRepo>) => (
<div className="flex gaps">
<span>{value.name}</span>
{isSelected && (
<Icon
small
material="check"
className="box right" />
)}
</div>
);
renderRepositories() {
const repos = Array.from(this.addedRepos);
if (this.loadingRepos) {
return <div className="pt-5 relative"><Spinner center/></div>;
}
if (!repos.length) {
return (
<Notice>
<div className="flex-grow text-center">The repositories have not been added yet</div>
</Notice>
);
}
return repos.map(([name, repo]) => {
return (
<RemovableItem
key={name}
onRemove={() => this.removeRepo(repo)}
className="mt-3"
>
<div>
<div data-testid="repository-name" className={styles.repoName}>{name}</div>
<div className={styles.repoUrl}>{repo.url}</div>
</div>
</RemovableItem>
);
});
}
render() {
return (
<div>
<div className="flex gaps">
<Select
id="HelmRepoSelect"
placeholder="Repositories"
isLoading={this.loadingAvailableRepos}
isDisabled={this.loadingAvailableRepos}
options={this.repoOptions}
onChange={this.onRepoSelect}
value={this.repos}
formatOptionLabel={this.formatOptionLabel}
controlShouldRenderValue={false}
className="box grow"
themeName="lens"
/>
<Button
primary
label="Add Custom Helm Repo"
onClick={AddHelmRepoDialog.open}
/>
</div>
<AddHelmRepoDialog onAddRepo={() => this.loadRepos()}/>
<div className={styles.repos}>
{this.renderRepositories()}
</div>
</div>
);
}
}

View File

@ -4,8 +4,7 @@
*/ */
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import React from "react"; import React from "react";
import { HelmCharts } from "./kubernetes/helm-charts/helm-charts";
import { HelmCharts } from "./helm-charts";
import { KubeconfigSyncs } from "./kubeconfig-syncs"; import { KubeconfigSyncs } from "./kubeconfig-syncs";
import { KubectlBinaries } from "./kubectl-binaries"; import { KubectlBinaries } from "./kubectl-binaries";
import { Preferences } from "./preferences"; import { Preferences } from "./preferences";

View File

@ -0,0 +1,43 @@
/**
* 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 getActiveHelmRepositoriesChannelInjectable from "../../../../../common/helm/get-active-helm-repositories-channel.injectable";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import showErrorNotificationInjectable from "../../../notifications/show-error-notification.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import { runInAction } from "mobx";
const activeHelmRepositoriesInjectable = getInjectable({
id: "active-helm-repositories",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const getHelmRepositoriesChannel = di.inject(getActiveHelmRepositoriesChannelInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const helmRepositoriesErrorState = di.inject(helmRepositoriesErrorStateInjectable);
return asyncComputed(async () => {
const result = await requestFromChannel(getHelmRepositoriesChannel);
if (result.callWasSuccessful) {
return result.response;
} else {
showErrorNotification(result.error);
runInAction(() =>
helmRepositoriesErrorState.set({
controlsAreShown: false,
errorMessage: result.error,
}),
);
return [];
}
}, []);
},
});
export default activeHelmRepositoriesInjectable;

View File

@ -0,0 +1,152 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import { Wizard, WizardStep } from "../../../../wizard";
import { Input } from "../../../../input";
import { systemName, isUrl } from "../../../../input/input_validators";
import { withInjectables } from "@ogre-tools/injectable-react";
import customHelmRepoInjectable from "./custom-helm-repo.injectable";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import { observer } from "mobx-react";
import type { IObservableValue } from "mobx";
import { action } from "mobx";
import submitCustomHelmRepositoryInjectable from "./submit-custom-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
import { Button } from "../../../../button";
import { Icon } from "../../../../icon";
import maximalCustomHelmRepoOptionsAreShownInjectable from "./maximal-custom-helm-repo-options-are-shown.injectable";
import { SubTitle } from "../../../../layout/sub-title";
import { Checkbox } from "../../../../checkbox";
import { HelmFileInput } from "./helm-file-input/helm-file-input";
interface Dependencies {
helmRepo: HelmRepo;
hideDialog: () => void;
submitCustomRepository: (repository: HelmRepo) => Promise<void>;
maximalOptionsAreShown: IObservableValue<boolean>;
}
const NonInjectedActivationOfCustomHelmRepositoryDialogContent = observer(({ helmRepo, submitCustomRepository, maximalOptionsAreShown, hideDialog } : Dependencies) => (
<Wizard header={<h5>Add custom Helm Repo</h5>} done={hideDialog}>
<WizardStep
contentClass="flow column"
nextLabel="Add"
next={() => submitCustomRepository(helmRepo)}
testIdForNext="custom-helm-repository-submit-button"
testIdForPrev="custom-helm-repository-cancel-button"
>
<div className="flex column gaps" data-testid="add-custom-helm-repository-dialog">
<Input
autoFocus
required
placeholder="Helm repo name"
trim
validators={systemName}
value={helmRepo.name}
onChange={action(v => helmRepo.name = v)}
data-testid="custom-helm-repository-name-input"
/>
<Input
required
placeholder="URL"
validators={isUrl}
value={helmRepo.url}
onChange={action(v => helmRepo.url = v)}
data-testid="custom-helm-repository-url-input"
/>
<Button
plain
className="accordion"
data-testid="toggle-maximal-options-for-custom-helm-repository-button"
onClick={action(() => maximalOptionsAreShown.set(!maximalOptionsAreShown.get()))}
>
More
<Icon
small
tooltip="More"
material={maximalOptionsAreShown.get() ? "remove" : "add"}
/>
</Button>
{maximalOptionsAreShown.get() && (
<div data-testid="maximal-options-for-custom-helm-repository-dialog">
<SubTitle title="Security settings" />
<Checkbox
label="Skip TLS certificate checks for the repository"
value={helmRepo.insecureSkipTlsVerify}
onChange={action(v => {
helmRepo.insecureSkipTlsVerify = v;
})}
data-testid="custom-helm-repository-verify-tls-input"
/>
<HelmFileInput
placeholder="Key file"
value={helmRepo.keyFile || ""}
setValue={action((value) => helmRepo.keyFile = value)}
fileExtensions={keyExtensions}
data-testid="custom-helm-repository-key-file-input"
/>
<HelmFileInput
placeholder="Ca file"
value={helmRepo.caFile || ""}
setValue={action((value) => helmRepo.caFile = value)}
fileExtensions={certExtensions}
data-testid="custom-helm-repository-ca-cert-file-input"
/>
<HelmFileInput
placeholder="Certificate file"
value={helmRepo.certFile || ""}
setValue={action((value) => helmRepo.certFile = value)}
fileExtensions={certExtensions}
data-testid="custom-helm-repository-cert-file-input"
/>
<SubTitle title="Chart Repository Credentials" />
<Input
placeholder="Username"
value={helmRepo.username}
onChange= {action(v => helmRepo.username = v)}
data-testid="custom-helm-repository-username-input"
/>
<Input
type="password"
placeholder="Password"
value={helmRepo.password}
onChange={action(v => helmRepo.password = v)}
data-testid="custom-helm-repository-password-input"
/>
</div>
)}
</div>
</WizardStep>
</Wizard>
));
export const AddingOfCustomHelmRepositoryDialogContent = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryDialogContent,
{
getProps: (di) => ({
helmRepo: di.inject(customHelmRepoInjectable),
hideDialog: di.inject(hideDialogForAddingCustomHelmRepositoryInjectable),
submitCustomRepository: di.inject(submitCustomHelmRepositoryInjectable),
maximalOptionsAreShown: di.inject(maximalCustomHelmRepoOptionsAreShownInjectable),
}),
},
);
const keyExtensions = ["key", "keystore", "jks", "p12", "pfx", "pem"];
const certExtensions = ["crt", "cer", "ca-bundle", "p7b", "p7c", "p7s", "p12", "pfx", "pem"];

View File

@ -0,0 +1,47 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "./add-helm-repo-dialog.scss";
import React from "react";
import { Dialog } from "../../../../dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import { AddingOfCustomHelmRepositoryDialogContent } from "./adding-of-custom-helm-repository-dialog-content";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./dialog-visibility/adding-of-custom-helm-repository-dialog-is-visible.injectable";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
contentIsVisible: IObservableValue<boolean>;
hideDialog: () => void;
}
const NonInjectedActivationOfCustomHelmRepositoryDialog = observer(({
contentIsVisible,
hideDialog,
}: Dependencies) => (
<div>
<Dialog
className="AddHelmRepoDialog"
isOpen={contentIsVisible.get()}
close={hideDialog}
>
{contentIsVisible.get() && <AddingOfCustomHelmRepositoryDialogContent />}
</Dialog>
</div>
));
export const AddingOfCustomHelmRepositoryDialog = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryDialog,
{
getProps: (di) => ({
contentIsVisible: di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable),
hideDialog: di.inject(hideDialogForAddingCustomHelmRepositoryInjectable),
}),
},
);

View File

@ -0,0 +1,32 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import { Button } from "../../../../button";
import showDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/show-dialog-for-adding-custom-helm-repository.injectable";
interface Dependencies {
showDialog: () => void;
}
const NonInjectedActivationOfCustomHelmRepositoryOpenButton = ({ showDialog }: Dependencies) => (
<Button
primary
label="Add Custom Helm Repo"
onClick={showDialog}
data-testid="add-custom-helm-repo-button"
/>
);
export const AddingOfCustomHelmRepositoryOpenButton = withInjectables<Dependencies>(
NonInjectedActivationOfCustomHelmRepositoryOpenButton,
{
getProps: (di) => ({
showDialog: di.inject(showDialogForAddingCustomHelmRepositoryInjectable),
}),
},
);

View File

@ -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, lifecycleEnum } from "@ogre-tools/injectable";
import { observable } from "mobx";
const customHelmRepoInjectable = getInjectable({
id: "custom-helm-repo",
instantiate: () => observable({
name: "",
url: "",
username: "",
password: "",
insecureSkipTlsVerify: false,
caFile: "",
keyFile: "",
certFile: "",
}),
lifecycle: lifecycleEnum.transient,
});
export default customHelmRepoInjectable;

View File

@ -0,0 +1,13 @@
/**
* 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 { observable } from "mobx";
const addingOfCustomHelmRepositoryDialogIsVisibleInjectable = getInjectable({
id: "adding-of-custom-helm-repository-dialog-is-visible",
instantiate: () => observable.box(false),
});
export default addingOfCustomHelmRepositoryDialogIsVisibleInjectable;

View File

@ -0,0 +1,21 @@
/**
* 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 { action } from "mobx";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./adding-of-custom-helm-repository-dialog-is-visible.injectable";
const hideDialogForAddingCustomHelmRepositoryInjectable = getInjectable({
id: "hide-dialog-for-adding-custom-helm-repository",
instantiate: (di) => {
const state = di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable);
return action(() => {
state.set(false);
});
},
});
export default hideDialogForAddingCustomHelmRepositoryInjectable;

View File

@ -0,0 +1,21 @@
/**
* 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 { action } from "mobx";
import addingOfCustomHelmRepositoryDialogIsVisibleInjectable from "./adding-of-custom-helm-repository-dialog-is-visible.injectable";
const showDialogForAddingCustomHelmRepositoryInjectable = getInjectable({
id: "show-dialog-for-adding-custom-helm-repository",
instantiate: (di) => {
const state = di.inject(addingOfCustomHelmRepositoryDialogIsVisibleInjectable);
return action(() => {
state.set(true);
});
},
});
export default showDialogForAddingCustomHelmRepositoryInjectable;

View File

@ -0,0 +1,23 @@
/**
* 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 { FileFilter } from "electron";
import { requestOpenFilePickingDialog } from "../../../../../../ipc";
const getFilePathsInjectable = getInjectable({
id: "get-file-paths",
instantiate: () => async (fileFilter: FileFilter) =>
await requestOpenFilePickingDialog({
properties: ["openFile", "showHiddenFiles"],
message: `Select file`,
buttonLabel: `Use file`,
filters: [fileFilter, { name: "Any", extensions: ["*"] }],
}),
causesSideEffects: true,
});
export default getFilePathsInjectable;

View File

@ -0,0 +1,74 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { InputValidator } from "../../../../../input";
import { Input } from "../../../../../input";
import { Icon } from "../../../../../icon";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import getFilePathsInjectable from "./get-file-paths.injectable";
import type { FileFilter } from "electron";
import isPathInjectable from "../../../../../input/validators/is-path.injectable";
interface HelmFileInputProps {
placeholder: string;
fileExtensions: string[];
value: string;
setValue: (value: string) => void;
"data-testid"?: string;
}
interface Dependencies {
getFilePaths: (fileFilter: FileFilter) => Promise<{ canceled: boolean; filePaths: string[] }>;
isPath: InputValidator<true>;
}
const NonInjectedHelmFileInput = ({
placeholder,
value,
setValue,
fileExtensions,
getFilePaths,
isPath,
"data-testid": testId,
}: Dependencies & HelmFileInputProps) => (
<div className="flex gaps align-center">
<Input
placeholder={placeholder}
validators={isPath}
className="box grow"
value={value}
onChange={(v) => setValue(v)}
data-testid={testId}
/>
<Icon
material="folder"
onClick={async () => {
const { canceled, filePaths } = await getFilePaths({
name: placeholder,
extensions: fileExtensions,
});
if (!canceled && filePaths.length) {
setValue(filePaths[0]);
}
}}
tooltip="Browse"
/>
</div>
);
export const HelmFileInput = withInjectables<Dependencies, HelmFileInputProps>(
NonInjectedHelmFileInput,
{
getProps: (di, props) => ({
getFilePaths: di.inject(getFilePathsInjectable),
isPath: di.inject(isPathInjectable),
...props,
}),
},
);

View File

@ -0,0 +1,13 @@
/**
* 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 { observable } from "mobx";
const maximalCustomHelmRepoOptionsAreShownInjectable = getInjectable({
id: "maximal-custom-helm-repo-options-are-shown",
instantiate: () => observable.box(false),
});
export default maximalCustomHelmRepoOptionsAreShownInjectable;

View File

@ -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 type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import addHelmRepositoryInjectable from "../adding-of-public-helm-repository/select-helm-repository/add-helm-repository.injectable";
import hideDialogForAddingCustomHelmRepositoryInjectable from "./dialog-visibility/hide-dialog-for-adding-custom-helm-repository.injectable";
const submitCustomHelmRepositoryInjectable = getInjectable({
id: "submit-custom-helm-repository",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const hideDialog = di.inject(hideDialogForAddingCustomHelmRepositoryInjectable);
return async (repository: HelmRepo) => {
await addHelmRepository(repository);
hideDialog();
};
},
});
export default submitCustomHelmRepositoryInjectable;

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { IAsyncComputed } from "@ogre-tools/injectable-react";
import { withInjectables } from "@ogre-tools/injectable-react";
import React from "react";
import publicHelmRepositoriesInjectable from "./public-helm-repositories/public-helm-repositories.injectable";
import type { HelmRepo } from "../../../../../../common/helm/helm-repo";
import type { SelectOption } from "../../../../select";
import { Select } from "../../../../select";
import { Icon } from "../../../../icon";
import { observer } from "mobx-react";
import type { SingleValue } from "react-select";
import selectHelmRepositoryInjectable from "./select-helm-repository/select-helm-repository.injectable";
import { matches } from "lodash/fp";
import activeHelmRepositoriesInjectable from "../active-helm-repositories.injectable";
interface Dependencies {
publicRepositories: IAsyncComputed<HelmRepo[]>;
activeRepositories: IAsyncComputed<HelmRepo[]>;
selectRepository: (value: SingleValue<SelectOption<HelmRepo>>) => void;
}
const NonInjectedAddingOfPublicHelmRepository = observer(({
publicRepositories,
activeRepositories,
selectRepository,
}: Dependencies) => {
const dereferencesPublicRepositories = publicRepositories.value.get();
const dereferencesActiveRepositories = activeRepositories.value.get();
const valuesAreLoading = publicRepositories.pending.get() || activeRepositories.pending.get();
const repositoryOptions = dereferencesPublicRepositories.map(repository => ({
value: repository,
label: repository.name,
isSelected: !!dereferencesActiveRepositories.find(matches({ name: repository.name })),
}));
return (
<Select
id="selection-of-active-public-helm-repository"
placeholder="Repositories"
isLoading={valuesAreLoading}
isDisabled={valuesAreLoading}
options={repositoryOptions}
onChange={selectRepository}
value={dereferencesPublicRepositories}
formatOptionLabel={formatOptionLabel}
controlShouldRenderValue={false}
className="box grow"
themeName="lens"
/>
);
});
export const AddingOfPublicHelmRepository = withInjectables<Dependencies>(
NonInjectedAddingOfPublicHelmRepository,
{
getProps: (di) => ({
publicRepositories: di.inject(publicHelmRepositoriesInjectable),
activeRepositories: di.inject(activeHelmRepositoriesInjectable),
selectRepository: di.inject(selectHelmRepositoryInjectable),
}),
},
);
const formatOptionLabel = ({ value, isSelected }: SelectOption<HelmRepo>) => (
<div className="flex gaps">
<span>{value.name}</span>
{isSelected && (
<Icon
small
material="check"
className="box right" />
)}
</div>
);

View File

@ -0,0 +1,29 @@
/**
* 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 { sortBy } from "lodash/fp";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import { customRequestPromise } from "../../../../../../../common/request";
const callForPublicHelmRepositoriesInjectable = getInjectable({
id: "call-for-public-helm-repositories",
instantiate: () => async (): Promise<HelmRepo[]> => {
const res = await customRequestPromise({
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
json: true,
resolveWithFullResponse: true,
timeout: 10000,
});
const repositories = res.body as HelmRepo[];
return sortBy(repo => repo.name, repositories);
},
causesSideEffects: true,
});
export default callForPublicHelmRepositoriesInjectable;

View File

@ -0,0 +1,21 @@
/**
* 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 callForPublicHelmRepositoriesInjectable from "./call-for-public-helm-repositories.injectable";
const publicHelmRepositoriesInjectable = getInjectable({
id: "public-helm-repositories",
instantiate: (di) => {
const callForPublicHelmRepositories = di.inject(callForPublicHelmRepositoriesInjectable);
return asyncComputed(async () => {
return await callForPublicHelmRepositories();
}, []);
},
});
export default publicHelmRepositoriesInjectable;

View File

@ -0,0 +1,42 @@
/**
* 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 addHelmRepositoryChannelInjectable from "../../../../../../../common/helm/add-helm-repository-channel.injectable";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "../../active-helm-repositories.injectable";
import showErrorNotificationInjectable from "../../../../../notifications/show-error-notification.injectable";
import showSuccessNotificationInjectable from "../../../../../notifications/show-success-notification.injectable";
const addHelmRepositoryInjectable = getInjectable({
id: "add-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const addHelmRepositoryChannel = di.inject(addHelmRepositoryChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
const showErrorNotification = di.inject(showErrorNotificationInjectable);
const showSuccessNotification = di.inject(showSuccessNotificationInjectable);
return async (repository: HelmRepo) => {
const result = await requestFromChannel(
addHelmRepositoryChannel,
repository,
);
if (result.callWasSuccessful) {
showSuccessNotification(
`Helm repository ${repository.name} has been added.`,
);
activeHelmRepositories.invalidate();
} else {
showErrorNotification(result.error);
}
};
},
});
export default addHelmRepositoryInjectable;

View File

@ -0,0 +1,33 @@
/**
* 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 addHelmRepositoryInjectable from "./add-helm-repository.injectable";
import type { SelectOption } from "../../../../../select";
import type { HelmRepo } from "../../../../../../../common/helm/helm-repo";
import type { SingleValue } from "react-select";
import removeHelmRepositoryInjectable from "../../remove-helm-repository.injectable";
const selectHelmRepositoryInjectable = getInjectable({
id: "select-helm-repository",
instantiate: (di) => {
const addHelmRepository = di.inject(addHelmRepositoryInjectable);
const removeHelmRepository = di.inject(removeHelmRepositoryInjectable);
return (selected: SingleValue<SelectOption<HelmRepo>>) => {
if (!selected) {
return;
}
if (!selected.isSelected) {
addHelmRepository(selected.value);
} else {
removeHelmRepository(selected.value);
}
};
},
});
export default selectHelmRepositoryInjectable;

View File

@ -0,0 +1,61 @@
/**
* 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";
import { AddingOfPublicHelmRepository } from "./adding-of-public-helm-repository/adding-of-public-helm-repository";
import { AddingOfCustomHelmRepositoryOpenButton } from "./adding-of-custom-helm-repository/adding-of-custom-helm-repository-open-button";
import { AddingOfCustomHelmRepositoryDialog } from "./adding-of-custom-helm-repository/adding-of-custom-helm-repository-dialog";
import { withInjectables } from "@ogre-tools/injectable-react";
import type { HelmRepositoriesErrorState } from "./helm-repositories-error-state.injectable";
import helmRepositoriesErrorStateInjectable from "./helm-repositories-error-state.injectable";
import type { IObservableValue } from "mobx";
import { observer } from "mobx-react";
import { Notice } from "../../../+extensions/notice";
interface Dependencies {
helmRepositoriesErrorState: IObservableValue<HelmRepositoriesErrorState>;
}
const NonInjectedHelmCharts = observer(
({ helmRepositoriesErrorState }: Dependencies) => {
const state = helmRepositoriesErrorState.get();
return (
<div>
{!state.controlsAreShown && (
<Notice>
<div className="flex-grow text-center">{state.errorMessage}</div>
</Notice>
)}
{state.controlsAreShown && (
<div data-testid="helm-controls">
<div className="flex gaps">
<AddingOfPublicHelmRepository />
<AddingOfCustomHelmRepositoryOpenButton />
</div>
<HelmRepositories />
<AddingOfCustomHelmRepositoryDialog />
</div>
)}
</div>
);
},
);
export const HelmCharts = withInjectables<Dependencies>(
NonInjectedHelmCharts,
{
getProps: (di) => ({
helmRepositoriesErrorState: di.inject(helmRepositoriesErrorStateInjectable),
}),
},
);

View File

@ -0,0 +1,19 @@
/**
* 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 { observable } from "mobx";
export type HelmRepositoriesErrorState =
| { controlsAreShown: true }
| { controlsAreShown: false; errorMessage: string };
const helmRepositoriesErrorStateInjectable = getInjectable({
id: "helm-repositories-error-state",
instantiate: () =>
observable.box<HelmRepositoriesErrorState>({ controlsAreShown: true }),
});
export default helmRepositoriesErrorStateInjectable;

View File

@ -0,0 +1,68 @@
/**
* 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/helm-repo";
import { RemovableItem } from "../../removable-item";
import removeHelmRepositoryInjectable from "./remove-helm-repository.injectable";
interface Dependencies {
activeHelmRepositories: IAsyncComputed<HelmRepo[]>;
removeRepository: (repository: HelmRepo) => Promise<void>;
}
const NonInjectedActiveHelmRepositories = observer(({ activeHelmRepositories, removeRepository }: Dependencies) => {
if (activeHelmRepositories.pending.get()) {
return (
<div className={styles.repos}>
<div className="pt-5 relative">
<Spinner center data-testid="helm-repositories-are-loading" />
</div>
</div>
);
}
const repositories = activeHelmRepositories.value.get();
return (
<div className={styles.repos}>
{repositories.map((repository) => (
<RemovableItem
key={repository.name}
onRemove={() => removeRepository(repository)}
className="mt-3"
data-testid={`remove-helm-repository-${repository.name}`}
>
<div data-testid={`helm-repository-${repository.name}`} className={styles.repoName}>
{repository.name}
</div>
<div className={styles.repoUrl}>{repository.url}</div>
</RemovableItem>
))}
</div>
);
});
export const HelmRepositories = withInjectables<Dependencies>(
NonInjectedActiveHelmRepositories,
{
getProps: (di) => ({
activeHelmRepositories: di.inject(activeHelmRepositoriesInjectable),
removeRepository: di.inject(removeHelmRepositoryInjectable),
}),
},
);

View File

@ -0,0 +1,27 @@
/**
* 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/helm-repo";
import { requestFromChannelInjectionToken } from "../../../../../common/utils/channel/request-from-channel-injection-token";
import activeHelmRepositoriesInjectable from "./active-helm-repositories.injectable";
import removeHelmRepositoryChannelInjectable from "../../../../../common/helm/remove-helm-repository-channel.injectable";
const removePublicHelmRepositoryInjectable = getInjectable({
id: "remove-public-helm-repository",
instantiate: (di) => {
const requestFromChannel = di.inject(requestFromChannelInjectionToken);
const removeHelmRepositoryChannel = di.inject(removeHelmRepositoryChannelInjectable);
const activeHelmRepositories = di.inject(activeHelmRepositoriesInjectable);
return async (repository: HelmRepo) => {
await requestFromChannel(removeHelmRepositoryChannel, repository);
activeHelmRepositories.invalidate();
};
},
});
export default removePublicHelmRepositoryInjectable;

View File

@ -14,9 +14,10 @@ export interface RemovableItemProps extends DOMAttributes<any>{
icon?: string; icon?: string;
onRemove: () => void; onRemove: () => void;
className?: string; className?: string;
"data-testid"?: string;
} }
export function RemovableItem({ icon, onRemove, children, className, ...rest }: RemovableItemProps) { export function RemovableItem({ icon, onRemove, children, className, "data-testid": testId, ...rest }: RemovableItemProps) {
return ( return (
<div className={cssNames(styles.item, "flex gaps align-center justify-space-between", className)} {...rest}> <div className={cssNames(styles.item, "flex gaps align-center justify-space-between", className)} {...rest}>
{icon && ( {icon && (
@ -27,6 +28,7 @@ export function RemovableItem({ icon, onRemove, children, className, ...rest }:
material="delete" material="delete"
onClick={onRemove} onClick={onRemove}
tooltip="Remove" tooltip="Remove"
data-testid={testId}
/> />
</div> </div>
); );

View File

@ -0,0 +1,32 @@
/**
* 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 { AsyncInputValidationError, inputValidator } from "../input_validators";
import pathExistsInjectable from "../../../../common/fs/path-exists.injectable";
const isPathInjectable = getInjectable({
id: "is-path",
instantiate: (di) => {
const pathExists = di.inject(pathExistsInjectable);
return inputValidator<true>({
debounce: 100,
condition: ({ type }) => type === "text",
validate: async (value) => {
try {
await pathExists(value);
} catch {
throw new AsyncInputValidationError(
`${value} is not a valid file path`,
);
}
},
});
},
});
export default isPathInjectable;

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 type { NotificationMessage, Notification } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import notificationsStoreInjectable from "./notifications-store.injectable";
const showErrorNotificationInjectable = getInjectable({
id: "show-error-notification",
instantiate: (di) => {
const notificationsStore = di.inject(notificationsStoreInjectable);
return (message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) =>
notificationsStore.add({
status: NotificationStatus.ERROR,
timeout: 5000,
message,
...customOpts,
});
},
});
export default showErrorNotificationInjectable;

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 type { NotificationMessage, Notification } from "./notifications.store";
import { NotificationStatus } from "./notifications.store";
import notificationsStoreInjectable from "./notifications-store.injectable";
const showSuccessNotificationInjectable = getInjectable({
id: "show-success-notification",
instantiate: (di) => {
const notificationsStore = di.inject(notificationsStoreInjectable);
return (message: NotificationMessage, customOpts: Partial<Omit<Notification, "message">> = {}) =>
notificationsStore.add({
status: NotificationStatus.OK,
timeout: 5000,
message,
...customOpts,
});
},
});
export default showSuccessNotificationInjectable;

View File

@ -233,7 +233,9 @@ class NonInjectedSelect<
Menu: ({ className, ...props }) => ( Menu: ({ className, ...props }) => (
<WrappedMenu <WrappedMenu
{...props} {...props}
className={cssNames(menuClass, this.themeClass, className)} className={cssNames(menuClass, this.themeClass, className, {
[`${inputId}-options`]: !!inputId,
})}
/> />
), ),
}} }}

View File

@ -15,7 +15,7 @@ import { Observer } from "mobx-react";
import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable"; import subscribeStoresInjectable from "../../kube-watch-api/subscribe-stores.injectable";
import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable"; import allowedResourcesInjectable from "../../../common/cluster-store/allowed-resources.injectable";
import type { RenderResult } from "@testing-library/react"; import type { RenderResult } from "@testing-library/react";
import { fireEvent } from "@testing-library/react"; import { getByText, fireEvent } from "@testing-library/react";
import type { KubeResource } from "../../../common/rbac"; import type { KubeResource } from "../../../common/rbac";
import { Sidebar } from "../layout/sidebar"; import { Sidebar } from "../layout/sidebar";
import type { DiContainer } from "@ogre-tools/injectable"; import type { DiContainer } from "@ogre-tools/injectable";
@ -52,6 +52,9 @@ import { getDiForUnitTesting as getMainDi } from "../../../main/getDiForUnitTest
import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels"; import { overrideChannels } from "../../../test-utils/channel-fakes/override-channels";
import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token"; import type { TrayMenuItem } from "../../../main/tray/tray-menu-item/tray-menu-item-injection-token";
import trayIconPathsInjectable from "../../../main/tray/tray-icon-path.injectable"; import trayIconPathsInjectable from "../../../main/tray/tray-icon-path.injectable";
import assert from "assert";
import { openMenu } from "react-select-event";
import userEvent from "@testing-library/user-event";
type Callback = (dis: DiContainers) => void | Promise<void>; type Callback = (dis: DiContainers) => void | Promise<void>;
@ -85,6 +88,11 @@ export interface ApplicationBuilder {
helmCharts: { helmCharts: {
navigate: () => void; navigate: () => void;
}; };
select: {
openMenu: (id: string) => void;
selectOption: (menuId: string, labelText: string) => void;
};
} }
interface DiContainers { interface DiContainers {
@ -433,6 +441,30 @@ export const getApplicationBuilder = () => {
return rendered; return rendered;
}, },
select: {
openMenu: (menuId) => {
const selector = rendered.container.querySelector<HTMLElement>(
`#${menuId}`,
);
assert(selector);
openMenu(selector);
},
selectOption: (menuId, labelText) => {
const menuOptions = rendered.baseElement.querySelector<HTMLElement>(
`.${menuId}-options`,
);
assert(menuOptions);
const option = getByText(menuOptions, labelText);
userEvent.click(option);
},
},
}; };
return builder; return builder;

View File

@ -129,6 +129,8 @@ export interface WizardStepProps<D> extends WizardCommonProps<D> {
skip?: boolean; // don't render the step skip?: boolean; // don't render the step
scrollable?: boolean; scrollable?: boolean;
children?: React.ReactNode | React.ReactNode[]; children?: React.ReactNode | React.ReactNode[];
testIdForNext?: string;
testIdForPrev?: string;
} }
interface WizardStepState { interface WizardStepState {
@ -214,7 +216,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
step, isFirst, isLast, children, step, isFirst, isLast, children,
loading, customButtons, disabledNext, scrollable, loading, customButtons, disabledNext, scrollable,
hideNextBtn, hideBackBtn, beforeContent, afterContent, noValidate, skip, moreButtons, hideNextBtn, hideBackBtn, beforeContent, afterContent, noValidate, skip, moreButtons,
waiting, className, contentClass, prevLabel, nextLabel, waiting, className, contentClass, prevLabel, nextLabel, testIdForNext, testIdForPrev,
} = this.props; } = this.props;
if (skip) { if (skip) {
@ -242,6 +244,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
label={prevLabel || (isFirst?.() ? "Cancel" : "Back")} label={prevLabel || (isFirst?.() ? "Cancel" : "Back")}
hidden={hideBackBtn} hidden={hideBackBtn}
onClick={this.prev} onClick={this.prev}
data-testid={testIdForPrev}
/> />
<Button <Button
primary primary
@ -250,6 +253,7 @@ export class WizardStep<D> extends React.Component<WizardStepProps<D>, WizardSte
hidden={hideNextBtn} hidden={hideNextBtn}
waiting={waiting ?? this.state.waiting} waiting={waiting ?? this.state.waiting}
disabled={disabledNext} disabled={disabledNext}
data-testid={testIdForNext}
/> />
</div> </div>
)} )}

View File

@ -5,7 +5,12 @@
import glob from "glob"; import glob from "glob";
import { memoize, noop } from "lodash/fp"; import { memoize, noop } from "lodash/fp";
import { createContainer } from "@ogre-tools/injectable"; import type {
DiContainer,
Injectable } from "@ogre-tools/injectable";
import {
createContainer,
} from "@ogre-tools/injectable";
import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import { Environments, setLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import requestFromChannelInjectable from "./utils/channel/request-from-channel.injectable"; import requestFromChannelInjectable from "./utils/channel/request-from-channel.injectable";
import loggerInjectable from "../common/logger.injectable"; import loggerInjectable from "../common/logger.injectable";
@ -45,6 +50,8 @@ import appVersionInjectable from "../common/get-configuration-file-model/app-ver
import provideInitialValuesForSyncBoxesInjectable from "./utils/sync-box/provide-initial-values-for-sync-boxes.injectable"; import provideInitialValuesForSyncBoxesInjectable from "./utils/sync-box/provide-initial-values-for-sync-boxes.injectable";
import requestAnimationFrameInjectable from "./components/animate/request-animation-frame.injectable"; import requestAnimationFrameInjectable from "./components/animate/request-animation-frame.injectable";
import getRandomIdInjectable from "../common/utils/get-random-id.injectable"; import getRandomIdInjectable from "../common/utils/get-random-id.injectable";
import getFilePathsInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-custom-helm-repository/helm-file-input/get-file-paths.injectable";
import callForPublicHelmRepositoriesInjectable from "./components/+preferences/kubernetes/helm-charts/adding-of-public-helm-repository/public-helm-repositories/call-for-public-helm-repositories.injectable";
export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => { export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {}) => {
const { const {
@ -91,9 +98,11 @@ export const getDiForUnitTesting = (opts: { doGeneralOverrides?: boolean } = {})
on: () => {}, on: () => {},
}) as unknown as IpcRenderer); }) as unknown as IpcRenderer);
di.override(broadcastMessageInjectable, () => () => { overrideFunctionalInjectables(di, [
throw new Error("Tried to broadcast message over IPC without explicit override."); broadcastMessageInjectable,
}); getFilePathsInjectable,
callForPublicHelmRepositoriesInjectable,
]);
// eslint-disable-next-line unused-imports/no-unused-vars-ts // eslint-disable-next-line unused-imports/no-unused-vars-ts
di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore); di.override(extensionsStoreInjectable, () => ({ isEnabled: ({ id, isBundled }) => false }) as ExtensionsStore);
@ -147,3 +156,11 @@ const getInjectableFilePaths = memoize(() => [
...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("../common/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }), ...glob.sync("../extensions/**/*.injectable.{ts,tsx}", { cwd: __dirname }),
]); ]);
const overrideFunctionalInjectables = (di: DiContainer, injectables: Injectable<any, any, any>[]) => {
injectables.forEach(injectable => {
di.override(injectable, () => () => {
throw new Error(`Tried to run "${injectable.id}" without explicit override.`);
});
});
};