mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Make navigating to Helm Charts not blow up
Co-authored-by: Janne Savolainen <janne.savolainen@live.fi> Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
parent
203418ac37
commit
823cab8170
@ -0,0 +1,454 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`helm-charts - navigation to Helm charts when navigating to Helm charts renders 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex flex-col"
|
||||
data-testid="cluster-sidebar"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class=""
|
||||
style="width: 40px; height: 40px;"
|
||||
>
|
||||
??
|
||||
</div>
|
||||
<div />
|
||||
</div>
|
||||
<div
|
||||
class="undefined sidebar-active-status"
|
||||
>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="workloads"
|
||||
data-is-active-test="false"
|
||||
data-test-id="workloads"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center expandable"
|
||||
data-testid="sidebar-item-link-for-workloads"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon svg focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Workloads
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="config"
|
||||
data-is-active-test="false"
|
||||
data-test-id="config"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center"
|
||||
data-testid="sidebar-item-link-for-config"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="list"
|
||||
>
|
||||
list
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Config
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="network"
|
||||
data-is-active-test="false"
|
||||
data-test-id="network"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center expandable"
|
||||
data-testid="sidebar-item-link-for-network"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="device_hub"
|
||||
>
|
||||
device_hub
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Network
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="storage"
|
||||
data-is-active-test="false"
|
||||
data-test-id="storage"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center"
|
||||
data-testid="sidebar-item-link-for-storage"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="storage"
|
||||
>
|
||||
storage
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Storage
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="helm"
|
||||
data-is-active-test="true"
|
||||
data-test-id="helm"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
aria-current="page"
|
||||
class="nav-item flex gaps align-center expandable active"
|
||||
data-testid="sidebar-item-link-for-helm"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon svg focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
/>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Helm
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="user-management"
|
||||
data-is-active-test="false"
|
||||
data-test-id="user-management"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center"
|
||||
data-testid="sidebar-item-link-for-user-management"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="security"
|
||||
>
|
||||
security
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Access Control
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
class="SidebarItem"
|
||||
data-id-test="custom-resources"
|
||||
data-is-active-test="false"
|
||||
data-test-id="custom-resources"
|
||||
data-testid="sidebar-item"
|
||||
>
|
||||
<a
|
||||
class="nav-item flex gaps align-center expandable"
|
||||
data-testid="sidebar-item-link-for-custom-resources"
|
||||
href="/"
|
||||
>
|
||||
<i
|
||||
class="Icon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="extension"
|
||||
>
|
||||
extension
|
||||
</span>
|
||||
</i>
|
||||
<span
|
||||
class="link-text box grow"
|
||||
>
|
||||
Custom Resources
|
||||
</span>
|
||||
<i
|
||||
class="Icon expand-icon box right material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="keyboard_arrow_down"
|
||||
>
|
||||
keyboard_arrow_down
|
||||
</span>
|
||||
</i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TabLayout"
|
||||
data-testid="tab-layout"
|
||||
>
|
||||
<div
|
||||
class="Tabs center scrollable"
|
||||
>
|
||||
<div
|
||||
class="Tab flex gaps align-center active"
|
||||
data-is-active-test="true"
|
||||
data-testid="tab-link-for-charts"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Charts
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Tab flex gaps align-center"
|
||||
data-is-active-test="false"
|
||||
data-testid="tab-link-for-releases"
|
||||
role="tab"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="label"
|
||||
>
|
||||
Releases
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<main>
|
||||
<div
|
||||
data-testid="page-for-helm-charts"
|
||||
style="display: none;"
|
||||
/>
|
||||
<div
|
||||
class="ItemListLayout flex column HelmCharts"
|
||||
>
|
||||
<div
|
||||
class="header flex gaps align-center"
|
||||
>
|
||||
<div
|
||||
class="Input SearchInput focused"
|
||||
>
|
||||
<label
|
||||
class="input-area flex gaps align-center"
|
||||
id=""
|
||||
>
|
||||
<input
|
||||
class="input box grow"
|
||||
placeholder="Search Helm Charts..."
|
||||
spellcheck="false"
|
||||
value=""
|
||||
/>
|
||||
<i
|
||||
class="Icon material focusable small"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="search"
|
||||
>
|
||||
search
|
||||
</span>
|
||||
</i>
|
||||
</label>
|
||||
<div
|
||||
class="input-info flex gaps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="items box grow flex column"
|
||||
>
|
||||
<div
|
||||
class="Table flex column HelmCharts box grow some-active-theme-type selectable scrollable sortable autoSize virtual"
|
||||
>
|
||||
<div
|
||||
class="TableHead sticky nowrap topLine"
|
||||
>
|
||||
<div
|
||||
class="TableCell icon nowrap"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell name nowrap sorting"
|
||||
id="name"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Name
|
||||
</div>
|
||||
<i
|
||||
class="Icon sortIcon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="arrow_drop_down"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell description nowrap"
|
||||
id="description"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Description
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell version nowrap"
|
||||
id="version"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Version
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell app-version nowrap"
|
||||
id="app-version"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
App Version
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell repository nowrap sorting"
|
||||
id="repo"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
Repository
|
||||
</div>
|
||||
<i
|
||||
class="Icon sortIcon material focusable"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="arrow_drop_down"
|
||||
>
|
||||
arrow_drop_down
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
<div
|
||||
class="TableCell menu nowrap"
|
||||
>
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<i
|
||||
class="Icon material interactive focusable"
|
||||
id="menu_actions_15"
|
||||
tabindex="0"
|
||||
>
|
||||
<span
|
||||
class="icon"
|
||||
data-icon-name="more_vert"
|
||||
>
|
||||
more_vert
|
||||
</span>
|
||||
</i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="Spinner singleColor center"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="AddRemoveButtons flex gaps"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
51
src/behaviours/helm-charts/navigation-to-helm-charts.test.ts
Normal file
51
src/behaviours/helm-charts/navigation-to-helm-charts.test.ts
Normal file
@ -0,0 +1,51 @@
|
||||
/**
|
||||
* 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 {
|
||||
ApplicationBuilder,
|
||||
getApplicationBuilder,
|
||||
} from "../../renderer/components/test-utils/get-application-builder";
|
||||
|
||||
describe("helm-charts - navigation to Helm charts", () => {
|
||||
let applicationBuilder: ApplicationBuilder;
|
||||
|
||||
beforeEach(() => {
|
||||
applicationBuilder = getApplicationBuilder();
|
||||
|
||||
// applicationBuilder.beforeSetups(({ rendererDi }) => {
|
||||
// const userStoreStub = {
|
||||
// extensionRegistryUrl: { customUrl: "some-custom-url" },
|
||||
// } as unknown as UserStore;
|
||||
//
|
||||
// rendererDi.override(userStoreInjectable, () => userStoreStub);
|
||||
//
|
||||
// const themeStoreStub = ({ themeOptions: [] }) as unknown as ThemeStore;
|
||||
//
|
||||
// rendererDi.override(themeStoreInjectable, () => themeStoreStub);
|
||||
// });
|
||||
});
|
||||
|
||||
describe("when navigating to Helm charts", () => {
|
||||
let rendered: RenderResult;
|
||||
|
||||
beforeEach(async () => {
|
||||
applicationBuilder.setEnvironmentToClusterFrame();
|
||||
|
||||
rendered = await applicationBuilder.render();
|
||||
|
||||
applicationBuilder.helmCharts.navigate();
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
expect(rendered.container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("shows page for Helm charts", () => {
|
||||
const page = rendered.getByTestId("page-for-helm-charts");
|
||||
|
||||
expect(page).not.toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -66,6 +66,8 @@ import getElectronAppPathInjectable from "./app-paths/get-electron-app-path/get-
|
||||
import setElectronAppPathInjectable from "./app-paths/set-electron-app-path/set-electron-app-path.injectable";
|
||||
import setupMainWindowVisibilityAfterActivationInjectable from "./electron-app/after-application-is-ready/setup-main-window-visibility-after-activation.injectable";
|
||||
import setupDeviceShutdownInjectable from "./electron-app/after-application-is-ready/setup-device-shutdown.injectable";
|
||||
import setupApplicationNameInjectable from "./electron-app/before-application-is-ready/setup-application-name.injectable";
|
||||
import setupRunnablesBeforeClosingOfApplicationInjectable from "./electron-app/before-application-is-ready/setup-runnables-before-closing-of-application.injectable";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
@ -195,6 +197,8 @@ const overrideElectronFeatures = (di: DiContainer) => {
|
||||
|
||||
di.override(setupDeepLinkingInjectable, () => ({ run: () => {} }));
|
||||
di.override(exitAppInjectable, () => () => {});
|
||||
di.override(setupApplicationNameInjectable, () => ({ run: () => {} }));
|
||||
di.override(setupRunnablesBeforeClosingOfApplicationInjectable, () => ({ run: () => {} }));
|
||||
di.override(getCommandLineSwitchInjectable, () => () => "irrelevant");
|
||||
di.override(requestSingleInstanceLockInjectable, () => () => true);
|
||||
di.override(disableHardwareAccelerationInjectable, () => () => {});
|
||||
|
||||
@ -17,14 +17,11 @@ describe("Helm Service tests", () => {
|
||||
|
||||
it("list charts with deprecated entries", async () => {
|
||||
mockHelmRepoManager.mockReturnValue({
|
||||
init: jest.fn(),
|
||||
repositories: jest.fn().mockImplementation(async () => {
|
||||
return [
|
||||
{ name: "stable", url: "stableurl" },
|
||||
{ name: "experiment", url: "experimenturl" },
|
||||
];
|
||||
}),
|
||||
});
|
||||
repositories: jest.fn().mockImplementation(async () => [
|
||||
{ name: "stable", url: "stableurl" },
|
||||
{ name: "experiment", url: "experimenturl" },
|
||||
]),
|
||||
} as any);
|
||||
|
||||
const charts = await helmService.listCharts();
|
||||
|
||||
@ -127,13 +124,12 @@ describe("Helm Service tests", () => {
|
||||
|
||||
it("list charts sorted by version in descending order", async () => {
|
||||
mockHelmRepoManager.mockReturnValue({
|
||||
init: jest.fn(),
|
||||
repositories: jest.fn().mockImplementation(async () => {
|
||||
return [
|
||||
{ name: "bitnami", url: "bitnamiurl" },
|
||||
];
|
||||
}),
|
||||
});
|
||||
} as any);
|
||||
|
||||
const charts = await helmService.listCharts();
|
||||
|
||||
|
||||
@ -3,16 +3,161 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { HelmRepoManager } from "./helm-repo-manager";
|
||||
|
||||
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";
|
||||
|
||||
class HelmRepoManager {
|
||||
protected repos: HelmRepo[];
|
||||
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() {
|
||||
this.helmEnv ??= await this.parseHelmEnv();
|
||||
|
||||
const repos = await this.list();
|
||||
|
||||
if (repos.length === 0) {
|
||||
await this.addRepo({
|
||||
name: "bitnami",
|
||||
url: "https://charts.bitnami.com/bitnami",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.didUpdateOnce) {
|
||||
await this.update();
|
||||
this.didUpdateOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected async parseHelmEnv() {
|
||||
const output = await execHelm(["env"]);
|
||||
const lines = output.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 env;
|
||||
}
|
||||
|
||||
public async repo(name: string): Promise<HelmRepo> {
|
||||
const repos = await this.repositories();
|
||||
|
||||
return repos.find((repo) => repo.name === name);
|
||||
}
|
||||
|
||||
private async list(): Promise<HelmRepo[]> {
|
||||
try {
|
||||
const rawConfig = await readFile(
|
||||
this.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 {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const repos = await this.list();
|
||||
|
||||
return repos.map((repo) => ({
|
||||
...repo,
|
||||
cacheFilePath: `${this.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: () => {
|
||||
HelmRepoManager.resetInstance();
|
||||
|
||||
return HelmRepoManager.createInstance();
|
||||
},
|
||||
instantiate: () => new HelmRepoManager(),
|
||||
});
|
||||
|
||||
export default helmRepoManagerInjectable;
|
||||
|
||||
@ -3,13 +3,11 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import { readFile } from "fs-extra";
|
||||
import { Singleton } from "../../common/utils/singleton";
|
||||
import { customRequestPromise } from "../../common/request";
|
||||
import orderBy from "lodash/orderBy";
|
||||
import logger from "../logger";
|
||||
import { execHelm } from "./exec";
|
||||
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 = Record<string, string> & {
|
||||
HELM_REPOSITORY_CACHE?: string;
|
||||
@ -32,144 +30,4 @@ export interface HelmRepo {
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export class HelmRepoManager extends Singleton {
|
||||
protected repos: HelmRepo[];
|
||||
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() {
|
||||
this.helmEnv ??= await this.parseHelmEnv();
|
||||
|
||||
const repos = await this.list();
|
||||
|
||||
if (repos.length === 0) {
|
||||
await this.addRepo({
|
||||
name: "bitnami",
|
||||
url: "https://charts.bitnami.com/bitnami",
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.didUpdateOnce) {
|
||||
await this.update();
|
||||
this.didUpdateOnce = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected async parseHelmEnv() {
|
||||
const output = await execHelm(["env"]);
|
||||
const lines = output.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 env;
|
||||
}
|
||||
|
||||
public async repo(name: string): Promise<HelmRepo> {
|
||||
const repos = await this.repositories();
|
||||
|
||||
return repos.find(repo => repo.name === name);
|
||||
}
|
||||
|
||||
private async list(): Promise<HelmRepo[]> {
|
||||
try {
|
||||
const rawConfig = await readFile(this.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 {
|
||||
await this.ensureInitialized();
|
||||
|
||||
const repos = await this.list();
|
||||
|
||||
return repos.map(repo => ({
|
||||
...repo,
|
||||
cacheFilePath: `${this.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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
export const HelmRepoManager = asLegacyGlobalSingletonForExtensionApi(helmRepoManagerInjectable);
|
||||
|
||||
@ -74,6 +74,8 @@ class NonInjectedHelmCharts extends Component<Dependencies> {
|
||||
render() {
|
||||
return (
|
||||
<SiblingsInTabLayout>
|
||||
<div data-testid="page-for-helm-charts" style={{ display: "none" }}/>
|
||||
|
||||
<ItemListLayout
|
||||
isConfigurable
|
||||
tableId="helm_charts"
|
||||
|
||||
@ -21,7 +21,7 @@ import { Spinner } from "../../spinner";
|
||||
import { Table, TableCell, TableHead, TableRow } from "../../table";
|
||||
import { Button } from "../../button";
|
||||
import { Notifications } from "../../notifications";
|
||||
import { ThemeStore } from "../../../theme.store";
|
||||
import type { ThemeStore } from "../../../theme.store";
|
||||
import { apiManager } from "../../../../common/k8s-api/api-manager";
|
||||
import { SubTitle } from "../../layout/sub-title";
|
||||
import { getDetailsUrl } from "../../kube-detail-params";
|
||||
@ -37,6 +37,7 @@ import releaseValuesInjectable from "./release-values.injectable";
|
||||
import userSuppliedValuesAreShownInjectable from "./user-supplied-values-are-shown.injectable";
|
||||
import type { KubeObject } from "../../../../common/k8s-api/kube-object";
|
||||
import { KubeObjectAge } from "../../kube-object/age";
|
||||
import themeStoreInjectable from "../../../theme-store.injectable";
|
||||
|
||||
export interface ReleaseDetailsProps {
|
||||
hideDetails(): void;
|
||||
@ -49,6 +50,7 @@ interface Dependencies {
|
||||
updateRelease: (name: string, namespace: string, payload: IReleaseUpdatePayload) => Promise<IReleaseUpdateDetails>;
|
||||
createUpgradeChartTab: (release: HelmRelease) => void;
|
||||
userSuppliedValuesAreShown: { toggle: () => void; value: boolean };
|
||||
themeStore: ThemeStore;
|
||||
}
|
||||
|
||||
@observer
|
||||
@ -249,13 +251,13 @@ class NonInjectedReleaseDetails extends Component<ReleaseDetailsProps & Dependen
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hideDetails } = this.props;
|
||||
const { hideDetails, themeStore } = this.props;
|
||||
const title = this.release ? `Release: ${this.release.getName()}` : "";
|
||||
const toolbar = <HelmReleaseMenu release={this.release} toolbar hideDetails={hideDetails}/>;
|
||||
|
||||
return (
|
||||
<Drawer
|
||||
className={cssNames("ReleaseDetails", ThemeStore.getInstance().activeTheme.type)}
|
||||
className={cssNames("ReleaseDetails", themeStore.activeTheme.type)}
|
||||
usePortal={true}
|
||||
open={!!this.release}
|
||||
title={title}
|
||||
@ -281,6 +283,8 @@ export const ReleaseDetails = withInjectables<Dependencies, ReleaseDetailsProps>
|
||||
|
||||
updateRelease: di.inject(updateReleaseInjectable),
|
||||
createUpgradeChartTab: di.inject(createUpgradeChartTabInjectable),
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
|
||||
@ -21,13 +21,16 @@ import { Spinner } from "../spinner";
|
||||
import type { ItemObject, ItemStore } from "../../../common/item.store";
|
||||
import type { Filter } from "./page-filters.store";
|
||||
import { pageFilters } from "./page-filters.store";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import type { ThemeStore } from "../../theme.store";
|
||||
import { MenuActions } from "../menu/menu-actions";
|
||||
import { MenuItem } from "../menu";
|
||||
import { Checkbox } from "../checkbox";
|
||||
import { UserStore } from "../../../common/user-store";
|
||||
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
|
||||
import type { UserStore } from "../../../common/user-store";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import type { OpenConfirmDialog } from "../confirm-dialog/open.injectable";
|
||||
|
||||
import themeStoreInjectable from "../../theme-store.injectable";
|
||||
import userStoreInjectable from "../../../common/user-store/user-store.injectable";
|
||||
import openConfirmDialogInjectable from "../confirm-dialog/open.injectable";
|
||||
|
||||
export interface ItemListLayoutContentProps<I extends ItemObject> {
|
||||
@ -66,12 +69,14 @@ export interface ItemListLayoutContentProps<I extends ItemObject> {
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
userStore: UserStore;
|
||||
openConfirmDialog: OpenConfirmDialog;
|
||||
}
|
||||
|
||||
@observer
|
||||
class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Component<ItemListLayoutContentProps<I> & Dependencies> {
|
||||
constructor(props: ItemListLayoutContentProps<I> & Dependencies) {
|
||||
class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Component<Dependencies & ItemListLayoutContentProps<I>> {
|
||||
constructor(props: Dependencies & ItemListLayoutContentProps<I>) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
@ -255,7 +260,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
|
||||
detailsItem, className, tableProps = {}, tableId, getItems,
|
||||
} = this.props;
|
||||
const selectedItemId = detailsItem && detailsItem.getId();
|
||||
const classNames = cssNames(className, "box", "grow", ThemeStore.getInstance().activeTheme.type);
|
||||
const classNames = cssNames(className, "box", "grow", this.props.themeStore.activeTheme.type);
|
||||
const items = getItems();
|
||||
const selectedItems = store.pickOnlySelected(items);
|
||||
|
||||
@ -298,7 +303,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
|
||||
showColumn({ id: columnId, showWithColumn }: TableCellProps): boolean {
|
||||
const { tableId, isConfigurable } = this.props;
|
||||
|
||||
return !isConfigurable || !UserStore.getInstance().isTableColumnHidden(tableId, columnId, showWithColumn);
|
||||
return !isConfigurable || !this.props.userStore.isTableColumnHidden(tableId, columnId, showWithColumn);
|
||||
}
|
||||
|
||||
renderColumnVisibilityMenu() {
|
||||
@ -312,7 +317,7 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
|
||||
<Checkbox
|
||||
label={cellProps.title ?? `<${cellProps.className}>`}
|
||||
value={this.showColumn(cellProps)}
|
||||
onChange={() => UserStore.getInstance().toggleTableColumnVisibility(tableId, cellProps.id)}
|
||||
onChange={() => this.props.userStore.toggleTableColumnVisibility(tableId, cellProps.id)}
|
||||
/>
|
||||
</MenuItem>
|
||||
)
|
||||
@ -322,9 +327,20 @@ class NonInjectedItemListLayoutContent<I extends ItemObject> extends React.Compo
|
||||
}
|
||||
}
|
||||
|
||||
export const ItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject>>(NonInjectedItemListLayoutContent, {
|
||||
getProps: (di, props) => ({
|
||||
...props,
|
||||
openConfirmDialog: di.inject(openConfirmDialogInjectable),
|
||||
}),
|
||||
}) as <I extends ItemObject>(props: ItemListLayoutContentProps<I>) => React.ReactElement;
|
||||
const InjectedItemListLayoutContent = withInjectables<Dependencies, ItemListLayoutContentProps<ItemObject>>(
|
||||
NonInjectedItemListLayoutContent,
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
userStore: di.inject(userStoreInjectable),
|
||||
openConfirmDialog: di.inject(openConfirmDialogInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
export function ItemListLayoutContent<I extends ItemObject>(
|
||||
props: ItemListLayoutContentProps<I>,
|
||||
) {
|
||||
return <InjectedItemListLayoutContent {...props} />;
|
||||
}
|
||||
|
||||
@ -18,9 +18,11 @@ import ReactSelect, {
|
||||
type StylesConfig,
|
||||
} from "react-select";
|
||||
import ReactSelectCreatable, { type CreatableProps } from "react-select/creatable";
|
||||
import { ThemeStore } from "../../theme.store";
|
||||
import type { ThemeStore } from "../../theme.store";
|
||||
import { autoBind, cssNames } from "../../utils";
|
||||
import { merge } from "lodash";
|
||||
import { withInjectables } from "@ogre-tools/injectable-react";
|
||||
import themeStoreInjectable from "../../theme-store.injectable";
|
||||
|
||||
const { Menu } = components;
|
||||
|
||||
@ -34,6 +36,10 @@ export interface SelectOption<T = any> {
|
||||
label?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
themeStore: ThemeStore;
|
||||
}
|
||||
|
||||
export interface SelectProps<T = any> extends ReactSelectProps<T, boolean>, CreatableProps<T, boolean, GroupBase<T>> {
|
||||
id?: string; // Optional only because of Extension API. Required to make Select deterministic in unit tests
|
||||
value?: T;
|
||||
@ -45,21 +51,21 @@ export interface SelectProps<T = any> extends ReactSelectProps<T, boolean>, Crea
|
||||
}
|
||||
|
||||
@observer
|
||||
export class Select extends React.Component<SelectProps> {
|
||||
class NonInjectedSelect extends React.Component<SelectProps & Dependencies> {
|
||||
static defaultProps: Omit<SelectProps, "id"> = {
|
||||
autoConvertOptions: true,
|
||||
menuPortalTarget: document.body,
|
||||
menuPlacement: "auto",
|
||||
};
|
||||
|
||||
constructor(props: SelectProps) {
|
||||
constructor(props: SelectProps & Dependencies) {
|
||||
super(props);
|
||||
makeObservable(this);
|
||||
autoBind(this);
|
||||
}
|
||||
|
||||
@computed get themeClass() {
|
||||
const themeName = this.props.themeName || ThemeStore.getInstance().activeTheme.type;
|
||||
const themeName = this.props.themeName || this.props.themeStore.activeTheme.type;
|
||||
|
||||
return `theme-${themeName}`;
|
||||
}
|
||||
@ -153,3 +159,14 @@ export class Select extends React.Component<SelectProps> {
|
||||
: <ReactSelect {...selectProps} />;
|
||||
}
|
||||
}
|
||||
|
||||
export const Select = withInjectables<Dependencies, SelectProps>(
|
||||
NonInjectedSelect,
|
||||
|
||||
{
|
||||
getProps: (di, props) => ({
|
||||
themeStore: di.inject(themeStoreInjectable),
|
||||
...props,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
@ -33,6 +33,10 @@ import preferenceNavigationItemsInjectable from "../+preferences/preferences-nav
|
||||
import navigateToPreferencesInjectable from "../../../common/front-end-routing/routes/preferences/navigate-to-preferences.injectable";
|
||||
import type { MenuItemOpts } from "../../../main/menu/application-menu-items.injectable";
|
||||
import applicationMenuItemsInjectable from "../../../main/menu/application-menu-items.injectable";
|
||||
import navigateToHelmChartsInjectable from "../../../common/front-end-routing/routes/cluster/helm/charts/navigate-to-helm-charts.injectable";
|
||||
import clusterFrameContextInjectable from "../../cluster-frame-context/cluster-frame-context.injectable";
|
||||
import { KubeObjectStore } from "../../../common/k8s-api/kube-object.store";
|
||||
import hostedClusterInjectable from "../../../common/cluster-store/hosted-cluster.injectable";
|
||||
|
||||
type Callback = (dis: DiContainers) => void | Promise<void>;
|
||||
|
||||
@ -56,6 +60,10 @@ export interface ApplicationBuilder {
|
||||
click: (id: string) => void;
|
||||
};
|
||||
};
|
||||
|
||||
helmCharts: {
|
||||
navigate: () => void;
|
||||
};
|
||||
}
|
||||
|
||||
interface DiContainers {
|
||||
@ -192,6 +200,14 @@ export const getApplicationBuilder = () => {
|
||||
},
|
||||
},
|
||||
|
||||
helmCharts: {
|
||||
navigate: () => {
|
||||
const navigateToHelmCharts = rendererDi.inject(navigateToHelmChartsInjectable);
|
||||
|
||||
navigateToHelmCharts();
|
||||
},
|
||||
},
|
||||
|
||||
setEnvironmentToClusterFrame: () => {
|
||||
environment = environments.clusterFrame;
|
||||
|
||||
@ -206,6 +222,17 @@ export const getApplicationBuilder = () => {
|
||||
() => "/irrelevant",
|
||||
);
|
||||
|
||||
rendererDi.override(hostedClusterInjectable, () => ({
|
||||
accessibleNamespaces: [],
|
||||
}));
|
||||
|
||||
const clusterFrameContext = rendererDi.inject(
|
||||
clusterFrameContextInjectable,
|
||||
);
|
||||
|
||||
// Todo: get rid of global state.
|
||||
KubeObjectStore.defaultContext.set(clusterFrameContext);
|
||||
|
||||
return builder;
|
||||
},
|
||||
|
||||
|
||||
@ -33,6 +33,9 @@ import { getAbsolutePathFake } from "../common/test-utils/get-absolute-path-fake
|
||||
import joinPathsInjectable from "../common/path/join-paths.injectable";
|
||||
import { joinPathsFake } from "../common/test-utils/join-paths-fake";
|
||||
import hotbarStoreInjectable from "../common/hotbar-store.injectable";
|
||||
import themeStoreInjectable from "./theme-store.injectable";
|
||||
import apiManagerInjectable from "./components/kube-object-menu/dependencies/api-manager.injectable";
|
||||
import { ApiManager } from "../common/k8s-api/api-manager";
|
||||
|
||||
export const getDiForUnitTesting = (
|
||||
{ doGeneralOverrides } = { doGeneralOverrides: false },
|
||||
@ -69,7 +72,16 @@ export const getDiForUnitTesting = (
|
||||
|
||||
// eslint-disable-next-line unused-imports/no-unused-vars-ts
|
||||
di.override(clusterStoreInjectable, () => ({ getById: (id): Cluster => ({}) as Cluster }) as ClusterStore);
|
||||
di.override(userStoreInjectable, () => ({}) as UserStore);
|
||||
|
||||
di.override(
|
||||
userStoreInjectable,
|
||||
() =>
|
||||
({
|
||||
isTableColumnHidden: () => false,
|
||||
} as unknown as UserStore),
|
||||
);
|
||||
|
||||
di.override(apiManagerInjectable, () => new ApiManager());
|
||||
|
||||
di.override(getValueFromRegisteredChannelInjectable, () => () => undefined);
|
||||
di.override(registerIpcChannelListenerInjectable, () => () => undefined);
|
||||
@ -92,6 +104,13 @@ export const getDiForUnitTesting = (
|
||||
error: (message: string, ...args: any) => console.error(message, ...args),
|
||||
info: noop,
|
||||
}));
|
||||
|
||||
di.override(themeStoreInjectable, () => ({
|
||||
activeTheme: {
|
||||
type: "some-active-theme-type",
|
||||
},
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
return di;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user