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:
parent
40c81d74f3
commit
ab3e24fc54
6
.github/workflows/mkdocs-manual.yml
vendored
6
.github/workflows/mkdocs-manual.yml
vendored
@ -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
14
.github/workflows/require-milestone.yml
vendored
Normal 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 }}
|
||||
@ -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.
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -51,9 +51,9 @@ clusterPageMenus = [
|
||||
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).
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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": "",
|
||||
|
||||
21
src/common/cluster-store/get-by-id.injectable.ts
Normal file
21
src/common/cluster-store/get-by-id.injectable.ts
Normal 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;
|
||||
30
src/common/fs/create-read-file-stream.injectable.ts
Normal file
30
src/common/fs/create-read-file-stream.injectable.ts
Normal 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;
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
37
src/common/logger/prefixed-logger.injectable.ts
Normal file
37
src/common/logger/prefixed-logger.injectable.ts
Normal 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;
|
||||
@ -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),
|
||||
}),
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
}),
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>[]) =>
|
||||
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 after other runnable which does not same injection token.",
|
||||
);
|
||||
throw new Error(`Tried to run runnable "${runnable.id}" after the runnable "${runnable.runAfter.id}" which does not share the "${injectionTokenId}" injection token.`);
|
||||
}
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
17
src/common/user-store/kubeconfig-syncs.injectable.ts
Normal file
17
src/common/user-store/kubeconfig-syncs.injectable.ts
Normal 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;
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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: {},
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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"
|
||||
>
|
||||
|
||||
@ -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";
|
||||
|
||||
|
||||
@ -27,6 +27,7 @@ const setupAppPathsInjectable = getInjectable({
|
||||
const joinPaths = di.inject(joinPathsInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-app-paths",
|
||||
run: () => {
|
||||
if (directoryForIntegrationTesting) {
|
||||
setElectronAppPath("appData", directoryForIntegrationTesting);
|
||||
|
||||
@ -16,6 +16,7 @@ const emitCurrentVersionToAnalyticsInjectable = getInjectable({
|
||||
const buildVersion = di.inject(buildVersionInjectable);
|
||||
|
||||
return {
|
||||
id: "emit-current-version-to-analytics",
|
||||
run: () => {
|
||||
emitEvent({
|
||||
name: "app",
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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",
|
||||
});
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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();
|
||||
}];
|
||||
};
|
||||
|
||||
@ -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;
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
14
src/main/cluster/are-being-deleted.injectable.ts
Normal file
14
src/main/cluster/are-being-deleted.injectable.ts
Normal 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;
|
||||
25
src/main/cluster/initialize-manager.injectable.ts
Normal file
25
src/main/cluster/initialize-manager.injectable.ts
Normal 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;
|
||||
21
src/main/cluster/manager.injectable.ts
Normal file
21
src/main/cluster/manager.injectable.ts
Normal 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;
|
||||
@ -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 {
|
||||
@ -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,
|
||||
|
||||
@ -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");
|
||||
});
|
||||
14
src/main/crypto/random-uuid.injectable.ts
Normal file
14
src/main/crypto/random-uuid.injectable.ts
Normal 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;
|
||||
@ -13,6 +13,7 @@ const cleanUpDeepLinkingInjectable = getInjectable({
|
||||
const lensProtocolRouterMain = di.inject(lensProtocolRouterMainInjectable);
|
||||
|
||||
return {
|
||||
id: "clean-up-deep-linking",
|
||||
run: () => {
|
||||
lensProtocolRouterMain.cleanup();
|
||||
},
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ const showDockForFirstOpenedWindowInjectable = getInjectable({
|
||||
const app = di.inject(electronAppInjectable);
|
||||
|
||||
return {
|
||||
id: "show-dock-for-first-opened-window",
|
||||
run: () => {
|
||||
app.dock?.show();
|
||||
},
|
||||
|
||||
@ -15,6 +15,7 @@ const enforceSingleApplicationInstanceInjectable = getInjectable({
|
||||
const exitApp = di.inject(exitAppInjectable);
|
||||
|
||||
return {
|
||||
id: "enforce-single-application-instance",
|
||||
run: () => {
|
||||
if (!requestSingleInstanceLock()) {
|
||||
exitApp();
|
||||
|
||||
@ -15,6 +15,7 @@ const setupApplicationNameInjectable = getInjectable({
|
||||
const appName = di.inject(appNameInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-application-name",
|
||||
run: () => {
|
||||
app.setName(appName);
|
||||
},
|
||||
|
||||
@ -26,6 +26,7 @@ const setupDeepLinkingInjectable = getInjectable({
|
||||
);
|
||||
|
||||
return {
|
||||
id: "setup-deep-linking",
|
||||
run: async () => {
|
||||
logger.info(`📟 Setting protocol client for lens://`);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -15,6 +15,7 @@ const setupDeviceShutdownInjectable = getInjectable({
|
||||
const exitApp = di.inject(exitAppInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-device-shutdown",
|
||||
run: () => {
|
||||
powerMonitor.on("shutdown", async () => {
|
||||
exitApp();
|
||||
|
||||
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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[]) => {
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -26,6 +26,7 @@ const setupRunnablesBeforeClosingOfApplicationInjectable = getInjectable({
|
||||
);
|
||||
|
||||
return {
|
||||
id: "setup-closing-of-application",
|
||||
run: () => {
|
||||
const app = di.inject(electronAppInjectable);
|
||||
|
||||
|
||||
@ -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, () => ({
|
||||
[
|
||||
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, () => () => {});
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -15,6 +15,7 @@ const startApplicationMenuInjectable = getInjectable({
|
||||
);
|
||||
|
||||
return {
|
||||
id: "start-application-menu",
|
||||
run: async () => {
|
||||
await applicationMenu.start();
|
||||
},
|
||||
|
||||
@ -15,6 +15,7 @@ const stopApplicationMenuInjectable = getInjectable({
|
||||
);
|
||||
|
||||
return {
|
||||
id: "stop-application-menu",
|
||||
run: async () => {
|
||||
await applicationMenu.stop();
|
||||
},
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ const cleanUpShellSessionsInjectable = getInjectable({
|
||||
id: "clean-up-shell-sessions",
|
||||
|
||||
instantiate: () => ({
|
||||
id: "clean-up-shell-sessions",
|
||||
run: () => {
|
||||
ShellSession.cleanup();
|
||||
},
|
||||
|
||||
@ -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" });
|
||||
},
|
||||
|
||||
@ -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" });
|
||||
},
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -21,6 +21,7 @@ const initializeExtensionsInjectable = getInjectable({
|
||||
const showErrorPopup = di.inject(showErrorPopupInjectable);
|
||||
|
||||
return {
|
||||
id: "initialize-extensions",
|
||||
run: async () => {
|
||||
logger.info("🧩 Initializing extensions");
|
||||
|
||||
|
||||
@ -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;
|
||||
@ -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),
|
||||
};
|
||||
},
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ const stopKubeConfigSyncInjectable = getInjectable({
|
||||
const kubeConfigSyncManager = di.inject(kubeconfigSyncManagerInjectable);
|
||||
|
||||
return {
|
||||
id: "stop-kube-config-sync",
|
||||
run: () => {
|
||||
kubeConfigSyncManager.stopSync();
|
||||
},
|
||||
|
||||
@ -14,6 +14,7 @@ const setupSentryInjectable = getInjectable({
|
||||
const initializeSentryOnMain = di.inject(initializeSentryOnMainInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-sentry",
|
||||
run: () => initializeSentryReportingWith(initializeSentryOnMain),
|
||||
};
|
||||
},
|
||||
|
||||
@ -18,6 +18,7 @@ const setupDetectorRegistryInjectable = getInjectable({
|
||||
const detectorRegistry = di.inject(detectorRegistryInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-detector-registry",
|
||||
run: () => {
|
||||
detectorRegistry
|
||||
.add(ClusterIdDetector)
|
||||
|
||||
@ -15,6 +15,7 @@ const setupHardwareAccelerationInjectable = getInjectable({
|
||||
const disableHardwareAcceleration = di.inject(disableHardwareAccelerationInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-hardware-acceleration",
|
||||
run: () => {
|
||||
if (hardwareAccelerationShouldBeDisabled) {
|
||||
disableHardwareAcceleration();
|
||||
|
||||
@ -11,6 +11,7 @@ const setupHotbarStoreInjectable = getInjectable({
|
||||
id: "setup-hotbar-store",
|
||||
|
||||
instantiate: (di) => ({
|
||||
id: "setup-hotbar-store",
|
||||
run: () => {
|
||||
const hotbarStore = di.inject(hotbarStoreInjectable);
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
@ -26,6 +26,7 @@ const setupLensProxyInjectable = getInjectable({
|
||||
const buildVersion = di.inject(buildVersionInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-lens-proxy",
|
||||
run: async () => {
|
||||
try {
|
||||
logger.info("🔌 Starting LensProxy");
|
||||
|
||||
@ -10,6 +10,7 @@ const setupMobxInjectable = getInjectable({
|
||||
id: "setup-mobx",
|
||||
|
||||
instantiate: () => ({
|
||||
id: "setup-mobx",
|
||||
run: () => {
|
||||
// Docs: https://mobx.js.org/configuration.html
|
||||
Mobx.configure({
|
||||
|
||||
@ -18,6 +18,7 @@ const setupPrometheusRegistryInjectable = getInjectable({
|
||||
const prometheusProviderRegistry = di.inject(prometheusProviderRegistryInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-prometheus-registry",
|
||||
run: () => {
|
||||
prometheusProviderRegistry
|
||||
.registerProvider(new PrometheusLens())
|
||||
|
||||
@ -13,6 +13,7 @@ const setupProxyEnvInjectable = getInjectable({
|
||||
const getCommandLineSwitch = di.inject(getCommandLineSwitchInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-proxy-env",
|
||||
run: () => {
|
||||
const switchValue = getCommandLineSwitch("proxy-server");
|
||||
|
||||
|
||||
@ -13,6 +13,7 @@ const setupReactionsInUserStoreInjectable = getInjectable({
|
||||
const userStore = di.inject(userStoreInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-reactions-in-user-store",
|
||||
run: () => {
|
||||
userStore.startMainReactions();
|
||||
},
|
||||
|
||||
@ -15,6 +15,7 @@ const setupSyncingOfGeneralCatalogEntitiesInjectable = getInjectable({
|
||||
);
|
||||
|
||||
return {
|
||||
id: "setup-syncing-of-general-catalog-entities",
|
||||
run: () => {
|
||||
syncGeneralCatalogEntities();
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ const setupSyncingOfWeblinksInjectable = getInjectable({
|
||||
const syncWeblinks = di.inject(syncWeblinksInjectable);
|
||||
|
||||
return {
|
||||
id: "setup-syncing-of-weblinks",
|
||||
run: () => {
|
||||
syncWeblinks();
|
||||
},
|
||||
|
||||
@ -10,6 +10,7 @@ const setupSystemCaInjectable = getInjectable({
|
||||
id: "setup-system-ca",
|
||||
|
||||
instantiate: () => ({
|
||||
id: "setup-system-ca",
|
||||
run: async () => {
|
||||
await injectSystemCAs();
|
||||
},
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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";
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -13,6 +13,7 @@ const startBroadcastingThemeChangeInjectable = getInjectable({
|
||||
const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable);
|
||||
|
||||
return {
|
||||
id: "start-broadcasting-theme-change",
|
||||
run: async () => {
|
||||
await broadcastThemeChange.start();
|
||||
},
|
||||
|
||||
@ -13,6 +13,7 @@ const stopBroadcastingThemeChangeInjectable = getInjectable({
|
||||
const broadcastThemeChange = di.inject(broadcastThemeChangeInjectable);
|
||||
|
||||
return {
|
||||
id: "stop-broadcasting-theme-change",
|
||||
run: async () => {
|
||||
await broadcastThemeChange.stop();
|
||||
},
|
||||
|
||||
@ -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();
|
||||
},
|
||||
|
||||
@ -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
Loading…
Reference in New Issue
Block a user