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

Release 6.1.13 (#6463)

* Release 6.1.13

Signed-off-by: Sebastian Malton <sebastian@malton.name>

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

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Require milestones on PRs before merging (#6431)

* Require milestones on PRs before merging

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix AppImage windows not showing the application icon (#6444)

Signed-off-by: Damien Degois <damien@degois.info>

Signed-off-by: Damien Degois <damien@degois.info>

* Fix links in Readme (#6441)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Update all links within application (#6442)

- URLs removed the /latest/ and /main/ pathname prefixes

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Bump lens-k8s-proxy to v0.3.0 (#6461)

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix syncing shell env on TCSH and CSH (#6453)

* Fix syncing shell env on TCSH and CSH

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Refactor computeUnixShellEnvironment to be clearer

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>

* Fix manually set prometheus service address to work after re-connect (#6435)

* Fix manually set prometheus service address to work after re-connect

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Fix manually set prometheus service address to work after re-connect

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Setup prometheus also on contenxt handler constructor

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Temp fix

Signed-off-by: Sebastian Malton <sebastian@malton.name>

Signed-off-by: Sebastian Malton <sebastian@malton.name>
Signed-off-by: Damien Degois <damien@degois.info>
Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
Co-authored-by: Damien Degois <damien@degois.info>
Co-authored-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
Sebastian Malton 2022-10-24 17:32:25 -04:00 committed by GitHub
parent 40c81d74f3
commit ab3e24fc54
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
127 changed files with 1397 additions and 595 deletions

View File

@ -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: |

14
.github/workflows/require-milestone.yml vendored Normal file
View File

@ -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 }}

View File

@ -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.

View File

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

View File

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

View File

@ -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).

View File

@ -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`

View File

@ -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": "",

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ClusterId } from "../cluster-types";
import type { Cluster } from "../cluster/cluster";
import clusterStoreInjectable from "./cluster-store.injectable";
export type GetClusterById = (id: ClusterId) => Cluster | undefined;
const getClusterByIdInjectable = getInjectable({
id: "get-cluster-by-id",
instantiate: (di): GetClusterById => {
const store = di.inject(clusterStoreInjectable);
return (id) => store.getById(id);
},
});
export default getClusterByIdInjectable;

View File

@ -0,0 +1,30 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { ReadStream } from "fs";
import fsInjectable from "./fs.injectable";
export interface CreateReadStreamOptions {
mode?: number;
end?: number | undefined;
flags?: string | undefined;
encoding?: BufferEncoding | undefined;
autoClose?: boolean | undefined;
/**
* @default false
*/
emitClose?: boolean | undefined;
start?: number | undefined;
highWaterMark?: number | undefined;
}
export type CreateReadFileStream = (filePath: string, options?: CreateReadStreamOptions) => ReadStream;
const createReadFileStreamInjectable = getInjectable({
id: "create-read-file-stream",
instantiate: (di): CreateReadFileStream => di.inject(fsInjectable).createReadStream,
});
export default createReadFileStreamInjectable;

View File

@ -3,12 +3,14 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { Stats } from "fs";
import fsInjectable from "../fs.injectable";
export type Stat = (path: string) => Promise<Stats>;
const statInjectable = getInjectable({
id: "stat",
instantiate: (di) => di.inject(fsInjectable).stat,
instantiate: (di): Stat => di.inject(fsInjectable).stat,
});
export default statInjectable;

View File

@ -3,15 +3,161 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { FSWatcher, WatchOptions } from "chokidar";
import { watch } from "chokidar";
import type { Stats } from "fs";
import type TypedEventEmitter from "typed-emitter";
import type { SingleOrMany } from "../../utils";
export type Watch = (path: string, options?: WatchOptions) => FSWatcher;
export interface AlwaysStatWatcherEvents {
add: (path: string, stats: Stats) => void;
addDir: (path: string, stats: Stats) => void;
change: (path: string, stats: Stats) => void;
}
export interface MaybeStatWatcherEvents {
add: (path: string, stats?: Stats) => void;
addDir: (path: string, stats?: Stats) => void;
change: (path: string, stats?: Stats) => void;
}
export type WatcherEvents<AlwaysStat extends boolean> = BaseWatcherEvents
& (
AlwaysStat extends true
? AlwaysStatWatcherEvents
: MaybeStatWatcherEvents
);
export interface BaseWatcherEvents {
error: (error: Error) => void;
ready: () => void;
unlink: (path: string) => void;
unlinkDir: (path: string) => void;
}
export interface Watcher<AlwaysStat extends boolean> extends TypedEventEmitter<WatcherEvents<AlwaysStat>> {
close: () => Promise<void>;
}
export type WatcherOptions<AlwaysStat extends boolean> = {
/**
* Indicates whether the process should continue to run as long as files are being watched. If
* set to `false` when using `fsevents` to watch, no more events will be emitted after `ready`,
* even if the process continues to run.
*/
persistent?: boolean;
/**
* ([anymatch](https://github.com/micromatch/anymatch)-compatible definition) Defines files/paths to
* be ignored. The whole relative or absolute path is tested, not just filename. If a function
* with two arguments is provided, it gets called twice per path - once with a single argument
* (the path), second time with two arguments (the path and the
* [`fs.Stats`](https://nodejs.org/api/fs.html#fs_class_fs_stats) object of that path).
*/
ignored?: SingleOrMany<string | RegExp | ((path: string) => boolean)>;
/**
* If set to `false` then `add`/`addDir` events are also emitted for matching paths while
* instantiating the watching as chokidar discovers these file paths (before the `ready` event).
*/
ignoreInitial?: boolean;
/**
* When `false`, only the symlinks themselves will be watched for changes instead of following
* the link references and bubbling events through the link's path.
*/
followSymlinks?: boolean;
/**
* The base directory from which watch `paths` are to be derived. Paths emitted with events will
* be relative to this.
*/
cwd?: string;
/**
* If set to true then the strings passed to .watch() and .add() are treated as literal path
* names, even if they look like globs. Default: false.
*/
disableGlobbing?: boolean;
/**
* Whether to use fs.watchFile (backed by polling), or fs.watch. If polling leads to high CPU
* utilization, consider setting this to `false`. It is typically necessary to **set this to
* `true` to successfully watch files over a network**, and it may be necessary to successfully
* watch files in other non-standard situations. Setting to `true` explicitly on OS X overrides
* the `useFsEvents` default.
*/
usePolling?: boolean;
/**
* Whether to use the `fsevents` watching interface if available. When set to `true` explicitly
* and `fsevents` is available this supercedes the `usePolling` setting. When set to `false` on
* OS X, `usePolling: true` becomes the default.
*/
useFsEvents?: boolean;
/**
* If set, limits how many levels of subdirectories will be traversed.
*/
depth?: number;
/**
* Interval of file system polling.
*/
interval?: number;
/**
* Interval of file system polling for binary files. ([see list of binary extensions](https://gi
* thub.com/sindresorhus/binary-extensions/blob/master/binary-extensions.json))
*/
binaryInterval?: number;
/**
* Indicates whether to watch files that don't have read permissions if possible. If watching
* fails due to `EPERM` or `EACCES` with this set to `true`, the errors will be suppressed
* silently.
*/
ignorePermissionErrors?: boolean;
/**
* `true` if `useFsEvents` and `usePolling` are `false`). Automatically filters out artifacts
* that occur when using editors that use "atomic writes" instead of writing directly to the
* source file. If a file is re-added within 100 ms of being deleted, Chokidar emits a `change`
* event rather than `unlink` then `add`. If the default of 100 ms does not work well for you,
* you can override it by setting `atomic` to a custom value, in milliseconds.
*/
atomic?: boolean | number;
/**
* can be set to an object in order to adjust timing params:
*/
awaitWriteFinish?: AwaitWriteFinishOptions | boolean;
} & (AlwaysStat extends true
? {
alwaysStat: true;
}
: {
alwaysStat?: false;
}
);
export interface AwaitWriteFinishOptions {
/**
* Amount of time in milliseconds for a file size to remain constant before emitting its event.
*/
stabilityThreshold?: number;
/**
* File size polling interval.
*/
pollInterval?: number;
}
export type Watch = <AlwaysStat extends boolean = false>(path: string, options?: WatcherOptions<AlwaysStat>) => Watcher<AlwaysStat>;
// TODO: Introduce wrapper to allow simpler API
const watchInjectable = getInjectable({
id: "watch",
instantiate: (): Watch => watch,
instantiate: () => watch as Watch,
causesSideEffects: true,
});

View File

@ -0,0 +1,37 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { Logger } from "../logger";
import loggerInjectable from "../logger.injectable";
const prefixedLoggerInjectable = getInjectable({
id: "prefixed-logger",
instantiate: (di, prefix): Logger => {
const logger = di.inject(loggerInjectable);
return {
debug: (message, ...args) => {
logger.debug(`[${prefix}]: ${message}`, ...args);
},
error: (message, ...args) => {
logger.error(`[${prefix}]: ${message}`, ...args);
},
info: (message, ...args) => {
logger.info(`[${prefix}]: ${message}`, ...args);
},
silly: (message, ...args) => {
logger.silly(`[${prefix}]: ${message}`, ...args);
},
warn: (message, ...args) => {
logger.warn(`[${prefix}]: ${message}`, ...args);
},
};
},
lifecycle: lifecycleEnum.keyedSingleton({
getInstanceKey: (di, prefix: string) => prefix,
}),
});
export default prefixedLoggerInjectable;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,17 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import userStoreInjectable from "./user-store.injectable";
const kubeconfigSyncsInjectable = getInjectable({
id: "kubeconfig-syncs",
instantiate: (di) => {
const store = di.inject(userStoreInjectable);
return store.syncKubeconfigEntries;
},
});
export default kubeconfigSyncsInjectable;

View File

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

View File

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

View File

@ -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: {},

View File

@ -5,16 +5,16 @@
import { getInjectable } from "@ogre-tools/injectable";
import packageJson from "../../../package.json";
export type ApplicationInformation = Pick<typeof packageJson, "version" | "config" | "productName" | "copyright" | "description"> & {
export type ApplicationInformation = Pick<typeof packageJson, "version" | "config" | "productName" | "copyright" | "description" | "name"> & {
build: Partial<typeof packageJson["build"]> & { 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,
});

View File

@ -278,7 +278,7 @@ exports[`add-cluster - navigation using application menu when navigating to add
</code>
file.
<a
href="https://docs.k8slens.dev/main/getting-started/add-cluster/"
href="https://docs.k8slens.dev/getting-started/add-cluster/"
rel="noreferrer"
target="_blank"
>

View File

@ -274,7 +274,7 @@ exports[`extensions - navigation using application menu when navigating to exten
<p>
Add new features via Lens Extensions. Check out the
<a
href="https://docs.k8slens.dev/main/extensions/"
href="https://docs.k8slens.dev/extensions/"
rel="noreferrer"
target="_blank"
>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,25 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { ClusterManager } from "./cluster-manager";
import clusterStoreInjectable from "../common/cluster-store/cluster-store.injectable";
import catalogEntityRegistryInjectable from "./catalog/entity-registry.injectable";
const clusterManagerInjectable = getInjectable({
id: "cluster-manager",
instantiate: (di) => {
const clusterManager = new ClusterManager({
store: di.inject(clusterStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
});
clusterManager.init();
return clusterManager;
},
});
export default clusterManagerInjectable;

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { observable } from "mobx";
import type { ClusterId } from "../../common/cluster-types";
const clustersThatAreBeingDeletedInjectable = getInjectable({
id: "clusters-that-are-being-deleted",
instantiate: () => observable.set<ClusterId>(),
});
export default clustersThatAreBeingDeletedInjectable;

View File

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

View File

@ -0,0 +1,21 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import clusterStoreInjectable from "../../common/cluster-store/cluster-store.injectable";
import catalogEntityRegistryInjectable from "../catalog/entity-registry.injectable";
import clustersThatAreBeingDeletedInjectable from "./are-being-deleted.injectable";
import { ClusterManager } from "./manager";
const clusterManagerInjectable = getInjectable({
id: "cluster-manager",
instantiate: (di) => new ClusterManager({
store: di.inject(clusterStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
clustersThatAreBeingDeleted: di.inject(clustersThatAreBeingDeletedInjectable),
}),
});
export default clusterManagerInjectable;

View File

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

View File

@ -94,6 +94,8 @@ export class ContextHandler implements ClusterContextHandler {
}
protected async getPrometheusService(): Promise<PrometheusService> {
this.setupPrometheus(this.cluster.preferences);
if (this.prometheus && this.prometheusProvider) {
return {
id: this.prometheusProvider,

View File

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

View File

@ -0,0 +1,14 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { randomUUID } from "crypto";
const randomUUIDInjectable = getInjectable({
id: "random-uuid",
instantiate: () => randomUUID,
causesSideEffects: true,
});
export default randomUUIDInjectable;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,26 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import kubeconfigSyncManagerInjectable from "../../../catalog-sources/kubeconfig-sync/manager.injectable";
import catalogEntityRegistryInjectable from "../../../catalog/entity-registry.injectable";
import { afterApplicationIsLoadedInjectionToken } from "../../runnable-tokens/after-application-is-loaded-injection-token";
const addKubeconfigSyncAsEntitySourceInjectable = getInjectable({
id: "add-kubeconfig-sync-as-entity-source",
instantiate: (di) => {
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
const entityRegistry = di.inject(catalogEntityRegistryInjectable);
return {
id: "add-kubeconfig-sync-as-entity-source",
run: () => {
entityRegistry.addComputedSource("kubeconfig-sync", kubeConfigSyncManager.source);
},
};
},
injectionToken: afterApplicationIsLoadedInjectionToken,
});
export default addKubeconfigSyncAsEntitySourceInjectable;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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