mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
* mobx-6 migration -- part 1
Signed-off-by: Roman <ixrock@gmail.com>
* mobx-6 migration -- part 2 (npx mobx-undecorate --keepDecorators)
Signed-off-by: Roman <ixrock@gmail.com>
* mobx-6 migration -- part 3 (more fixes)
Signed-off-by: Roman <ixrock@gmail.com>
* unwrap possible observables from IPC-messaging
Signed-off-by: Roman <ixrock@gmail.com>
* mobx-6 migration -- remove @autobind as class-decorator
Signed-off-by: Roman <ixrock@gmail.com>
* mobx-6: replacing @autobind() as method-decorator to @boundMethod
Signed-off-by: Roman <ixrock@gmail.com>
* mobx-6: use toJS()-wrapper since monkey-patching require(mobx).toJS doesn't work
Signed-off-by: Roman <ixrock@gmail.com>
* removed `@observable static`
Signed-off-by: Roman <ixrock@gmail.com>
* use {useDefineForClassFields: true} in tsconfig.json
Signed-off-by: Roman <ixrock@gmail.com>
* remove ExtendedObservableMap
Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
* fix: removed makeObservable(this) from "terminal-tab.tsx"
Signed-off-by: Roman <ixrock@gmail.com>
* storage-helper refactoring
Signed-off-by: Roman <ixrock@gmail.com>
* normalize usages of #observable-value.toJSON() / attempt to catch the wind
Signed-off-by: Roman <ixrock@gmail.com>
* refactoring, more possible branch fixes + lint
Signed-off-by: Roman <ixrock@gmail.com>
* debugging cluster-view error -- part 1
Signed-off-by: Roman <ixrock@gmail.com>
* fix: refreshing cluster-view on ready
Signed-off-by: Roman <ixrock@gmail.com>
* fix: various app-crashes related to KubeObject.spec.* access from "undefined"
fix: config-map-details crash
Signed-off-by: Roman <ixrock@gmail.com>
* fix: namespace-store refactoring / saving selected-namespaces to external json-file
Signed-off-by: Roman <ixrock@gmail.com>
* fix: don't cache mobx.when(() => this.someObservable) cause might not work as expected due later call of makeObservable(this) in constructor
Signed-off-by: Roman <ixrock@gmail.com>
* fix: app-crash on editing k8s resource
Signed-off-by: Roman <ixrock@gmail.com>
* fix: restore "all namespaces" on page reload
Signed-off-by: Roman <ixrock@gmail.com>
* - fix: persist table-sort params and cluster-view's sidebar state to lens-local-storage
- new-feature: auto-open main-window's devtools in development-mode (yes/no/ugly?)
Signed-off-by: Roman <ixrock@gmail.com>
* fix: crd definition details -> crashing with <AceEditor mode="json"> (added missing mode-file in ace-editor.tsx)
Signed-off-by: Roman <ixrock@gmail.com>
* fix: crd definitions -> groups selector couldn't deselect last selected option
Signed-off-by: Roman <ixrock@gmail.com>
* refactoring: extensions-api exports clarification for "@k8slens/extensions"
Signed-off-by: Roman <ixrock@gmail.com>
* fix: various app-crashes related to kube-events (events page, some details page, overview, etc.)
Signed-off-by: Roman <ixrock@gmail.com>
* Reverted "use {useDefineForClassFields: true} in tsconfig.json" (various app-crash fixes)
This flag seems to be not possible to use with class-inheritance in some cases.
Example / demo:
`KubeObject` class has initial type definitions for the fields like: "metadata", "kind", etc.
and constructor() has Object.assign(this, data);
Meanwhile child class, e.g. KubeEvent inherited from KubeObject and has it's own extra type definitions for underlying resource, e.g. "involvedObject", "source", etc.
So calling super(data) doesn't work as expected for child class as it's own type definitions overwrites data from parent's constructor with `undefined` at later point.
Signed-off-by: Roman <ixrock@gmail.com>
* master-merge lint-fixes
Signed-off-by: Roman <ixrock@gmail.com>
* catalog.tsx / catalog-entities.store.ts refactoring & fixes
Signed-off-by: Roman <ixrock@gmail.com>
* fix: Catalog -> Browse all tab
Signed-off-by: Roman <ixrock@gmail.com>
* fix: CommandPalette doesn't appear from global menu by click/hotkey
Signed-off-by: Roman <ixrock@gmail.com>
* - Merging interfaces & classses to avoid overwriting fields from parent's super(data)-call with Object.assign(this, data). Otherwise use "declare" keyword at class field definition.
- Revamping {useDefineForClassFields: true} to avoid issues with non-observable class fields in some cases (from previous commit):
```
@observer
export class CommandContainer extends React.Component<CommandContainerProps> {
// without some defined initial value "commandComponent" is non-observable for some reasons
// when tsconfig.ts has {useDefineForClassFields:false}
@observable.ref commandComponent: React.ReactNode = null;
constructor(props: CommandContainerProps) {
super(props);
makeObservable(this);
}
```
Signed-off-by: Roman <ixrock@gmail.com>
* update KubeObject class type definition
Signed-off-by: Roman <ixrock@gmail.com>
* clean up / responding to comments
Signed-off-by: Roman <ixrock@gmail.com>
* fix: app-crash when navigating to catalog from active cluster-view, refactoring `catalog-entity-store`
Signed-off-by: Roman <ixrock@gmail.com>
* catalog-pusher clean up, replaced .observe_() to external observe() helper from "mobx"
Signed-off-by: Roman <ixrock@gmail.com>
* fix: catalog's items stale/non-observable (after connection to the cluster status still "disconnected"), lint-fixes
Signed-off-by: Roman <ixrock@gmail.com>
* fix: Catalog is empty after closing main-window and re-opening app from Tray
Signed-off-by: Roman <ixrock@gmail.com>
* fix: HotBar's icon context menu items non-observable (no "disconnect cluster", etc.)
Signed-off-by: Roman <ixrock@gmail.com>
* lint-fix/license check
Signed-off-by: Roman <ixrock@gmail.com>
* fix: redirect to catalog when disconnecting active cluster
Signed-off-by: Roman <ixrock@gmail.com>
* fix: refresh visibility of active cluster-view on switching from hotbar/catalog
Signed-off-by: Roman <ixrock@gmail.com>
* updated package.json for built-in extensions to use "*" version for packages served from main app
Signed-off-by: Roman <ixrock@gmail.com>
* - added missing makeObservable(this) to metrics-settings.tsx
- updated package-lock.json for built-in extensions
- lint fixes
Signed-off-by: Roman <ixrock@gmail.com>
* master-merge clean up fix, updated package-lock.json for built-in extensions after `make clean-extensions && make build-extensions`
Signed-off-by: Roman <ixrock@gmail.com>
* fix unit-tests
Signed-off-by: Roman <ixrock@gmail.com>
* master-merge fixes
Signed-off-by: Roman <ixrock@gmail.com>
* make lint happy
Signed-off-by: Roman <ixrock@gmail.com>
* reverted some changes, removed auto-opening devtools in dev-mode
Signed-off-by: Roman <ixrock@gmail.com>
* merge fixes
Signed-off-by: Roman <ixrock@gmail.com>
* master-merge conflict fixes:
- proper handling and navigating into catalog's active category via URL-builder
Signed-off-by: Roman <ixrock@gmail.com>
* reverting splitted params for catalog's page route to "/catalog/:group?/:kind?"
Signed-off-by: Roman <ixrock@gmail.com>
* clean-up: remove app's injecting dependencies from `extensions/kube-object-event-status/package.json`
Signed-off-by: Roman <ixrock@gmail.com>
* master-merge fix: added missing makeObservable(this) for extensions.tsx
Signed-off-by: Roman <ixrock@gmail.com>
* fix: catalog entity context menu stale/unobservable
Signed-off-by: Roman <ixrock@gmail.com>
Co-authored-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
279 lines
10 KiB
TypeScript
279 lines
10 KiB
TypeScript
/**
|
|
* Copyright (c) 2021 OpenLens Authors
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
* this software and associated documentation files (the "Software"), to deal in
|
|
* the Software without restriction, including without limitation the rights to
|
|
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
* subject to the following conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be included in all
|
|
* copies or substantial portions of the Software.
|
|
*
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
*/
|
|
|
|
import { action, observable, IComputedValue, computed, ObservableMap, runInAction, makeObservable, observe } from "mobx";
|
|
import type { CatalogEntity } from "../../common/catalog";
|
|
import { catalogEntityRegistry } from "../../main/catalog";
|
|
import { watch } from "chokidar";
|
|
import fs from "fs";
|
|
import fse from "fs-extra";
|
|
import type stream from "stream";
|
|
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
|
|
import logger from "../logger";
|
|
import type { KubeConfig } from "@kubernetes/client-node";
|
|
import { loadConfigFromString, splitConfig, validateKubeConfig } from "../../common/kube-helpers";
|
|
import { Cluster } from "../cluster";
|
|
import { catalogEntityFromCluster } from "../cluster-manager";
|
|
import { UserStore } from "../../common/user-store";
|
|
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
|
|
import { createHash } from "crypto";
|
|
|
|
const logPrefix = "[KUBECONFIG-SYNC]:";
|
|
|
|
export class KubeconfigSyncManager extends Singleton {
|
|
protected sources = observable.map<string, [IComputedValue<CatalogEntity[]>, Disposer]>();
|
|
protected syncing = false;
|
|
protected syncListDisposer?: Disposer;
|
|
|
|
protected static readonly syncName = "lens:kube-sync";
|
|
|
|
constructor() {
|
|
super();
|
|
|
|
makeObservable(this);
|
|
}
|
|
|
|
@action
|
|
startSync(): void {
|
|
if (this.syncing) {
|
|
return;
|
|
}
|
|
|
|
this.syncing = true;
|
|
|
|
logger.info(`${logPrefix} starting requested syncs`);
|
|
|
|
catalogEntityRegistry.addComputedSource(KubeconfigSyncManager.syncName, computed(() => (
|
|
Array.from(iter.flatMap(
|
|
this.sources.values(),
|
|
([entities]) => entities.get()
|
|
))
|
|
)));
|
|
|
|
// This must be done so that c&p-ed clusters are visible
|
|
this.startNewSync(ClusterStore.storedKubeConfigFolder);
|
|
|
|
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
|
|
this.startNewSync(filePath);
|
|
}
|
|
|
|
this.syncListDisposer = observe(UserStore.getInstance().syncKubeconfigEntries, change => {
|
|
switch (change.type) {
|
|
case "add":
|
|
this.startNewSync(change.name);
|
|
break;
|
|
case "delete":
|
|
this.stopOldSync(change.name);
|
|
break;
|
|
}
|
|
});
|
|
}
|
|
|
|
@action
|
|
stopSync() {
|
|
this.syncListDisposer?.();
|
|
|
|
for (const filePath of this.sources.keys()) {
|
|
this.stopOldSync(filePath);
|
|
}
|
|
|
|
catalogEntityRegistry.removeSource(KubeconfigSyncManager.syncName);
|
|
this.syncing = false;
|
|
}
|
|
|
|
@action
|
|
protected async startNewSync(filePath: string): Promise<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 });
|
|
}
|
|
|
|
try {
|
|
this.sources.set(filePath, await watchFileChanges(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()) });
|
|
} catch (error) {
|
|
logger.warn(`${logPrefix} failed to start watching changes: ${error}`);
|
|
}
|
|
}
|
|
|
|
@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 });
|
|
}
|
|
|
|
logger.info(`${logPrefix} stopping sync of file/folder`, { filePath });
|
|
logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
|
|
}
|
|
}
|
|
|
|
// exported for testing
|
|
export function configToModels(config: KubeConfig, filePath: string): UpdateClusterModel[] {
|
|
const validConfigs = [];
|
|
|
|
for (const contextConfig of splitConfig(config)) {
|
|
const error = validateKubeConfig(contextConfig, contextConfig.currentContext);
|
|
|
|
if (error) {
|
|
logger.debug(`${logPrefix} context failed validation: ${error}`, { context: contextConfig.currentContext, filePath });
|
|
} else {
|
|
validConfigs.push({
|
|
kubeConfigPath: filePath,
|
|
contextName: contextConfig.currentContext,
|
|
});
|
|
}
|
|
}
|
|
|
|
return validConfigs;
|
|
}
|
|
|
|
type RootSourceValue = [Cluster, CatalogEntity];
|
|
type RootSource = ObservableMap<string, RootSourceValue>;
|
|
|
|
// exported for testing
|
|
export function computeDiff(contents: string, source: RootSource, filePath: string): void {
|
|
runInAction(() => {
|
|
try {
|
|
const rawModels = configToModels(loadConfigFromString(contents), filePath);
|
|
const models = new Map(rawModels.map(m => [m.contextName, m]));
|
|
|
|
logger.debug(`${logPrefix} File now has ${models.size} entries`, { filePath });
|
|
|
|
for (const [contextName, value] of source) {
|
|
const model = models.get(contextName);
|
|
|
|
// remove and disconnect clusters that were removed from the config
|
|
if (!model) {
|
|
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(model);
|
|
models.delete(contextName);
|
|
logger.debug(`${logPrefix} Updated old cluster from sync`, { filePath, contextName });
|
|
}
|
|
|
|
for (const [contextName, model] of models) {
|
|
// add new clusters to the source
|
|
try {
|
|
const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
|
|
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId});
|
|
|
|
if (!cluster.apiUrl) {
|
|
throw new Error("Cluster constructor failed, see above error");
|
|
}
|
|
|
|
const entity = catalogEntityFromCluster(cluster);
|
|
|
|
entity.metadata.labels.file = filePath;
|
|
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
|
|
}
|
|
});
|
|
}
|
|
|
|
function diffChangedConfig(filePath: string, source: RootSource): Disposer {
|
|
logger.debug(`${logPrefix} file changed`, { filePath });
|
|
|
|
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
|
|
const fileReader = fs.createReadStream(filePath, {
|
|
mode: fs.constants.O_RDONLY,
|
|
});
|
|
const readStream: stream.Readable = fileReader;
|
|
const bufs: Buffer[] = [];
|
|
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 => bufs.push(chunk))
|
|
.on("close", () => cleanup())
|
|
.on("error", error => {
|
|
cleanup();
|
|
logger.warn(`${logPrefix} failed to read file: ${error}`, { filePath });
|
|
})
|
|
.on("end", () => {
|
|
if (!closed) {
|
|
computeDiff(Buffer.concat(bufs).toString("utf-8"), source, filePath);
|
|
}
|
|
});
|
|
|
|
return cleanup;
|
|
}
|
|
|
|
async function watchFileChanges(filePath: string): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
|
|
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
|
|
const watcher = watch(filePath, {
|
|
followSymlinks: true,
|
|
depth: stat.isDirectory() ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095)
|
|
disableGlobbing: true,
|
|
});
|
|
const rootSource = new ExtendedObservableMap<string, ObservableMap<string, RootSourceValue>>(observable.map);
|
|
const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
|
|
const stoppers = new Map<string, Disposer>();
|
|
|
|
watcher
|
|
.on("change", (childFilePath) => {
|
|
stoppers.get(childFilePath)();
|
|
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath)));
|
|
})
|
|
.on("add", (childFilePath) => {
|
|
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath)));
|
|
})
|
|
.on("unlink", (childFilePath) => {
|
|
stoppers.get(childFilePath)();
|
|
stoppers.delete(childFilePath);
|
|
rootSource.delete(childFilePath);
|
|
})
|
|
.on("error", error => logger.error(`${logPrefix} watching file/folder failed: ${error}`, { filePath }));
|
|
|
|
return [derivedSource, () => watcher.close()];
|
|
}
|