diff --git a/.github/workflows/mkdocs-manual.yml b/.github/workflows/mkdocs-manual.yml index 56bfd0c831..03ec36da46 100644 --- a/.github/workflows/mkdocs-manual.yml +++ b/.github/workflows/mkdocs-manual.yml @@ -53,9 +53,9 @@ jobs: rm -fr ./docs/clusters ./docs/contributing ./docs/faq ./docs/getting-started ./docs/helm ./docs/support ./docs/supporting sed -i '/Protocol Handlers/d' ./mkdocs.yml sed -i '/IPC/d' ./mkdocs.yml - sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/extensions/get-started/your-first-extension.md - sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev/latest/clusters/adding-clusters/#g' ./docs/README.md - sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/latest/contributing/#g' ./docs/extensions/guides/generator.md + sed -i 's#../../clusters/adding-clusters.md#https://docs.k8slens.dev/getting-started/add-cluster/#g' ./docs/extensions/get-started/your-first-extension.md + sed -i 's#clusters/adding-clusters.md#https://docs.k8slens.dev//getting-started/adding-clusters/#g' ./docs/README.md + sed -i 's#../../contributing/README.md#https://docs.k8slens.dev/contributing/#g' ./docs/extensions/guides/generator.md - name: git config run: | diff --git a/.github/workflows/require-milestone.yml b/.github/workflows/require-milestone.yml new file mode 100644 index 0000000000..388aa48013 --- /dev/null +++ b/.github/workflows/require-milestone.yml @@ -0,0 +1,14 @@ +name: Require Milestone +on: + pull_request: + types: [opened, edited, synchronize] +jobs: + milestone: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Require Milestone + run: | + exit $(gh pr view ${{ github.event.pull_request.number }} --json milestone | jq 'if .milestone == null then 1 else 0 end') + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1c745f852d..beb082afe9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,3 @@ # Contributing to Lens -See [Contributing to Lens](https://docs.k8slens.dev/latest/contributing/) documentation. +See [Contributing to Lens](https://docs.k8slens.dev/contributing/) documentation. diff --git a/README.md b/README.md index 0c4a63c174..f581e90401 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,15 @@ Lens IDE a standalone application for MacOS, Windows and Linux operating systems ## Installation -See [Getting Started](https://docs.k8slens.dev/main/getting-started/install-lens/) page. +See [Getting Started](https://docs.k8slens.dev/getting-started/install-lens/) page. ## Development -See [Development](https://docs.k8slens.dev/latest/contributing/development/) page. +See [Development](https://docs.k8slens.dev/contributing/development/) page. ## Contributing -See [Contributing](https://docs.k8slens.dev/latest/contributing/) page. +See [Contributing](https://docs.k8slens.dev/contributing/) page. ## License diff --git a/docs/extensions/get-started/your-first-extension.md b/docs/extensions/get-started/your-first-extension.md index c8c1167943..ec797f7441 100644 --- a/docs/extensions/get-started/your-first-extension.md +++ b/docs/extensions/get-started/your-first-extension.md @@ -78,7 +78,7 @@ npm run dev You must restart Lens for the extension to load. After this initial restart, reload Lens and it will automatically pick up changes any time the extension rebuilds. -With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/main/catalog/) for details on how to add a cluster in Lens IDE. +With Lens running, either connect to an existing cluster or create a new one - refer to the latest [Lens Documentation](https://docs.k8slens.dev/getting-started/add-cluster/) for details on how to add a cluster in Lens IDE. You will see the "Hello World" page in the left-side cluster menu. ## Develop the Extension diff --git a/docs/extensions/guides/generator.md b/docs/extensions/guides/generator.md index 64838c1bc4..fbc1723b0b 100644 --- a/docs/extensions/guides/generator.md +++ b/docs/extensions/guides/generator.md @@ -46,14 +46,14 @@ Open `my-first-lens-ext/renderer.tsx` and change the value of `title` from `"Hel ```typescript clusterPageMenus = [ - { - target: { pageId: "hello" }, - title: "Hello Lens", - components: { - Icon: ExampleIcon, - } - } -] + { + target: { pageId: "hello" }, + title: "Hello Lens", + components: { + Icon: ExampleIcon, + }, + }, +]; ``` Reload Lens and you will see that the menu item text has changed to "Hello Lens". @@ -70,6 +70,6 @@ To debug your extension, please see our instructions on [Testing Extensions](../ To dive deeper, consider looking at [Common Capabilities](../capabilities/common-capabilities.md), [Styling](../capabilities/styling.md), or [Extension Anatomy](anatomy.md). If you find problems with the Lens Extension Generator, or have feature requests, you are welcome to raise an [issue](https://github.com/lensapp/generator-lens-ext/issues). -You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/latest/contributing). +You can find the latest Lens contribution guidelines [here](https://docs.k8slens.dev/contributing). The Generator source code is hosted at [GitHub](https://github.com/lensapp/generator-lens-ext). diff --git a/docs/extensions/guides/renderer-extension.md b/docs/extensions/guides/renderer-extension.md index d90a343692..4cc6e338bf 100644 --- a/docs/extensions/guides/renderer-extension.md +++ b/docs/extensions/guides/renderer-extension.md @@ -771,7 +771,7 @@ Construct the table using the `Renderer.Component.Table` and related elements. For each pod the name, age, and status are obtained using the `Renderer.K8sApi.Pod` methods. The table is constructed using the `Renderer.Component.Table` and related elements. -See [Component documentation](https://docs.k8slens.dev/latest/extensions/api/modules/_renderer_api_components_/) for further details. +See [Component documentation](https://api-docs.k8slens.dev/latest/extensions/api/modules/Renderer.Component/) for further details. ### `kubeObjectStatusTexts` diff --git a/package.json b/package.json index c10a1829d0..82e511a81a 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "6.1.12", + "version": "6.1.13", "main": "static/build/main.js", "copyright": "© 2022 OpenLens Authors", "license": "MIT", @@ -52,7 +52,7 @@ "create-release-pr": "node ./scripts/create-release-pr.mjs" }, "config": { - "k8sProxyVersion": "0.2.1", + "k8sProxyVersion": "0.3.0", "bundledKubectlVersion": "1.23.3", "bundledHelmVersion": "3.7.2", "sentryDsn": "", diff --git a/src/common/cluster-store/get-by-id.injectable.ts b/src/common/cluster-store/get-by-id.injectable.ts new file mode 100644 index 0000000000..534bdb5e76 --- /dev/null +++ b/src/common/cluster-store/get-by-id.injectable.ts @@ -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; diff --git a/src/common/fs/create-read-file-stream.injectable.ts b/src/common/fs/create-read-file-stream.injectable.ts new file mode 100644 index 0000000000..8714e1cdcd --- /dev/null +++ b/src/common/fs/create-read-file-stream.injectable.ts @@ -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; diff --git a/src/common/fs/stat/stat.injectable.ts b/src/common/fs/stat/stat.injectable.ts index aa1ce44447..e9924fc088 100644 --- a/src/common/fs/stat/stat.injectable.ts +++ b/src/common/fs/stat/stat.injectable.ts @@ -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; + const statInjectable = getInjectable({ id: "stat", - - instantiate: (di) => di.inject(fsInjectable).stat, + instantiate: (di): Stat => di.inject(fsInjectable).stat, }); export default statInjectable; diff --git a/src/common/fs/watch/watch.injectable.ts b/src/common/fs/watch/watch.injectable.ts index 44d34f20f5..50f96cdf57 100644 --- a/src/common/fs/watch/watch.injectable.ts +++ b/src/common/fs/watch/watch.injectable.ts @@ -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 = 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 extends TypedEventEmitter> { + close: () => Promise; +} + +export type WatcherOptions = { + /** + * 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 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 = (path: string, options?: WatcherOptions) => Watcher; // TODO: Introduce wrapper to allow simpler API const watchInjectable = getInjectable({ id: "watch", - instantiate: (): Watch => watch, + instantiate: () => watch as Watch, causesSideEffects: true, }); diff --git a/src/common/logger/prefixed-logger.injectable.ts b/src/common/logger/prefixed-logger.injectable.ts new file mode 100644 index 0000000000..36f86c532f --- /dev/null +++ b/src/common/logger/prefixed-logger.injectable.ts @@ -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; diff --git a/src/common/runnable/run-many-for.test.ts b/src/common/runnable/run-many-for.test.ts index 35db17952a..c73f448dd2 100644 --- a/src/common/runnable/run-many-for.test.ts +++ b/src/common/runnable/run-many-for.test.ts @@ -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), }), diff --git a/src/common/runnable/run-many-for.ts b/src/common/runnable/run-many-for.ts index f2c1a4ae56..478d7f84a4 100644 --- a/src/common/runnable/run-many-for.ts +++ b/src/common/runnable/run-many-for.ts @@ -11,8 +11,9 @@ import { filter, forEach, map, tap } from "lodash/fp"; import { throwWithIncorrectHierarchyFor } from "./throw-with-incorrect-hierarchy-for"; export interface Runnable { + id: string; run: Run; - runAfter?: this; + runAfter?: Runnable; } type Run = (parameter: Param) => Promise | 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 | undefined = undefined, diff --git a/src/common/runnable/run-many-sync-for.test.ts b/src/common/runnable/run-many-sync-for.test.ts index fe47516725..43d2832663 100644 --- a/src/common/runnable/run-many-sync-for.test.ts +++ b/src/common/runnable/run-many-sync-for.test.ts @@ -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), }), diff --git a/src/common/runnable/run-many-sync-for.ts b/src/common/runnable/run-many-sync-for.ts index cfe93fa4b3..1d3dcec2c5 100644 --- a/src/common/runnable/run-many-sync-for.ts +++ b/src/common/runnable/run-many-sync-for.ts @@ -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 { + id: string; run: RunSync; - runAfter?: this; + runAfter?: RunnableSync; } type RunSync = (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 | undefined = undefined, diff --git a/src/common/runnable/throw-with-incorrect-hierarchy-for.ts b/src/common/runnable/throw-with-incorrect-hierarchy-for.ts index 03073c4044..bddf8037c2 100644 --- a/src/common/runnable/throw-with-incorrect-hierarchy-for.ts +++ b/src/common/runnable/throw-with-incorrect-hierarchy-for.ts @@ -5,12 +5,10 @@ import type { Runnable } from "./run-many-for"; import type { RunnableSync } from "./run-many-sync-for"; -export const throwWithIncorrectHierarchyFor = - (allRunnables: Runnable[] | RunnableSync[]) => - (runnable: Runnable | RunnableSync) => { - 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[] | RunnableSync[]) => ( + (runnable: Runnable | RunnableSync) => { + 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.`); + } + } +); diff --git a/src/common/user-store/kubeconfig-syncs.injectable.ts b/src/common/user-store/kubeconfig-syncs.injectable.ts new file mode 100644 index 0000000000..bbe02fffad --- /dev/null +++ b/src/common/user-store/kubeconfig-syncs.injectable.ts @@ -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; diff --git a/src/common/utils/iter.ts b/src/common/utils/iter.ts index 05d097ed78..9752e56dc5 100644 --- a/src/common/utils/iter.ts +++ b/src/common/utils/iter.ts @@ -11,6 +11,7 @@ interface Iterator { find(fn: (val: T) => unknown): T | undefined; collect(fn: (values: Iterable) => U): U; map(fn: (val: T) => U): Iterator; + flatMap(fn: (val: T) => U[]): Iterator; join(sep?: string): string; } @@ -19,6 +20,7 @@ export function pipeline(src: IterableIterator): Iterator { 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), diff --git a/src/common/vars.ts b/src/common/vars.ts index a5507e1ee6..b809d5dd5a 100644 --- a/src/common/vars.ts +++ b/src/common/vars.ts @@ -120,7 +120,7 @@ export const apiKubePrefix = "/api-kube"; // k8s cluster apis // Links export const issuesTrackerUrl = "https://github.com/lensapp/lens/issues" as string; export const slackUrl = "https://join.slack.com/t/k8slens/shared_invite/zt-wcl8jq3k-68R5Wcmk1o95MLBE5igUDQ" as string; -export const supportUrl = "https://docs.k8slens.dev/latest/support/" as string; +export const supportUrl = "https://docs.k8slens.dev/support/" as string; export const lensWebsiteWeblinkId = "lens-website-link"; export const lensDocumentationWeblinkId = "lens-documentation-link"; @@ -129,4 +129,4 @@ export const lensTwitterWeblinkId = "lens-twitter-link"; export const lensBlogWeblinkId = "lens-blog-link"; export const kubernetesDocumentationWeblinkId = "kubernetes-documentation-link"; -export const docsUrl = "https://docs.k8slens.dev/main" as string; +export const docsUrl = "https://docs.k8slens.dev" as string; diff --git a/src/common/vars/application-information.global-override-for-injectable.ts b/src/common/vars/application-information.global-override-for-injectable.ts index ac53b9f341..232a189ce1 100644 --- a/src/common/vars/application-information.global-override-for-injectable.ts +++ b/src/common/vars/application-information.global-override-for-injectable.ts @@ -7,6 +7,7 @@ import { getGlobalOverride } from "../test-utils/get-global-override"; import applicationInformationInjectable from "./application-information.injectable"; export default getGlobalOverride(applicationInformationInjectable, () => ({ + name: "some-product-name", productName: "some-product-name", version: "6.0.0", build: {}, diff --git a/src/common/vars/application-information.injectable.ts b/src/common/vars/application-information.injectable.ts index 559b15294b..56dfcf9d7e 100644 --- a/src/common/vars/application-information.injectable.ts +++ b/src/common/vars/application-information.injectable.ts @@ -5,16 +5,16 @@ import { getInjectable } from "@ogre-tools/injectable"; import packageJson from "../../../package.json"; -export type ApplicationInformation = Pick & { +export type ApplicationInformation = Pick & { build: Partial & { publish?: unknown[] }; }; const applicationInformationInjectable = getInjectable({ id: "application-information", instantiate: (): ApplicationInformation => { - const { version, config, productName, build, copyright, description } = packageJson; + const { version, config, productName, build, copyright, description, name } = packageJson; - return { version, config, productName, build, copyright, description }; + return { version, config, productName, build, copyright, description, name }; }, causesSideEffects: true, }); diff --git a/src/features/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap b/src/features/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap index 132ef5e0b0..6bd286ad98 100644 --- a/src/features/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap +++ b/src/features/add-cluster/__snapshots__/navigation-using-application-menu.test.tsx.snap @@ -278,7 +278,7 @@ exports[`add-cluster - navigation using application menu when navigating to add file. diff --git a/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap b/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap index 66a49f4355..5750a6c447 100644 --- a/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap +++ b/src/features/extensions/__snapshots__/navigation-using-application-menu.test.ts.snap @@ -274,7 +274,7 @@ exports[`extensions - navigation using application menu when navigating to exten

Add new features via Lens Extensions. Check out the diff --git a/src/features/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts b/src/features/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts index fe903f93f3..5e0989aa91 100644 --- a/src/features/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts +++ b/src/features/quitting-and-restarting-the-app/quitting-the-app-using-application-menu.test.ts @@ -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"; diff --git a/src/main/app-paths/setup-app-paths.injectable.ts b/src/main/app-paths/setup-app-paths.injectable.ts index 3626bbd9c0..842eed89c7 100644 --- a/src/main/app-paths/setup-app-paths.injectable.ts +++ b/src/main/app-paths/setup-app-paths.injectable.ts @@ -27,6 +27,7 @@ const setupAppPathsInjectable = getInjectable({ const joinPaths = di.inject(joinPathsInjectable); return { + id: "setup-app-paths", run: () => { if (directoryForIntegrationTesting) { setElectronAppPath("appData", directoryForIntegrationTesting); diff --git a/src/main/application-update/emit-current-version-to-analytics.injectable.ts b/src/main/application-update/emit-current-version-to-analytics.injectable.ts index 61dd38ea87..4da1893cea 100644 --- a/src/main/application-update/emit-current-version-to-analytics.injectable.ts +++ b/src/main/application-update/emit-current-version-to-analytics.injectable.ts @@ -16,6 +16,7 @@ const emitCurrentVersionToAnalyticsInjectable = getInjectable({ const buildVersion = di.inject(buildVersionInjectable); return { + id: "emit-current-version-to-analytics", run: () => { emitEvent({ name: "app", diff --git a/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts index 347015b584..15088b3173 100644 --- a/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts +++ b/src/main/application-update/periodical-check-for-updates/start-checking-for-updates.injectable.ts @@ -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(); diff --git a/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts b/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts index 944642b674..15e71cc817 100644 --- a/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts +++ b/src/main/application-update/periodical-check-for-updates/stop-checking-for-updates.injectable.ts @@ -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(); diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts index ef31cf5db5..3206b29a69 100644 --- a/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts +++ b/src/main/application-update/watch-if-update-should-happen-on-quit/start-watching-if-update-should-happen-on-quit.injectable.ts @@ -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(); }, diff --git a/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts b/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts index b66cf927f2..b782f76966 100644 --- a/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts +++ b/src/main/application-update/watch-if-update-should-happen-on-quit/stop-watching-if-update-should-happen-on-quit.injectable.ts @@ -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(); }, diff --git a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts index 08ff464bf2..2998022e67 100644 --- a/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts +++ b/src/main/catalog-sources/__test__/kubeconfig-sync.test.ts @@ -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; + let computeKubeconfigDiff: ComputeKubeconfigDiff; + let configToModels: ConfigToModels; + let kubeconfigSyncs: ObservableMap; + let clusters: Map; + 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(); 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(); 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(); 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(); 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>; + let firstReadFoobarConfigSteam: ReadStream; + let secondReadFoobarConfigSteam: ReadStream; + let statMock: AsyncFnMock; + + 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 => { + 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", }); diff --git a/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts new file mode 100644 index 0000000000..cb1e62e6e8 --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/compute-diff.injectable.ts @@ -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, 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; diff --git a/src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts new file mode 100644 index 0000000000..8240d4e914 --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/config-to-models.injectable.ts @@ -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 = []; + + 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; diff --git a/src/main/catalog-sources/kubeconfig-sync/diff-changed-kubeconfig.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/diff-changed-kubeconfig.injectable.ts new file mode 100644 index 0000000000..f17293fc2d --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/diff-changed-kubeconfig.injectable.ts @@ -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; + 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; diff --git a/src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts new file mode 100644 index 0000000000..4705da78d2 --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/logger.injectable.ts @@ -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; diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts index f95fa0fb17..06265764c8 100644 --- a/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.injectable.ts @@ -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), }), }); diff --git a/src/main/catalog-sources/kubeconfig-sync/manager.ts b/src/main/catalog-sources/kubeconfig-sync/manager.ts index 1d281bc936..305f25a909 100644 --- a/src/main/catalog-sources/kubeconfig-sync/manager.ts +++ b/src/main/catalog-sources/kubeconfig-sync/manager.ts @@ -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; + watchKubeconfigFileChanges: WatchKubeconfigFileChanges; } -const kubeConfigSyncName = "lens:kube-sync"; - export class KubeconfigSyncManager { protected readonly sources = observable.map, 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(); + + 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 = []; - - 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; - -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, Disposer] => { - const rootSource = observable.map>(); - 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(); - 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(); - }]; -}; diff --git a/src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts b/src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts new file mode 100644 index 0000000000..c843222a5f --- /dev/null +++ b/src/main/catalog-sources/kubeconfig-sync/watch-file-changes.injectable.ts @@ -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, 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>(); + const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1])))); + + let watcher: Watcher; + + (async () => { + try { + const stats = await stat(filePath); + const isFolderSync = stats.isDirectory(); + const cleanupFns = new Map(); + 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, + }); + + 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; diff --git a/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts b/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts index 3dfd9ee2ae..ce06a44281 100644 --- a/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts +++ b/src/main/catalog-sync-to-renderer/start-catalog-sync.injectable.ts @@ -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(); diff --git a/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts b/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts index 71c5be55f1..fcd294fe02 100644 --- a/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts +++ b/src/main/catalog-sync-to-renderer/stop-catalog-sync.injectable.ts @@ -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(); diff --git a/src/main/cluster-manager.injectable.ts b/src/main/cluster-manager.injectable.ts deleted file mode 100644 index 2b55f0e854..0000000000 --- a/src/main/cluster-manager.injectable.ts +++ /dev/null @@ -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; diff --git a/src/main/cluster/are-being-deleted.injectable.ts b/src/main/cluster/are-being-deleted.injectable.ts new file mode 100644 index 0000000000..610791aee7 --- /dev/null +++ b/src/main/cluster/are-being-deleted.injectable.ts @@ -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(), +}); + +export default clustersThatAreBeingDeletedInjectable; diff --git a/src/main/cluster/initialize-manager.injectable.ts b/src/main/cluster/initialize-manager.injectable.ts new file mode 100644 index 0000000000..adabcf416c --- /dev/null +++ b/src/main/cluster/initialize-manager.injectable.ts @@ -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; diff --git a/src/main/cluster/manager.injectable.ts b/src/main/cluster/manager.injectable.ts new file mode 100644 index 0000000000..13f04f16ac --- /dev/null +++ b/src/main/cluster/manager.injectable.ts @@ -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; diff --git a/src/main/cluster-manager.ts b/src/main/cluster/manager.ts similarity index 89% rename from src/main/cluster-manager.ts rename to src/main/cluster/manager.ts index 787f2a1618..8ae6e5ce99 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster/manager.ts @@ -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 = new Set(Object.values(LensKubernetesClusterStatus)); interface Dependencies { - store: ClusterStore; - catalogEntityRegistry: CatalogEntityRegistry; + readonly store: ClusterStore; + readonly catalogEntityRegistry: CatalogEntityRegistry; + readonly clustersThatAreBeingDeleted: ObservableSet; } export class ClusterManager { - deleting = observable.set(); - @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 { diff --git a/src/main/context-handler/context-handler.ts b/src/main/context-handler/context-handler.ts index 7671754cc2..563c7afecc 100644 --- a/src/main/context-handler/context-handler.ts +++ b/src/main/context-handler/context-handler.ts @@ -94,6 +94,8 @@ export class ContextHandler implements ClusterContextHandler { } protected async getPrometheusService(): Promise { + this.setupPrometheus(this.cluster.preferences); + if (this.prometheus && this.prometheusProvider) { return { id: this.prometheusProvider, diff --git a/src/main/crypto/random-uuid.global-override-for-injectable.ts b/src/main/crypto/random-uuid.global-override-for-injectable.ts new file mode 100644 index 0000000000..63e5364a8a --- /dev/null +++ b/src/main/crypto/random-uuid.global-override-for-injectable.ts @@ -0,0 +1,11 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import { getGlobalOverride } from "../../common/test-utils/get-global-override"; +import randomUUIDInjectable from "./random-uuid.injectable"; + +export default getGlobalOverride(randomUUIDInjectable, () => () => { + throw new Error("Tried to get a randomUUID without override"); +}); diff --git a/src/main/crypto/random-uuid.injectable.ts b/src/main/crypto/random-uuid.injectable.ts new file mode 100644 index 0000000000..cecee487e2 --- /dev/null +++ b/src/main/crypto/random-uuid.injectable.ts @@ -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 { randomUUID } from "crypto"; + +const randomUUIDInjectable = getInjectable({ + id: "random-uuid", + instantiate: () => randomUUID, + causesSideEffects: true, +}); + +export default randomUUIDInjectable; diff --git a/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts b/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts index 5925197db3..9943516f72 100644 --- a/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts +++ b/src/main/electron-app/runnables/clean-up-deep-linking.injectable.ts @@ -13,6 +13,7 @@ const cleanUpDeepLinkingInjectable = getInjectable({ const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable); return { + id: "clean-up-deep-linking", run: () => { lensProtocolRouterMain.cleanup(); }, diff --git a/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts b/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts index 315b2a5689..d7fd4bdcb2 100644 --- a/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts +++ b/src/main/electron-app/runnables/dock-visibility/hide-dock-for-last-closed-window.injectable.ts @@ -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(); diff --git a/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts b/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts index 1dbafb60f5..b47e511c14 100644 --- a/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts +++ b/src/main/electron-app/runnables/dock-visibility/show-dock-for-first-opened-window.injectable.ts @@ -13,6 +13,7 @@ const showDockForFirstOpenedWindowInjectable = getInjectable({ const app = di.inject(electronAppInjectable); return { + id: "show-dock-for-first-opened-window", run: () => { app.dock?.show(); }, diff --git a/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts b/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts index 6908b6ac47..de192e3372 100644 --- a/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts +++ b/src/main/electron-app/runnables/enforce-single-application-instance.injectable.ts @@ -15,6 +15,7 @@ const enforceSingleApplicationInstanceInjectable = getInjectable({ const exitApp = di.inject(exitAppInjectable); return { + id: "enforce-single-application-instance", run: () => { if (!requestSingleInstanceLock()) { exitApp(); diff --git a/src/main/electron-app/runnables/setup-application-name.injectable.ts b/src/main/electron-app/runnables/setup-application-name.injectable.ts index 3a8efdfe05..8bafc3fe8b 100644 --- a/src/main/electron-app/runnables/setup-application-name.injectable.ts +++ b/src/main/electron-app/runnables/setup-application-name.injectable.ts @@ -15,6 +15,7 @@ const setupApplicationNameInjectable = getInjectable({ const appName = di.inject(appNameInjectable); return { + id: "setup-application-name", run: () => { app.setName(appName); }, diff --git a/src/main/electron-app/runnables/setup-deep-linking.injectable.ts b/src/main/electron-app/runnables/setup-deep-linking.injectable.ts index 47087cd1c1..5f82d60f42 100644 --- a/src/main/electron-app/runnables/setup-deep-linking.injectable.ts +++ b/src/main/electron-app/runnables/setup-deep-linking.injectable.ts @@ -26,6 +26,7 @@ const setupDeepLinkingInjectable = getInjectable({ ); return { + id: "setup-deep-linking", run: async () => { logger.info(`📟 Setting protocol client for lens://`); diff --git a/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts b/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts index 3438bc1428..07807a7965 100644 --- a/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts +++ b/src/main/electron-app/runnables/setup-developer-tools-in-development-environment.injectable.ts @@ -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; diff --git a/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts b/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts index b0b1ee8096..8c3c21ae30 100644 --- a/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts +++ b/src/main/electron-app/runnables/setup-device-shutdown.injectable.ts @@ -15,6 +15,7 @@ const setupDeviceShutdownInjectable = getInjectable({ const exitApp = di.inject(exitAppInjectable); return { + id: "setup-device-shutdown", run: () => { powerMonitor.on("shutdown", async () => { exitApp(); diff --git a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts index 24f90f01b3..c78514d631 100644 --- a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts +++ b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.injectable.ts @@ -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, }); }, }; diff --git a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts index 9342696c77..6f873cd2c3 100644 --- a/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts +++ b/src/main/electron-app/runnables/setup-ipc-main-handlers/setup-ipc-main-handlers.ts @@ -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; askUserForFilePaths: AskUserForFilePaths; + clustersThatAreBeingDeleted: ObservableSet; } -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[]) => { diff --git a/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts b/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts index 3642fa3c6d..c32cfe0765 100644 --- a/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts +++ b/src/main/electron-app/runnables/setup-main-window-visibility-after-activation.injectable.ts @@ -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 }); diff --git a/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts b/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts index 8e724fdf50..13650605fb 100644 --- a/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts +++ b/src/main/electron-app/runnables/setup-runnables-after-window-is-opened.injectable.ts @@ -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); diff --git a/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts b/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts index bb2c8f8970..5e8b24c2d8 100644 --- a/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts +++ b/src/main/electron-app/runnables/setup-runnables-before-closing-of-application.injectable.ts @@ -26,6 +26,7 @@ const setupRunnablesBeforeClosingOfApplicationInjectable = getInjectable({ ); return { + id: "setup-closing-of-application", run: () => { const app = di.inject(electronAppInjectable); diff --git a/src/main/getDiForUnitTesting.ts b/src/main/getDiForUnitTesting.ts index 8e08c9b86c..bbda6d5838 100644 --- a/src/main/getDiForUnitTesting.ts +++ b/src/main/getDiForUnitTesting.ts @@ -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, () => () => {}); diff --git a/src/main/lens-proxy/lens-proxy.injectable.ts b/src/main/lens-proxy/lens-proxy.injectable.ts index 5138ac191e..45d893514d 100644 --- a/src/main/lens-proxy/lens-proxy.injectable.ts +++ b/src/main/lens-proxy/lens-proxy.injectable.ts @@ -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"; diff --git a/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts index b491ab5456..a9dc0c6747 100644 --- a/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts +++ b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.injectable.ts @@ -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", diff --git a/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts index ef7e2d68d3..83559252a4 100644 --- a/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts +++ b/src/main/lens-proxy/proxy-functions/shell-api-request/shell-api-request.ts @@ -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"; diff --git a/src/main/menu/start-application-menu.injectable.ts b/src/main/menu/start-application-menu.injectable.ts index b241137cec..3365223c86 100644 --- a/src/main/menu/start-application-menu.injectable.ts +++ b/src/main/menu/start-application-menu.injectable.ts @@ -15,6 +15,7 @@ const startApplicationMenuInjectable = getInjectable({ ); return { + id: "start-application-menu", run: async () => { await applicationMenu.start(); }, diff --git a/src/main/menu/stop-application-menu.injectable.ts b/src/main/menu/stop-application-menu.injectable.ts index 1492da32de..73ed462242 100644 --- a/src/main/menu/stop-application-menu.injectable.ts +++ b/src/main/menu/stop-application-menu.injectable.ts @@ -15,6 +15,7 @@ const stopApplicationMenuInjectable = getInjectable({ ); return { + id: "stop-application-menu", run: async () => { await applicationMenu.stop(); }, diff --git a/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts index 252db16a1f..8b0c2a6cfa 100644 --- a/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts +++ b/src/main/start-main-application/lens-window/application-window/create-electron-window.injectable.ts @@ -10,6 +10,12 @@ import sendToChannelInElectronBrowserWindowInjectable from "./send-to-channel-in import type { ElectronWindow } from "./create-lens-window.injectable"; import type { RequireExactlyOne } from "type-fest"; import openLinkInBrowserInjectable from "../../../../common/utils/open-link-in-browser.injectable"; +import getAbsolutePathInjectable from "../../../../common/path/get-absolute-path.injectable"; +import lensResourcesDirInjectable from "../../../../common/vars/lens-resources-dir.injectable"; +import isLinuxInjectable from "../../../../common/vars/is-linux.injectable"; +import fsInjectable from "../../../../common/fs/fs.injectable"; +import applicationInformationInjectable from "../../../../common/vars/application-information.injectable"; + export type ElectronWindowTitleBarStyle = "hiddenInset" | "hidden" | "default" | "customButtonsOnHover"; @@ -47,6 +53,10 @@ const createElectronWindowInjectable = getInjectable({ const logger = di.inject(loggerInjectable); const sendToChannelInLensWindow = di.inject(sendToChannelInElectronBrowserWindowInjectable); const openLinkInBrowser = di.inject(openLinkInBrowserInjectable); + const getAbsolutePath = di.inject(getAbsolutePathInjectable); + const lensResourcesDir = di.inject(lensResourcesDirInjectable); + const isLinux = di.inject(isLinuxInjectable); + const applicationInformation = di.inject(applicationInformationInjectable); return (configuration) => { const applicationWindowState = di.inject( @@ -81,6 +91,23 @@ const createElectronWindowInjectable = getInjectable({ }, }); + if (isLinux) { + const iconFileName = [ + getAbsolutePath(lensResourcesDir, `../${applicationInformation.name}.png`), + `/usr/share/icons/hicolor/512x512/apps/${applicationInformation.name}.png`, + ].find(di.inject(fsInjectable).existsSync); + + if (iconFileName != null) { + try { + browserWindow.setIcon(iconFileName); + } catch (err) { + logger.warn(`Error while setting window icon ${err}`); + } + } else { + logger.warn(`No suitable icon found for task bar.`); + } + } + applicationWindowState.manage(browserWindow); browserWindow diff --git a/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts b/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts index fa8b36c03a..24554c474f 100644 --- a/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts +++ b/src/main/start-main-application/lens-window/current-cluster-frame/setup-listener-for-current-cluster-frame.injectable.ts @@ -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); diff --git a/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts b/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts index 07066535a1..40d50530ad 100644 --- a/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts +++ b/src/main/start-main-application/runnables/clean-up-shell-sessions.injectable.ts @@ -10,6 +10,7 @@ const cleanUpShellSessionsInjectable = getInjectable({ id: "clean-up-shell-sessions", instantiate: () => ({ + id: "clean-up-shell-sessions", run: () => { ShellSession.cleanup(); }, diff --git a/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts b/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts index 316114f205..d671d50823 100644 --- a/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts +++ b/src/main/start-main-application/runnables/emit-close-to-event-bus.injectable.ts @@ -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" }); }, diff --git a/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts b/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts index 0d3e4cf043..d535b38ad9 100644 --- a/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts +++ b/src/main/start-main-application/runnables/emit-service-start-to-event-bus.injectable.ts @@ -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" }); }, diff --git a/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts index 3c61b2a011..2bb05cfb52 100644 --- a/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts +++ b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-loaded.injectable.ts @@ -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. diff --git a/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts index d81f7287aa..8b0a4b8161 100644 --- a/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts +++ b/src/main/start-main-application/runnables/flag-renderer/flag-renderer-as-not-loaded.injectable.ts @@ -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. diff --git a/src/main/start-main-application/runnables/initialize-extensions.injectable.ts b/src/main/start-main-application/runnables/initialize-extensions.injectable.ts index 899d684abf..8765721d90 100644 --- a/src/main/start-main-application/runnables/initialize-extensions.injectable.ts +++ b/src/main/start-main-application/runnables/initialize-extensions.injectable.ts @@ -21,6 +21,7 @@ const initializeExtensionsInjectable = getInjectable({ const showErrorPopup = di.inject(showErrorPopupInjectable); return { + id: "initialize-extensions", run: async () => { logger.info("🧩 Initializing extensions"); diff --git a/src/main/start-main-application/runnables/kube-config-sync/add-source.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/add-source.injectable.ts new file mode 100644 index 0000000000..f95a39f665 --- /dev/null +++ b/src/main/start-main-application/runnables/kube-config-sync/add-source.injectable.ts @@ -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; diff --git a/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts index 0b585f3d65..ad0e446565 100644 --- a/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts +++ b/src/main/start-main-application/runnables/kube-config-sync/start-kube-config-sync.injectable.ts @@ -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), }; }, diff --git a/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts b/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts index 6bdf5a7476..77e987effc 100644 --- a/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts +++ b/src/main/start-main-application/runnables/kube-config-sync/stop-kube-config-sync.injectable.ts @@ -13,6 +13,7 @@ const stopKubeConfigSyncInjectable = getInjectable({ const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable); return { + id: "stop-kube-config-sync", run: () => { kubeConfigSyncManager.stopSync(); }, diff --git a/src/main/start-main-application/runnables/sentry/setup.injectable.ts b/src/main/start-main-application/runnables/sentry/setup.injectable.ts index 59eacc4199..9db6991fea 100644 --- a/src/main/start-main-application/runnables/sentry/setup.injectable.ts +++ b/src/main/start-main-application/runnables/sentry/setup.injectable.ts @@ -14,6 +14,7 @@ const setupSentryInjectable = getInjectable({ const initializeSentryOnMain = di.inject(initializeSentryOnMainInjectable); return { + id: "setup-sentry", run: () => initializeSentryReportingWith(initializeSentryOnMain), }; }, diff --git a/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts b/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts index 1c91e797f3..0e8b7ffba2 100644 --- a/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts +++ b/src/main/start-main-application/runnables/setup-detector-registry.injectable.ts @@ -18,6 +18,7 @@ const setupDetectorRegistryInjectable = getInjectable({ const detectorRegistry = di.inject(detectorRegistryInjectable); return { + id: "setup-detector-registry", run: () => { detectorRegistry .add(ClusterIdDetector) diff --git a/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts b/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts index ba1b5f2bb8..cbb9b940cb 100644 --- a/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts +++ b/src/main/start-main-application/runnables/setup-hardware-acceleration.injectable.ts @@ -15,6 +15,7 @@ const setupHardwareAccelerationInjectable = getInjectable({ const disableHardwareAcceleration = di.inject(disableHardwareAccelerationInjectable); return { + id: "setup-hardware-acceleration", run: () => { if (hardwareAccelerationShouldBeDisabled) { disableHardwareAcceleration(); diff --git a/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts b/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts index 372c339e5e..4f46e46073 100644 --- a/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts +++ b/src/main/start-main-application/runnables/setup-hotbar-store.injectable.ts @@ -11,6 +11,7 @@ const setupHotbarStoreInjectable = getInjectable({ id: "setup-hotbar-store", instantiate: (di) => ({ + id: "setup-hotbar-store", run: () => { const hotbarStore = di.inject(hotbarStoreInjectable); diff --git a/src/main/start-main-application/runnables/setup-immer.injectable.ts b/src/main/start-main-application/runnables/setup-immer.injectable.ts index 63cd6e3f2f..1d03bd8e9d 100644 --- a/src/main/start-main-application/runnables/setup-immer.injectable.ts +++ b/src/main/start-main-application/runnables/setup-immer.injectable.ts @@ -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` diff --git a/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts b/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts index a75f55ec72..58a4b24409 100644 --- a/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts +++ b/src/main/start-main-application/runnables/setup-lens-proxy.injectable.ts @@ -26,6 +26,7 @@ const setupLensProxyInjectable = getInjectable({ const buildVersion = di.inject(buildVersionInjectable); return { + id: "setup-lens-proxy", run: async () => { try { logger.info("🔌 Starting LensProxy"); diff --git a/src/main/start-main-application/runnables/setup-mobx.injectable.ts b/src/main/start-main-application/runnables/setup-mobx.injectable.ts index ca5a124b77..350911fecd 100644 --- a/src/main/start-main-application/runnables/setup-mobx.injectable.ts +++ b/src/main/start-main-application/runnables/setup-mobx.injectable.ts @@ -10,6 +10,7 @@ const setupMobxInjectable = getInjectable({ id: "setup-mobx", instantiate: () => ({ + id: "setup-mobx", run: () => { // Docs: https://mobx.js.org/configuration.html Mobx.configure({ diff --git a/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts b/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts index 28b410889d..ef755ac7a1 100644 --- a/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts +++ b/src/main/start-main-application/runnables/setup-prometheus-registry.injectable.ts @@ -18,6 +18,7 @@ const setupPrometheusRegistryInjectable = getInjectable({ const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable); return { + id: "setup-prometheus-registry", run: () => { prometheusProviderRegistry .registerProvider(new PrometheusLens()) diff --git a/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts b/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts index 7c05ad8b49..bc709f1f72 100644 --- a/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts +++ b/src/main/start-main-application/runnables/setup-proxy-env.injectable.ts @@ -13,6 +13,7 @@ const setupProxyEnvInjectable = getInjectable({ const getCommandLineSwitch = di.inject(getCommandLineSwitchInjectable); return { + id: "setup-proxy-env", run: () => { const switchValue = getCommandLineSwitch("proxy-server"); diff --git a/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts b/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts index 7ab26a1506..f40c3c8796 100644 --- a/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts +++ b/src/main/start-main-application/runnables/setup-reactions-in-user-store.injectable.ts @@ -13,6 +13,7 @@ const setupReactionsInUserStoreInjectable = getInjectable({ const userStore = di.inject(userStoreInjectable); return { + id: "setup-reactions-in-user-store", run: () => { userStore.startMainReactions(); }, diff --git a/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts b/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts index e44752b324..6dc0b27512 100644 --- a/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts +++ b/src/main/start-main-application/runnables/setup-syncing-of-general-catalog-entities.injectable.ts @@ -15,6 +15,7 @@ const setupSyncingOfGeneralCatalogEntitiesInjectable = getInjectable({ ); return { + id: "setup-syncing-of-general-catalog-entities", run: () => { syncGeneralCatalogEntities(); }, diff --git a/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts b/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts index 0e5ada1b78..fa4e6b436c 100644 --- a/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts +++ b/src/main/start-main-application/runnables/setup-syncing-of-weblinks.injectable.ts @@ -13,6 +13,7 @@ const setupSyncingOfWeblinksInjectable = getInjectable({ const syncWeblinks = di.inject(syncWeblinksInjectable); return { + id: "setup-syncing-of-weblinks", run: () => { syncWeblinks(); }, diff --git a/src/main/start-main-application/runnables/setup-system-ca.injectable.ts b/src/main/start-main-application/runnables/setup-system-ca.injectable.ts index 0a09ebebd4..94589fca7d 100644 --- a/src/main/start-main-application/runnables/setup-system-ca.injectable.ts +++ b/src/main/start-main-application/runnables/setup-system-ca.injectable.ts @@ -10,6 +10,7 @@ const setupSystemCaInjectable = getInjectable({ id: "setup-system-ca", instantiate: () => ({ + id: "setup-system-ca", run: async () => { await injectSystemCAs(); }, diff --git a/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts b/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts index d062270dd0..f8d6c0bdc7 100644 --- a/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts +++ b/src/main/start-main-application/runnables/stop-cluster-manager.injectable.ts @@ -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(); }, diff --git a/src/main/stop-services-and-exit-app.injectable.ts b/src/main/stop-services-and-exit-app.injectable.ts index 25b4324aec..9ac505fe65 100644 --- a/src/main/stop-services-and-exit-app.injectable.ts +++ b/src/main/stop-services-and-exit-app.injectable.ts @@ -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"; diff --git a/src/main/stores/init-user-store.injectable.ts b/src/main/stores/init-user-store.injectable.ts index 572c0154e4..c8d7ec9fe0 100644 --- a/src/main/stores/init-user-store.injectable.ts +++ b/src/main/stores/init-user-store.injectable.ts @@ -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, diff --git a/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts index 7794c07bd4..e76f251ec2 100644 --- a/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts +++ b/src/main/theme/broadcast-theme-change/start-broadcasting-theme-change.injectable.ts @@ -13,6 +13,7 @@ const startBroadcastingThemeChangeInjectable = getInjectable({ const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable); return { + id: "start-broadcasting-theme-change", run: async () => { await broadcastThemeChange.start(); }, diff --git a/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts b/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts index a5f922af13..81f530ad9b 100644 --- a/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts +++ b/src/main/theme/broadcast-theme-change/stop-broadcasting-theme-change.injectable.ts @@ -13,6 +13,7 @@ const stopBroadcastingThemeChangeInjectable = getInjectable({ const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable); return { + id: "stop-broadcasting-theme-change", run: async () => { await broadcastThemeChange.stop(); }, diff --git a/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts b/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts index 9bf9c5fe49..adc22c4e1e 100644 --- a/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts +++ b/src/main/theme/sync-theme-from-os/start-syncing-theme-from-operating-system.injectable.ts @@ -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(); }, diff --git a/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts b/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts index 08657281c2..8a11042883 100644 --- a/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts +++ b/src/main/theme/sync-theme-from-os/stop-syncing-theme-from-operating-system.injectable.ts @@ -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(); }, diff --git a/src/main/tray/electron-tray/start-tray.injectable.ts b/src/main/tray/electron-tray/start-tray.injectable.ts index 1a223ac3a5..2226205244 100644 --- a/src/main/tray/electron-tray/start-tray.injectable.ts +++ b/src/main/tray/electron-tray/start-tray.injectable.ts @@ -13,6 +13,7 @@ const startTrayInjectable = getInjectable({ const electronTray = di.inject(electronTrayInjectable); return { + id: "start-tray", run: () => { electronTray.start(); }, diff --git a/src/main/tray/electron-tray/stop-tray.injectable.ts b/src/main/tray/electron-tray/stop-tray.injectable.ts index f66ffb3a64..babf687772 100644 --- a/src/main/tray/electron-tray/stop-tray.injectable.ts +++ b/src/main/tray/electron-tray/stop-tray.injectable.ts @@ -14,6 +14,7 @@ const stopTrayInjectable = getInjectable({ const electronTray = di.inject(electronTrayInjectable); return { + id: "stop-tray", run: () => { electronTray.stop(); }, diff --git a/src/main/tray/menu-icon/start-reactivity.injectable.ts b/src/main/tray/menu-icon/start-reactivity.injectable.ts index 373c3cf8fb..19bc9aec20 100644 --- a/src/main/tray/menu-icon/start-reactivity.injectable.ts +++ b/src/main/tray/menu-icon/start-reactivity.injectable.ts @@ -14,6 +14,7 @@ const startReactiveTrayMenuIconInjectable = getInjectable({ const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable); return { + id: "start-reactive-tray-menu-icon", run: async () => { await reactiveTrayMenuIcon.start(); }, diff --git a/src/main/tray/menu-icon/stop-reactivity.injectable.ts b/src/main/tray/menu-icon/stop-reactivity.injectable.ts index 4b60aaaa54..b43661807d 100644 --- a/src/main/tray/menu-icon/stop-reactivity.injectable.ts +++ b/src/main/tray/menu-icon/stop-reactivity.injectable.ts @@ -13,6 +13,7 @@ const stopReactiveTrayMenuIconInjectable = getInjectable({ const reactiveTrayMenuIcon = di.inject(reactiveTrayMenuIconInjectable); return { + id: "stop-reactive-tray-menu-icon", run: async () => { await reactiveTrayMenuIcon.stop(); }, diff --git a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts index 63025e6a9a..7abd07e91b 100644 --- a/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts +++ b/src/main/tray/reactive-tray-menu-items/start-reactive-tray-menu-items.injectable.ts @@ -14,6 +14,7 @@ const startReactiveTrayMenuItemsInjectable = getInjectable({ const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable); return { + id: "start-reactive-tray-menu-items", run: async () => { await reactiveTrayMenuItems.start(); }, diff --git a/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts index 384cdc253a..dbf5753c23 100644 --- a/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts +++ b/src/main/tray/reactive-tray-menu-items/stop-reactive-tray-menu-items.injectable.ts @@ -13,6 +13,7 @@ const stopReactiveTrayMenuItemsInjectable = getInjectable({ const reactiveTrayMenuItems = di.inject(reactiveTrayMenuItemsInjectable); return { + id: "stop-reactive-tray-menu-items", run: async () => { await reactiveTrayMenuItems.stop(); }, diff --git a/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts index 96fea0a2f0..78d73044e9 100644 --- a/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts +++ b/src/main/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts @@ -13,6 +13,7 @@ const startListeningOfChannelsInjectable = getInjectable({ const listeningOfChannels = di.inject(listeningOfChannelsInjectable); return { + id: "start-listening-of-channels-main", run: async () => { await listeningOfChannels.start(); }, diff --git a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts index 7186e96a88..b3d3d4abb5 100644 --- a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts +++ b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts @@ -2,13 +2,13 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { spawn } from "child_process"; -import { randomUUID } from "crypto"; -import { basename } from "path"; import type { EnvironmentVariables } from "./compute-shell-environment.injectable"; import { getInjectable } from "@ogre-tools/injectable"; +import spawnInjectable from "../../child-process/spawn.injectable"; +import randomUUIDInjectable from "../../crypto/random-uuid.injectable"; +import { basename } from "path"; -interface UnixShellEnvOptions { +export interface UnixShellEnvOptions { signal?: AbortSignal; } @@ -16,76 +16,90 @@ export type ComputeUnixShellEnvironment = (shell: string, opts?: UnixShellEnvOpt const computeUnixShellEnvironmentInjectable = getInjectable({ id: "compute-unix-shell-environment", - instantiate: (): ComputeUnixShellEnvironment => async (shell, opts) => { - const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; - const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; - const env = { - ...process.env, - ELECTRON_RUN_AS_NODE: "1", - ELECTRON_NO_ATTACH_CONSOLE: "1", - }; - const mark = randomUUID().replace(/-/g, ""); - const regex = new RegExp(`${mark}(.*)${mark}`); - const shellName = basename(shell); - let command: string; - let shellArgs: string[]; + instantiate: (di): ComputeUnixShellEnvironment => { + const powerShellName = /^pwsh(-preview)?$/; + const nonBashLikeShellName = /^t?csh$/; - if (/^pwsh(-preview)?$/.test(shellName)) { - // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how - // you escape single quotes inside of a single quoted string. - command = `& '${process.execPath}' -p '''${mark}'' + JSON.stringify(process.env) + ''${mark}'''`; - shellArgs = ["-Login", "-Command"]; - } else { - command = `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`; + const spawn = di.inject(spawnInjectable); + const randomUUID = di.inject(randomUUIDInjectable); - if (shellName === "tcsh") { - shellArgs = ["-ic"]; - } else { - shellArgs = ["-ilc"]; + const getShellSpecifices = (shellPath: string, mark: string) => { + const shellName = basename(shellPath); + + if (powerShellName.test(shellName)) { + // Older versions of PowerShell removes double quotes sometimes so we use "double single quotes" which is how + // you escape single quotes inside of a single quoted string. + return { + command: `Command '${process.execPath}' -p '\\"${mark}\\" + JSON.stringify(process.env) + \\"${mark}\\"'`, + shellArgs: ["-Login"], + }; } - } - return new Promise((resolve, reject) => { - const shellProcess = spawn(shell, [...shellArgs, command], { - detached: true, - stdio: ["ignore", "pipe", "pipe"], - env, - }); - const stdout: Buffer[] = []; + return { + command: `'${process.execPath}' -p '"${mark}" + JSON.stringify(process.env) + "${mark}"'`, + shellArgs: nonBashLikeShellName.test(shellName) + // tcsh and csh don't support any other options when providing the -l (login) shell option + ? ["-l"] + // zsh (at least, maybe others) don't load RC files when in non-interactive mode, even when using -l (login) option + : ["-li"], + }; + }; - opts?.signal?.addEventListener("abort", () => shellProcess.kill()); + return async (shellPath, opts = {}) => { + const runAsNode = process.env["ELECTRON_RUN_AS_NODE"]; + const noAttach = process.env["ELECTRON_NO_ATTACH_CONSOLE"]; + const env = { + ...process.env, + ELECTRON_RUN_AS_NODE: "1", + ELECTRON_NO_ATTACH_CONSOLE: "1", + }; + const mark = randomUUID().replace(/-/g, ""); + const regex = new RegExp(`${mark}(\\{.*\\})${mark}`); + const { command, shellArgs } = getShellSpecifices(shellPath, mark); - shellProcess.on("error", (err) => reject(err)); - shellProcess.stdout.on("data", b => stdout.push(b)); - shellProcess.on("close", (code, signal) => { - if (code || signal) { - return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`)); - } + return new Promise((resolve, reject) => { + const shellProcess = spawn(shellPath, shellArgs, { + detached: true, + env, + }); + const stdout: Buffer[] = []; - try { - const rawOutput = Buffer.concat(stdout).toString("utf-8"); - const match = regex.exec(rawOutput); - const strippedRawOutput = match ? match[1] : "{}"; - const resolvedEnv = JSON.parse(strippedRawOutput); + opts.signal?.addEventListener("abort", () => shellProcess.kill()); - if (runAsNode) { - resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode; - } else { - delete resolvedEnv["ELECTRON_RUN_AS_NODE"]; + shellProcess.stdout.on("data", b => stdout.push(b)); + + shellProcess.on("error", (err) => reject(err)); + shellProcess.on("close", (code, signal) => { + if (code || signal) { + return reject(new Error(`Unexpected return code from spawned shell (code: ${code}, signal: ${signal})`)); } - if (noAttach) { - resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach; - } else { - delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"]; - } + try { + const rawOutput = Buffer.concat(stdout).toString("utf-8"); + const match = regex.exec(rawOutput); + const strippedRawOutput = match ? match[1] : "{}"; + const resolvedEnv = JSON.parse(strippedRawOutput); - resolve(resolvedEnv); - } catch(err) { - reject(err); - } + if (runAsNode) { + resolvedEnv["ELECTRON_RUN_AS_NODE"] = runAsNode; + } else { + delete resolvedEnv["ELECTRON_RUN_AS_NODE"]; + } + + if (noAttach) { + resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"] = noAttach; + } else { + delete resolvedEnv["ELECTRON_NO_ATTACH_CONSOLE"]; + } + + resolve(resolvedEnv); + } catch (err) { + reject(err); + } + }); + shellProcess.stdin.end(command); }); - }); + }; }, causesSideEffects: true, }); diff --git a/src/main/vars/build-version/init.injectable.ts b/src/main/vars/build-version/init.injectable.ts index 0d879715a4..f53e735a47 100644 --- a/src/main/vars/build-version/init.injectable.ts +++ b/src/main/vars/build-version/init.injectable.ts @@ -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; diff --git a/src/main/vars/default-update-channel/init.injectable.ts b/src/main/vars/default-update-channel/init.injectable.ts index e1680efa39..be588780af 100644 --- a/src/main/vars/default-update-channel/init.injectable.ts +++ b/src/main/vars/default-update-channel/init.injectable.ts @@ -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; diff --git a/src/main/vars/release-channel/init.injectable.ts b/src/main/vars/release-channel/init.injectable.ts index ed358af550..59527d2194 100644 --- a/src/main/vars/release-channel/init.injectable.ts +++ b/src/main/vars/release-channel/init.injectable.ts @@ -13,6 +13,7 @@ const initReleaseChannelInjectable = getInjectable({ const releaseChannel = di.inject(releaseChannelInjectable); return { + id: "init-release-channel", run: () => releaseChannel.init(), runAfter: di.inject(initSemanticBuildVersionInjectable), }; diff --git a/src/main/vars/semantic-build-version/init.injectable.ts b/src/main/vars/semantic-build-version/init.injectable.ts index 8b33e27576..ee822a8796 100644 --- a/src/main/vars/semantic-build-version/init.injectable.ts +++ b/src/main/vars/semantic-build-version/init.injectable.ts @@ -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), }; diff --git a/src/migrations/weblinks-store/currentVersion.ts b/src/migrations/weblinks-store/currentVersion.ts index 40025e64cb..afa404847b 100644 --- a/src/migrations/weblinks-store/currentVersion.ts +++ b/src/migrations/weblinks-store/currentVersion.ts @@ -3,7 +3,7 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import { lensSlackWeblinkId, slackUrl } from "../../common/vars"; +import { docsUrl, lensDocumentationWeblinkId, lensSlackWeblinkId, slackUrl } from "../../common/vars"; import type { WeblinkData } from "../../common/weblink-store"; import type { MigrationDeclaration } from "../helpers"; import packageJson from "../../../package.json"; @@ -20,6 +20,12 @@ export default { slackWeblink.url = slackUrl; } + const docsWeblink = weblinks.find(weblink => weblink.id === lensDocumentationWeblinkId); + + if (docsWeblink) { + docsWeblink.url = docsUrl; + } + store.set("weblinks", weblinks); }, } as MigrationDeclaration; diff --git a/src/renderer/api/setup-on-api-errors.injectable.ts b/src/renderer/api/setup-on-api-errors.injectable.ts index 859b333586..a352293b5e 100644 --- a/src/renderer/api/setup-on-api-errors.injectable.ts +++ b/src/renderer/api/setup-on-api-errors.injectable.ts @@ -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); }, diff --git a/src/renderer/app-paths/setup-app-paths.injectable.ts b/src/renderer/app-paths/setup-app-paths.injectable.ts index 14242347f4..3660ad9db8 100644 --- a/src/renderer/app-paths/setup-app-paths.injectable.ts +++ b/src/renderer/app-paths/setup-app-paths.injectable.ts @@ -17,6 +17,7 @@ const setupAppPathsInjectable = getInjectable({ const appPathsState = di.inject(appPathsStateInjectable); return { + id: "setup-app-paths", run: async () => { const appPaths = await requestFromChannel( appPathsChannel, diff --git a/src/renderer/components/hotbar/hotbar-entity-icon.tsx b/src/renderer/components/hotbar/hotbar-entity-icon.tsx index e07f320fad..b3f635acb5 100644 --- a/src/renderer/components/hotbar/hotbar-entity-icon.tsx +++ b/src/renderer/components/hotbar/hotbar-entity-icon.tsx @@ -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; + navigate: Navigate; } @observer class NonInjectedHotbarEntityIcon extends React.Component { private readonly menuItems = observable.array(); - 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; } - get ledIcon() { + private renderLedIcon() { if (this.props.entity.kind !== "KubernetesCluster") { return null; } @@ -86,7 +88,7 @@ class NonInjectedHotbarEntityIcon extends React.Component - { this.ledIcon } - { this.kindIcon } + {this.renderLedIcon()} + {this.renderKindIcon()} ); } @@ -126,5 +128,6 @@ export const HotbarEntityIcon = withInjectables { ipcRenderer.on("history:can-go-back", action((event, canGoBack: boolean) => { state.prevEnabled = canGoBack; diff --git a/src/renderer/components/test-utils/get-application-builder.tsx b/src/renderer/components/test-utils/get-application-builder.tsx index 9f9ab7d903..f7e28c7b81 100644 --- a/src/renderer/components/test-utils/get-application-builder.tsx +++ b/src/renderer/components/test-utils/get-application-builder.tsx @@ -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"; diff --git a/src/renderer/frames/root-frame/setup-system-ca.injectable.ts b/src/renderer/frames/root-frame/setup-system-ca.injectable.ts index ca913a4371..3ee10748f9 100644 --- a/src/renderer/frames/root-frame/setup-system-ca.injectable.ts +++ b/src/renderer/frames/root-frame/setup-system-ca.injectable.ts @@ -10,6 +10,7 @@ const setupSystemCaInjectable = getInjectable({ id: "setup-system-ca", instantiate: () => ({ + id: "setup-system-ca", run: async () => { await injectSystemCAs(); }, diff --git a/src/renderer/getDiForUnitTesting.tsx b/src/renderer/getDiForUnitTesting.tsx index 58b17b7eaf..aa146ad385 100644 --- a/src/renderer/getDiForUnitTesting.tsx +++ b/src/renderer/getDiForUnitTesting.tsx @@ -104,9 +104,17 @@ export const getDiForUnitTesting = ( di.override(getRandomIdInjectable, () => () => "some-irrelevant-random-id"); di.override(platformInjectable, () => "darwin"); - di.override(startTopbarStateSyncInjectable, () => ({ - run: () => {}, - })); + + [ + startTopbarStateSyncInjectable, + setupSystemCaInjectable, + setupOnApiErrorListenersInjectable, + ].forEach((injectable) => { + di.override(injectable, () => ({ + id: injectable.id, + run: () => {}, + })); + }); di.override(terminalSpawningPoolInjectable, () => document.createElement("div")); di.override(hostedClusterIdInjectable, () => undefined); @@ -180,9 +188,6 @@ export const getDiForUnitTesting = ( di.override(fileSystemProvisionerStoreInjectable, () => ({}) as FileSystemProvisionerStore); - di.override(setupSystemCaInjectable, () => ({ run: () => {} })); - di.override(setupOnApiErrorListenersInjectable, () => ({ run: () => {} })); - di.override(defaultShellInjectable, () => "some-default-shell"); di.override(userStoreInjectable, () => ({ diff --git a/src/renderer/stores/init-user-store.injectable.ts b/src/renderer/stores/init-user-store.injectable.ts index 36f2caf015..2c5b15dd1b 100644 --- a/src/renderer/stores/init-user-store.injectable.ts +++ b/src/renderer/stores/init-user-store.injectable.ts @@ -5,7 +5,7 @@ import { getInjectable } from "@ogre-tools/injectable"; import userStoreInjectable from "../../common/user-store/user-store.injectable"; import { beforeFrameStartsInjectionToken } from "../before-frame-starts/before-frame-starts-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", @@ -13,8 +13,9 @@ const initUserStoreInjectable = getInjectable({ const userStore = di.inject(userStoreInjectable); return { + id: "init-user-store", run: () => userStore.load(), - runAfter: di.inject(initDefaultUpdateChannelInjectableInjectable), + runAfter: di.inject(initDefaultUpdateChannelInjectable), }; }, injectionToken: beforeFrameStartsInjectionToken, diff --git a/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts b/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts index c37c9b1864..e423299915 100644 --- a/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts +++ b/src/renderer/utils/channel/channel-listeners/start-listening-of-channels.injectable.ts @@ -13,6 +13,7 @@ const startListeningOfChannelsInjectable = getInjectable({ const listeningOfChannels = di.inject(listeningOfChannelsInjectable); return { + id: "start-listening-of-channels-renderer", run: async () => { await listeningOfChannels.start(); }, diff --git a/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts b/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts index 472aee497a..f7d692cdef 100644 --- a/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts +++ b/src/renderer/utils/sync-box/provide-initial-values-for-sync-boxes.injectable.ts @@ -25,6 +25,7 @@ const provideInitialValuesForSyncBoxesInjectable = getInjectable({ di.inject(createSyncBoxStateInjectable, syncBox.id).set(state); return { + id: "provide-initial-values-for-sync-boxes", run: async () => { const initialValues = await requestFromChannel(syncBoxInitialValueChannel); diff --git a/src/renderer/vars/build-version/init.injectable.ts b/src/renderer/vars/build-version/init.injectable.ts index 0dc867f0ca..25aeb8d2bc 100644 --- a/src/renderer/vars/build-version/init.injectable.ts +++ b/src/renderer/vars/build-version/init.injectable.ts @@ -12,6 +12,7 @@ const initializeBuildVersionInjectable = getInjectable({ const buildVersion = di.inject(buildVersionInjectable); return { + id: "initialize-build-version", run: () => buildVersion.init(), }; }, diff --git a/src/renderer/vars/default-update-channel/init.injectable.ts b/src/renderer/vars/default-update-channel/init.injectable.ts index e486be0aaf..36bbec8157 100644 --- a/src/renderer/vars/default-update-channel/init.injectable.ts +++ b/src/renderer/vars/default-update-channel/init.injectable.ts @@ -7,12 +7,13 @@ import defaultUpdateChannelInjectable from "../../../common/application-update/s import { beforeFrameStartsInjectionToken } from "../../before-frame-starts/before-frame-starts-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: beforeFrameStartsInjectionToken, }); -export default initDefaultUpdateChannelInjectableInjectable; +export default initDefaultUpdateChannelInjectable; diff --git a/src/renderer/vars/release-channel/init.injectable.ts b/src/renderer/vars/release-channel/init.injectable.ts index f384fd2716..14e92fccda 100644 --- a/src/renderer/vars/release-channel/init.injectable.ts +++ b/src/renderer/vars/release-channel/init.injectable.ts @@ -13,6 +13,7 @@ const initReleaseChannelInjectable = getInjectable({ const releaseChannel = di.inject(releaseChannelInjectable); return { + id: "init-release-channel", run: () => releaseChannel.init(), runAfter: di.inject(initSemanticBuildVersionInjectable), }; diff --git a/src/renderer/vars/semantic-build-version/init.injectable.ts b/src/renderer/vars/semantic-build-version/init.injectable.ts index 572ccc63ac..393143219f 100644 --- a/src/renderer/vars/semantic-build-version/init.injectable.ts +++ b/src/renderer/vars/semantic-build-version/init.injectable.ts @@ -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), };