Merge branch 'master' into wrap-pod-logs

This commit is contained in:
Alex Andreev committed 2022-11-28 10:11:28 +03:00
commit 31c7ae118d
1328 files changed
+73244 -43415

No files matched your search

+2 -2
View File
@@ -18,13 +18,13 @@ import { createClusterInjectionToken } from "../cluster/create-cluster-injection
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../vars/app-version.injectable";
import assert from "assert";
import directoryForTempInjectable from "../app-paths/directory-for-temp/directory-for-temp.injectable";
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
import fsInjectable from "../fs/fs.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
console = new Console(stdout, stderr);
@@ -372,7 +372,7 @@ users:
mockFs(mockOpts);
mainDi.override(appVersionInjectable, () => "3.6.0");
mainDi.override(storeMigrationVersionInjectable, () => "3.6.0");
createCluster = mainDi.inject(createClusterInjectionToken);
-27
View File
@@ -1,27 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AppEvent } from "../app-event-bus/event-bus";
import { appEventBus } from "../app-event-bus/event-bus";
import { assert, Console } from "console";
import { stdout, stderr } from "process";
console = new Console(stdout, stderr);
describe("event bus tests", () => {
describe("emit", () => {
it("emits an event", () => {
let event: AppEvent | undefined;
appEventBus.addListener((data) => {
event = data;
});
appEventBus.emit({ name: "foo", action: "bar" });
assert(event);
expect(event?.name).toBe("foo");
});
});
});
+2 -2
View File
@@ -8,7 +8,6 @@ import mockFs from "mock-fs";
import type { CatalogEntity, CatalogEntityData, CatalogEntityKindData } from "../catalog";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../vars/app-version.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import hotbarStoreInjectable from "../hotbars/store.injectable";
import type { HotbarStore } from "../hotbars/store";
@@ -19,6 +18,7 @@ import catalogCatalogEntityInjectable from "../catalog-entities/general-catalog-
import loggerInjectable from "../logger.injectable";
import type { Logger } from "../logger";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
function getMockCatalogEntity(data: Partial<CatalogEntityData> & CatalogEntityKindData): CatalogEntity {
return {
@@ -348,7 +348,7 @@ describe("HotbarStore", () => {
mockFs(configurationToBeMigrated);
di.override(appVersionInjectable, () => "5.0.0-beta.10");
di.override(storeMigrationVersionInjectable, () => "5.0.0-beta.10");
hotbarStore = di.inject(hotbarStoreInjectable);
-99
View File
@@ -1,99 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import https from "https";
import os from "os";
import { getMacRootCA, getWinRootCA, injectCAs, DSTRootCAX3 } from "../system-ca";
import { dependencies, devDependencies } from "../../../package.json";
import assert from "assert";
const deps = { ...dependencies, ...devDependencies };
// Skip the test if mac-ca is not installed, or os is not darwin
(deps["mac-ca"] && os.platform().includes("darwin") ? describe: describe.skip)("inject CA for Mac", () => {
// for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => {
_ca = https.globalAgent.options.ca;
});
afterEach(() => {
https.globalAgent.options.ca = _ca;
});
/**
* The test to ensure using getMacRootCA + injectCAs injects CAs in the same way as using
* the auto injection (require('mac-ca'))
*/
it("should inject the same ca as mac-ca", async () => {
const osxCAs = await getMacRootCA();
injectCAs(osxCAs);
const injected = https.globalAgent.options.ca as (string | Buffer)[];
await import("mac-ca");
const injectedByMacCA = https.globalAgent.options.ca as (string | Buffer)[];
expect(new Set(injected)).toEqual(new Set(injectedByMacCA));
});
it("shouldn't included the expired DST Root CA X3 on Mac", async () => {
const osxCAs = await getMacRootCA();
injectCAs(osxCAs);
const injected = https.globalAgent.options.ca;
assert(injected);
expect(injected.includes(DSTRootCAX3)).toBeFalsy();
});
});
// Skip the test if win-ca is not installed, or os is not win32
(deps["win-ca"] && os.platform().includes("win32") ? describe: describe.skip)("inject CA for Windows", () => {
// for reset https.globalAgent.options.ca after testing
let _ca: string | Buffer | (string | Buffer)[] | undefined;
beforeEach(() => {
_ca = https.globalAgent.options.ca;
});
afterEach(() => {
https.globalAgent.options.ca = _ca;
});
/**
* The test to ensure using win-ca/api injects CAs in the same way as using
* the auto injection (require('win-ca').inject('+'))
*/
it("should inject the same ca as winca.inject('+')", async () => {
const winCAs = await getWinRootCA();
const wincaAPI = await import("win-ca/api");
wincaAPI.inject("+", winCAs);
const injected = https.globalAgent.options.ca as (string | Buffer)[];
const winca = await import("win-ca");
winca.inject("+"); // see: https://github.com/ukoloff/win-ca#caveats
const injectedByWinCA = https.globalAgent.options.ca as (string | Buffer)[];
expect(new Set(injected)).toEqual(new Set(injectedByWinCA));
});
it("shouldn't included the expired DST Root CA X3 on Windows", async () => {
const winCAs = await getWinRootCA();
const wincaAPI = await import("win-ca/api");
wincaAPI.inject("true", winCAs);
const injected = https.globalAgent.options.ca as (string | Buffer)[];
expect(injected.includes(DSTRootCAX3)).toBeFalsy();
});
});
+13 -12
View File
@@ -23,8 +23,6 @@ jest.mock("electron", () => ({
import type { UserStore } from "../user-store";
import { Console } from "console";
import { SemVer } from "semver";
import electron from "electron";
import { stdout, stderr } from "process";
import userStoreInjectable from "../user-store/user-store.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
@@ -34,7 +32,9 @@ import { defaultThemeId } from "../vars";
import writeFileInjectable from "../fs/write-file.injectable";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "../vars/app-version.injectable";
import storeMigrationVersionInjectable from "../vars/store-migration-version.injectable";
import releaseChannelInjectable from "../vars/release-channel.injectable";
import defaultUpdateChannelInjectable from "../../features/application-update/common/selected-update-channel/default-update-channel.injectable";
console = new Console(stdout, stderr);
@@ -42,7 +42,7 @@ describe("user store tests", () => {
let userStore: UserStore;
let di: DiContainer;
beforeEach(() => {
beforeEach(async () => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
@@ -52,6 +52,12 @@ describe("user store tests", () => {
di.permitSideEffects(getConfigurationFileModelInjectable);
di.permitSideEffects(userStoreInjectable);
di.override(releaseChannelInjectable, () => ({
get: () => "latest" as const,
init: async () => {},
}));
await di.inject(defaultUpdateChannelInjectable).init();
di.unoverride(userStoreInjectable);
});
@@ -64,6 +70,7 @@ describe("user store tests", () => {
mockFs({ "some-directory-for-user-data": { "config.json": "{}", "kube_config": "{}" }});
userStore = di.inject(userStoreInjectable);
userStore.load();
});
it("allows setting and retrieving lastSeenAppVersion", () => {
@@ -86,13 +93,6 @@ describe("user store tests", () => {
userStore.resetTheme();
expect(userStore.colorTheme).toBe(defaultThemeId);
});
it("correctly calculates if the last seen version is an old release", () => {
expect(userStore.isNewVersion).toBe(true);
userStore.lastSeenAppVersion = (new SemVer(electron.app.getVersion())).inc("major").format();
expect(userStore.isNewVersion).toBe(false);
});
});
describe("migrations", () => {
@@ -125,9 +125,10 @@ describe("user store tests", () => {
},
});
di.override(appVersionInjectable, () => "10.0.0");
di.override(storeMigrationVersionInjectable, () => "10.0.0");
userStore = di.inject(userStoreInjectable);
userStore.load();
});
it("sets last seen app version to 0.0.0", () => {
@@ -3,12 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { appEventBus } from "./event-bus";
import { EventEmitter } from "../event-emitter";
import type { AppEvent } from "./event-bus";
const appEventBusInjectable = getInjectable({
id: "app-event-bus",
instantiate: () => appEventBus,
causesSideEffects: true,
instantiate: () => new EventEmitter<[AppEvent]>,
decorable: false,
});
@@ -4,11 +4,18 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import appEventBusInjectable from "./app-event-bus.injectable";
import type { AppEvent } from "./event-bus";
const emitEventInjectable = getInjectable({
id: "emit-event",
instantiate: (di) => di.inject(appEventBusInjectable).emit,
export type EmitAppEvent = (event: AppEvent) => void;
const emitAppEventInjectable = getInjectable({
id: "emit-app-event",
instantiate: (di): EmitAppEvent => {
const bus = di.inject(appEventBusInjectable);
return (event) => bus.emit(event);
},
decorable: false,
});
export default emitEventInjectable;
export default emitAppEventInjectable;
+3 -4
View File
@@ -3,13 +3,12 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { EventEmitter } from "../event-emitter";
/**
* Data for telemetry
*/
export interface AppEvent {
name: string;
action: string;
destination?: string;
params?: Record<string, any>;
}
export const appEventBus = new EventEmitter<[AppEvent]>();
@@ -1,22 +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 type { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AppPathsChannel = RequestChannel<void, AppPaths>;
const appPathsChannelInjectable = getInjectable({
id: "app-paths-channel",
instantiate: (): AppPathsChannel => ({
id: "app-paths",
}),
injectionToken: messageChannelInjectionToken,
});
export default appPathsChannelInjectable;
+13
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 type { AppPaths } from "./app-path-injection-token";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
export type AppPathsChannel = RequestChannel<void, AppPaths>;
export const appPathsChannel: AppPathsChannel = {
id: "app-paths",
};
+4 -7
View File
@@ -7,7 +7,6 @@ import { appPathsInjectionToken } from "./app-path-injection-token";
import getElectronAppPathInjectable from "../../main/app-paths/get-electron-app-path/get-electron-app-path.injectable";
import type { PathName } from "./app-path-names";
import setElectronAppPathInjectable from "../../main/app-paths/set-electron-app-path/set-electron-app-path.injectable";
import appNameInjectable from "../../main/app-paths/app-name/app-name.injectable";
import directoryForIntegrationTestingInjectable from "../../main/app-paths/directory-for-integration-testing/directory-for-integration-testing.injectable";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
@@ -53,8 +52,6 @@ describe("app-paths", () => {
defaultAppPathsStub[key] = path;
},
);
mainDi.override(appNameInjectable, () => "some-app-name");
});
});
@@ -88,7 +85,7 @@ describe("app-paths", () => {
recent: "some-recent",
temp: "some-temp",
videos: "some-videos",
userData: "some-app-data/some-app-name",
userData: "some-app-data/some-product-name",
});
});
@@ -111,7 +108,7 @@ describe("app-paths", () => {
recent: "some-recent",
temp: "some-temp",
videos: "some-videos",
userData: "some-app-data/some-app-name",
userData: "some-app-data/some-product-name",
});
});
});
@@ -137,7 +134,7 @@ describe("app-paths", () => {
expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data",
userData: `some-integration-testing-app-data/some-app-name`,
userData: `some-integration-testing-app-data/some-product-name`,
});
});
@@ -146,7 +143,7 @@ describe("app-paths", () => {
expect({ appData, userData }).toEqual({
appData: "some-integration-testing-app-data",
userData: "some-integration-testing-app-data/some-app-name",
userData: "some-integration-testing-app-data/some-product-name",
});
});
});
@@ -4,16 +4,16 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable";
import joinPathsInjectable from "../../path/join-paths.injectable";
const directoryForBinariesInjectable = getInjectable({
id: "directory-for-binaries",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForUserData = di.inject(directoryForUserDataInjectable);
return getAbsolutePath(directoryForUserData, "binaries");
return joinPaths(directoryForUserData, "binaries");
},
});
@@ -4,19 +4,16 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../directory-for-user-data/directory-for-user-data.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable";
import joinPathsInjectable from "../../path/join-paths.injectable";
const directoryForKubeConfigsInjectable = getInjectable({
id: "directory-for-kube-configs",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForUserData = di.inject(directoryForUserDataInjectable);
return getAbsolutePath(
directoryForUserData,
"kubeconfigs",
);
return joinPaths(directoryForUserData, "kubeconfigs");
},
});
@@ -4,17 +4,16 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForBinariesInjectable from "../directory-for-binaries/directory-for-binaries.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable";
import joinPathsInjectable from "../../path/join-paths.injectable";
const directoryForKubectlBinariesInjectable = getInjectable({
id: "directory-for-kubectl-binaries",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForBinaries = di.inject(directoryForBinariesInjectable);
return getAbsolutePath(directoryForBinaries, "kubectl");
return joinPaths(directoryForBinaries, "kubectl");
},
});
@@ -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 appPathsInjectable from "./app-paths.injectable";
const directoryForLogsInjectable = getInjectable({
id: "directory-for-logs",
instantiate: (di) => di.inject(appPathsInjectable).logs,
});
export default directoryForLogsInjectable;
@@ -4,17 +4,16 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../directory-for-kube-configs/directory-for-kube-configs.injectable";
import getAbsolutePathInjectable from "../../path/get-absolute-path.injectable";
import joinPathsInjectable from "../../path/join-paths.injectable";
const getCustomKubeConfigDirectoryInjectable = getInjectable({
id: "get-custom-kube-config-directory",
instantiate: (di) => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
return (directoryName: string) =>
getAbsolutePath(directoryForKubeConfigs, directoryName);
return (directoryName: string) => joinPaths(directoryForKubeConfigs, directoryName);
},
});
@@ -1,21 +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 type { MessageChannel } from "../../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../../utils/channel/message-channel-injection-token";
export type RestartAndInstallUpdateChannel = MessageChannel;
const restartAndInstallUpdateChannel = getInjectable({
id: "restart-and-install-update-channel",
instantiate: (): RestartAndInstallUpdateChannel => ({
id: "restart-and-install-update-channel",
}),
injectionToken: messageChannelInjectionToken,
});
export default restartAndInstallUpdateChannel;
@@ -1,25 +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 appSemanticVersionInjectable from "../../vars/app-semantic-version.injectable";
import type { UpdateChannelId } from "../update-channels";
import { updateChannels } from "../update-channels";
const defaultUpdateChannelInjectable = getInjectable({
id: "default-update-channel",
instantiate: (di) => {
const appSemanticVersion = di.inject(appSemanticVersionInjectable);
const currentReleaseChannel = appSemanticVersion.prerelease[0]?.toString();
if (currentReleaseChannel in updateChannels) {
return updateChannels[currentReleaseChannel as UpdateChannelId];
}
return updateChannels.latest;
},
});
export default defaultUpdateChannelInjectable;
+2 -2
View File
@@ -19,7 +19,7 @@ import { kebabCase } from "lodash";
import { getLegacyGlobalDiForExtensionApi } from "../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import directoryForUserDataInjectable from "./app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getConfigurationFileModelInjectable from "./get-configuration-file-model/get-configuration-file-model.injectable";
import appVersionInjectable from "./vars/app-version.injectable";
import storeMigrationVersionInjectable from "./vars/store-migration-version.injectable";
export interface BaseStoreParams<T> extends ConfOptions<T> {
syncOptions?: {
@@ -60,7 +60,7 @@ export abstract class BaseStore<T extends object> extends Singleton {
this.storeConfig = getConfigurationFileModel({
projectName: "lens",
projectVersion: di.inject(appVersionInjectable),
projectVersion: di.inject(storeMigrationVersionInjectable),
cwd: this.cwd(),
...this.params,
});
+5 -1
View File
@@ -3,9 +3,10 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import type { CatalogEntityContextMenuContext, CatalogEntityMetadata, CatalogEntityStatus } from "../catalog";
import { CatalogCategory, CatalogEntity, categoryVersion } from "../catalog/catalog-entity";
import { productName } from "../vars";
import productNameInjectable from "../vars/product-name.injectable";
import { WeblinkStore } from "../weblink-store";
export type WebLinkStatusPhase = "available" | "unavailable";
@@ -30,6 +31,9 @@ export class WebLink extends CatalogEntity<CatalogEntityMetadata, WebLinkStatus,
}
onContextMenuOpen(context: CatalogEntityContextMenuContext) {
const di = getLegacyGlobalDiForExtensionApi();
const productName = di.inject(productNameInjectable);
if (this.metadata.source === "local") {
context.menuItems.push({
title: "Delete",
@@ -0,0 +1,18 @@
/**
* 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 { computed } from "mobx";
import catalogCategoryRegistryInjectable from "./category-registry.injectable";
const filteredCategoriesInjectable = getInjectable({
id: "filtered-categories",
instantiate: (di) => {
const registry = di.inject(catalogCategoryRegistryInjectable);
return computed(() => [...registry.filteredItems]);
},
});
export default filteredCategoriesInjectable;
@@ -0,0 +1,60 @@
/**
* 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 { globalAgent } from "https";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
// DST Root CA X3, which was expired on 9.30.2021
const DSTRootCAX3 = "-----BEGIN CERTIFICATE-----\nMIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/\nMSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT\nDkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow\nPzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD\nEw5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB\nAN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O\nrz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq\nOLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b\nxiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw\n7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD\naeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV\nHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG\nSIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69\nikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr\nAvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz\nR8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5\nJDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo\nOb8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ\n-----END CERTIFICATE-----\n";
function isCertActive(cert: string) {
const isExpired = typeof cert !== "string" || cert.includes(DSTRootCAX3);
return !isExpired;
}
const injectSystemCAsInjectable = getInjectable({
id: "inject-system-cas",
instantiate: (di) => {
const requestSystemCAs = di.inject(requestSystemCAsInjectionToken);
return async () => {
const certs = await requestSystemCAs();
if (certs.length === 0) {
// Leave the global option alone
return;
}
const cas = (() => {
if (Array.isArray(globalAgent.options.ca)) {
return globalAgent.options.ca;
}
if (globalAgent.options.ca) {
return [globalAgent.options.ca];
}
return [];
})();
for (const cert of certs) {
if (!isCertActive(cert)) {
continue;
}
if (!cas.includes(cert)) {
cas.push(cert);
}
}
globalAgent.options.ca = cas;
};
},
});
export default injectSystemCAsInjectable;
@@ -0,0 +1,10 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectionToken } from "@ogre-tools/injectable";
export const requestSystemCAsInjectionToken = getInjectionToken<() => Promise<string[]>>({
id: "request-system-cas-token",
});
@@ -0,0 +1,57 @@
/**
* 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 "../fs/exec-file.injectable";
import loggerInjectable from "../logger.injectable";
import type { AsyncResult } from "../utils/async-result";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Cheatsheet#other_assertions
const certSplitPattern = /(?=-----BEGIN\sCERTIFICATE-----)/g;
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: (di) => {
const execFile = di.inject(execFileInjectable);
const logger = di.inject(loggerInjectable);
const execSecurity = async (...args: string[]): Promise<AsyncResult<string[]>> => {
const result = await execFile("/usr/bin/security", args);
if (!result.callWasSuccessful) {
return {
callWasSuccessful: false,
error: result.error.stderr || result.error.message,
};
}
return {
callWasSuccessful: true,
response: result.response.split(certSplitPattern),
};
};
return async () => {
const [trustedResult, rootCAResult] = await Promise.all([
execSecurity("find-certificate", "-a", "-p"),
execSecurity("find-certificate", "-a", "-p", "/System/Library/Keychains/SystemRootCertificates.keychain"),
]);
if (!trustedResult.callWasSuccessful) {
logger.warn(`[INJECT-CAS]: Error retreiving trusted CAs: ${trustedResult.error}`);
} else if (!rootCAResult.callWasSuccessful) {
logger.warn(`[INJECT-CAS]: Error retreiving root CAs: ${rootCAResult.error}`);
} else {
return [...new Set([...trustedResult.response, ...rootCAResult.response])];
}
return [];
};
},
causesSideEffects: true,
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;
@@ -0,0 +1,14 @@
/**
* 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 { requestSystemCAsInjectionToken } from "./request-system-cas-token";
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: () => async () => [],
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;
@@ -0,0 +1,14 @@
/**
* 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 { requestSystemCAsInjectionToken } from "./request-system-cas-token";
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: () => async () => [],
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;
@@ -0,0 +1,56 @@
/**
* 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 "../fs/exec-file.injectable";
import loggerInjectable from "../logger.injectable";
import { requestSystemCAsInjectionToken } from "./request-system-cas-token";
const pemEncoding = (hexEncodedCert: String) => {
const certData = Buffer.from(hexEncodedCert, "hex").toString("base64");
const lines = ["-----BEGIN CERTIFICATE-----"];
for (let i = 0; i < certData.length; i += 64) {
lines.push(certData.substring(i, i + 64));
}
lines.push("-----END CERTIFICATE-----", "");
return lines.join("\r\n");
};
const requestSystemCAsInjectable = getInjectable({
id: "request-system-cas",
instantiate: (di) => {
const wincaRootsExePath: string = __non_webpack_require__.resolve("win-ca/lib/roots.exe");
const execFile = di.inject(execFileInjectable);
const logger = di.inject(loggerInjectable);
return async () => {
/**
* This needs to be done manually because for some reason calling the api from "win-ca"
* directly fails to load "child_process" correctly on renderer
*/
const result = await execFile(wincaRootsExePath, {
maxBuffer: 128 * 1024 * 1024, // 128 MiB
});
if (!result.callWasSuccessful) {
logger.warn(`[INJECT-CAS]: Error retreiving CAs`, result.error);
return [];
}
return result
.response
.split("\r\n")
.filter(Boolean)
.map(pemEncoding);
};
},
causesSideEffects: true,
injectionToken: requestSystemCAsInjectionToken,
});
export default requestSystemCAsInjectable;
@@ -6,6 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { ClusterStore } from "./cluster-store";
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
import emitAppEventInjectable from "../app-event-bus/emit-event.injectable";
const clusterStoreInjectable = getInjectable({
id: "cluster-store",
@@ -16,6 +17,7 @@ const clusterStoreInjectable = getInjectable({
return ClusterStore.createInstance({
createCluster: di.inject(createClusterInjectionToken),
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
emitAppEvent: di.inject(emitAppEventInjectable),
});
},
+4 -3
View File
@@ -10,7 +10,6 @@ import { BaseStore } from "../base-store";
import { Cluster } from "../cluster/cluster";
import migrations from "../../migrations/cluster-store";
import logger from "../../main/logger";
import { appEventBus } from "../app-event-bus/event-bus";
import { ipcMainHandle } from "../ipc";
import { disposer, toJS } from "../utils";
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
@@ -18,6 +17,7 @@ import { requestInitialClusterStates } from "../../renderer/ipc";
import { clusterStates } from "../ipc/cluster";
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
import type { EmitAppEvent } from "../app-event-bus/emit-event.injectable";
export interface ClusterStoreModel {
clusters?: ClusterModel[];
@@ -26,6 +26,7 @@ export interface ClusterStoreModel {
interface Dependencies {
createCluster: CreateCluster;
readClusterConfigSync: ReadClusterConfigSync;
emitAppEvent: EmitAppEvent;
}
export class ClusterStore extends BaseStore<ClusterStoreModel> {
@@ -34,7 +35,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
protected disposer = disposer();
constructor(private dependencies: Dependencies) {
constructor(private readonly dependencies: Dependencies) {
super({
configName: "lens-cluster-store",
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
@@ -115,7 +116,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
addCluster(clusterOrModel: ClusterModel | Cluster): Cluster {
appEventBus.emit({ name: "cluster", action: "add" });
this.dependencies.emitAppEvent({ name: "cluster", action: "add" });
const cluster = clusterOrModel instanceof Cluster
? clusterOrModel
@@ -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 type { ClusterId } from "../cluster-types";
import type { Cluster } from "../cluster/cluster";
import clusterStoreInjectable from "./cluster-store.injectable";
export type GetClusterById = (id: ClusterId) => Cluster | undefined;
const getClusterByIdInjectable = getInjectable({
id: "get-cluster-by-id",
instantiate: (di): GetClusterById => {
const store = di.inject(clusterStoreInjectable);
return (id) => store.getById(id);
},
});
export default getClusterByIdInjectable;
+33 -7
View File
@@ -3,6 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import Joi from "joi";
/**
* JSON serializable metadata type
*/
@@ -27,6 +29,37 @@ export type ClusterId = string;
*/
export type UpdateClusterModel = Omit<ClusterModel, "id">;
/**
* A type validator for `UpdateClusterModel` so that only expected types are present
*/
export const updateClusterModelChecker = Joi.object<UpdateClusterModel>({
kubeConfigPath: Joi.string()
.required()
.min(1),
contextName: Joi.string()
.required()
.min(1),
workspace: Joi.string()
.optional(),
workspaces: Joi.array()
.items(Joi.string()),
preferences: Joi.object(),
metadata: Joi.object(),
accessibleNamespaces: Joi.array()
.items(Joi.string()),
labels: Joi.object().pattern(Joi.string(), Joi.string()),
});
/**
* A type validator for just the `id` fields of `ClusterModel`. The rest is
* covered by `updateClusterModelChecker`
*/
export const clusterModelIdChecker = Joi.object<Pick<ClusterModel, "id">>({
id: Joi.string()
.required()
.min(1),
});
/**
* The model for passing cluster data around, including to disk
*/
@@ -162,13 +195,6 @@ export enum ClusterMetricsResourceType {
*/
export const initialNodeShellImage = "docker.io/alpine:3.13";
/**
* The arguments for requesting to refresh a cluster's metadata
*/
export interface ClusterRefreshOptions {
refreshMetadata?: boolean;
}
/**
* The data representing a cluster's state, for passing between main and renderer
*/
@@ -0,0 +1,87 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource } from "../rbac";
/**
* Requests the permissions for actions on the kube cluster
* @param namespace The namespace of the resources
* @param availableResources List of available resources in the cluster to resolve glob values fir api groups
* @returns list of allowed resources names
*/
export type RequestNamespaceResources = (namespace: string, availableResources: KubeApiResource[]) => Promise<string[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type AuthorizationNamespaceReview = (proxyConfig: KubeConfig) => RequestNamespaceResources;
interface Dependencies {
logger: Logger;
}
const authorizationNamespaceReview = ({ logger }: Dependencies): AuthorizationNamespaceReview => {
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (namespace, availableResources) => {
try {
const { body } = await api.createSelfSubjectRulesReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectRulesReview",
spec: { namespace },
});
const resources = new Set<string>();
body.status?.resourceRules.forEach(resourceRule => {
if (!resourceRule.verbs.some(verb => ["*", "list"].includes(verb)) || !resourceRule.resources) {
return;
}
const apiGroups = resourceRule.apiGroups;
if (resourceRule.resources.length === 1 && resourceRule.resources[0] === "*" && apiGroups) {
if (apiGroups[0] === "*") {
availableResources.forEach(resource => resources.add(resource.apiName));
} else {
availableResources.forEach((apiResource)=> {
if (apiGroups.includes(apiResource.group || "")) {
resources.add(apiResource.apiName);
}
});
}
} else {
resourceRule.resources.forEach(resource => resources.add(resource));
}
});
return [...resources];
} catch (error) {
logger.error(`[AUTHORIZATION-NAMESPACE-REVIEW]: failed to create subject rules review: ${error}`, { namespace });
return [];
}
};
};
};
const authorizationNamespaceReviewInjectable = getInjectable({
id: "authorization-namespace-review",
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return authorizationNamespaceReview({ logger });
},
});
export default authorizationNamespaceReviewInjectable;
@@ -5,42 +5,55 @@
import type { KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
import { AuthorizationV1Api } from "@kubernetes/client-node";
import logger from "../logger";
import { getInjectable } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
/**
* Requests the permissions for actions on the kube cluster
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
* @returns `true` if the actions described are allowed
*/
export type CanI = (resourceAttributes: V1ResourceAttributes) => Promise<boolean>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export function authorizationReview(proxyConfig: KubeConfig): CanI {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
*/
export type AuthorizationReview = (proxyConfig: KubeConfig) => CanI;
/**
* Requests the permissions for actions on the kube cluster
* @param resourceAttributes The descriptor of the action that is desired to be known if it is allowed
* @returns `true` if the actions described are allowed
*/
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
try {
const { body } = await api.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes },
});
return body.status?.allowed ?? false;
} catch (error) {
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
return false;
}
};
interface Dependencies {
logger: Logger;
}
const authorizationReview = ({ logger }: Dependencies): AuthorizationReview => {
return (proxyConfig) => {
const api = proxyConfig.makeApiClient(AuthorizationV1Api);
return async (resourceAttributes: V1ResourceAttributes): Promise<boolean> => {
try {
const { body } = await api.createSelfSubjectAccessReview({
apiVersion: "authorization.k8s.io/v1",
kind: "SelfSubjectAccessReview",
spec: { resourceAttributes },
});
return body.status?.allowed ?? false;
} catch (error) {
logger.error(`[AUTHORIZATION-REVIEW]: failed to create access review: ${error}`, { resourceAttributes });
return false;
}
};
};
};
const authorizationReviewInjectable = getInjectable({
id: "authorization-review",
instantiate: () => authorizationReview,
instantiate: (di) => {
const logger = di.inject(loggerInjectable);
return authorizationReview({ logger });
},
});
export default authorizationReviewInjectable;
+100 -64
View File
@@ -9,14 +9,13 @@ import type { KubeConfig } from "@kubernetes/client-node";
import { HttpError } from "@kubernetes/client-node";
import type { Kubectl } from "../../main/kubectl/kubectl";
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
import { loadConfigFromFile } from "../kube-helpers";
import type { KubeApiResource, KubeResource } from "../rbac";
import { apiResourceRecord, apiResources } from "../rbac";
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
import plimit from "p-limit";
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
import type { ClusterState, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus, clusterModelIdChecker, updateClusterModelChecker } from "../cluster-types";
import { disposer, isDefined, isRequestError, toJS } from "../utils";
import type { Response } from "request";
import { clusterListNamespaceForbiddenChannel } from "../ipc/cluster";
@@ -25,6 +24,9 @@ import type { ListNamespaces } from "./list-namespaces.injectable";
import assert from "assert";
import type { Logger } from "../logger";
import type { BroadcastMessage } from "../ipc/broadcast-message.injectable";
import type { LoadConfigfromFile } from "../kube-helpers/load-config-from-file.injectable";
import type { RequestNamespaceResources } from "./authorization-namespace-review.injectable";
import type { RequestListApiResources } from "./list-api-resources.injectable";
export interface ClusterDependencies {
readonly directoryForKubeConfigs: string;
@@ -34,9 +36,12 @@ export interface ClusterDependencies {
createContextHandler: (cluster: Cluster) => ClusterContextHandler;
createKubectl: (clusterVersion: string) => Kubectl;
createAuthorizationReview: (config: KubeConfig) => CanI;
createAuthorizationNamespaceReview: (config: KubeConfig) => RequestNamespaceResources;
createListApiResources: (cluster: Cluster) => RequestListApiResources;
createListNamespaces: (config: KubeConfig) => ListNamespaces;
createVersionDetector: (cluster: Cluster) => VersionDetector;
broadcastMessage: BroadcastMessage;
loadConfigfromFile: LoadConfigfromFile;
}
/**
@@ -237,9 +242,16 @@ export class Cluster implements ClusterModel, ClusterState {
return this.preferences.defaultNamespace;
}
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel, configData: ClusterConfigData) {
constructor(private readonly dependencies: ClusterDependencies, { id, ...model }: ClusterModel, configData: ClusterConfigData) {
makeObservable(this);
this.id = model.id;
const { error } = clusterModelIdChecker.validate({ id });
if (error) {
throw error;
}
this.id = id;
this.updateModel(model);
this.apiUrl = configData.clusterServerUrl;
@@ -261,6 +273,12 @@ export class Cluster implements ClusterModel, ClusterState {
@action updateModel(model: UpdateClusterModel) {
// Note: do not assign ID as that should never be updated
const { error } = updateClusterModelChecker.validate(model, { allowUnknown: true });
if (error) {
throw error;
}
this.kubeConfigPath = model.kubeConfigPath;
this.contextName = model.contextName;
@@ -295,7 +313,7 @@ export class Cluster implements ClusterModel, ClusterState {
protected bindEvents() {
this.dependencies.logger.info(`[CLUSTER]: bind events`, this.getMeta());
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
const refreshMetadataTimer = setInterval(() => this.available && this.refreshAccessibilityAndMetadata(), 900000); // every 15 minutes
this.eventsDisposer.push(
reaction(() => this.getState(), state => this.pushState(state)),
@@ -425,69 +443,71 @@ export class Cluster implements ClusterModel, ClusterState {
/**
* @internal
* @param opts refresh options
*/
@action
async refresh(opts: ClusterRefreshOptions = {}) {
async refresh() {
this.dependencies.logger.info(`[CLUSTER]: refresh`, this.getMeta());
await this.refreshConnectionStatus();
if (this.accessible) {
await this.refreshAccessibility();
if (opts.refreshMetadata) {
this.refreshMetadata();
}
}
this.pushState();
}
/**
* @internal
*/
@action
async refreshMetadata() {
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await this.dependencies.detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
this.metadata = Object.assign(existingMetadata, metadata);
@action
async refreshAccessibilityAndMetadata() {
await this.refreshAccessibility();
await this.refreshMetadata();
}
/**
/**
* @internal
*/
private async refreshAccessibility(): Promise<void> {
const proxyConfig = await this.getProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
async refreshMetadata() {
this.dependencies.logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
const metadata = await this.dependencies.detectorRegistry.detectForCluster(this);
const existingMetadata = this.metadata;
this.isAdmin = await canI({
namespace: "kube-system",
resource: "*",
verb: "create",
});
this.isGlobalWatchEnabled = await canI({
verb: "watch",
resource: "*",
});
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
this.allowedResources = await this.getAllowedResources(canI);
this.ready = true;
}
this.metadata = Object.assign(existingMetadata, metadata);
}
/**
* @internal
*/
private async refreshAccessibility(): Promise<void> {
this.dependencies.logger.info(`[CLUSTER]: refreshAccessibility`, this.getMeta());
const proxyConfig = await this.getProxyKubeconfig();
const canI = this.dependencies.createAuthorizationReview(proxyConfig);
const requestNamespaceResources = this.dependencies.createAuthorizationNamespaceReview(proxyConfig);
const listApiResources = this.dependencies.createListApiResources(this);
this.isAdmin = await canI({
namespace: "kube-system",
resource: "*",
verb: "create",
});
this.isGlobalWatchEnabled = await canI({
verb: "watch",
resource: "*",
});
this.allowedNamespaces = await this.getAllowedNamespaces(proxyConfig);
this.allowedResources = await this.getAllowedResources(listApiResources, requestNamespaceResources);
this.ready = true;
}
/**
* @internal
*/
@action
async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
async refreshConnectionStatus() {
const connectionStatus = await this.getConnectionStatus();
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
}
this.online = connectionStatus > ClusterStatus.Offline;
this.accessible = connectionStatus == ClusterStatus.AccessGranted;
}
async getKubeconfig(): Promise<KubeConfig> {
const { config } = await loadConfigFromFile(this.kubeConfigPath);
const { config } = await this.dependencies.loadConfigfromFile(this.kubeConfigPath);
return config;
}
@@ -497,7 +517,7 @@ export class Cluster implements ClusterModel, ClusterState {
*/
async getProxyKubeconfig(): Promise<KubeConfig> {
const proxyKCPath = await this.getProxyKubeconfigPath();
const { config } = await loadConfigFromFile(proxyKCPath);
const { config } = await this.dependencies.loadConfigfromFile(proxyKCPath);
return config;
}
@@ -653,32 +673,48 @@ export class Cluster implements ClusterModel, ClusterState {
}
}
protected async getAllowedResources(canI: CanI) {
protected async getAllowedResources(listApiResources:RequestListApiResources, requestNamespaceResources: RequestNamespaceResources) {
try {
if (!this.allowedNamespaces.length) {
return [];
}
const resources = apiResources.filter((resource) => this.resourceAccessStatuses.get(resource) === undefined);
const apiLimit = plimit(5); // 5 concurrent api requests
const requests = [];
for (const apiResource of resources) {
requests.push(apiLimit(async () => {
for (const namespace of this.allowedNamespaces.slice(0, 10)) {
if (!this.resourceAccessStatuses.get(apiResource)) {
const result = await canI({
resource: apiResource.apiName,
group: apiResource.group,
verb: "list",
namespace,
});
const unknownResources = new Map<string, KubeApiResource>(apiResources.map(resource => ([resource.apiName, resource])));
this.resourceAccessStatuses.set(apiResource, result);
const availableResources = await listApiResources();
const availableResourcesNames = new Set(availableResources.map(apiResource => apiResource.apiName));
[...unknownResources.values()].map(unknownResource => {
if (!availableResourcesNames.has(unknownResource.apiName)) {
this.resourceAccessStatuses.set(unknownResource, false);
unknownResources.delete(unknownResource.apiName);
}
});
if (unknownResources.size > 0) {
const apiLimit = plimit(5); // 5 concurrent api requests
await Promise.all(this.allowedNamespaces.map(namespace => apiLimit(async () => {
if (unknownResources.size === 0) {
return;
}
const namespaceResources = await requestNamespaceResources(namespace, availableResources);
for (const resourceName of namespaceResources) {
const unknownResource = unknownResources.get(resourceName);
if (unknownResource) {
this.resourceAccessStatuses.set(unknownResource, true);
unknownResources.delete(resourceName);
}
}
}));
})));
for (const forbiddenResource of unknownResources.values()) {
this.resourceAccessStatuses.set(forbiddenResource, false);
}
}
await Promise.all(requests);
return apiResources
.filter((resource) => this.resourceAccessStatuses.get(resource))
@@ -0,0 +1,91 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type {
V1APIGroupList,
V1APIResourceList,
V1APIVersions,
} from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { K8sRequest } from "../../main/k8s-request.injectable";
import k8SRequestInjectable from "../../main/k8s-request.injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
import type { KubeApiResource, KubeResource } from "../rbac";
import type { Cluster } from "./cluster";
import plimit from "p-limit";
export type RequestListApiResources = () => Promise<KubeApiResource[]>;
/**
* @param proxyConfig This config's `currentContext` field must be set, and will be used as the target cluster
*/
export type ListApiResources = (cluster: Cluster) => RequestListApiResources;
interface Dependencies {
logger: Logger;
k8sRequest: K8sRequest;
}
const listApiResources = ({ k8sRequest, logger }: Dependencies): ListApiResources => {
return (cluster) => {
const clusterRequest = (path: string) => k8sRequest(cluster, path);
const apiLimit = plimit(5);
return async () => {
const resources: KubeApiResource[] = [];
try {
const resourceListGroups:{ group:string;path:string }[] = [];
await Promise.all(
[
clusterRequest("/api").then((response:V1APIVersions)=>response.versions.forEach(version => resourceListGroups.push({ group:version, path:`/api/${version}` }))),
clusterRequest("/apis").then((response:V1APIGroupList) => response.groups.forEach(group => {
const preferredVersion = group.preferredVersion?.groupVersion;
if (preferredVersion) {
resourceListGroups.push({ group:group.name, path:`/apis/${preferredVersion}` });
}
})),
],
);
await Promise.all(
resourceListGroups.map(({ group, path }) => apiLimit(async () => {
const apiResources:V1APIResourceList = await clusterRequest(path);
if (apiResources.resources) {
resources.push(
...apiResources.resources.filter(resource => resource.verbs.includes("list")).map((resource) => ({
apiName: resource.name as KubeResource,
kind: resource.kind,
group,
})),
);
}
}),
),
);
} catch (error) {
logger.error(`[LIST-API-RESOURCES]: failed to list api resources: ${error}`);
}
return resources;
};
};
};
const listApiResourcesInjectable = getInjectable({
id: "list-api-resources",
instantiate: (di) => {
const k8sRequest = di.inject(k8SRequestInjectable);
const logger = di.inject(loggerInjectable);
return listApiResources({ k8sRequest, logger });
},
});
export default listApiResourcesInjectable;
+11
View File
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { ClusterId } from "../cluster-types";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export const clusterVisibilityChannel: MessageChannel<ClusterId | null> = {
id: "cluster-visibility",
};
@@ -4,16 +4,16 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
import getAbsolutePathInjectable from "../path/get-absolute-path.injectable";
import joinPathsInjectable from "../path/join-paths.injectable";
const directoryForLensLocalStorageInjectable = getInjectable({
id: "directory-for-lens-local-storage",
instantiate: (di) => {
const getAbsolutePath = di.inject(getAbsolutePathInjectable);
const joinPaths = di.inject(joinPathsInjectable);
const directoryForUserData = di.inject(directoryForUserDataInjectable);
return getAbsolutePath(
return joinPaths(
directoryForUserData,
"lens-local-storage",
);
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import initializeSentryReportingWithInjectable from "./initialize-sentry-reporting.injectable";
export default getGlobalOverride(initializeSentryReportingWithInjectable, () => () => {});
@@ -0,0 +1,63 @@
/**
* 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 { ElectronMainOptions } from "@sentry/electron/main";
import type { BrowserOptions } from "@sentry/electron/renderer";
import isProductionInjectable from "../vars/is-production.injectable";
import sentryDataSourceNameInjectable from "../vars/sentry-dsn-url.injectable";
import { Dedupe, Offline } from "@sentry/integrations";
import { inspect } from "util";
import userStoreInjectable from "../user-store/user-store.injectable";
export type InitializeSentryReportingWith = (initSentry: (opts: BrowserOptions | ElectronMainOptions) => void) => void;
const mapProcessName = (type: "browser" | "renderer" | "worker") => type === "browser" ? "main" : type;
const initializeSentryReportingWithInjectable = getInjectable({
id: "initialize-sentry-reporting-with",
instantiate: (di): InitializeSentryReportingWith => {
const sentryDataSourceName = di.inject(sentryDataSourceNameInjectable);
const isProduction = di.inject(isProductionInjectable);
const userStore = di.inject(userStoreInjectable);
if (!sentryDataSourceName) {
return () => {};
}
return (initSentry) => initSentry({
beforeSend: (event) => {
if (userStore.allowErrorReporting) {
return event;
}
/**
* Directly write to stdout so that no other integrations capture this and create an infinite loop
*/
process.stdout.write(`🔒 [SENTRY-BEFORE-SEND-HOOK]: Sentry event is caught but not sent to server.`);
process.stdout.write("🔒 [SENTRY-BEFORE-SEND-HOOK]: === START OF SENTRY EVENT ===");
process.stdout.write(inspect(event, false, null, true));
process.stdout.write("🔒 [SENTRY-BEFORE-SEND-HOOK]: === END OF SENTRY EVENT ===");
// if return null, the event won't be sent
// ref https://github.com/getsentry/sentry-javascript/issues/2039
return null;
},
dsn: sentryDataSourceName,
integrations: [
new Dedupe(),
new Offline(),
],
initialScope: {
tags: {
"process": mapProcessName(process.type),
},
},
environment: isProduction ? "production" : "development",
});
},
causesSideEffects: true,
});
export default initializeSentryReportingWithInjectable;
+2 -2
View File
@@ -29,7 +29,7 @@ export class EventEmitter<D extends [...any[]]> {
this.listeners.length = 0;
}
emit = (...data: D) => {
emit(...data: D) {
for (const [callback, { once }] of this.listeners) {
if (once) {
this.removeListener(callback);
@@ -39,5 +39,5 @@ export class EventEmitter<D extends [...any[]]> {
break;
}
}
};
}
}
@@ -0,0 +1,56 @@
/**
* 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 { RequestInit, Response } from "node-fetch";
import type { AsyncResult } from "../utils/async-result";
import fetchInjectable from "./fetch.injectable";
export interface DownloadBinaryOptions {
signal?: AbortSignal | null | undefined;
}
export type DownloadBinary = (url: string, opts?: DownloadBinaryOptions) => Promise<AsyncResult<Buffer, string>>;
const downloadBinaryInjectable = getInjectable({
id: "download-binary",
instantiate: (di): DownloadBinary => {
const fetch = di.inject(fetchInjectable);
return async (url, opts) => {
let result: Response;
try {
// TODO: upgrade node-fetch once we switch to ESM
result = await fetch(url, opts as RequestInit);
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
if (result.status < 200 || 300 <= result.status) {
return {
callWasSuccessful: false,
error: result.statusText,
};
}
try {
return {
callWasSuccessful: true,
response: await result.buffer(),
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
export default downloadBinaryInjectable;
@@ -0,0 +1,55 @@
/**
* 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 { RequestInit, Response } from "node-fetch";
import type { AsyncResult } from "../utils/async-result";
import fetchInjectable from "./fetch.injectable";
export interface DownloadJsonOptions {
signal?: AbortSignal | null | undefined;
}
export type DownloadJson = (url: string, opts?: DownloadJsonOptions) => Promise<AsyncResult<unknown, string>>;
const downloadJsonInjectable = getInjectable({
id: "download-json",
instantiate: (di): DownloadJson => {
const fetch = di.inject(fetchInjectable);
return async (url, opts) => {
let result: Response;
try {
result = await fetch(url, opts as RequestInit);
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
if (result.status < 200 || 300 <= result.status) {
return {
callWasSuccessful: false,
error: result.statusText,
};
}
try {
return {
callWasSuccessful: true,
response: await result.json(),
};
} catch (error) {
return {
callWasSuccessful: false,
error: String(error),
};
}
};
},
});
export default downloadJsonInjectable;
@@ -0,0 +1,9 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverrideForFunction } from "../test-utils/get-global-override-for-function";
import fetchInjectable from "./fetch.injectable";
export default getGlobalOverrideForFunction(fetchInjectable);
+21
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 type * as FetchModule from "node-fetch";
const { NodeFetch: { default: fetch }} = require("../../../build/webpack/node-fetch.bundle") as { NodeFetch: typeof FetchModule };
type Response = FetchModule.Response;
type RequestInit = FetchModule.RequestInit;
export type Fetch = (url: string, init?: RequestInit) => Promise<Response>;
const fetchInjectable = getInjectable({
id: "fetch",
instantiate: (): Fetch => fetch,
causesSideEffects: true,
});
export default fetchInjectable;
+17
View File
@@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
/**
* Creates an AbortController with an associated timeout
* @param timeout The number of milliseconds before this controller will auto abort
*/
export function withTimeout(timeout: number): AbortController {
const controller = new AbortController();
const id = setTimeout(() => controller.abort(), timeout);
controller.signal.addEventListener("abort", () => clearTimeout(id));
return controller;
}
@@ -1,22 +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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type AppNavigationChannel = MessageChannel<string>;
const appNavigationChannelInjectable = getInjectable({
id: "app-navigation-channel",
instantiate: (): AppNavigationChannel => ({
id: IpcRendererNavigationEvents.NAVIGATE_IN_APP,
}),
injectionToken: messageChannelInjectionToken,
});
export default appNavigationChannelInjectable;
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export type AppNavigationChannel = MessageChannel<string>;
export const appNavigationChannel: AppNavigationChannel = {
id: IpcRendererNavigationEvents.NAVIGATE_IN_APP,
};
@@ -1,22 +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 { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-injection-token";
import { messageChannelInjectionToken } from "../utils/channel/message-channel-injection-token";
export type ClusterFrameNavigationChannel = MessageChannel<string>;
const clusterFrameNavigationChannelInjectable = getInjectable({
id: "cluster-frame-navigation-channel",
instantiate: (): ClusterFrameNavigationChannel => ({
id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
}),
injectionToken: messageChannelInjectionToken,
});
export default clusterFrameNavigationChannelInjectable;
@@ -0,0 +1,12 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { IpcRendererNavigationEvents } from "../../renderer/navigation/events";
import type { MessageChannel } from "../utils/channel/message-channel-listener-injection-token";
export type ClusterFrameNavigationChannel = MessageChannel<string>;
export const clusterFrameNavigationChannel: ClusterFrameNavigationChannel = {
id: IpcRendererNavigationEvents.NAVIGATE_IN_CLUSTER,
};
@@ -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 isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const leasesRouteInjectable = getInjectable({
id: "leases",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "leases");
return {
path: "/leases",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
injectionToken: frontEndRouteInjectionToken,
});
export default leasesRouteInjectable;
@@ -0,0 +1,20 @@
/**
* 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 leasesRouteInjectable from "./leases-route.injectable";
import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token";
const navigateToLeasesInjectable = getInjectable({
id: "navigate-to-leases",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(leasesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToLeasesInjectable;
@@ -3,18 +3,18 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import appPreferencesRouteInjectable from "./app-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
import runtimeClassesRouteInjectable from "./runtime-classes-route.injectable";
import { navigateToRouteInjectionToken } from "../../../../navigate-to-route-injection-token";
const navigateToAppPreferencesInjectable = getInjectable({
id: "navigate-to-app-preferences",
const navigateToRuntimeClassesInjectable = getInjectable({
id: "navigate-to-runtime-classes",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(appPreferencesRouteInjectable);
const route = di.inject(runtimeClassesRouteInjectable);
return () => navigateToRoute(route);
},
});
export default navigateToAppPreferencesInjectable;
export default navigateToRuntimeClassesInjectable;
@@ -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 isAllowedResourceInjectable from "../../../../../utils/is-allowed-resource.injectable";
import { frontEndRouteInjectionToken } from "../../../../front-end-route-injection-token";
const runtimeClassesRouteInjectable = getInjectable({
id: "runtime-classes-route",
instantiate: (di) => {
const isAllowedResource = di.inject(isAllowedResourceInjectable, "runtimeclasses");
return {
path: "/runtimeclasses",
clusterFrame: true,
isEnabled: isAllowedResource,
};
},
injectionToken: frontEndRouteInjectionToken,
});
export default runtimeClassesRouteInjectable;
@@ -1,27 +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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
import type { Route } from "../../../front-end-route-injection-token";
interface ExtensionPreferenceRouteParams {
extensionId: string;
tabId?: string;
}
const extensionPreferencesRouteInjectable = getInjectable({
id: "extension-preferences-route",
instantiate: (): Route<ExtensionPreferenceRouteParams> => ({
path: "/preferences/extension/:extensionId/:tabId?",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default extensionPreferencesRouteInjectable;
@@ -1,26 +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 extensionPreferencesRouteInjectable from "./extension-preferences-route.injectable";
import { navigateToRouteInjectionToken } from "../../../navigate-to-route-injection-token";
const navigateToExtensionPreferencesInjectable = getInjectable({
id: "navigate-to-extension-preferences",
instantiate: (di) => {
const navigateToRoute = di.inject(navigateToRouteInjectionToken);
const route = di.inject(extensionPreferencesRouteInjectable);
return (extensionId: string, tabId?: string) => navigateToRoute(route, {
parameters: {
extensionId,
tabId,
},
withoutAffectingBackButton: true,
});
},
});
export default navigateToExtensionPreferencesInjectable;
@@ -1,21 +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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const kubernetesPreferencesRouteInjectable = getInjectable({
id: "kubernetes-preferences-route",
instantiate: () => ({
path: "/preferences/kubernetes",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default kubernetesPreferencesRouteInjectable;
@@ -1,14 +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 navigateToAppPreferencesInjectable from "./app/navigate-to-app-preferences.injectable";
const navigateToPreferencesInjectable = getInjectable({
id: "navigate-to-preferences",
instantiate: (di) => di.inject(navigateToAppPreferencesInjectable),
});
export default navigateToPreferencesInjectable;
@@ -1,21 +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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const proxyPreferencesRouteInjectable = getInjectable({
id: "proxy-preferences-route",
instantiate: () => ({
path: "/preferences/proxy",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default proxyPreferencesRouteInjectable;
@@ -1,21 +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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const telemetryPreferencesRouteInjectable = getInjectable({
id: "telemetry-preferences-route",
instantiate: () => ({
path: "/preferences/telemetry",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default telemetryPreferencesRouteInjectable;
@@ -1,21 +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 { computed } from "mobx";
import { frontEndRouteInjectionToken } from "../../../front-end-route-injection-token";
const terminalPreferencesRouteInjectable = getInjectable({
id: "terminal-preferences-route",
instantiate: () => ({
path: "/preferences/terminal",
clusterFrame: false,
isEnabled: computed(() => true),
}),
injectionToken: frontEndRouteInjectionToken,
});
export default terminalPreferencesRouteInjectable;
@@ -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 { computed } from "mobx";
import welcomeRouteConfigInjectable from "./welcome-route-config.injectable";
import { frontEndRouteInjectionToken } from "../../front-end-route-injection-token";
const defaultWelcomeRouteInjectable = getInjectable({
id: "default-welcome-route",
instantiate: (di) => {
const welcomeRoute = di.inject(welcomeRouteConfigInjectable);
return {
path: "/welcome",
clusterFrame: false,
isEnabled: computed(() => welcomeRoute === "/welcome"),
};
},
injectionToken: frontEndRouteInjectionToken,
});
export default defaultWelcomeRouteInjectable;
@@ -0,0 +1,14 @@
/**
* 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 applicationInformationInjectable from "../../../vars/application-information.injectable";
const welcomeRouteConfigInjectable = getInjectable({
id: "welcome-route-config",
instantiate: (di) => di.inject(applicationInformationInjectable).config.welcomeRoute,
});
export default welcomeRouteConfigInjectable;
@@ -4,16 +4,21 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import { computed } from "mobx";
import welcomeRouteConfigInjectable from "./welcome-route-config.injectable";
import { frontEndRouteInjectionToken } from "../../front-end-route-injection-token";
const welcomeRouteInjectable = getInjectable({
id: "welcome-route",
instantiate: () => ({
path: "/welcome",
clusterFrame: false,
isEnabled: computed(() => true),
}),
instantiate: (di) => {
const welcomeRoute = di.inject(welcomeRouteConfigInjectable);
return {
path: welcomeRoute,
clusterFrame: false,
isEnabled: computed(() => true),
};
},
injectionToken: frontEndRouteInjectionToken,
});
@@ -5,7 +5,7 @@
import { getDiForUnitTesting } from "../../renderer/getDiForUnitTesting";
import { routeSpecificComponentInjectionToken } from "../../renderer/routes/route-specific-component-injection-token";
import { frontEndRouteInjectionToken } from "./front-end-route-injection-token";
import { filter, map, matches } from "lodash/fp";
import { filter, map } from "lodash/fp";
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
import type { ClusterStore } from "../cluster-store/cluster-store";
import { pipeline } from "@ogre-tools/fp";
@@ -27,9 +27,11 @@ describe("verify-that-all-routes-have-component", () => {
routes,
map(
(route) => ({
path: route.path,
routeComponent: routeComponents.find(matches({ route })),
(currentRoute) => ({
path: currentRoute.path,
routeComponent: routeComponents.find(({ route }) => (
route.path === currentRoute.path
&& route.clusterFrame === currentRoute.clusterFrame)),
}),
),
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import accessPathInjectable from "./access-path.injectable";
export default getGlobalOverride(accessPathInjectable, () => async () => {
throw new Error("tried to verify path access without override");
});
+27
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 fsInjectable from "./fs.injectable";
export type AccessPath = (path: string, mode?: number) => Promise<boolean>;
const accessPathInjectable = getInjectable({
id: "access-path",
instantiate: (di): AccessPath => {
const { access } = di.inject(fsInjectable);
return async (path, mode) => {
try {
await access(path, mode);
return true;
} catch {
return false;
}
};
},
});
export default accessPathInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import copyInjectable from "./copy.injectable";
export default getGlobalOverride(copyInjectable, () => async () => {
throw new Error("tried to copy filepaths without override");
});
+16
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.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { CopyOptions } from "fs-extra";
import fsInjectable from "./fs.injectable";
export type Copy = (src: string, dest: string, options?: CopyOptions | undefined) => Promise<void>;
const copyInjectable = getInjectable({
id: "copy",
instantiate: (di): Copy => di.inject(fsInjectable).copy,
});
export default copyInjectable;
@@ -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 { ReadStream } from "fs";
import fsInjectable from "./fs.injectable";
export interface CreateReadStreamOptions {
mode?: number;
end?: number | undefined;
flags?: string | undefined;
encoding?: BufferEncoding | undefined;
autoClose?: boolean | undefined;
/**
* @default false
*/
emitClose?: boolean | undefined;
start?: number | undefined;
highWaterMark?: number | undefined;
}
export type CreateReadFileStream = (filePath: string, options?: CreateReadStreamOptions) => ReadStream;
const createReadFileStreamInjectable = getInjectable({
id: "create-read-file-stream",
instantiate: (di): CreateReadFileStream => di.inject(fsInjectable).createReadStream,
});
export default createReadFileStreamInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import deleteFileInjectable from "./delete-file.injectable";
export default getGlobalOverride(deleteFileInjectable, () => async () => {
throw new Error("tried to delete file without override");
});
+15
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.
*/
import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type DeleteFile = (filePath: string) => Promise<void>;
const deleteFileInjectable = getInjectable({
id: "delete-file",
instantiate: (di): DeleteFile => di.inject(fsInjectable).unlink,
});
export default deleteFileInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import ensureDirInjectable from "./ensure-dir.injectable";
export default getGlobalOverride(ensureDirInjectable, () => async () => {
throw new Error("tried to ensure directory without override");
});
+3 -3
View File
@@ -5,14 +5,14 @@
import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type EnsureDirectory = (dirPath: string) => Promise<void>;
const ensureDirInjectable = getInjectable({
id: "ensure-dir",
// TODO: Remove usages of ensureDir from business logic.
// TODO: Read, Write, Watch etc. operations should do this internally.
instantiate: (di) => di.inject(fsInjectable).ensureDir,
causesSideEffects: true,
instantiate: (di): EnsureDirectory => di.inject(fsInjectable).ensureDir,
});
export default ensureDirInjectable;
+38 -8
View File
@@ -3,22 +3,52 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ExecFileOptions } from "child_process";
import type { ExecFileException, ExecFileOptions } from "child_process";
import { execFile } from "child_process";
import { promisify } from "util";
import type { AsyncResult } from "../utils/async-result";
export type ExecFile = (filePath: string, args: string[], options: ExecFileOptions) => Promise<string>;
export type ExecFileError = ExecFileException & { stderr: string };
export interface ExecFile {
(filePath: string): Promise<AsyncResult<string, ExecFileError>>;
(filePath: string, argsOrOptions: string[] | ExecFileOptions): Promise<AsyncResult<string, ExecFileError>>;
(filePath: string, args: string[], options: ExecFileOptions): Promise<AsyncResult<string, ExecFileError>>;
}
const execFileInjectable = getInjectable({
id: "exec-file",
instantiate: (): ExecFile => {
const asyncExecFile = promisify(execFile);
return (filePath: string, argsOrOptions?: string[] | ExecFileOptions, maybeOptions?: ExecFileOptions) => {
const { args, options } = (() => {
if (Array.isArray(argsOrOptions)) {
return {
args: argsOrOptions,
options: maybeOptions ?? {},
};
} else {
return {
args: [],
options: argsOrOptions ?? {},
};
}
})();
return async (filePath, args, options) => {
const result = await asyncExecFile(filePath, args, options);
return result.stdout;
return new Promise((resolve) => {
execFile(filePath, args, options, (error, stdout, stderr) => {
if (error) {
resolve({
callWasSuccessful: false,
error: Object.assign(error, { stderr }),
});
} else {
resolve({
callWasSuccessful: true,
response: stdout,
});
}
});
});
};
},
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import extractTarInjectable from "./extract-tar.injectable";
export default getGlobalOverride(extractTarInjectable, () => async () => {
throw new Error("tried to extract a tar file without override");
});
+26
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 { ExtractOptions } from "tar";
import { extract } from "tar";
import getDirnameOfPathInjectable from "../path/get-dirname.injectable";
export type ExtractTar = (filePath: string, opts?: ExtractOptions) => Promise<void>;
const extractTarInjectable = getInjectable({
id: "extract-tar",
instantiate: (di): ExtractTar => {
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
return (filePath, opts = {}) => extract({
file: filePath,
cwd: getDirnameOfPath(filePath),
...opts,
});
},
causesSideEffects: true,
});
export default extractTarInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import lstatInjectable from "./lstat.injectable";
export default getGlobalOverride(lstatInjectable, () => async () => {
throw new Error("tried to lstat a filepath without override");
});
+16
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.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs";
import fsInjectable from "./fs.injectable";
export type LStat = (path: string) => Promise<Stats>;
const lstatInjectable = getInjectable({
id: "lstat",
instantiate: (di): LStat => di.inject(fsInjectable).lstat,
});
export default lstatInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import moveInjectable from "./move.injectable";
export default getGlobalOverride(moveInjectable, () => async () => {
throw new Error("tried to move without override");
});
+16
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.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { MoveOptions } from "fs-extra";
import fsInjectable from "./fs.injectable";
export type Move = (src: string, dest: string, options?: MoveOptions) => Promise<void>;
const moveInjectable = getInjectable({
id: "move",
instantiate: (di): Move => di.inject(fsInjectable).move,
});
export default moveInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import pathExistsInjectable from "./path-exists.injectable";
export default getGlobalOverride(pathExistsInjectable, () => async () => {
throw new Error("Tried to check if a path exists without override");
});
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import readDirectoryInjectable from "./read-directory.injectable";
export default getGlobalOverride(readDirectoryInjectable, () => async () => {
throw new Error("tried to read a directory's content without override");
});
@@ -0,0 +1,37 @@
/**
* 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 { Dirent } from "fs";
import fsInjectable from "./fs.injectable";
export interface ReadDirectory {
(
path: string,
options: "buffer" | { encoding: "buffer"; withFileTypes?: false | undefined }
): Promise<Buffer[]>;
(
path: string,
options?:
| { encoding: BufferEncoding | string | null; withFileTypes?: false | undefined }
| BufferEncoding
| string
| null,
): Promise<string[]>;
(
path: string,
options?: { encoding?: BufferEncoding | string | null | undefined; withFileTypes?: false | undefined },
): Promise<string[] | Buffer[]>;
(
path: string,
options: { encoding?: BufferEncoding | string | null | undefined; withFileTypes: true },
): Promise<Dirent[]>;
}
const readDirectoryInjectable = getInjectable({
id: "read-directory",
instantiate: (di): ReadDirectory => di.inject(fsInjectable).readdir,
});
export default readDirectoryInjectable;
+7 -2
View File
@@ -5,11 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
export type ReadFile = (filePath: string) => Promise<string>;
const readFileInjectable = getInjectable({
id: "read-file",
instantiate: (di) => (filePath: string) =>
di.inject(fsInjectable).readFile(filePath, "utf-8"),
instantiate: (di): ReadFile => {
const { readFile } = di.inject(fsInjectable);
return (filePath) => readFile(filePath, "utf-8");
},
});
export default readFileInjectable;
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import removePathInjectable from "./remove-path.injectable";
export default getGlobalOverride(removePathInjectable, () => async () => {
throw new Error("tried to remove a path without override");
});
@@ -5,9 +5,11 @@
import { getInjectable } from "@ogre-tools/injectable";
import fsInjectable from "./fs.injectable";
const readDirInjectable = getInjectable({
id: "read-dir",
instantiate: (di) => di.inject(fsInjectable).readdir,
export type RemovePath = (path: string) => Promise<void>;
const removePathInjectable = getInjectable({
id: "remove-path",
instantiate: (di): RemovePath => di.inject(fsInjectable).remove,
});
export default readDirInjectable;
export default removePathInjectable;
+4 -2
View File
@@ -3,12 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs";
import fsInjectable from "../fs.injectable";
export type Stat = (path: string) => Promise<Stats>;
const statInjectable = getInjectable({
id: "stat",
instantiate: (di) => di.inject(fsInjectable).stat,
instantiate: (di): Stat => di.inject(fsInjectable).stat,
});
export default statInjectable;
+149 -3
View File
@@ -3,15 +3,161 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { FSWatcher, WatchOptions } from "chokidar";
import { watch } from "chokidar";
import type { Stats } from "fs";
import type TypedEventEmitter from "typed-emitter";
import type { SingleOrMany } from "../../utils";
export type Watch = (path: string, options?: WatchOptions) => FSWatcher;
export interface AlwaysStatWatcherEvents {
add: (path: string, stats: Stats) => void;
addDir: (path: string, stats: Stats) => void;
change: (path: string, stats: Stats) => void;
}
export interface MaybeStatWatcherEvents {
add: (path: string, stats?: Stats) => void;
addDir: (path: string, stats?: Stats) => void;
change: (path: string, stats?: Stats) => void;
}
export type WatcherEvents<AlwaysStat extends boolean> = BaseWatcherEvents
& (
AlwaysStat extends true
? AlwaysStatWatcherEvents
: MaybeStatWatcherEvents
);
export interface BaseWatcherEvents {
error: (error: Error) => void;
ready: () => void;
unlink: (path: string) => void;
unlinkDir: (path: string) => void;
}
export interface Watcher<AlwaysStat extends boolean> extends TypedEventEmitter<WatcherEvents<AlwaysStat>> {
close: () => Promise<void>;
}
export type WatcherOptions<AlwaysStat extends boolean> = {
/**
* Indicates whether the process should continue to run as long as files are being watched. If
* set to `false` when using `fsevents` to watch, no more events will be emitted after `ready`,
* even if the process continues to run.
*/
persistent?: boolean;
/**
* ([anymatch](https://github.com/micromatch/anymatch)-compatible definition) Defines files/paths to
* be ignored. The whole relative or absolute path is tested, not just filename. If a function
* with two arguments is provided, it gets called twice per path - once with a single argument
* (the path), second time with two arguments (the path and the
* [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object of that path).
*/
ignored?: SingleOrMany<string | RegExp | ((path: string) => boolean)>;
/**
* If set to `false` then `add`/`addDir` events are also emitted for matching paths while
* instantiating the watching as chokidar discovers these file paths (before the `ready` event).
*/
ignoreInitial?: boolean;
/**
* When `false`, only the symlinks themselves will be watched for changes instead of following
* the link references and bubbling events through the link's path.
*/
followSymlinks?: boolean;
/**
* The base directory from which watch `paths` are to be derived. Paths emitted with events will
* be relative to this.
*/
cwd?: string;
/**
* If set to true then the strings passed to .watch() and .add() are treated as literal path
* names, even if they look like globs. Default: false.
*/
disableGlobbing?: boolean;
/**
* Whether to use fs.watchFile (backed by polling), or fs.watch. If polling leads to high CPU
* utilization, consider setting this to `false`. It is typically necessary to **set this to
* `true` to successfully watch files over a network**, and it may be necessary to successfully
* watch files in other non-standard situations. Setting to `true` explicitly on OS X overrides
* the `useFsEvents` default.
*/
usePolling?: boolean;
/**
* Whether to use the `fsevents` watching interface if available. When set to `true` explicitly
* and `fsevents` is available this supercedes the `usePolling` setting. When set to `false` on
* OS X, `usePolling: true` becomes the default.
*/
useFsEvents?: boolean;
/**
* If set, limits how many levels of subdirectories will be traversed.
*/
depth?: number;
/**
* Interval of file system polling.
*/
interval?: number;
/**
* Interval of file system polling for binary files. ([see list of binary extensions](https://gi
* thub.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json))
*/
binaryInterval?: number;
/**
* Indicates whether to watch files that don't have read permissions if possible. If watching
* fails due to `EPERM` or `EACCES` with this set to `true`, the errors will be suppressed
* silently.
*/
ignorePermissionErrors?: boolean;
/**
* `true` if `useFsEvents` and `usePolling` are `false`). Automatically filters out artifacts
* that occur when using editors that use "atomic writes" instead of writing directly to the
* source file. If a file is re-added within 100 ms of being deleted, Chokidar emits a `change`
* event rather than `unlink` then `add`. If the default of 100 ms does not work well for you,
* you can override it by setting `atomic` to a custom value, in milliseconds.
*/
atomic?: boolean | number;
/**
* can be set to an object in order to adjust timing params:
*/
awaitWriteFinish?: AwaitWriteFinishOptions | boolean;
} & (AlwaysStat extends true
? {
alwaysStat: true;
}
: {
alwaysStat?: false;
}
);
export interface AwaitWriteFinishOptions {
/**
* Amount of time in milliseconds for a file size to remain constant before emitting its event.
*/
stabilityThreshold?: number;
/**
* File size polling interval.
*/
pollInterval?: number;
}
export type Watch = <AlwaysStat extends boolean = false>(path: string, options?: WatcherOptions<AlwaysStat>) => Watcher<AlwaysStat>;
// TODO: Introduce wrapper to allow simpler API
const watchInjectable = getInjectable({
id: "watch",
instantiate: (): Watch => watch,
instantiate: () => watch as Watch,
causesSideEffects: true,
});
@@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "../test-utils/get-global-override";
import writeFileInjectable from "./write-file.injectable";
export default getGlobalOverride(writeFileInjectable, () => async () => {
throw new Error("tried to write file without override");
});
+12 -4
View File
@@ -3,20 +3,28 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import path from "path";
import type { WriteFileOptions } from "fs-extra";
import getDirnameOfPathInjectable from "../path/get-dirname.injectable";
import fsInjectable from "./fs.injectable";
export type WriteFile = (filePath: string, content: string | Buffer, opts?: WriteFileOptions) => Promise<void>;
const writeFileInjectable = getInjectable({
id: "write-file",
instantiate: (di) => {
instantiate: (di): WriteFile => {
const { writeFile, ensureDir } = di.inject(fsInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
return async (filePath: string, content: string | Buffer) => {
await ensureDir(path.dirname(filePath), { mode: 0o755 });
return async (filePath, content, opts) => {
await ensureDir(getDirnameOfPath(filePath), {
mode: 0o755,
...(opts ?? {}),
});
await writeFile(filePath, content, {
encoding: "utf-8",
...(opts ?? {}),
});
};
},
+11 -21
View File
@@ -3,37 +3,27 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { EnsureOptions, WriteOptions } from "fs-extra";
import path from "path";
import type { JsonValue } from "type-fest";
import getDirnameOfPathInjectable from "../path/get-dirname.injectable";
import fsInjectable from "./fs.injectable";
export type WriteJson = (filePath: string, contents: JsonValue) => Promise<void>;
interface Dependencies {
writeJson: (file: string, object: any, options?: WriteOptions | BufferEncoding | string) => Promise<void>;
ensureDir: (dir: string, options?: EnsureOptions | number) => Promise<void>;
}
const writeJsonFile = ({ writeJson, ensureDir }: Dependencies): WriteJson => async (filePath, content) => {
await ensureDir(path.dirname(filePath), { mode: 0o755 });
await writeJson(filePath, content, {
encoding: "utf-8",
spaces: 2,
});
};
const writeJsonFileInjectable = getInjectable({
id: "write-json-file",
instantiate: (di) => {
instantiate: (di): WriteJson => {
const { writeJson, ensureDir } = di.inject(fsInjectable);
const getDirnameOfPath = di.inject(getDirnameOfPathInjectable);
return writeJsonFile({
writeJson,
ensureDir,
});
return async (filePath, content) => {
await ensureDir(getDirnameOfPath(filePath), { mode: 0o755 });
await writeJson(filePath, content, {
encoding: "utf-8",
spaces: 2,
});
};
},
});
@@ -1,23 +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 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;
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { HelmRepo } from "./helm-repo";
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
export type AddHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<void, string>>;
export const addHelmRepositoryChannel: AddHelmRepositoryChannel = {
id: "add-helm-repository-channel",
};
@@ -1,23 +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 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;
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { HelmRepo } from "./helm-repo";
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
export type GetActiveHelmRepositoriesChannel = RequestChannel<void, AsyncResult<HelmRepo[]>>;
export const getActiveHelmRepositoriesChannel: GetActiveHelmRepositoriesChannel = {
id: "get-helm-active-list-repositories",
};
@@ -1,22 +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 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;
@@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncResult } from "../utils/async-result";
import type { RequestChannel } from "../utils/channel/request-channel-listener-injection-token";
import type { HelmRepo } from "./helm-repo";
export type RemoveHelmRepositoryChannel = RequestChannel<HelmRepo, AsyncResult<void, string>>;
export const removeHelmRepositoryChannel: RemoveHelmRepositoryChannel = {
id: "remove-helm-repository-channel",
};
@@ -0,0 +1,77 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest";
import type { DiContainer, Injectable } from "@ogre-tools/injectable";
import { runInAction } from "mobx";
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
import type { InitializableState } from "./create";
import { createInitializableState } from "./create";
describe("InitializableState tests", () => {
let di: DiContainer;
beforeEach(() => {
di = getDiForUnitTesting({ doGeneralOverrides: true });
});
describe("when created", () => {
let stateInjectable: Injectable<InitializableState<number>, unknown, void>;
let initMock: AsyncFnMock<() => number>;
beforeEach(() => {
initMock = asyncFn();
stateInjectable = createInitializableState({
id: "my-state",
init: initMock,
});
runInAction(() => {
di.register(stateInjectable);
});
});
describe("when injected", () => {
let state: InitializableState<number>;
beforeEach(() => {
state = di.inject(stateInjectable);
});
it("when get is called, throw", () => {
expect(() => state.get()).toThrowError("InitializableState(my-state) has not been initialized yet");
});
describe("when init is called", () => {
beforeEach(() => {
state.init();
});
it("should call provided initialization function", () => {
expect(initMock).toBeCalled();
});
it("when get is called, throw", () => {
expect(() => state.get()).toThrowError("InitializableState(my-state) has not finished initializing");
});
describe("when initialization resolves", () => {
beforeEach(async () => {
await initMock.resolve(42);
});
it("when get is called, returns value", () => {
expect(state.get()).toBe(42);
});
it("when init is called again, throws", async () => {
await expect(() => state.init()).rejects.toThrow("Cannot initialize InitializableState(my-state) more than once");
});
});
});
});
});
});
+62
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 type { DiContainerForInjection, Injectable, InjectionToken } from "@ogre-tools/injectable";
import { getInjectable } from "@ogre-tools/injectable";
export interface CreateInitializableStateArgs<T> {
id: string;
init: (di: DiContainerForInjection) => Promise<T> | T;
injectionToken?: InjectionToken<InitializableState<T>, void>;
}
export interface InitializableState<T> {
get: () => T;
init: () => Promise<void>;
}
type InitializableStateValue<T> =
| { set: false }
| { set: true; value: T } ;
export function createInitializableState<T>(args: CreateInitializableStateArgs<T>): Injectable<InitializableState<T>, unknown, void> {
const { id, init, injectionToken } = args;
return getInjectable({
id,
instantiate: (di) => {
let box: InitializableStateValue<T> = {
set: false,
};
let initCalled = false;
return {
init: async () => {
if (initCalled) {
throw new Error(`Cannot initialize InitializableState(${id}) more than once`);
}
initCalled = true;
box = {
set: true,
value: await init(di),
};
},
get: () => {
if (!initCalled) {
throw new Error(`InitializableState(${id}) has not been initialized yet`);
}
if (box.set === false) {
throw new Error(`InitializableState(${id}) has not finished initializing`);
}
return box.value;
},
};
},
injectionToken,
});
}
@@ -1,131 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { EventEmitter } from "events";
import { onCorrect, onceCorrect } from "../type-enforced-ipc";
describe("type enforced ipc tests", () => {
describe("onCorrect tests", () => {
it("should call the handler if the args are valid", () => {
let called = false;
const source = new EventEmitter();
const listener = () => called = true;
const verifier = (args: unknown[]): args is [] => true;
const channel = "foobar";
onCorrect({ source, listener, verifier, channel });
source.emit(channel);
expect(called).toBe(true);
});
it("should not call the handler if the args are not valid", () => {
let called = false;
const source = new EventEmitter();
const listener = () => called = true;
const verifier = (args: unknown[]): args is [] => false;
const channel = "foobar";
onCorrect({ source, listener, verifier, channel });
source.emit(channel);
expect(called).toBe(false);
});
it("should call the handler twice if the args are valid on two emits", () => {
let called = 0;
const source = new EventEmitter();
const listener = () => called += 1;
const verifier = (args: unknown[]): args is [] => true;
const channel = "foobar";
onCorrect({ source, listener, verifier, channel });
source.emit(channel);
source.emit(channel);
expect(called).toBe(2);
});
it("should call the handler twice if the args are [valid, invalid, valid]", () => {
let called = 0;
const source = new EventEmitter();
const listener = () => called += 1;
const results = [true, false, true];
const verifier = (args: unknown[]): args is [] => results.pop() ?? false;
const channel = "foobar";
onCorrect({ source, listener, verifier, channel });
source.emit(channel);
source.emit(channel);
source.emit(channel);
expect(called).toBe(2);
});
});
describe("onceCorrect tests", () => {
it("should call the handler if the args are valid", () => {
let called = false;
const source = new EventEmitter();
const listener = () => called = true;
const verifier = (args: unknown[]): args is [] => true;
const channel = "foobar";
onceCorrect({ source, listener, verifier, channel });
source.emit(channel);
expect(called).toBe(true);
});
it("should not call the handler if the args are not valid", () => {
let called = false;
const source = new EventEmitter();
const listener = () => called = true;
const verifier = (args: unknown[]): args is [] => false;
const channel = "foobar";
onceCorrect({ source, listener, verifier, channel });
source.emit(channel);
expect(called).toBe(false);
});
it("should call the handler only once even if args are valid multiple times", () => {
let called = 0;
const source = new EventEmitter();
const listener = () => called += 1;
const verifier = (args: unknown[]): args is [] => true;
const channel = "foobar";
onceCorrect({ source, listener, verifier, channel });
source.emit(channel);
source.emit(channel);
expect(called).toBe(1);
});
it("should call the handler on only the first valid set of args", () => {
let called = "";
let verifierCalled = 0;
const source = new EventEmitter();
const listener = (info: any, arg: string) => called = arg;
const verifier = (args: unknown[]): args is [string] => (++verifierCalled) % 3 === 0;
const channel = "foobar";
onceCorrect({ source, listener, verifier, channel });
source.emit(channel, {}, "a");
source.emit(channel, {}, "b");
source.emit(channel, {}, "c");
source.emit(channel, {}, "d");
source.emit(channel, {}, "e");
source.emit(channel, {}, "f");
source.emit(channel, {}, "g");
source.emit(channel, {}, "h");
source.emit(channel, {}, "i");
expect(called).toBe("c");
});
});
});
Loaded 100 of 1328 files, more files were not shown because too many files have changed in this diff. Show more