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

Fix kubeconfig-sync sometimes producing multiple identical entities (#5855)

This commit is contained in:
Sebastian Malton 2022-10-03 11:38:41 -03:00 committed by GitHub
parent e6396b7314
commit b9cc0d5687
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
109 changed files with 1219 additions and 505 deletions

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 { 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;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { 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;

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;

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,
});

View File

@ -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, lifecycleEnum } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
const prefixedLoggerInjectable = getInjectable({
id: "prefixed-logger",
instantiate: (di, prefix): Logger => {
const logger = di.inject(loggerInjectable);
return {
debug: (message, ...args) => {
logger.debug(`[${prefix}]: ${message}`, ...args);
},
error: (message, ...args) => {
logger.error(`[${prefix}]: ${message}`, ...args);
},
info: (message, ...args) => {
logger.info(`[${prefix}]: ${message}`, ...args);
},
silly: (message, ...args) => {
logger.silly(`[${prefix}]: ${message}`, ...args);
},
warn: (message, ...args) => {
logger.warn(`[${prefix}]: ${message}`, ...args);
},
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, prefix: string) => prefix,
}),
});
export default prefixedLoggerInjectable;

View File

@ -25,13 +25,19 @@ describe("runManyFor", () => {
const someInjectable = getInjectable({
id: "some-injectable",
instantiate: () => ({ run: () => runMock("some-call") }),
instantiate: () => ({
id: "some-injectable",
run: () => runMock("some-call"),
}),
injectionToken: someInjectionTokenForRunnables,
});
const someOtherInjectable = getInjectable({
id: "some-other-injectable",
instantiate: () => ({ run: () => runMock("some-other-call") }),
instantiate: () => ({
id: "some-other-injectable",
run: () => runMock("some-other-call"),
}),
injectionToken: someInjectionTokenForRunnables,
});
@ -79,6 +85,7 @@ describe("runManyFor", () => {
id: "some-injectable-1",
instantiate: (di) => ({
id: "some-injectable-1",
run: () => runMock("third-level-run"),
runAfter: di.inject(someInjectable2),
}),
@ -90,6 +97,7 @@ describe("runManyFor", () => {
id: "some-injectable-2",
instantiate: (di) => ({
id: "some-injectable-2",
run: () => runMock("second-level-run"),
runAfter: di.inject(someInjectable3),
}),
@ -99,7 +107,10 @@ describe("runManyFor", () => {
const someInjectable3 = getInjectable({
id: "some-injectable-3",
instantiate: () => ({ run: () => runMock("first-level-run") }),
instantiate: () => ({
id: "some-injectable-3",
run: () => runMock("first-level-run"),
}),
injectionToken: someInjectionTokenForRunnables,
});
@ -186,6 +197,7 @@ describe("runManyFor", () => {
id: "some-runnable-1",
instantiate: (di) => ({
id: "some-runnable-1",
run: () => runMock("some-runnable-1"),
runAfter: di.inject(someOtherInjectable),
}),
@ -197,6 +209,7 @@ describe("runManyFor", () => {
id: "some-runnable-2",
instantiate: () => ({
id: "some-runnable-2",
run: () => runMock("some-runnable-2"),
}),
@ -210,7 +223,7 @@ describe("runManyFor", () => {
);
return expect(() => runMany()).rejects.toThrow(
"Tried to run runnable after other runnable which does not same injection token.",
'Tried to run runnable "some-runnable-1" after the runnable "some-runnable-2" which does not share the "some-injection-token" injection token.',
);
});
@ -232,6 +245,7 @@ describe("runManyFor", () => {
id: "some-runnable-1",
instantiate: () => ({
id: "some-runnable-1",
run: (parameter) => runMock("run-of-some-runnable-1", parameter),
}),
@ -242,6 +256,7 @@ describe("runManyFor", () => {
id: "some-runnable-2",
instantiate: () => ({
id: "some-runnable-2",
run: (parameter) => runMock("run-of-some-runnable-2", parameter),
}),

View File

@ -11,8 +11,9 @@ import { filter, forEach, map, tap } from "lodash/fp";
import { throwWithIncorrectHierarchyFor } from "./throw-with-incorrect-hierarchy-for";
export interface Runnable<TParameter = void> {
id: string;
run: Run<TParameter>;
runAfter?: this;
runAfter?: Runnable<TParameter>;
}
type Run<Param> = (parameter: Param) => Promise<void> | void;
@ -25,7 +26,7 @@ export function runManyFor(di: DiContainerForInjection): RunMany {
return (injectionToken) => async (parameter) => {
const allRunnables = di.injectMany(injectionToken);
const throwWithIncorrectHierarchy = throwWithIncorrectHierarchyFor(allRunnables);
const throwWithIncorrectHierarchy = throwWithIncorrectHierarchyFor((injectionToken as any).id, allRunnables);
const recursedRun = async (
runAfterRunnable: Runnable<any> | undefined = undefined,

View File

@ -21,13 +21,19 @@ describe("runManySyncFor", () => {
const someInjectable = getInjectable({
id: "some-injectable",
instantiate: () => ({ run: () => runMock("some-call") }),
instantiate: () => ({
id: "some-injectable",
run: () => runMock("some-call"),
}),
injectionToken: someInjectionTokenForRunnables,
});
const someOtherInjectable = getInjectable({
id: "some-other-injectable",
instantiate: () => ({ run: () => runMock("some-other-call") }),
instantiate: () => ({
id: "some-other-injectable",
run: () => runMock("some-other-call"),
}),
injectionToken: someInjectionTokenForRunnables,
});
@ -62,6 +68,7 @@ describe("runManySyncFor", () => {
id: "some-injectable-1",
instantiate: (di) => ({
id: "some-injectable-1",
run: () => runMock("third-level-run"),
runAfter: di.inject(someInjectable2),
}),
@ -73,6 +80,7 @@ describe("runManySyncFor", () => {
id: "some-injectable-2",
instantiate: (di) => ({
id: "some-injectable-2",
run: () => runMock("second-level-run"),
runAfter: di.inject(someInjectable3),
}),
@ -82,7 +90,10 @@ describe("runManySyncFor", () => {
const someInjectable3 = getInjectable({
id: "some-injectable-3",
instantiate: () => ({ run: () => runMock("first-level-run") }),
instantiate: () => ({
id: "some-injectable-3",
run: () => runMock("first-level-run"),
}),
injectionToken: someInjectionTokenForRunnables,
});
@ -115,6 +126,7 @@ describe("runManySyncFor", () => {
id: "some-runnable-1",
instantiate: (di) => ({
id: "some-runnable-1",
run: () => runMock("some-runnable-1"),
runAfter: di.inject(someOtherInjectable),
}),
@ -126,6 +138,7 @@ describe("runManySyncFor", () => {
id: "some-runnable-2",
instantiate: () => ({
id: "some-runnable-2",
run: () => runMock("some-runnable-2"),
}),
@ -139,7 +152,7 @@ describe("runManySyncFor", () => {
);
return expect(() => runMany()).rejects.toThrow(
"Tried to run runnable after other runnable which does not same injection token.",
'Tried to run runnable "some-runnable-1" after the runnable "some-runnable-2" which does not share the "some-injection-token" injection token.',
);
});
@ -161,6 +174,7 @@ describe("runManySyncFor", () => {
id: "some-runnable-1",
instantiate: () => ({
id: "some-runnable-1",
run: (parameter) => runMock("run-of-some-runnable-1", parameter),
}),
@ -171,6 +185,7 @@ describe("runManySyncFor", () => {
id: "some-runnable-2",
instantiate: () => ({
id: "some-runnable-2",
run: (parameter) => runMock("run-of-some-runnable-2", parameter),
}),

View File

@ -3,17 +3,15 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import type {
DiContainerForInjection,
InjectionToken,
} from "@ogre-tools/injectable";
import type { DiContainerForInjection, InjectionToken } from "@ogre-tools/injectable";
import { filter, forEach, map, tap } from "lodash/fp";
import type { Runnable } from "./run-many-for";
import { throwWithIncorrectHierarchyFor } from "./throw-with-incorrect-hierarchy-for";
export interface RunnableSync<TParameter = void> {
id: string;
run: RunSync<TParameter>;
runAfter?: this;
runAfter?: RunnableSync<TParameter>;
}
type RunSync<Param> = (parameter: Param) => void;
@ -26,7 +24,7 @@ export function runManySyncFor(di: DiContainerForInjection): RunManySync {
return (injectionToken) => async (parameter) => {
const allRunnables = di.injectMany(injectionToken);
const throwWithIncorrectHierarchy = throwWithIncorrectHierarchyFor(allRunnables);
const throwWithIncorrectHierarchy = throwWithIncorrectHierarchyFor((injectionToken as any).id, allRunnables);
const recursedRun = (
runAfterRunnable: RunnableSync<any> | undefined = undefined,

View File

@ -5,12 +5,10 @@
import type { Runnable } from "./run-many-for";
import type { RunnableSync } from "./run-many-sync-for";
export const throwWithIncorrectHierarchyFor =
(allRunnables: Runnable<any>[] | RunnableSync<any>[]) =>
(runnable: Runnable<any> | RunnableSync<any>) => {
if (runnable.runAfter && !allRunnables.includes(runnable.runAfter)) {
throw new Error(
"Tried to run runnable after other runnable which does not same injection token.",
);
}
};
export const throwWithIncorrectHierarchyFor = (injectionTokenId: string, allRunnables: Runnable<any>[] | RunnableSync<any>[]) => (
(runnable: Runnable<any> | RunnableSync<any>) => {
if (runnable.runAfter && !allRunnables.includes(runnable.runAfter)) {
throw new Error(`Tried to run runnable "${runnable.id}" after the runnable "${runnable.runAfter.id}" which does not share the "${injectionTokenId}" injection token.`);
}
}
);

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.
*/
import { getInjectable } from "@ogre-tools/injectable";
import userStoreInjectable from "./user-store.injectable";
const kubeconfigSyncsInjectable = getInjectable({
id: "kubeconfig-syncs",
instantiate: (di) => {
const store = di.inject(userStoreInjectable);
return store.syncKubeconfigEntries;
},
});
export default kubeconfigSyncsInjectable;

View File

@ -11,6 +11,7 @@ interface Iterator<T> {
find(fn: (val: T) => unknown): T | undefined;
collect<U>(fn: (values: Iterable<T>) => U): U;
map<U>(fn: (val: T) => U): Iterator<U>;
flatMap<U>(fn: (val: T) => U[]): Iterator<U>;
join(sep?: string): string;
}
@ -19,6 +20,7 @@ export function pipeline<T>(src: IterableIterator<T>): Iterator<T> {
filter: (fn) => pipeline(filter(src, fn)),
filterMap: (fn) => pipeline(filterMap(src, fn)),
map: (fn) => pipeline(map(src, fn)),
flatMap: (fn) => pipeline(flatMap(src, fn)),
find: (fn) => find(src, fn),
join: (sep) => join(src, sep),
collect: (fn) => fn(src),

View File

@ -5,9 +5,9 @@
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import type { ClusterManager } from "../../main/cluster-manager";
import type { ClusterManager } from "../../main/cluster/manager";
import exitAppInjectable from "../../main/electron-app/features/exit-app.injectable";
import clusterManagerInjectable from "../../main/cluster-manager.injectable";
import clusterManagerInjectable from "../../main/cluster/manager.injectable";
import stopServicesAndExitAppInjectable from "../../main/stop-services-and-exit-app.injectable";
import { advanceFakeTime, useFakeTime } from "../../common/test-utils/use-fake-time";

View File

@ -27,6 +27,7 @@ const setupAppPathsInjectable = getInjectable({
const joinPaths = di.inject(joinPathsInjectable);
return {
id: "setup-app-paths",
run: () => {
if (directoryForIntegrationTesting) {
setElectronAppPath("appData", directoryForIntegrationTesting);

View File

@ -16,6 +16,7 @@ const emitCurrentVersionToAnalyticsInjectable = getInjectable({
const buildVersion = di.inject(buildVersionInjectable);
return {
id: "emit-current-version-to-analytics",
run: () => {
emitEvent({
name: "app",

View File

@ -15,6 +15,7 @@ const startCheckingForUpdatesInjectable = getInjectable({
const updatingIsEnabled = di.inject(updatingIsEnabledInjectable);
return {
id: "start-checking-for-updates",
run: async () => {
if (updatingIsEnabled && !periodicalCheckForUpdates.started) {
await periodicalCheckForUpdates.start();

View File

@ -13,6 +13,7 @@ const stopCheckingForUpdatesInjectable = getInjectable({
const periodicalCheckForUpdates = di.inject(periodicalCheckForUpdatesInjectable);
return {
id: "stop-checking-for-updates",
run: async () => {
if (periodicalCheckForUpdates.started) {
await periodicalCheckForUpdates.stop();

View File

@ -13,6 +13,7 @@ const startWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({
const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable);
return {
id: "start-watching-if-update-should-happen-on-quit",
run: () => {
watchIfUpdateShouldHappenOnQuit.start();
},

View File

@ -13,6 +13,7 @@ const stopWatchingIfUpdateShouldHappenOnQuitInjectable = getInjectable({
const watchIfUpdateShouldHappenOnQuit = di.inject(watchIfUpdateShouldHappenOnQuitInjectable);
return {
id: "stop-watching-if-update-should-happen-on-quit",
run: () => {
watchIfUpdateShouldHappenOnQuit.stop();
},

View File

@ -3,71 +3,56 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { ObservableMap } from "mobx";
import { observable, ObservableMap } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import { loadFromOptions } from "../../../common/kube-helpers";
import type { Cluster } from "../../../common/cluster/cluster";
import { computeDiff as computeDiffFor, configToModels } from "../kubeconfig-sync/manager";
import mockFs from "mock-fs";
import fs from "fs";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.injectable";
import clusterManagerInjectable from "../../cluster-manager.injectable";
import directoryForUserDataInjectable from "../../../common/app-paths/directory-for-user-data/directory-for-user-data.injectable";
import directoryForTempInjectable from "../../../common/app-paths/directory-for-temp/directory-for-temp.injectable";
import kubectlBinaryNameInjectable from "../../kubectl/binary-name.injectable";
import kubectlDownloadingNormalizedArchInjectable from "../../kubectl/normalized-arch.injectable";
import normalizedPlatformInjectable from "../../../common/vars/normalized-platform.injectable";
import { iter } from "../../../common/utils";
import fsInjectable from "../../../common/fs/fs.injectable";
jest.mock("electron", () => ({
app: {
getVersion: () => "99.99.99",
getName: () => "lens",
setName: jest.fn(),
setPath: jest.fn(),
getPath: () => "tmp",
getLocale: () => "en",
setLoginItemSettings: jest.fn(),
},
ipcMain: {
on: jest.fn(),
handle: jest.fn(),
},
}));
import { iter, strictGet } from "../../../common/utils";
import type { ComputeKubeconfigDiff } from "../kubeconfig-sync/compute-diff.injectable";
import computeKubeconfigDiffInjectable from "../kubeconfig-sync/compute-diff.injectable";
import type { ConfigToModels } from "../kubeconfig-sync/config-to-models.injectable";
import configToModelsInjectable from "../kubeconfig-sync/config-to-models.injectable";
import kubeconfigSyncManagerInjectable from "../kubeconfig-sync/manager.injectable";
import type { KubeconfigSyncManager } from "../kubeconfig-sync/manager";
import type { KubeconfigSyncValue } from "../../../common/user-store";
import kubeconfigSyncsInjectable from "../../../common/user-store/kubeconfig-syncs.injectable";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import type { DiContainer } from "@ogre-tools/injectable";
import type { AsyncFnMock } from "@async-fn/jest";
import type { Stat } from "../../../common/fs/stat/stat.injectable";
import asyncFn from "@async-fn/jest";
import statInjectable from "../../../common/fs/stat/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable";
import EventEmitter from "events";
import type { ReadStream, Stats } from "fs";
import createReadFileStreamInjectable from "../../../common/fs/create-read-file-stream.injectable";
describe("kubeconfig-sync.source tests", () => {
let computeDiff: ReturnType<typeof computeDiffFor>;
let computeKubeconfigDiff: ComputeKubeconfigDiff;
let configToModels: ConfigToModels;
let kubeconfigSyncs: ObservableMap<string, KubeconfigSyncValue>;
let clusters: Map<string, Cluster>;
let di: DiContainer;
beforeEach(async () => {
const di = getDiForUnitTesting({ doGeneralOverrides: true });
di = getDiForUnitTesting({ doGeneralOverrides: true });
mockFs();
di.override(directoryForUserDataInjectable, () => "/some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "/some-directory-for-temp");
di.override(directoryForUserDataInjectable, () => "some-directory-for-user-data");
di.override(directoryForTempInjectable, () => "some-directory-for-temp");
di.override(kubectlBinaryNameInjectable, () => "kubectl");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(normalizedPlatformInjectable, () => "darwin");
clusters = new Map();
di.override(getClusterByIdInjectable, () => id => clusters.get(id));
di.permitSideEffects(fsInjectable);
di.unoverride(clusterStoreInjectable);
di.permitSideEffects(clusterStoreInjectable);
di.permitSideEffects(getConfigurationFileModelInjectable);
kubeconfigSyncs = observable.map();
computeDiff = computeDiffFor({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
clusterManager: di.inject(clusterManagerInjectable),
});
});
di.override(kubeconfigSyncsInjectable, () => kubeconfigSyncs);
afterEach(() => {
mockFs.restore();
computeKubeconfigDiff = di.inject(computeKubeconfigDiffInjectable);
configToModels = di.inject(configToModelsInjectable);
});
describe("configsToModels", () => {
@ -108,13 +93,13 @@ describe("kubeconfig-sync.source tests", () => {
});
});
describe("computeDiff", () => {
describe("computeKubeconfigDiff", () => {
it("should leave an empty source empty if there are no entries", () => {
const contents = "";
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
computeDiff(contents, rootSource, filePath);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(0);
});
@ -149,9 +134,7 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
computeDiff(contents, rootSource, filePath);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(1);
@ -193,9 +176,7 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
computeDiff(contents, rootSource, filePath);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(1);
@ -204,7 +185,7 @@ describe("kubeconfig-sync.source tests", () => {
expect(c.kubeConfigPath).toBe("/bar");
expect(c.contextName).toBe("context-name");
computeDiff("{}", rootSource, filePath);
computeKubeconfigDiff("{}", rootSource, filePath);
expect(rootSource.size).toBe(0);
});
@ -247,9 +228,7 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
computeDiff(contents, rootSource, filePath);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(2);
@ -289,7 +268,7 @@ describe("kubeconfig-sync.source tests", () => {
currentContext: "foobar",
});
computeDiff(newContents, rootSource, filePath);
computeKubeconfigDiff(newContents, rootSource, filePath);
expect(rootSource.size).toBe(1);
@ -301,4 +280,181 @@ describe("kubeconfig-sync.source tests", () => {
}
});
});
describe("given a config file at /foobar/config", () => {
let manager: KubeconfigSyncManager;
let watchInstances: Map<string, Watcher<true>>;
let firstReadFoobarConfigSteam: ReadStream;
let secondReadFoobarConfigSteam: ReadStream;
let statMock: AsyncFnMock<Stat>;
beforeEach(() => {
statMock = asyncFn();
di.override(statInjectable, () => statMock);
watchInstances = new Map();
di.override(watchInjectable, () => (path) => {
const fakeWatchInstance = getFakeWatchInstance();
watchInstances.set(path, fakeWatchInstance);
return fakeWatchInstance;
});
di.override(createReadFileStreamInjectable, () => (filePath) => {
if (filePath !== "/foobar/config") {
throw new Error(`unexpected file path "${filePath}"`);
}
if (!firstReadFoobarConfigSteam) {
return firstReadFoobarConfigSteam = getFakeReadStream(filePath);
}
if (!secondReadFoobarConfigSteam) {
return secondReadFoobarConfigSteam = getFakeReadStream(filePath);
}
return getFakeReadStream(filePath);
});
manager = di.inject(kubeconfigSyncManagerInjectable);
});
afterEach(() => {
(firstReadFoobarConfigSteam as any) = undefined;
(secondReadFoobarConfigSteam as any) = undefined;
});
it("should not find any entities", () => {
expect(manager.source.get()).toEqual([]);
});
describe("when sync has started", () => {
beforeEach(() => {
manager.startSync();
});
it("should not find any entities", () => {
expect(manager.source.get()).toEqual([]);
});
describe("when a file sync target for /foobar/config is added", () => {
beforeEach(() => {
kubeconfigSyncs.set("/foobar/config", {});
});
describe("when stat resolves as not a directory", () => {
beforeEach(async () => {
await statMock.resolveSpecific(["/foobar/config"], {
isDirectory: () => false,
} as Stats);
});
describe("when the watch emits that the file is added", () => {
beforeEach(() => {
strictGet(watchInstances, "/foobar/config").emit("add", "/foobar/config", {
size: foobarConfig.length,
} as Stats);
});
it("starts to read the file", () => {
expect(firstReadFoobarConfigSteam).toBeDefined();
});
describe("when the data is read in", () => {
beforeEach(() => {
firstReadFoobarConfigSteam.emit("data", Buffer.from(foobarConfig));
firstReadFoobarConfigSteam.emit("end");
firstReadFoobarConfigSteam.emit("close");
});
it("should find a single entity", () => {
expect(manager.source.get().length).toBe(1);
});
describe("when a folder sync target for /foobar is added", () => {
beforeEach(() => {
kubeconfigSyncs.set("/foobar", {});
});
describe("when stat resolves as not a directory", () => {
beforeEach(async () => {
await statMock.resolveSpecific(["/foobar"], {
isDirectory: () => true,
} as Stats);
});
describe("when the watch emits that the file is added", () => {
beforeEach(() => {
strictGet(watchInstances, "/foobar").emit("add", "/foobar/config", {
size: foobarConfig.length,
} as Stats);
});
it("starts to read the file", () => {
expect(secondReadFoobarConfigSteam).toBeDefined();
});
describe("when the data is read in", () => {
beforeEach(() => {
secondReadFoobarConfigSteam.emit("data", Buffer.from(foobarConfig));
secondReadFoobarConfigSteam.emit("end");
secondReadFoobarConfigSteam.emit("close");
});
it("should still only find a single entity", () => {
expect(manager.source.get().length).toBe(1);
});
});
});
});
});
});
});
});
});
});
});
});
const getFakeWatchInstance = (): Watcher<true> => {
return Object.assign(new EventEmitter(), {
close: jest.fn().mockImplementation(async () => {}),
});
};
const getFakeReadStream = (path: string): ReadStream => {
return Object.assign(new EventEmitter(), {
path,
close: () => {},
push: () => true,
read: () => {},
}) as unknown as ReadStream;
};
const foobarConfig = JSON.stringify({
clusters: [{
name: "cluster-name",
cluster: {
server: "1.2.3.4",
},
skipTLSVerify: false,
}],
users: [{
name: "user-name",
}],
contexts: [{
name: "context-name",
context: {
cluster: "cluster-name",
user: "user-name",
},
}, {
name: "context-the-second",
context: {
cluster: "missing-cluster",
user: "user-name",
},
}],
currentContext: "foobar",
});

View File

@ -0,0 +1,102 @@
/**
* 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 { createHash } from "crypto";
import type { ObservableMap } from "mobx";
import { action } from "mobx";
import { homedir } from "os";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import type { CatalogEntity } from "../../../common/catalog";
import getClusterByIdInjectable from "../../../common/cluster-store/get-by-id.injectable";
import type { Cluster } from "../../../common/cluster/cluster";
import { loadConfigFromString } from "../../../common/kube-helpers";
import clustersThatAreBeingDeletedInjectable from "../../cluster/are-being-deleted.injectable";
import { catalogEntityFromCluster } from "../../cluster/manager";
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
import configToModelsInjectable from "./config-to-models.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
export type ComputeKubeconfigDiff = (contents: string, source: ObservableMap<string, [Cluster, CatalogEntity]>, filePath: string) => void;
const computeKubeconfigDiffInjectable = getInjectable({
id: "compute-kubeconfig-diff",
instantiate: (di): ComputeKubeconfigDiff => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
const createCluster = di.inject(createClusterInjectable);
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
const configToModels = di.inject(configToModelsInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
return action((contents, source, filePath) => {
try {
const { config, error } = loadConfigFromString(contents);
if (error) {
logger.warn(`encountered errors while loading config: ${error.message}`, { filePath, details: error.details });
}
const rawModels = configToModels(config, filePath);
const models = new Map(rawModels.map(([model, configData]) => [model.contextName, [model, configData] as const]));
logger.debug(`File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
const data = models.get(contextName);
// remove and disconnect clusters that were removed from the config
if (!data) {
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
clustersThatAreBeingDeleted.delete(value[0].id);
value[0].disconnect();
source.delete(contextName);
logger.debug(`Removed old cluster from sync`, { filePath, contextName });
continue;
}
// TODO: For the update check we need to make sure that the config itself hasn't changed.
// Probably should make it so that cluster keeps a copy of the config in its memory and
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(data[0]);
models.delete(contextName);
logger.debug(`Updated old cluster from sync`, { filePath, contextName });
}
for (const [contextName, [model, configData]] of models) {
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = getClusterById(clusterId) ?? createCluster({ ...model, id: clusterId }, configData);
if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error");
}
const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(directoryForKubeConfigs)) {
entity.metadata.labels.file = filePath.replace(homedir(), "~");
}
source.set(contextName, [cluster, entity]);
logger.debug(`Added new cluster from sync`, { filePath, contextName });
} catch (error) {
logger.warn(`Failed to create cluster from model: ${error}`, { filePath, contextName });
}
}
} catch (error) {
logger.warn(`Failed to compute diff: ${error}`, { filePath });
source.clear(); // clear source if we have failed so as to not show outdated information
}
logger.debug("Finished computing diff", { filePath });
});
},
});
export default computeKubeconfigDiffInjectable;

View File

@ -0,0 +1,42 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { KubeConfig } from "@kubernetes/client-node";
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterConfigData, UpdateClusterModel } from "../../../common/cluster-types";
import { splitConfig } from "../../../common/kube-helpers";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
export type ConfigToModels = (rootConfig: KubeConfig, filePath: string) => [UpdateClusterModel, ClusterConfigData][];
const configToModelsInjectable = getInjectable({
id: "config-to-models",
instantiate: (di): ConfigToModels => {
const logger = di.inject(kubeconfigSyncLoggerInjectable);
return (rootConfig, filePath) => {
const validConfigs: ReturnType<ConfigToModels> = [];
for (const { config, validationResult } of splitConfig(rootConfig)) {
if (validationResult.error) {
logger.debug(`context failed validation: ${validationResult.error}`, { context: config.currentContext, filePath });
} else {
validConfigs.push([
{
kubeConfigPath: filePath,
contextName: config.currentContext,
},
{
clusterServerUrl: validationResult.cluster.server,
},
]);
}
}
return validConfigs;
};
},
});
export default configToModelsInjectable;

View File

@ -0,0 +1,90 @@
/**
* 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 { constants } from "fs";
import type { ObservableMap } from "mobx";
import type { Readable } from "stream";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import createReadFileStreamInjectable from "../../../common/fs/create-read-file-stream.injectable";
import type { Disposer } from "../../../common/utils";
import { bytesToUnits, noop } from "../../../common/utils";
import computeKubeconfigDiffInjectable from "./compute-diff.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
export interface DiffChangedKubeconfigArgs {
filePath: string;
source: ObservableMap<string, [Cluster, CatalogEntity]>;
stats: Stats;
maxAllowedFileReadSize: number;
}
export type DiffChangedKubeconfig = (args: DiffChangedKubeconfigArgs) => Disposer;
const diffChangedKubeconfigInjectable = getInjectable({
id: "diff-changed-kubeconfig",
instantiate: (di): DiffChangedKubeconfig => {
const computeKubeconfigDiff = di.inject(computeKubeconfigDiffInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const createReadFileStream = di.inject(createReadFileStreamInjectable);
return ({ filePath, maxAllowedFileReadSize, source, stats }) => {
logger.debug(`file changed`, { filePath });
if (stats.size >= maxAllowedFileReadSize) {
logger.warn(`skipping ${filePath}: size=${bytesToUnits(stats.size)} is larger than maxSize=${bytesToUnits(maxAllowedFileReadSize)}`);
source.clear();
return noop;
}
const fileReader = createReadFileStream(filePath, {
mode: constants.O_RDONLY,
});
const readStream = fileReader as Readable;
const decoder = new TextDecoder("utf-8", { fatal: true });
let fileString = "";
let closed = false;
const cleanup = () => {
closed = true;
fileReader.close(); // This may not close the stream.
// Artificially marking end-of-stream, as if the underlying resource had
// indicated end-of-file by itself, allows the stream to close.
// This does not cancel pending read operations, and if there is such an
// operation, the process may still not be able to exit successfully
// until it finishes.
fileReader.push(null);
fileReader.read(0);
readStream.removeAllListeners();
};
readStream
.on("data", (chunk: Buffer) => {
try {
fileString += decoder.decode(chunk, { stream: true });
} catch (error) {
logger.warn(`skipping ${filePath}: ${error}`);
source.clear();
cleanup();
}
})
.on("close", () => cleanup())
.on("error", error => {
cleanup();
logger.warn(`failed to read file: ${error}`, { filePath });
})
.on("end", () => {
if (!closed) {
computeKubeconfigDiff(fileString, source, filePath);
}
});
return cleanup;
};
},
});
export default diffChangedKubeconfigInjectable;

View File

@ -0,0 +1,13 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import prefixedLoggerInjectable from "../../../common/logger/prefixed-logger.injectable";
const kubeconfigSyncLoggerInjectable = getInjectable({
id: "kubeconfig-sync-logger",
instantiate: (di) => di.inject(prefixedLoggerInjectable, "KUBECONFIG-SYNC"),
});
export default kubeconfigSyncLoggerInjectable;

View File

@ -5,18 +5,18 @@
import { getInjectable } from "@ogre-tools/injectable";
import directoryForKubeConfigsInjectable from "../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import { KubeconfigSyncManager } from "./manager";
import { createClusterInjectionToken } from "../../../common/cluster/create-cluster-injection-token";
import clusterManagerInjectable from "../../cluster-manager.injectable";
import catalogEntityRegistryInjectable from "../../catalog/entity-registry.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
import watchKubeconfigFileChangesInjectable from "./watch-file-changes.injectable";
import kubeconfigSyncsInjectable from "../../../common/user-store/kubeconfig-syncs.injectable";
const kubeconfigSyncManagerInjectable = getInjectable({
id: "kubeconfig-sync-manager",
instantiate: (di) => new KubeconfigSyncManager({
directoryForKubeConfigs: di.inject(directoryForKubeConfigsInjectable),
createCluster: di.inject(createClusterInjectionToken),
clusterManager: di.inject(clusterManagerInjectable),
entityRegistry: di.inject(catalogEntityRegistryInjectable),
logger: di.inject(kubeconfigSyncLoggerInjectable),
watchKubeconfigFileChanges: di.inject(watchKubeconfigFileChangesInjectable),
kubeconfigSyncs: di.inject(kubeconfigSyncsInjectable),
}),
});

View File

@ -4,97 +4,61 @@
*/
import type { IComputedValue, ObservableMap } from "mobx";
import { action, observable, computed, runInAction, makeObservable, observe } from "mobx";
import { action, observable, computed, makeObservable, observe } from "mobx";
import type { CatalogEntity } from "../../../common/catalog";
import type { FSWatcher } from "chokidar";
import { watch } from "chokidar";
import type { Stats } from "fs";
import fs from "fs";
import path from "path";
import type { Disposer } from "../../../common/utils";
import { disposer, bytesToUnits, getOrInsertWith, iter, noop } from "../../../common/utils";
import logger from "../../logger";
import type { KubeConfig } from "@kubernetes/client-node";
import { loadConfigFromString, splitConfig } from "../../../common/kube-helpers";
import type { ClusterManager } from "../../cluster-manager";
import { catalogEntityFromCluster } from "../../cluster-manager";
import { UserStore } from "../../../common/user-store";
import { ClusterStore } from "../../../common/cluster-store/cluster-store";
import { createHash } from "crypto";
import { homedir } from "os";
import globToRegExp from "glob-to-regexp";
import { inspect } from "util";
import type { ClusterConfigData, UpdateClusterModel } from "../../../common/cluster-types";
import type { Cluster } from "../../../common/cluster/cluster";
import type { CatalogEntityRegistry } from "../../catalog/entity-registry";
import type { CreateCluster } from "../../../common/cluster/create-cluster-injection-token";
const logPrefix = "[KUBECONFIG-SYNC]:";
/**
* This is the list of globs of which files are ignored when under a folder sync
*/
const ignoreGlobs = [
"*.lock", // kubectl lock files
"*.swp", // vim swap files
".DS_Store", // macOS specific
].map(rawGlob => ({
rawGlob,
matcher: globToRegExp(rawGlob),
}));
/**
* This should be much larger than any kubeconfig text file
*
* Even if you have a cert-file, key-file, and client-cert files that is only
* 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
*/
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
import { iter } from "../../../common/utils";
import type { KubeconfigSyncValue } from "../../../common/user-store";
import type { Logger } from "../../../common/logger";
import type { WatchKubeconfigFileChanges } from "./watch-file-changes.injectable";
interface KubeconfigSyncManagerDependencies {
readonly directoryForKubeConfigs: string;
readonly entityRegistry: CatalogEntityRegistry;
readonly clusterManager: ClusterManager;
createCluster: CreateCluster;
readonly logger: Logger;
readonly kubeconfigSyncs: ObservableMap<string, KubeconfigSyncValue>;
watchKubeconfigFileChanges: WatchKubeconfigFileChanges;
}
const kubeConfigSyncName = "lens:kube-sync";
export class KubeconfigSyncManager {
protected readonly sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
protected syncing = false;
protected syncListDisposer?: Disposer;
constructor(protected readonly dependencies: KubeconfigSyncManagerDependencies) {
makeObservable(this);
}
public readonly source = computed(() => {
/**
* This prevents multiple overlapping syncs from leading to multiple entities with the same IDs
*/
const seenIds = new Set<string>();
return (
iter.pipeline(this.sources.values())
.flatMap(([entities]) => entities.get())
.filter(entity => {
const alreadySeen = seenIds.has(entity.getId());
seenIds.add(entity.getId());
return !alreadySeen;
})
.collect(items => [...items])
);
});
@action
startSync(): void {
if (this.syncing) {
return;
}
this.syncing = true;
logger.info(`${logPrefix} starting requested syncs`);
this.dependencies.entityRegistry.addComputedSource(kubeConfigSyncName, computed(() => (
Array.from(iter.flatMap(
this.sources.values(),
([entities]) => entities.get(),
))
)));
this.dependencies.logger.info(`starting requested syncs`);
// This must be done so that c&p-ed clusters are visible
this.startNewSync(this.dependencies.directoryForKubeConfigs);
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
for (const filePath of this.dependencies.kubeconfigSyncs.keys()) {
this.startNewSync(filePath);
}
this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => {
this.syncListDisposer = observe(this.dependencies.kubeconfigSyncs, change => {
switch (change.type) {
case "add":
this.startNewSync(change.name);
@ -108,275 +72,38 @@ export class KubeconfigSyncManager {
@action
stopSync() {
this.dependencies.logger.info(`stopping requested syncs`);
this.syncListDisposer?.();
for (const filePath of this.sources.keys()) {
this.stopOldSync(filePath);
}
this.dependencies.entityRegistry.removeSource(kubeConfigSyncName);
this.syncing = false;
}
@action
protected startNewSync(filePath: string): void {
if (this.sources.has(filePath)) {
// don't start a new sync if we already have one
return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath });
return this.dependencies.logger.debug(`already syncing file/folder`, { filePath });
}
this.sources.set(
filePath,
watchFileChanges(filePath, this.dependencies),
this.dependencies.watchKubeconfigFileChanges(filePath),
);
logger.info(`${logPrefix} starting sync of file/folder`, { filePath });
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
this.dependencies.logger.info(`starting sync of file/folder`, { filePath });
this.dependencies.logger.debug(`${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
}
@action
protected stopOldSync(filePath: string): void {
if (!this.sources.delete(filePath)) {
// already stopped
return void logger.debug(`${logPrefix} no syncing file/folder to stop`, { filePath });
return this.dependencies.logger.debug(`no syncing file/folder to stop`, { filePath });
}
logger.info(`${logPrefix} stopping sync of file/folder`, { filePath });
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
this.dependencies.logger.info(`stopping sync of file/folder`, { filePath });
this.dependencies.logger.debug(`${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
}
}
// exported for testing
export function configToModels(rootConfig: KubeConfig, filePath: string): [UpdateClusterModel, ClusterConfigData][] {
const validConfigs: ReturnType<typeof configToModels> = [];
for (const { config, validationResult } of splitConfig(rootConfig)) {
if (validationResult.error) {
logger.debug(`${logPrefix} context failed validation: ${validationResult.error}`, { context: config.currentContext, filePath });
} else {
validConfigs.push([
{
kubeConfigPath: filePath,
contextName: config.currentContext,
},
{
clusterServerUrl: validationResult.cluster.server,
},
]);
}
}
return validConfigs;
}
type RootSourceValue = [Cluster, CatalogEntity];
type RootSource = ObservableMap<string, RootSourceValue>;
interface ComputeDiffDependencies {
directoryForKubeConfigs: string;
createCluster: CreateCluster;
clusterManager: ClusterManager;
}
// exported for testing
export const computeDiff = ({ directoryForKubeConfigs, createCluster, clusterManager }: ComputeDiffDependencies) => (contents: string, source: RootSource, filePath: string): void => {
runInAction(() => {
try {
const { config, error } = loadConfigFromString(contents);
if (error) {
logger.warn(`${logPrefix} encountered errors while loading config: ${error.message}`, { filePath, details: error.details });
}
const rawModels = configToModels(config, filePath);
const models = new Map(rawModels.map(([model, configData]) => [model.contextName, [model, configData] as const]));
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
for (const [contextName, value] of source) {
const data = models.get(contextName);
// remove and disconnect clusters that were removed from the config
if (!data) {
// remove from the deleting set, so that if a new context of the same name is added, it isn't marked as deleting
clusterManager.deleting.delete(value[0].id);
value[0].disconnect();
source.delete(contextName);
logger.debug(`${logPrefix} Removed old cluster from sync`, { filePath, contextName });
continue;
}
// TODO: For the update check we need to make sure that the config itself hasn't changed.
// Probably should make it so that cluster keeps a copy of the config in its memory and
// diff against that
// or update the model and mark it as not needed to be added
value[0].updateModel(data[0]);
models.delete(contextName);
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
}
for (const [contextName, [model, configData]] of models) {
// add new clusters to the source
try {
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = ClusterStore.getInstance().getById(clusterId) || createCluster({ ...model, id: clusterId }, configData);
if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error");
}
const entity = catalogEntityFromCluster(cluster);
if (!filePath.startsWith(directoryForKubeConfigs)) {
entity.metadata.labels.file = filePath.replace(homedir(), "~");
}
source.set(contextName, [cluster, entity]);
logger.debug(`${logPrefix} Added new cluster from sync`, { filePath, contextName });
} catch (error) {
logger.warn(`${logPrefix} Failed to create cluster from model: ${error}`, { filePath, contextName });
}
}
} catch (error) {
logger.warn(`${logPrefix} Failed to compute diff: ${error}`, { filePath });
source.clear(); // clear source if we have failed so as to not show outdated information
}
});
};
interface DiffChangedConfigArgs {
filePath: string;
source: RootSource;
stats: fs.Stats;
maxAllowedFileReadSize: number;
}
const diffChangedConfigFor = (dependencies: ComputeDiffDependencies) => ({ filePath, source, stats, maxAllowedFileReadSize }: DiffChangedConfigArgs): Disposer => {
logger.debug(`${logPrefix} file changed`, { filePath });
if (stats.size >= maxAllowedFileReadSize) {
logger.warn(`${logPrefix} skipping ${filePath}: size=${bytesToUnits(stats.size)} is larger than maxSize=${bytesToUnits(maxAllowedFileReadSize)}`);
source.clear();
return noop;
}
const controller = new AbortController();
const fileContentsP = fs.promises.readFile(filePath, {
signal: controller.signal,
});
const cleanup = disposer(
() => controller.abort(),
);
fileContentsP
.then((fileData) => {
const decoder = new TextDecoder("utf-8", { fatal: true });
try {
const fileString = decoder.decode(fileData);
computeDiff(dependencies)(fileString, source, filePath);
} catch (error) {
logger.warn(`${logPrefix} skipping ${filePath}: ${error}`);
source.clear();
cleanup();
}
})
.catch(error => {
if (controller.signal.aborted) {
return;
}
logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath });
cleanup();
});
return cleanup;
};
const watchFileChanges = (filePath: string, dependencies: ComputeDiffDependencies): [IComputedValue<CatalogEntity[]>, Disposer] => {
const rootSource = observable.map<string, ObservableMap<string, RootSourceValue>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: FSWatcher;
(async () => {
try {
const stat = await fs.promises.stat(filePath);
const isFolderSync = stat.isDirectory();
const cleanupFns = new Map<string, Disposer>();
const maxAllowedFileReadSize = isFolderSync
? folderSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize;
watcher = watch(filePath, {
followSymlinks: true,
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
atomic: 150, // for "atomic writes"
alwaysStat: true,
});
const diffChangedConfig = diffChangedConfigFor(dependencies);
watcher
.on("change", (childFilePath, stats: Stats): void => {
const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) {
// file was previously ignored, do nothing
return void logger.debug(`${logPrefix} ${inspect(childFilePath)} that should have been previously ignored has changed. Doing nothing`);
}
cleanup();
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("add", (childFilePath, stats: Stats): void => {
if (isFolderSync) {
const fileName = path.basename(childFilePath);
for (const ignoreGlob of ignoreGlobs) {
if (ignoreGlob.matcher.test(fileName)) {
return void logger.info(`${logPrefix} ignoring ${inspect(childFilePath)} due to ignore glob: ${ignoreGlob.rawGlob}`);
}
}
}
cleanupFns.set(childFilePath, diffChangedConfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("unlink", (childFilePath) => {
cleanupFns.get(childFilePath)?.();
cleanupFns.delete(childFilePath);
rootSource.delete(childFilePath);
})
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
} catch (error) {
console.log((error as { stack: unknown }).stack);
logger.warn(`${logPrefix} failed to start watching changes: ${error}`);
}
})();
return [derivedSource, () => {
watcher?.close();
}];
};

View File

@ -0,0 +1,134 @@
/**
* 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 GlobToRegExp from "glob-to-regexp";
import type { IComputedValue, ObservableMap } from "mobx";
import { computed, observable } from "mobx";
import path from "path";
import { inspect } from "util";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import statInjectable from "../../../common/fs/stat/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable";
import type { Disposer } from "../../../common/utils";
import { getOrInsertWith, iter } from "../../../common/utils";
import diffChangedKubeconfigInjectable from "./diff-changed-kubeconfig.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
export type WatchKubeconfigFileChanges = (filepath: string) => [IComputedValue<CatalogEntity[]>, Disposer];
/**
* This is the list of globs of which files are ignored when under a folder sync
*/
const ignoreGlobs = [
"*.lock", // kubectl lock files
"*.swp", // vim swap files
".DS_Store", // macOS specific
].map(rawGlob => ({
rawGlob,
matcher: GlobToRegExp(rawGlob),
}));
/**
* This should be much larger than any kubeconfig text file
*
* Even if you have a cert-file, key-file, and client-cert files that is only
* 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
*/
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB
const watchKubeconfigFileChangesInjectable = getInjectable({
id: "watch-kubeconfig-file-changes",
instantiate: (di): WatchKubeconfigFileChanges => {
const diffChangedKubeconfig = di.inject(diffChangedKubeconfigInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const stat = di.inject(statInjectable);
const watch = di.inject(watchInjectable);
return (filePath) => {
const rootSource = observable.map<string, ObservableMap<string, [Cluster, CatalogEntity]>>();
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: Watcher<true>;
(async () => {
try {
const stats = await stat(filePath);
const isFolderSync = stats.isDirectory();
const cleanupFns = new Map<string, Disposer>();
const maxAllowedFileReadSize = isFolderSync
? folderSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize;
watcher = watch<true>(filePath, {
followSymlinks: true,
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
disableGlobbing: true,
ignorePermissionErrors: true,
usePolling: false,
awaitWriteFinish: {
pollInterval: 100,
stabilityThreshold: 1000,
},
atomic: 150, // for "atomic writes"
alwaysStat: true,
});
watcher
.on("change", (childFilePath, stats): void => {
const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) {
// file was previously ignored, do nothing
return void logger.debug(`${inspect(childFilePath)} that should have been previously ignored has changed. Doing nothing`);
}
cleanup();
cleanupFns.set(childFilePath, diffChangedKubeconfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("add", (childFilePath, stats): void => {
if (isFolderSync) {
const fileName = path.basename(childFilePath);
for (const ignoreGlob of ignoreGlobs) {
if (ignoreGlob.matcher.test(fileName)) {
return void logger.info(`ignoring ${inspect(childFilePath)} due to ignore glob: ${ignoreGlob.rawGlob}`);
}
}
}
cleanupFns.set(childFilePath, diffChangedKubeconfig({
filePath: childFilePath,
source: getOrInsertWith(rootSource, childFilePath, observable.map),
stats,
maxAllowedFileReadSize,
}));
})
.on("unlink", (childFilePath) => {
cleanupFns.get(childFilePath)?.();
cleanupFns.delete(childFilePath);
rootSource.delete(childFilePath);
})
.on("error", error => logger.error(`watching file/folder failed: ${error}`, { filePath }));
} catch (error) {
logger.warn(`failed to start watching changes: ${error}`);
}
})();
return [derivedSource, () => {
watcher?.close();
}];
};
},
});
export default watchKubeconfigFileChangesInjectable;

View File

@ -13,6 +13,7 @@ const startCatalogSyncInjectable = getInjectable({
const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable);
return {
id: "start-catalog-sync",
run: async () => {
if (!catalogSyncToRenderer.started) {
await catalogSyncToRenderer.start();

View File

@ -13,6 +13,7 @@ const stopCatalogSyncInjectable = getInjectable({
const catalogSyncToRenderer = di.inject(catalogSyncToRendererInjectable);
return {
id: "stop-catalog-sync",
run: async () => {
if (catalogSyncToRenderer.started) {
await catalogSyncToRenderer.stop();

View File

@ -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 { ClusterManager } from "./cluster-manager";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import catalogEntityRegistryInjectable from "./catalog/entity-registry.injectable";
const clusterManagerInjectable = getInjectable({
id: "cluster-manager",
instantiate: (di) => {
const clusterManager = new ClusterManager({
store: di.inject(clusterStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
});
clusterManager.init();
return clusterManager;
},
});
export default clusterManagerInjectable;

View File

@ -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 { observable } from "mobx";
import type { ClusterId } from "../../common/cluster-types";
const clustersThatAreBeingDeletedInjectable = getInjectable({
id: "clusters-that-are-being-deleted",
instantiate: () => observable.set<ClusterId>(),
});
export default clustersThatAreBeingDeletedInjectable;

View File

@ -0,0 +1,25 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { onLoadOfApplicationInjectionToken } from "../start-main-application/runnable-tokens/on-load-of-application-injection-token";
import clusterManagerInjectable from "./manager.injectable";
const initializeClusterManagerInjectable = getInjectable({
id: "initialize-cluster-manager",
instantiate: (di) => {
const clusterManager = di.inject(clusterManagerInjectable);
return {
id: "initialize-cluster-manager",
run: () => {
clusterManager.init();
},
};
},
injectionToken: onLoadOfApplicationInjectionToken,
causesSideEffects: true,
});
export default initializeClusterManagerInjectable;

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 clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable";
import { ClusterManager } from "./manager";
const clusterManagerInjectable = getInjectable({
id: "cluster-manager",
instantiate: (di) => new ClusterManager({
store: di.inject(clusterStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
clustersThatAreBeingDeleted: di.inject(clustersThatAreBeingDeletedInjectable),
}),
});
export default clusterManagerInjectable;

View File

@ -3,36 +3,36 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import "../common/ipc/cluster";
import "../../common/ipc/cluster";
import type http from "http";
import type { ObservableSet } from "mobx";
import { action, makeObservable, observable, observe, reaction, toJS } from "mobx";
import type { Cluster } from "../common/cluster/cluster";
import logger from "./logger";
import { apiKubePrefix } from "../common/vars";
import { getClusterIdFromHost, isErrnoException } from "../common/utils";
import type { KubernetesClusterPrometheusMetrics } from "../common/catalog-entities/kubernetes-cluster";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../common/ipc";
import type { Cluster } from "../../common/cluster/cluster";
import logger from "../logger";
import { apiKubePrefix } from "../../common/vars";
import { getClusterIdFromHost, isErrnoException } from "../../common/utils";
import type { KubernetesClusterPrometheusMetrics } from "../../common/catalog-entities/kubernetes-cluster";
import { isKubernetesCluster, KubernetesCluster, LensKubernetesClusterStatus } from "../../common/catalog-entities/kubernetes-cluster";
import { ipcMainOn } from "../../common/ipc";
import { once } from "lodash";
import type { ClusterStore } from "../common/cluster-store/cluster-store";
import type { ClusterId } from "../common/cluster-types";
import type { CatalogEntityRegistry } from "./catalog";
import type { ClusterStore } from "../../common/cluster-store/cluster-store";
import type { ClusterId } from "../../common/cluster-types";
import type { CatalogEntityRegistry } from "../catalog";
const logPrefix = "[CLUSTER-MANAGER]:";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
interface Dependencies {
store: ClusterStore;
catalogEntityRegistry: CatalogEntityRegistry;
readonly store: ClusterStore;
readonly catalogEntityRegistry: CatalogEntityRegistry;
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
}
export class ClusterManager {
deleting = observable.set<ClusterId>();
@observable visibleCluster: ClusterId | undefined = undefined;
constructor(private dependencies: Dependencies) {
constructor(private readonly dependencies: Dependencies) {
makeObservable(this);
}
@ -69,7 +69,7 @@ export class ClusterManager {
}
});
observe(this.deleting, change => {
observe(this.dependencies.clustersThatAreBeingDeleted, change => {
if (change.type === "add") {
this.updateEntityStatus(this.dependencies.catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
}
@ -141,7 +141,7 @@ export class ClusterManager {
@action
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
if (this.deleting.has(entity.getId())) {
if (this.dependencies.clustersThatAreBeingDeleted.has(entity.getId())) {
entity.status.phase = LensKubernetesClusterStatus.DELETING;
entity.status.enabled = false;
} else {

View File

@ -13,6 +13,7 @@ const cleanUpDeepLinkingInjectable = getInjectable({
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
return {
id: "clean-up-deep-linking",
run: () => {
lensProtocolRouterMain.cleanup();
},

View File

@ -16,6 +16,7 @@ const hideDockForLastClosedWindowInjectable = getInjectable({
const getVisibleWindows = di.inject(getVisibleWindowsInjectable);
return {
id: "hide-dock-when-there-are-no-windows",
run: () => {
const visibleWindows = getVisibleWindows();

View File

@ -13,6 +13,7 @@ const showDockForFirstOpenedWindowInjectable = getInjectable({
const app = di.inject(electronAppInjectable);
return {
id: "show-dock-for-first-opened-window",
run: () => {
app.dock?.show();
},

View File

@ -15,6 +15,7 @@ const enforceSingleApplicationInstanceInjectable = getInjectable({
const exitApp = di.inject(exitAppInjectable);
return {
id: "enforce-single-application-instance",
run: () => {
if (!requestSingleInstanceLock()) {
exitApp();

View File

@ -15,6 +15,7 @@ const setupApplicationNameInjectable = getInjectable({
const appName = di.inject(appNameInjectable);
return {
id: "setup-application-name",
run: () => {
app.setName(appName);
},

View File

@ -26,6 +26,7 @@ const setupDeepLinkingInjectable = getInjectable({
);
return {
id: "setup-deep-linking",
run: async () => {
logger.info(`📟 Setting protocol client for lens://`);

View File

@ -13,6 +13,7 @@ const setupDeveloperToolsInDevelopmentEnvironmentInjectable = getInjectable({
const logger = di.inject(loggerInjectable);
return {
id: "setup-developer-tools-in-development-environment",
run: () => {
if (process.env.NODE_ENV !== "development") {
return;

View File

@ -15,6 +15,7 @@ const setupDeviceShutdownInjectable = getInjectable({
const exitApp = di.inject(exitAppInjectable);
return {
id: "setup-device-shutdown",
run: () => {
powerMonitor.on("shutdown", async () => {
exitApp();

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import directoryForLensLocalStorageInjectable from "../../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import { setupIpcMainHandlers } from "./setup-ipc-main-handlers";
import loggerInjectable from "../../../../common/logger.injectable";
import clusterManagerInjectable from "../../../cluster-manager.injectable";
import clusterManagerInjectable from "../../../cluster/manager.injectable";
import applicationMenuItemsInjectable from "../../../menu/application-menu-items.injectable";
import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable";
import clusterStoreInjectable from "../../../../common/cluster-store/cluster-store.injectable";
@ -14,6 +14,7 @@ import { onLoadOfApplicationInjectionToken } from "../../../start-main-applicati
import operatingSystemThemeInjectable from "../../../theme/operating-system-theme.injectable";
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
import askUserForFilePathsInjectable from "../../../ipc/ask-user-for-file-paths.injectable";
import clustersThatAreBeingDeletedInjectable from "../../../cluster/are-being-deleted.injectable";
const setupIpcMainHandlersInjectable = getInjectable({
id: "setup-ipc-main-handlers",
@ -32,8 +33,10 @@ const setupIpcMainHandlersInjectable = getInjectable({
const clusterStore = di.inject(clusterStoreInjectable);
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return {
id: "setup-ipc-main-handlers",
run: () => {
logger.debug("[APP-MAIN] initializing ipc main handlers");
@ -46,6 +49,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
clustersThatAreBeingDeleted,
});
},
};

View File

@ -12,10 +12,10 @@ import { appEventBus } from "../../../../common/app-event-bus/event-bus";
import { broadcastMainChannel, broadcastMessage, ipcMainHandle, ipcMainOn } from "../../../../common/ipc";
import type { CatalogEntityRegistry } from "../../../catalog";
import { pushCatalogToRenderer } from "../../../catalog-pusher";
import type { ClusterManager } from "../../../cluster-manager";
import type { ClusterManager } from "../../../cluster/manager";
import { ResourceApplier } from "../../../resource-applier";
import { remove } from "fs-extra";
import type { IComputedValue } from "mobx";
import type { IComputedValue, ObservableSet } from "mobx";
import type { GetAbsolutePath } from "../../../../common/path/get-absolute-path.injectable";
import type { MenuItemOpts } from "../../../menu/application-menu-items.injectable";
import { windowActionHandleChannel, windowLocationChangedChannel, windowOpenAppMenuAsContextMenuChannel } from "../../../../common/ipc/window";
@ -34,9 +34,20 @@ interface Dependencies {
clusterStore: ClusterStore;
operatingSystemTheme: IComputedValue<Theme>;
askUserForFilePaths: AskUserForFilePaths;
clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
}
export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLocalStorage, getAbsolutePath, clusterManager, catalogEntityRegistry, clusterStore, operatingSystemTheme, askUserForFilePaths }: Dependencies) => {
export const setupIpcMainHandlers = ({
applicationMenuItems,
directoryForLensLocalStorage,
getAbsolutePath,
clusterManager,
catalogEntityRegistry,
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
clustersThatAreBeingDeleted,
}: Dependencies) => {
ipcMainHandle(clusterActivateHandler, (event, clusterId: ClusterId, force = false) => {
return ClusterStore.getInstance()
.getById(clusterId)
@ -101,11 +112,11 @@ export const setupIpcMainHandlers = ({ applicationMenuItems, directoryForLensLoc
});
ipcMainHandle(clusterSetDeletingHandler, (event, clusterId: string) => {
clusterManager.deleting.add(clusterId);
clustersThatAreBeingDeleted.add(clusterId);
});
ipcMainHandle(clusterClearDeletingHandler, (event, clusterId: string) => {
clusterManager.deleting.delete(clusterId);
clustersThatAreBeingDeleted.delete(clusterId);
});
ipcMainHandle(clusterKubectlApplyAllHandler, async (event, clusterId: ClusterId, resources: string[], extraArgs: string[]) => {

View File

@ -17,6 +17,7 @@ const setupMainWindowVisibilityAfterActivationInjectable = getInjectable({
const logger = di.inject(loggerInjectable);
return {
id: "setup-main-window-visibility-after-activation",
run: () => {
app.on("activate", async (_, windowIsVisible) => {
logger.info("APP:ACTIVATE", { hasVisibleWindows: windowIsVisible });

View File

@ -15,6 +15,7 @@ const setupRunnablesAfterWindowIsOpenedInjectable = getInjectable({
const afterWindowIsOpened = runManyFor(di)(afterWindowIsOpenedInjectionToken);
return {
id: "setup-runnables-after-window-is-opened",
run: () => {
const app = di.inject(electronAppInjectable);

View File

@ -26,6 +26,7 @@ const setupRunnablesBeforeClosingOfApplicationInjectable = getInjectable({
);
return {
id: "setup-closing-of-application",
run: () => {
const app = di.inject(electronAppInjectable);

View File

@ -88,6 +88,9 @@ import { registerMobX } from "@ogre-tools/injectable-extension-for-mobx";
import electronInjectable from "./utils/resolve-system-proxy/electron.injectable";
import type { HotbarStore } from "../common/hotbars/store";
import focusApplicationInjectable from "./electron-app/features/focus-application.injectable";
import kubectlDownloadingNormalizedArchInjectable from "./kubectl/normalized-arch.injectable";
import initializeClusterManagerInjectable from "./cluster/initialize-manager.injectable";
import addKubeconfigSyncAsEntitySourceInjectable from "./start-main-application/runnables/kube-config-sync/add-source.injectable";
import type { GlobalOverride } from "../common/test-utils/get-global-override";
export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {}) {
@ -125,7 +128,7 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
di.override(electronInjectable, () => ({}));
di.override(waitUntilBundledExtensionsAreLoadedInjectable, () => async () => {});
di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id");
di.override(kubectlDownloadingNormalizedArchInjectable, () => "amd64");
di.override(hotbarStoreInjectable, () => ({
load: () => {},
getActive: () => ({ name: "some-hotbar", items: [] }),
@ -204,6 +207,8 @@ export function getDiForUnitTesting(opts: { doGeneralOverrides?: boolean } = {})
const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
[
initializeExtensionsInjectable,
initializeClusterManagerInjectable,
addKubeconfigSyncAsEntitySourceInjectable,
setupIpcMainHandlersInjectable,
setupLensProxyInjectable,
setupShellInjectable,
@ -214,7 +219,10 @@ const overrideRunnablesHavingSideEffects = (di: DiContainer) => {
startCatalogSyncInjectable,
startKubeConfigSyncInjectable,
].forEach((injectable) => {
di.override(injectable, () => ({ run: () => {} }));
di.override(injectable, () => ({
id: injectable.id,
run: () => {},
}));
});
};
@ -226,18 +234,20 @@ const overrideOperatingSystem = (di: DiContainer) => {
};
const overrideElectronFeatures = (di: DiContainer) => {
di.override(setupMainWindowVisibilityAfterActivationInjectable, () => ({
run: () => {},
}));
[
setupMainWindowVisibilityAfterActivationInjectable,
setupDeviceShutdownInjectable,
setupDeepLinkingInjectable,
setupApplicationNameInjectable,
setupRunnablesBeforeClosingOfApplicationInjectable,
].forEach((injectable) => {
di.override(injectable, () => ({
id: injectable.id,
run: () => {},
}));
});
di.override(setupDeviceShutdownInjectable, () => ({
run: () => {},
}));
di.override(setupDeepLinkingInjectable, () => ({ run: () => {} }));
di.override(exitAppInjectable, () => () => {});
di.override(setupApplicationNameInjectable, () => ({ run: () => {} }));
di.override(setupRunnablesBeforeClosingOfApplicationInjectable, () => ({ run: () => {} }));
di.override(getCommandLineSwitchInjectable, () => () => "irrelevant");
di.override(requestSingleInstanceLockInjectable, () => () => true);
di.override(disableHardwareAccelerationInjectable, () => () => {});

View File

@ -7,7 +7,7 @@ import { LensProxy } from "./lens-proxy";
import { kubeApiUpgradeRequest } from "./proxy-functions";
import routerInjectable from "../router/router.injectable";
import httpProxy from "http-proxy";
import clusterManagerInjectable from "../cluster-manager.injectable";
import clusterManagerInjectable from "../cluster/manager.injectable";
import shellApiRequestInjectable from "./proxy-functions/shell-api-request/shell-api-request.injectable";
import lensProxyPortInjectable from "./lens-proxy-port.injectable";
import contentSecurityPolicyInjectable from "../../common/vars/content-security-policy.injectable";

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { shellApiRequest } from "./shell-api-request";
import createShellSessionInjectable from "../../../shell-session/create-shell-session.injectable";
import shellRequestAuthenticatorInjectable from "./shell-request-authenticator/shell-request-authenticator.injectable";
import clusterManagerInjectable from "../../../cluster-manager.injectable";
import clusterManagerInjectable from "../../../cluster/manager.injectable";
const shellApiRequestInjectable = getInjectable({
id: "shell-api-request",

View File

@ -7,7 +7,7 @@ import logger from "../../../logger";
import type WebSocket from "ws";
import { Server as WebSocketServer } from "ws";
import type { ProxyApiRequestArgs } from "../types";
import type { ClusterManager } from "../../../cluster-manager";
import type { ClusterManager } from "../../../cluster/manager";
import URLParse from "url-parse";
import type { Cluster } from "../../../../common/cluster/cluster";
import type { ClusterId } from "../../../../common/cluster-types";

View File

@ -15,6 +15,7 @@ const startApplicationMenuInjectable = getInjectable({
);
return {
id: "start-application-menu",
run: async () => {
await applicationMenu.start();
},

View File

@ -15,6 +15,7 @@ const stopApplicationMenuInjectable = getInjectable({
);
return {
id: "stop-application-menu",
run: async () => {
await applicationMenu.stop();
},

View File

@ -14,6 +14,7 @@ const setupListenerForCurrentClusterFrameInjectable = getInjectable({
id: "setup-listener-for-current-cluster-frame",
instantiate: (di) => ({
id: "setup-listener-for-current-cluster-frame",
run: () => {
const currentClusterFrameState = di.inject(currentClusterFrameClusterIdStateInjectable);

View File

@ -10,6 +10,7 @@ const cleanUpShellSessionsInjectable = getInjectable({
id: "clean-up-shell-sessions",
instantiate: () => ({
id: "clean-up-shell-sessions",
run: () => {
ShellSession.cleanup();
},

View File

@ -13,6 +13,7 @@ const emitCloseToEventBusInjectable = getInjectable({
const appEventBus = di.inject(appEventBusInjectable);
return {
id: "emit-close-to-event-bus",
run: () => {
appEventBus.emit({ name: "app", action: "close" });
},

View File

@ -13,6 +13,7 @@ const emitServiceStartToEventBusInjectable = getInjectable({
const appEventBus = di.inject(appEventBusInjectable);
return {
id: "emit-service-start-to-event-bus",
run: () => {
appEventBus.emit({ name: "service", action: "start" });
},

View File

@ -14,6 +14,7 @@ const flagRendererAsLoadedInjectable = getInjectable({
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
return {
id: "flag-renderer-as-loaded",
run: () => {
runInAction(() => {
// Todo: remove this kludge which enables out-of-place temporal dependency.

View File

@ -14,6 +14,7 @@ const flagRendererAsNotLoadedInjectable = getInjectable({
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
return {
id: "stop-deep-linking",
run: () => {
runInAction(() => {
// Todo: remove this kludge which enables out-of-place temporal dependency.

View File

@ -21,6 +21,7 @@ const initializeExtensionsInjectable = getInjectable({
const showErrorPopup = di.inject(showErrorPopupInjectable);
return {
id: "initialize-extensions",
run: async () => {
logger.info("🧩 Initializing extensions");

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 kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable";
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
import { afterApplicationIsLoadedInjectionToken } from "../../runnable-tokens/after-application-is-loaded-injection-token";
const addKubeconfigSyncAsEntitySourceInjectable = getInjectable({
id: "add-kubeconfig-sync-as-entity-source",
instantiate: (di) => {
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
return {
id: "add-kubeconfig-sync-as-entity-source",
run: () => {
entityRegistry.addComputedSource("kubeconfig-sync", kubeConfigSyncManager.source);
},
};
},
injectionToken: afterApplicationIsLoadedInjectionToken,
});
export default addKubeconfigSyncAsEntitySourceInjectable;

View File

@ -7,6 +7,7 @@ import { afterApplicationIsLoadedInjectionToken } from "../../runnable-tokens/af
import directoryForKubeConfigsInjectable from "../../../../common/app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
import ensureDirInjectable from "../../../../common/fs/ensure-dir.injectable";
import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable";
import addKubeconfigSyncAsEntitySourceInjectable from "./add-source.injectable";
const startKubeConfigSyncInjectable = getInjectable({
id: "start-kubeconfig-sync",
@ -17,11 +18,13 @@ const startKubeConfigSyncInjectable = getInjectable({
const ensureDir = di.inject(ensureDirInjectable);
return {
id: "start-kubeconfig-sync",
run: async () => {
await ensureDir(directoryForKubeConfigs);
kubeConfigSyncManager.startSync();
},
runAfter: di.inject(addKubeconfigSyncAsEntitySourceInjectable),
};
},

View File

@ -13,6 +13,7 @@ const stopKubeConfigSyncInjectable = getInjectable({
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
return {
id: "stop-kube-config-sync",
run: () => {
kubeConfigSyncManager.stopSync();
},

View File

@ -14,6 +14,7 @@ const setupSentryInjectable = getInjectable({
const initializeSentryOnMain = di.inject(initializeSentryOnMainInjectable);
return {
id: "setup-sentry",
run: () => initializeSentryReportingWith(initializeSentryOnMain),
};
},

View File

@ -18,6 +18,7 @@ const setupDetectorRegistryInjectable = getInjectable({
const detectorRegistry = di.inject(detectorRegistryInjectable);
return {
id: "setup-detector-registry",
run: () => {
detectorRegistry
.add(ClusterIdDetector)

View File

@ -15,6 +15,7 @@ const setupHardwareAccelerationInjectable = getInjectable({
const disableHardwareAcceleration = di.inject(disableHardwareAccelerationInjectable);
return {
id: "setup-hardware-acceleration",
run: () => {
if (hardwareAccelerationShouldBeDisabled) {
disableHardwareAcceleration();

View File

@ -11,6 +11,7 @@ const setupHotbarStoreInjectable = getInjectable({
id: "setup-hotbar-store",
instantiate: (di) => ({
id: "setup-hotbar-store",
run: () => {
const hotbarStore = di.inject(hotbarStoreInjectable);

View File

@ -10,6 +10,7 @@ const setupImmerInjectable = getInjectable({
id: "setup-immer",
instantiate: () => ({
id: "setup-immer",
run: () => {
// Docs: https://immerjs.github.io/immer/
// Required in `utils/storage-helper.ts`

View File

@ -26,6 +26,7 @@ const setupLensProxyInjectable = getInjectable({
const buildVersion = di.inject(buildVersionInjectable);
return {
id: "setup-lens-proxy",
run: async () => {
try {
logger.info("🔌 Starting LensProxy");

View File

@ -10,6 +10,7 @@ const setupMobxInjectable = getInjectable({
id: "setup-mobx",
instantiate: () => ({
id: "setup-mobx",
run: () => {
// Docs: https://mobx.js.org/configuration.html
Mobx.configure({

View File

@ -18,6 +18,7 @@ const setupPrometheusRegistryInjectable = getInjectable({
const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable);
return {
id: "setup-prometheus-registry",
run: () => {
prometheusProviderRegistry
.registerProvider(new PrometheusLens())

View File

@ -13,6 +13,7 @@ const setupProxyEnvInjectable = getInjectable({
const getCommandLineSwitch = di.inject(getCommandLineSwitchInjectable);
return {
id: "setup-proxy-env",
run: () => {
const switchValue = getCommandLineSwitch("proxy-server");

View File

@ -13,6 +13,7 @@ const setupReactionsInUserStoreInjectable = getInjectable({
const userStore = di.inject(userStoreInjectable);
return {
id: "setup-reactions-in-user-store",
run: () => {
userStore.startMainReactions();
},

View File

@ -20,6 +20,7 @@ const setupShellInjectable = getInjectable({
const electronApp = di.inject(electronAppInjectable);
return {
id: "setup-shell",
run: async () => {
logger.info("🐚 Syncing shell environment");

View File

@ -15,6 +15,7 @@ const setupSyncingOfGeneralCatalogEntitiesInjectable = getInjectable({
);
return {
id: "setup-syncing-of-general-catalog-entities",
run: () => {
syncGeneralCatalogEntities();
},

View File

@ -13,6 +13,7 @@ const setupSyncingOfWeblinksInjectable = getInjectable({
const syncWeblinks = di.inject(syncWeblinksInjectable);
return {
id: "setup-syncing-of-weblinks",
run: () => {
syncWeblinks();
},

View File

@ -10,6 +10,7 @@ const setupSystemCaInjectable = getInjectable({
id: "setup-system-ca",
instantiate: () => ({
id: "setup-system-ca",
run: async () => {
await injectSystemCAs();
},

View File

@ -3,7 +3,7 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import clusterManagerInjectable from "../../cluster-manager.injectable";
import clusterManagerInjectable from "../../cluster/manager.injectable";
import { beforeQuitOfFrontEndInjectionToken } from "../runnable-tokens/before-quit-of-front-end-injection-token";
const stopClusterManagerInjectable = getInjectable({
@ -13,6 +13,7 @@ const stopClusterManagerInjectable = getInjectable({
const clusterManager = di.inject(clusterManagerInjectable);
return {
id: "stop-cluster-manager",
run: () => {
clusterManager.stop();
},

View File

@ -4,7 +4,7 @@
*/
import { getInjectable } from "@ogre-tools/injectable";
import exitAppInjectable from "./electron-app/features/exit-app.injectable";
import clusterManagerInjectable from "./cluster-manager.injectable";
import clusterManagerInjectable from "./cluster/manager.injectable";
import appEventBusInjectable from "../common/app-event-bus/app-event-bus.injectable";
import loggerInjectable from "../common/logger.injectable";
import closeAllWindowsInjectable from "./start-main-application/lens-window/hide-all-windows/close-all-windows.injectable";

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import userStoreFileNameMigrationInjectable from "../../common/user-store/file-name-migration.injectable";
import userStoreInjectable from "../../common/user-store/user-store.injectable";
import { beforeApplicationIsLoadingInjectionToken } from "../start-main-application/runnable-tokens/before-application-is-loading-injection-token";
import initDefaultUpdateChannelInjectableInjectable from "../vars/default-update-channel/init.injectable";
import initDefaultUpdateChannelInjectable from "../vars/default-update-channel/init.injectable";
const initUserStoreInjectable = getInjectable({
id: "init-user-store",
@ -15,11 +15,12 @@ const initUserStoreInjectable = getInjectable({
const userStoreFileNameMigration = di.inject(userStoreFileNameMigrationInjectable);
return {
id: "init-user-store",
run: async () => {
await userStoreFileNameMigration();
userStore.load();
},
runAfter: di.inject(initDefaultUpdateChannelInjectableInjectable),
runAfter: di.inject(initDefaultUpdateChannelInjectable),
};
},
injectionToken: beforeApplicationIsLoadingInjectionToken,

View File

@ -13,6 +13,7 @@ const startBroadcastingThemeChangeInjectable = getInjectable({
const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable);
return {
id: "start-broadcasting-theme-change",
run: async () => {
await broadcastThemeChange.start();
},

View File

@ -13,6 +13,7 @@ const stopBroadcastingThemeChangeInjectable = getInjectable({
const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable);
return {
id: "stop-broadcasting-theme-change",
run: async () => {
await broadcastThemeChange.stop();
},

View File

@ -13,6 +13,7 @@ const startSyncingThemeFromOperatingSystemInjectable = getInjectable({
const syncTheme = di.inject(syncThemeFromOperatingSystemInjectable);
return {
id: "start-syncing-theme-from-operating-system",
run: async () => {
await syncTheme.start();
},

View File

@ -13,6 +13,7 @@ const stopSyncingThemeFromOperatingSystemInjectable = getInjectable({
const syncTheme = di.inject(syncThemeFromOperatingSystemInjectable);
return {
id: "stop-syncing-theme-from-operating-system",
run: async () => {
await syncTheme.stop();
},

View File

@ -13,6 +13,7 @@ const startTrayInjectable = getInjectable({
const electronTray = di.inject(electronTrayInjectable);
return {
id: "start-tray",
run: () => {
electronTray.start();
},

View File

@ -14,6 +14,7 @@ const stopTrayInjectable = getInjectable({
const electronTray = di.inject(electronTrayInjectable);
return {
id: "stop-tray",
run: () => {
electronTray.stop();
},

View File

@ -14,6 +14,7 @@ const startReactiveTrayMenuIconInjectable = getInjectable({
const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable);
return {
id: "start-reactive-tray-menu-icon",
run: async () => {
await reactiveTrayMenuIcon.start();
},

View File

@ -13,6 +13,7 @@ const stopReactiveTrayMenuIconInjectable = getInjectable({
const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable);
return {
id: "stop-reactive-tray-menu-icon",
run: async () => {
await reactiveTrayMenuIcon.stop();
},

View File

@ -14,6 +14,7 @@ const startReactiveTrayMenuItemsInjectable = getInjectable({
const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable);
return {
id: "start-reactive-tray-menu-items",
run: async () => {
await reactiveTrayMenuItems.start();
},

View File

@ -13,6 +13,7 @@ const stopReactiveTrayMenuItemsInjectable = getInjectable({
const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable);
return {
id: "stop-reactive-tray-menu-items",
run: async () => {
await reactiveTrayMenuItems.stop();
},

View File

@ -13,6 +13,7 @@ const startListeningOfChannelsInjectable = getInjectable({
const listeningOfChannels = di.inject(listeningOfChannelsInjectable);
return {
id: "start-listening-of-channels-main",
run: async () => {
await listeningOfChannels.start();
},

View File

@ -6,16 +6,17 @@ import { getInjectable } from "@ogre-tools/injectable";
import { beforeApplicationIsLoadingInjectionToken } from "../../start-main-application/runnable-tokens/before-application-is-loading-injection-token";
import buildVersionInjectable from "./build-version.injectable";
const initializeBuildVersionAsyncSyncBoxInjectable = getInjectable({
id: "initialize-build-version-async-sync-box",
const initializeBuildVersionInjectable = getInjectable({
id: "initialize-build-version",
instantiate: (di) => {
const buildVersion = di.inject(buildVersionInjectable);
return {
id: "initialize-build-version",
run: () => buildVersion.init(),
};
},
injectionToken: beforeApplicationIsLoadingInjectionToken,
});
export default initializeBuildVersionAsyncSyncBoxInjectable;
export default initializeBuildVersionInjectable;

View File

@ -7,12 +7,13 @@ import defaultUpdateChannelInjectable from "../../../common/application-update/s
import { beforeApplicationIsLoadingInjectionToken } from "../../start-main-application/runnable-tokens/before-application-is-loading-injection-token";
import initReleaseChannelInjectable from "../release-channel/init.injectable";
const initDefaultUpdateChannelInjectableInjectable = getInjectable({
id: "init-default-update-channel-injectable",
const initDefaultUpdateChannelInjectable = getInjectable({
id: "init-default-update-channel",
instantiate: (di) => {
const defaultUpdateChannel = di.inject(defaultUpdateChannelInjectable);
return {
id: "init-default-update-channel",
run: () => defaultUpdateChannel.init(),
runAfter: di.inject(initReleaseChannelInjectable),
};
@ -20,4 +21,4 @@ const initDefaultUpdateChannelInjectableInjectable = getInjectable({
injectionToken: beforeApplicationIsLoadingInjectionToken,
});
export default initDefaultUpdateChannelInjectableInjectable;
export default initDefaultUpdateChannelInjectable;

View File

@ -13,6 +13,7 @@ const initReleaseChannelInjectable = getInjectable({
const releaseChannel = di.inject(releaseChannelInjectable);
return {
id: "init-release-channel",
run: () => releaseChannel.init(),
runAfter: di.inject(initSemanticBuildVersionInjectable),
};

View File

@ -13,6 +13,7 @@ const initSemanticBuildVersionInjectable = getInjectable({
const buildSemanticVersion = di.inject(buildSemanticVersionInjectable);
return {
id: "init-semantic-build-version",
run: () => buildSemanticVersion.init(),
runAfter: di.inject(initializeBuildVersionInjectable),
};

View File

@ -11,6 +11,7 @@ const setupOnApiErrorListenersInjectable = getInjectable({
id: "setup-on-api-error-listeners",
instantiate: () => ({
id: "setup-on-api-error-listeners",
run: () => {
apiBase?.onError.addListener(onApiError);
},

View File

@ -17,6 +17,7 @@ const setupAppPathsInjectable = getInjectable({
const appPathsState = di.inject(appPathsStateInjectable);
return {
id: "setup-app-paths",
run: async () => {
const appPaths = await requestFromChannel(
appPathsChannel,

View File

@ -17,11 +17,12 @@ import { Icon } from "../icon";
import { HotbarIcon } from "./hotbar-icon";
import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster";
import type { VisitEntityContextMenu } from "../../../common/catalog/visit-entity-context-menu.injectable";
import { navigate } from "../../navigation";
import { withInjectables } from "@ogre-tools/injectable-react";
import catalogCategoryRegistryInjectable from "../../../common/catalog/category-registry.injectable";
import visitEntityContextMenuInjectable from "../../../common/catalog/visit-entity-context-menu.injectable";
import activeEntityInjectable from "../../api/catalog/entity/active.injectable";
import type { Navigate } from "../../navigation/navigate.injectable";
import navigateInjectable from "../../navigation/navigate.injectable";
export interface HotbarEntityIconProps {
entity: CatalogEntity;
@ -38,13 +39,14 @@ interface Dependencies {
visitEntityContextMenu: VisitEntityContextMenu;
catalogCategoryRegistry: CatalogCategoryRegistry;
activeEntity: IComputedValue<CatalogEntity | undefined>;
navigate: Navigate;
}
@observer
class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps & Dependencies> {
private readonly menuItems = observable.array<CatalogEntityContextMenu>();
get kindIcon() {
private renderKindIcon() {
const className = styles.badge;
const category = this.props.catalogCategoryRegistry.getCategoryForEntity(this.props.entity);
@ -59,7 +61,7 @@ class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps
return <Icon material={category.metadata.icon} className={className} />;
}
get ledIcon() {
private renderLedIcon() {
if (this.props.entity.kind !== "KubernetesCluster") {
return null;
}
@ -86,7 +88,7 @@ class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps
this.props.visitEntityContextMenu(this.props.entity, {
menuItems: this.menuItems,
navigate,
navigate: this.props.navigate,
});
}
@ -113,8 +115,8 @@ class NonInjectedHotbarEntityIcon extends React.Component<HotbarEntityIconProps
)}
onClick={onClick}
>
{ this.ledIcon }
{ this.kindIcon }
{this.renderLedIcon()}
{this.renderKindIcon()}
</HotbarIcon>
);
}
@ -126,5 +128,6 @@ export const HotbarEntityIcon = withInjectables<Dependencies, HotbarEntityIconPr
catalogCategoryRegistry: di.inject(catalogCategoryRegistryInjectable),
visitEntityContextMenu: di.inject(visitEntityContextMenuInjectable),
activeEntity: di.inject(activeEntityInjectable),
navigate: di.inject(navigateInjectable),
}),
});

View File

@ -16,6 +16,7 @@ const startTopbarStateSyncInjectable = getInjectable({
const ipcRenderer = di.inject(ipcRendererInjectable);
return {
id: "start-topbar-state-sync",
run: () => {
ipcRenderer.on("history:can-go-back", action((event, canGoBack: boolean) => {
state.prevEnabled = canGoBack;

View File

@ -54,7 +54,7 @@ import { RootFrame } from "../../frames/root-frame/root-frame";
import { ClusterFrame } from "../../frames/cluster-frame/cluster-frame";
import hostedClusterIdInjectable from "../../cluster-frame-context/hosted-cluster-id.injectable";
import activeKubernetesClusterInjectable from "../../cluster-frame-context/active-kubernetes-cluster.injectable";
import { catalogEntityFromCluster } from "../../../main/cluster-manager";
import { catalogEntityFromCluster } from "../../../main/cluster/manager";
import namespaceStoreInjectable from "../+namespaces/store.injectable";
import { isAllowedResource } from "../../../common/cluster/is-allowed-resource";
import createApplicationWindowInjectable from "../../../main/start-main-application/lens-window/application-window/create-application-window.injectable";

Some files were not shown because too many files have changed in this diff Show More