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:
parent
91bb7109d5
commit
38ca06fc80
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,
|
||||
});
|
||||
|
||||
|
||||
@ -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",
|
||||
});
|
||||
|
||||
@ -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 });
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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])
|
||||
);
|
||||
});
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
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;
|
||||
@ -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),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@ -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[]) => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user