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

Switch to overriding dependencies to fix test flakiness

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2022-08-31 11:17:46 -04:00
parent 91bb7109d5
commit 38ca06fc80
13 changed files with 415 additions and 137 deletions

View File

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

View File

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

View File

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

View File

@ -3,72 +3,49 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { observable, ObservableMap, when } 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 mockFs from "mock-fs";
import fs from "fs";
import clusterStoreInjectable from "../../../common/cluster-store/cluster-store.injectable";
import { getDiForUnitTesting } from "../../getDiForUnitTesting";
import getConfigurationFileModelInjectable from "../../../common/get-configuration-file-model/get-configuration-file-model.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";
import { iter, strictGet } from "../../../common/utils";
import type { ComputeKubeconfigDiff } from "../kubeconfig-sync/compute-diff.injectable";
import computeKubeconfigDiffInjectable from "../kubeconfig-sync/compute-diff.injectable";
import watchInjectable from "../../../common/fs/watch/watch.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";
console.log("This is a reminder that mockFS breaks things and needs to be removed");
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 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 computeKubeconfigDiff: ComputeKubeconfigDiff;
let configToModels: ConfigToModels;
let manager: KubeconfigSyncManager;
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");
di.permitSideEffects(fsInjectable);
di.permitSideEffects(watchInjectable);
di.unoverride(clusterStoreInjectable);
di.permitSideEffects(clusterStoreInjectable);
di.permitSideEffects(getConfigurationFileModelInjectable);
clusters = new Map();
di.override(getClusterByIdInjectable, () => id => clusters.get(id));
kubeconfigSyncs = observable.map();
@ -76,11 +53,6 @@ describe("kubeconfig-sync.source tests", () => {
computeKubeconfigDiff = di.inject(computeKubeconfigDiffInjectable);
configToModels = di.inject(configToModelsInjectable);
manager = di.inject(kubeconfigSyncManagerInjectable);
});
afterEach(() => {
mockFs.restore();
});
describe("configsToModels", () => {
@ -162,8 +134,6 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(1);
@ -206,8 +176,6 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(1);
@ -260,8 +228,6 @@ describe("kubeconfig-sync.source tests", () => {
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const filePath = "/bar";
fs.writeFileSync(filePath, contents);
computeKubeconfigDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(2);
@ -316,34 +282,47 @@ 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(() => {
fs.mkdirSync("/foobar");
fs.writeFileSync("/foobar/config", 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",
}));
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", () => {
@ -364,30 +343,118 @@ describe("kubeconfig-sync.source tests", () => {
kubeconfigSyncs.set("/foobar/config", {});
});
it("should find a single entity", (done) => {
when(() => manager.source.get().length === 1, () => done());
});
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/config"], {
isDirectory: () => false,
} as Stats);
});
it("should still only find a single entity", (done) => {
when(() => manager.source.get().length === 1, () => done());
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);
});
});
});
});
});
});
});
});
});
describe("when a folder sync target for /foobar is added", () => {
beforeEach(() => {
kubeconfigSyncs.set("/foobar", {});
});
it("should find a single entity", (done) => {
when(() => manager.source.get().length === 1, () => done());
});
});
});
});
});
const getFakeWatchInstance = (): Watcher<true> => {
return Object.assign(new EventEmitter(), {
close: jest.fn().mockImplementation(async () => {}),
});
};
const getFakeReadStream = (path: string): ReadStream => {
return Object.assign(new EventEmitter(), {
path,
close: () => {},
push: () => true,
read: () => {},
}) as unknown as ReadStream;
};
const foobarConfig = JSON.stringify({
clusters: [{
name: "cluster-name",
cluster: {
server: "1.2.3.4",
},
skipTLSVerify: false,
}],
users: [{
name: "user-name",
}],
contexts: [{
name: "context-name",
context: {
cluster: "cluster-name",
user: "user-name",
},
}, {
name: "context-the-second",
context: {
cluster: "missing-cluster",
user: "user-name",
},
}],
currentContext: "foobar",
});

View File

@ -12,8 +12,8 @@ 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 clusterManagerInjectable from "../../cluster/manager.injectable";
import createClusterInjectable from "../../create-cluster/create-cluster.injectable";
import configToModelsInjectable from "./config-to-models.injectable";
import kubeconfigSyncLoggerInjectable from "./logger.injectable";
@ -25,7 +25,7 @@ const computeKubeconfigDiffInjectable = getInjectable({
instantiate: (di): ComputeKubeconfigDiff => {
const directoryForKubeConfigs = di.inject(directoryForKubeConfigsInjectable);
const createCluster = di.inject(createClusterInjectable);
const clusterManager = di.inject(clusterManagerInjectable);
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
const configToModels = di.inject(configToModelsInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const getClusterById = di.inject(getClusterByIdInjectable);
@ -48,8 +48,8 @@ const computeKubeconfigDiffInjectable = getInjectable({
// 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);
// 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);
@ -68,7 +68,7 @@ const computeKubeconfigDiffInjectable = getInjectable({
}
for (const [contextName, [model, configData]] of models) {
// add new clusters to the source
// 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);
@ -93,6 +93,8 @@ const computeKubeconfigDiffInjectable = getInjectable({
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 });
});
},
});

View File

@ -9,7 +9,7 @@ import type { ObservableMap } from "mobx";
import type { Readable } from "stream";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import fsInjectable from "../../../common/fs/fs.injectable";
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";
@ -28,7 +28,7 @@ const diffChangedKubeconfigInjectable = getInjectable({
instantiate: (di): DiffChangedKubeconfig => {
const computeKubeconfigDiff = di.inject(computeKubeconfigDiffInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const { createReadStream } = di.inject(fsInjectable);
const createReadFileStream = di.inject(createReadFileStreamInjectable);
return ({ filePath, maxAllowedFileReadSize, source, stats }) => {
logger.debug(`file changed`, { filePath });
@ -40,7 +40,7 @@ const diffChangedKubeconfigInjectable = getInjectable({
return noop;
}
const fileReader = createReadStream(filePath, {
const fileReader = createReadFileStream(filePath, {
mode: constants.O_RDONLY,
});
const readStream = fileReader as Readable;

View File

@ -36,11 +36,13 @@ export class KubeconfigSyncManager {
return (
iter.pipeline(this.sources.values())
.flatMap(([entities]) => entities.get())
.filter(entity => (
seenIds.has(entity.getId())
? false
: seenIds.add(entity.getId())
))
.filter(entity => {
const alreadySeen = seenIds.has(entity.getId());
seenIds.add(entity.getId());
return !alreadySeen;
})
.collect(items => [...items])
);
});

View File

@ -3,8 +3,6 @@
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { FSWatcher } from "chokidar";
import type { Stats } from "fs";
import GlobToRegExp from "glob-to-regexp";
import type { IComputedValue, ObservableMap } from "mobx";
import { computed, observable } from "mobx";
@ -12,7 +10,8 @@ import path from "path";
import { inspect } from "util";
import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster";
import fsInjectable from "../../../common/fs/fs.injectable";
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";
@ -47,14 +46,14 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
instantiate: (di): WatchKubeconfigFileChanges => {
const diffChangedKubeconfig = di.inject(diffChangedKubeconfigInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable);
const { stat } = di.inject(fsInjectable);
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: FSWatcher;
let watcher: Watcher<true>;
(async () => {
try {
@ -65,7 +64,7 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
? folderSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize;
watcher = watch(filePath, {
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,
@ -80,7 +79,7 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
});
watcher
.on("change", (childFilePath, stats: Stats): void => {
.on("change", (childFilePath, stats): void => {
const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) {
@ -96,7 +95,7 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
maxAllowedFileReadSize,
}));
})
.on("add", (childFilePath, stats: Stats): void => {
.on("add", (childFilePath, stats): void => {
if (isFolderSync) {
const fileName = path.basename(childFilePath);

View File

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

View File

@ -5,6 +5,7 @@
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({
@ -13,6 +14,7 @@ const clusterManagerInjectable = getInjectable({
instantiate: (di) => new ClusterManager({
store: di.inject(clusterStoreInjectable),
catalogEntityRegistry: di.inject(catalogEntityRegistryInjectable),
clustersThatAreBeingDeleted: di.inject(clustersThatAreBeingDeletedInjectable),
}),
});

View File

@ -5,6 +5,7 @@
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";
@ -23,16 +24,15 @@ const logPrefix = "[CLUSTER-MANAGER]:";
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
interface Dependencies {
store: ClusterStore;
catalogEntityRegistry: CatalogEntityRegistry;
readonly store: ClusterStore;
readonly catalogEntityRegistry: CatalogEntityRegistry;
readonly clustersThatAreBeingDeleted: ObservableSet<ClusterId>;
}
export class ClusterManager {
deleting = observable.set<ClusterId>();
@observable visibleCluster: ClusterId | undefined = undefined;
constructor(private dependencies: Dependencies) {
constructor(private readonly dependencies: Dependencies) {
makeObservable(this);
}
@ -69,7 +69,7 @@ export class ClusterManager {
}
});
observe(this.deleting, change => {
observe(this.dependencies.clustersThatAreBeingDeleted, change => {
if (change.type === "add") {
this.updateEntityStatus(this.dependencies.catalogEntityRegistry.findById(change.newValue) as KubernetesCluster);
}
@ -141,7 +141,7 @@ export class ClusterManager {
@action
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
if (this.deleting.has(entity.getId())) {
if (this.dependencies.clustersThatAreBeingDeleted.has(entity.getId())) {
entity.status.phase = LensKubernetesClusterStatus.DELETING;
entity.status.enabled = false;
} else {

View File

@ -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,6 +33,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
const clusterStore = di.inject(clusterStoreInjectable);
const operatingSystemTheme = di.inject(operatingSystemThemeInjectable);
const askUserForFilePaths = di.inject(askUserForFilePathsInjectable);
const clustersThatAreBeingDeleted = di.inject(clustersThatAreBeingDeletedInjectable);
return {
run: () => {
@ -46,6 +48,7 @@ const setupIpcMainHandlersInjectable = getInjectable({
clusterStore,
operatingSystemTheme,
askUserForFilePaths,
clustersThatAreBeingDeleted,
});
},
};

View File

@ -15,7 +15,7 @@ import { pushCatalogToRenderer } from "../../../catalog-pusher";
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[]) => {