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

Starting attempt to move kubeconfig sync tests to application builder

- Blocked on catalog sync not being injectable yet

Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
Sebastian Malton 2023-01-18 11:38:45 -05:00
parent d2ff35551c
commit 6f8de6ab80
15 changed files with 1141 additions and 44 deletions

View File

@ -2,7 +2,7 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getGlobalOverride } from "../../test-utils/get-global-override"; import { getGlobalOverride } from "../test-utils/get-global-override";
import watchInjectable from "./watch.injectable"; import watchInjectable from "./watch.injectable";
export default getGlobalOverride(watchInjectable, () => () => { export default getGlobalOverride(watchInjectable, () => () => {

View File

@ -6,7 +6,7 @@ import { getInjectable } from "@ogre-tools/injectable";
import { watch } from "chokidar"; import { watch } from "chokidar";
import type { Stats } from "fs"; import type { Stats } from "fs";
import type TypedEventEmitter from "typed-emitter"; import type TypedEventEmitter from "typed-emitter";
import type { SingleOrMany } from "../../utils"; import type { SingleOrMany } from "../utils";
export interface AlwaysStatWatcherEvents { export interface AlwaysStatWatcherEvents {
add: (path: string, stats: Stats) => void; add: (path: string, stats: Stats) => void;

View File

@ -13,7 +13,7 @@ import extensionPackageRootDirectoryInjectable from "../extension-installer/exte
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import loggerInjectable from "../../common/logger.injectable"; import loggerInjectable from "../../common/logger.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import watchInjectable from "../../common/fs/watch/watch.injectable"; import watchInjectable from "../../common/fs/watch.injectable";
import accessPathInjectable from "../../common/fs/access-path.injectable"; import accessPathInjectable from "../../common/fs/access-path.injectable";
import copyInjectable from "../../common/fs/copy.injectable"; import copyInjectable from "../../common/fs/copy.injectable";
import ensureDirInjectable from "../../common/fs/ensure-dir.injectable"; import ensureDirInjectable from "../../common/fs/ensure-dir.injectable";

View File

@ -13,7 +13,7 @@ import { delay } from "../../renderer/utils";
import { observable, runInAction, when } from "mobx"; import { observable, runInAction, when } from "mobx";
import readJsonFileInjectable from "../../common/fs/read-json-file.injectable"; import readJsonFileInjectable from "../../common/fs/read-json-file.injectable";
import pathExistsInjectable from "../../common/fs/path-exists.injectable"; import pathExistsInjectable from "../../common/fs/path-exists.injectable";
import watchInjectable from "../../common/fs/watch/watch.injectable"; import watchInjectable from "../../common/fs/watch.injectable";
import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable"; import extensionApiVersionInjectable from "../../common/vars/extension-api-version.injectable";
import removePathInjectable from "../../common/fs/remove.injectable"; import removePathInjectable from "../../common/fs/remove.injectable";
import type { JoinPaths } from "../../common/path/join-paths.injectable"; import type { JoinPaths } from "../../common/path/join-paths.injectable";

View File

@ -17,7 +17,7 @@ import { requestInitialExtensionDiscovery } from "../../renderer/ipc";
import type { ReadJson } from "../../common/fs/read-json-file.injectable"; import type { ReadJson } from "../../common/fs/read-json-file.injectable";
import type { Logger } from "../../common/logger"; import type { Logger } from "../../common/logger";
import type { PathExists } from "../../common/fs/path-exists.injectable"; import type { PathExists } from "../../common/fs/path-exists.injectable";
import type { Watch } from "../../common/fs/watch/watch.injectable"; import type { Watch } from "../../common/fs/watch.injectable";
import type { Stats } from "fs"; import type { Stats } from "fs";
import type { LStat } from "../../common/fs/lstat.injectable"; import type { LStat } from "../../common/fs/lstat.injectable";
import type { ReadDirectory } from "../../common/fs/read-directory.injectable"; import type { ReadDirectory } from "../../common/fs/read-directory.injectable";

View File

@ -0,0 +1,882 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`kubeconfig sync showing reactive catalog renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="home-button"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="flex justify-center Welcome align-center"
data-testid="welcome-page"
>
<div
data-testid="welcome-banner-container"
style="width: 320px;"
>
<i
class="Icon logo svg focusable"
>
<span
class="icon"
/>
</i>
<div
class="flex justify-center"
>
<div
data-testid="welcome-text-container"
style="width: 320px;"
>
<h2>
Welcome to some-product-name!
</h2>
<p>
To get you started we have auto-detected your clusters in your
kubeconfig file and added them to the catalog, your centralized
view for managing all your cloud-native resources.
<br />
<br />
If you have any questions or feedback, please join our
<a
class="link"
href="https://k8slens.dev/slack.html"
rel="noreferrer"
target="_blank"
>
Lens Community slack channel
</a>
.
</p>
<ul
class="block"
data-testid="welcome-menu-container"
style="width: 320px;"
>
<li
class="flex grid-12"
>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="view_list"
>
view_list
</span>
</i>
<a
class="box col-10"
>
Browse Clusters in Catalog
</a>
<i
class="Icon box col-1 material focusable"
>
<span
class="icon"
data-icon-name="navigate_next"
>
navigate_next
</span>
</i>
</li>
</ul>
</div>
</div>
</div>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;
exports[`kubeconfig sync showing reactive catalog when navigating to the catalog renders 1`] = `
<body>
<div>
<div
class="ClusterManager"
>
<div
class="topBar"
>
<div
class="items"
>
<div
class="preventedDragging"
>
<i
class="Icon material interactive focusable"
data-testid="home-button"
tabindex="0"
>
<span
class="icon"
data-icon-name="home"
>
home
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-back"
>
<span
class="icon"
data-icon-name="arrow_back"
>
arrow_back
</span>
</i>
</div>
<div
class="size-sm"
/>
<div
class="preventedDragging"
>
<i
class="Icon material interactive disabled focusable"
data-testid="history-forward"
>
<span
class="icon"
data-icon-name="arrow_forward"
>
arrow_forward
</span>
</i>
</div>
<div
class="separator"
/>
</div>
</div>
<main>
<div
id="lens-views"
/>
<div
class="mainLayout"
style="--sidebar-width: 200px;"
>
<div
class="sidebar"
>
<div
class="flex flex-col w-full"
>
<div
class="catalog"
>
Catalog
</div>
<ul
aria-multiselectable="false"
class="MuiTreeView-root"
role="tree"
>
<li
aria-selected="true"
class="MuiTreeItem-root Mui-selected"
data-testid="*-tab"
role="treeitem"
tabindex="0"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
/>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
Browse
</div>
</div>
</li>
<li
aria-expanded="true"
class="MuiTreeItem-root bordered Mui-expanded"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable"
>
<span
class="icon"
data-icon-name="expand_more"
>
expand_more
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="parent"
>
Categories
</div>
</div>
</div>
<ul
class="MuiCollapse-root MuiTreeItem-group MuiCollapse-entered"
role="group"
style="min-height: 0px;"
>
<div
class="MuiCollapse-wrapper"
>
<div
class="MuiCollapse-wrapperInner"
>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/General-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="settings"
>
settings
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
General
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/KubernetesCluster-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon focusable small"
>
<span
class="icon"
data-icon-name=""
/>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Clusters
</div>
</div>
</div>
</div>
</li>
<li
class="MuiTreeItem-root"
data-testid="entity.k8slens.dev/WebLink-tab"
role="treeitem"
tabindex="-1"
>
<div
class="MuiTreeItem-content"
>
<div
class="MuiTreeItem-iconContainer"
>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="public"
>
public
</span>
</i>
</div>
<div
class="MuiTypography-root MuiTreeItem-label MuiTypography-body1"
>
<div
class="flex"
>
<div>
Web Links
</div>
</div>
</div>
</div>
</li>
</div>
</div>
</ul>
</li>
</ul>
</div>
<div
class="ResizingAnchor horizontal trailing"
/>
</div>
<div
class="contents"
>
<div
class="views"
>
<div
class="ItemListLayout flex column Catalog"
data-testid="catalog-list-for-browse-all"
>
<div
class="header flex gaps align-center"
>
<h5
class="title"
>
Browse All
</h5>
<div
class="info-panel box grow"
>
0 items
</div>
<div
class="Input SearchInput focused"
>
<label
class="input-area flex gaps align-center"
id=""
>
<input
class="input box grow"
placeholder="Search..."
spellcheck="false"
value=""
/>
<i
class="Icon material focusable small"
>
<span
class="icon"
data-icon-name="search"
>
search
</span>
</i>
</label>
<div
class="input-info flex gaps"
/>
</div>
</div>
<div
class="items box grow flex column"
>
<div
class="Table flex column Catalog box grow dark selectable scrollable sortable autoSize virtual"
>
<div
class="TableHead sticky nowrap topLine"
>
<div
class="TableCell entityName nowrap sorting"
id="name"
>
<div
class="content"
>
Name
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell nowrap sorting"
id="kind"
>
<div
class="content"
>
Kind
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell sourceCell nowrap sorting"
id="source"
>
<div
class="content"
>
Source
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell labelsCell scrollable nowrap"
id="labels"
>
<div
class="content"
>
Labels
</div>
</div>
<div
class="TableCell statusCell nowrap sorting"
id="status"
>
<div
class="content"
>
Status
</div>
<i
class="Icon sortIcon material focusable"
>
<span
class="icon"
data-icon-name="arrow_drop_down"
>
arrow_drop_down
</span>
</i>
</div>
<div
class="TableCell menu nowrap"
>
<div
class="content"
>
<i
class="Icon material interactive focusable"
id="menu-actions-for-item-object-list-content"
tabindex="0"
>
<span
class="icon"
data-icon-name="more_vert"
>
more_vert
</span>
</i>
</div>
</div>
</div>
<div
class="NoItems flex box grow"
>
<div
class="box center"
>
Item list is empty
</div>
</div>
</div>
<div
class="AddRemoveButtons flex gaps"
/>
</div>
</div>
</div>
</div>
<div
class="footer"
/>
</div>
</main>
<div
class="HotbarMenu flex column"
>
<div
class="HotbarItems flex column gaps"
>
<div
class="HotbarCell isDraggingOwner animateDown"
index="0"
>
<div
style="z-index: 12; position: absolute;"
>
<div
class="HotbarIcon contextMenuAvailable"
>
<div
class="Avatar rounded disabled avatar"
id="hotbarIcon-hotbar-icon-catalog-entity"
style="width: 40px; height: 40px; background: rgb(5, 1, 130);"
>
Ca
</div>
</div>
</div>
</div>
<div
class="HotbarCell isDraggingOwner animateDown"
index="1"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="2"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="3"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="4"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="5"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="6"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="7"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="8"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="9"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="10"
/>
<div
class="HotbarCell isDraggingOwner animateDown"
index="11"
/>
</div>
<div
class="HotbarSelector"
>
<i
class="Icon Icon previous material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_left"
>
arrow_left
</span>
</i>
<div
class="HotbarIndex"
>
<div
class="badge Badge small clickable"
id="hotbarIndex"
>
1
</div>
</div>
<i
class="Icon material interactive focusable"
tabindex="0"
>
<span
class="icon"
data-icon-name="arrow_right"
>
arrow_right
</span>
</i>
</div>
</div>
<div
class="StatusBar"
data-testid="status-bar"
>
<div
class="leftSide"
data-testid="status-bar-left"
/>
<div
class="rightSide"
data-testid="status-bar-right"
/>
</div>
</div>
<div
class="Notifications flex column align-flex-end"
/>
</div>
</body>
`;

View File

@ -0,0 +1,80 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { DiContainer } from "@ogre-tools/injectable";
import type { RenderResult } from "@testing-library/react";
import navigateToCatalogInjectable from "../../common/front-end-routing/routes/catalog/navigate-to-catalog.injectable";
import writeFileInjectable from "../../common/fs/write-file.injectable";
import { dumpConfigYaml } from "../../common/kube-helpers";
import homeDirectoryPathInjectable from "../../common/os/home-directory-path.injectable";
import joinPathsInjectable from "../../common/path/join-paths.injectable";
import { flushPromises } from "../../common/test-utils/flush-promises";
import type { ApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
import { getApplicationBuilder } from "../../renderer/components/test-utils/get-application-builder";
describe("kubeconfig sync showing reactive catalog", () => {
let builder: ApplicationBuilder;
let rendered: RenderResult;
let windowDi: DiContainer;
let mainDi: DiContainer;
beforeEach(async () => {
builder = getApplicationBuilder();
// builder.mainDi.override(loggerInjectable, () => console as any);
rendered = await builder.render();
windowDi = builder.applicationWindow.only.di;
mainDi = builder.mainDi;
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when navigating to the catalog", () => {
beforeEach(() => {
const navigateToCatalog = windowDi.inject(navigateToCatalogInjectable);
navigateToCatalog();
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
describe("when a config file is written under ~/.kube", () => {
beforeEach(async () => {
const writeFile = mainDi.inject(writeFileInjectable);
const joinPaths = mainDi.inject(joinPathsInjectable);
const homeDirectoryPath = mainDi.inject(homeDirectoryPathInjectable);
const configContents = dumpConfigYaml({
clusters: [{
name: "some-cluster-name",
server: "https://1.2.3.4",
skipTLSVerify: false,
}],
users: [{
name: "some-user-name",
}],
contexts: [{
cluster: "some-cluster-name",
name: "some-context-name",
user: "some-user-name",
}],
});
await writeFile(joinPaths(homeDirectoryPath, ".kube", "config"), configContents);
await flushPromises();
});
it.only("eventually shows the cluster as a new entity", async () => {
await rendered.findByTestId("catalog-entity-row-for-some-cluster-name", undefined, {
timeout: 10_000,
});
}, 100_000);
});
});
});

View File

@ -25,8 +25,8 @@ import type { AsyncFnMock } from "@async-fn/jest";
import type { Stat } from "../../../common/fs/stat.injectable"; import type { Stat } from "../../../common/fs/stat.injectable";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
import statInjectable from "../../../common/fs/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable"; import type { Watcher } from "../../../common/fs/watch.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable"; import watchInjectable from "../../../common/fs/watch.injectable";
import EventEmitter from "events"; import EventEmitter from "events";
import type { ReadStream, Stats } from "fs"; import type { ReadStream, Stats } from "fs";
import createReadFileStreamInjectable from "../../../common/fs/create-read-file-stream.injectable"; import createReadFileStreamInjectable from "../../../common/fs/create-read-file-stream.injectable";

View File

@ -86,7 +86,7 @@ const computeKubeconfigDiffInjectable = getInjectable({
logger.debug(`Added new cluster from sync`, { filePath, contextName }); logger.debug(`Added new cluster from sync`, { filePath, contextName });
} catch (error) { } catch (error) {
logger.warn(`Failed to create cluster from model: ${error}`, { filePath, contextName }); logger.warn(`Failed to create cluster with context="${contextName}" from path="${filePath}"`, error);
} }
} }
} catch (error) { } catch (error) {

View File

@ -0,0 +1,66 @@
/**
* 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 watchInjectable from "../../../common/fs/watch.injectable";
import loggerInjectable from "../../../common/logger.injectable";
export interface CreateKubeSyncWatcherOptions {
isDirectorySync: boolean;
onChange: (filePath: string, stats: Stats) => void;
onAdd: (filePath: string, stats: Stats) => void;
onRemove: (filePath: string) => void;
onError: (error: Error) => void;
}
export interface KubeSyncWatcher {
stop: () => void;
}
export type CreateKubeSyncWatcher = (filePath: string, opts: CreateKubeSyncWatcherOptions) => KubeSyncWatcher;
const createKubeSyncWatcherInjectable = getInjectable({
id: "create-kube-sync-watcher",
instantiate: (di): CreateKubeSyncWatcher => {
const watch = di.inject(watchInjectable);
const logger = di.inject(loggerInjectable);
return (filePath, { isDirectorySync, ...handlers }) => {
const watcher = watch<true>(filePath, {
followSymlinks: true,
depth: isDirectorySync ? 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", handlers.onChange)
.on("add", handlers.onAdd)
.on("unlink", handlers.onRemove)
.on("error", handlers.onError);
return {
stop: () => {
void (async () => {
try {
await watcher.close();
} catch (error) {
logger.warn(`[KUBE-SYNC-WATCHER]: failed to stop watching "${filePath}": ${error}`);
}
})();
},
};
};
},
});
export default createKubeSyncWatcherInjectable;

View File

@ -11,8 +11,8 @@ import { inspect } from "util";
import type { CatalogEntity } from "../../../common/catalog"; import type { CatalogEntity } from "../../../common/catalog";
import type { Cluster } from "../../../common/cluster/cluster"; import type { Cluster } from "../../../common/cluster/cluster";
import statInjectable from "../../../common/fs/stat.injectable"; import statInjectable from "../../../common/fs/stat.injectable";
import type { Watcher } from "../../../common/fs/watch/watch.injectable"; import type { KubeSyncWatcher } from "./create-watcher.injectable";
import watchInjectable from "../../../common/fs/watch/watch.injectable"; import createKubeSyncWatcherInjectable from "./create-watcher.injectable";
import type { Disposer } from "../../../common/utils"; import type { Disposer } from "../../../common/utils";
import { getOrInsertWith, iter } from "../../../common/utils"; import { getOrInsertWith, iter } from "../../../common/utils";
import diffChangedKubeconfigInjectable from "./diff-changed-kubeconfig.injectable"; import diffChangedKubeconfigInjectable from "./diff-changed-kubeconfig.injectable";
@ -38,8 +38,8 @@ const ignoreGlobs = [
* Even if you have a cert-file, key-file, and client-cert files that is only * 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. * 12kb of extra data (at 4096 bytes each) which allows for around 150 entries.
*/ */
const folderSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB const dirSyncMaxAllowedFileReadSize = 2 * 1024 * 1024; // 2 MiB
const fileSyncMaxAllowedFileReadSize = 16 * folderSyncMaxAllowedFileReadSize; // 32 MiB const fileSyncMaxAllowedFileReadSize = 16 * dirSyncMaxAllowedFileReadSize; // 32 MiB
const watchKubeconfigFileChangesInjectable = getInjectable({ const watchKubeconfigFileChangesInjectable = getInjectable({
id: "watch-kubeconfig-file-changes", id: "watch-kubeconfig-file-changes",
@ -47,39 +47,27 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
const diffChangedKubeconfig = di.inject(diffChangedKubeconfigInjectable); const diffChangedKubeconfig = di.inject(diffChangedKubeconfigInjectable);
const logger = di.inject(kubeconfigSyncLoggerInjectable); const logger = di.inject(kubeconfigSyncLoggerInjectable);
const stat = di.inject(statInjectable); const stat = di.inject(statInjectable);
const watch = di.inject(watchInjectable); const createKubeSyncWatcher = di.inject(createKubeSyncWatcherInjectable);
return (filePath) => { return (filePath) => {
const rootSource = observable.map<string, ObservableMap<string, [Cluster, CatalogEntity]>>(); 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])))); const derivedSource = computed(() => Array.from(iter.flatMap(rootSource.values(), from => iter.map(from.values(), child => child[1]))));
let watcher: Watcher<true>; let watcher: KubeSyncWatcher;
(async () => { (async () => {
try { try {
const stats = await stat(filePath); const stats = await stat(filePath);
const isFolderSync = stats.isDirectory(); const isDirectorySync = stats.isDirectory();
const cleanupFns = new Map<string, Disposer>(); const cleanupFns = new Map<string, Disposer>();
const maxAllowedFileReadSize = isFolderSync const maxAllowedFileReadSize = isDirectorySync
? folderSyncMaxAllowedFileReadSize ? dirSyncMaxAllowedFileReadSize
: fileSyncMaxAllowedFileReadSize; : fileSyncMaxAllowedFileReadSize;
watcher = watch<true>(filePath, { watcher = createKubeSyncWatcher(filePath, {
followSymlinks: true, isDirectorySync,
depth: isFolderSync ? 0 : 1, // DIRs works with 0 but files need 1 (bug: https://github.com/paulmillr/chokidar/issues/1095) onChange: (childFilePath, stats): void => {
disableGlobbing: true, console.log("change", childFilePath);
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); const cleanup = cleanupFns.get(childFilePath);
if (!cleanup) { if (!cleanup) {
@ -94,9 +82,11 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
stats, stats,
maxAllowedFileReadSize, maxAllowedFileReadSize,
})); }));
}) },
.on("add", (childFilePath, stats): void => { onAdd: (childFilePath, stats): void => {
if (isFolderSync) { console.log("add", childFilePath);
if (isDirectorySync) {
const fileName = path.basename(childFilePath); const fileName = path.basename(childFilePath);
for (const ignoreGlob of ignoreGlobs) { for (const ignoreGlob of ignoreGlobs) {
@ -112,20 +102,24 @@ const watchKubeconfigFileChangesInjectable = getInjectable({
stats, stats,
maxAllowedFileReadSize, maxAllowedFileReadSize,
})); }));
}) },
.on("unlink", (childFilePath) => { onRemove: (childFilePath) => {
cleanupFns.get(childFilePath)?.(); cleanupFns.get(childFilePath)?.();
cleanupFns.delete(childFilePath); cleanupFns.delete(childFilePath);
rootSource.delete(childFilePath); rootSource.delete(childFilePath);
}) },
.on("error", error => logger.error(`watching file/folder failed: ${error}`, { filePath })); onError: (error) => {
console.log("error", error);
logger.error(`watching file/folder failed: ${error}`, { filePath });
},
});
} catch (error) { } catch (error) {
logger.warn(`failed to start watching changes: ${error}`); logger.warn(`failed to start watching changes: ${error}`);
} }
})(); })();
return [derivedSource, () => { return [derivedSource, () => {
watcher?.close(); watcher?.stop();
}]; }];
}; };
}, },

View File

@ -307,6 +307,7 @@ class NonInjectedCatalog extends React.Component<Dependencies> {
getItems={() => catalogEntityStore.entities.get()} getItems={() => catalogEntityStore.entities.get()}
customizeTableRowProps={entity => ({ customizeTableRowProps={entity => ({
disabled: !entity.isEnabled(), disabled: !entity.isEnabled(),
testId: `catalog-entity-row-for-${entity.getId()}`,
})} })}
{...getCategoryColumns({ activeCategory })} {...getCategoryColumns({ activeCategory })}
onDetails={this.onDetails} onDetails={this.onDetails}

View File

@ -9,7 +9,7 @@ import { readFile } from "fs/promises";
import { hasCorrectExtension } from "./has-correct-extension"; import { hasCorrectExtension } from "./has-correct-extension";
import type { RawTemplates } from "./create-resource-templates.injectable"; import type { RawTemplates } from "./create-resource-templates.injectable";
import joinPathsInjectable from "../../../../common/path/join-paths.injectable"; import joinPathsInjectable from "../../../../common/path/join-paths.injectable";
import watchInjectable from "../../../../common/fs/watch/watch.injectable"; import watchInjectable from "../../../../common/fs/watch.injectable";
import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable"; import getRelativePathInjectable from "../../../../common/path/get-relative-path.injectable";
import homeDirectoryPathInjectable from "../../../../common/os/home-directory-path.injectable"; import homeDirectoryPathInjectable from "../../../../common/os/home-directory-path.injectable";
import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable"; import getDirnameOfPathInjectable from "../../../../common/path/get-dirname.injectable";

View File

@ -192,7 +192,7 @@ export const getApplicationBuilder = ({ useFakeTime = true }: ApplicationBuilder
const overrideFsWithFakes = getOverrideFsWithFakes(); const overrideFsWithFakes = getOverrideFsWithFakes();
overrideFsWithFakes(mainDi); overrideFsWithFakes(mainDi, true);
// Set up ~/.kube as existing as a folder // Set up ~/.kube as existing as a folder
{ {

View File

@ -10,6 +10,9 @@ import type {
readJsonSync as readJsonSyncImpl, readJsonSync as readJsonSyncImpl,
writeJsonSync as writeJsonSyncImpl, writeJsonSync as writeJsonSyncImpl,
} from "fs-extra"; } from "fs-extra";
import createKubeSyncWatcherInjectable from "../main/catalog-sources/kubeconfig-sync/create-watcher.injectable";
import { isErrnoException } from "../common/utils";
import joinPathsInjectable from "../common/path/join-paths.injectable";
export const getOverrideFsWithFakes = () => { export const getOverrideFsWithFakes = () => {
const root = createFsFromVolume(Volume.fromJSON({})); const root = createFsFromVolume(Volume.fromJSON({}));
@ -41,7 +44,7 @@ export const getOverrideFsWithFakes = () => {
root.mkdirpSync(path, mode); root.mkdirpSync(path, mode);
}) as typeof ensureDirSyncImpl; }) as typeof ensureDirSyncImpl;
return (di: DiContainer) => { return (di: DiContainer, overrideWatches = false) => {
di.override(fsInjectable, () => ({ di.override(fsInjectable, () => ({
pathExists: async (path) => root.existsSync(path), pathExists: async (path) => root.existsSync(path),
pathExistsSync: root.existsSync, pathExistsSync: root.existsSync,
@ -63,5 +66,76 @@ export const getOverrideFsWithFakes = () => {
createReadStream: root.createReadStream as any, createReadStream: root.createReadStream as any,
stat: root.promises.stat as any, stat: root.promises.stat as any,
})); }));
if (overrideWatches) {
di.override(createKubeSyncWatcherInjectable, (di) => {
const joinPaths = di.inject(joinPathsInjectable);
return ((path, options) => {
const watcher = root.watch( path, {
recursive: options.isDirectorySync,
});
const seenPaths = new Set<string>();
console.log("watching", path);
watcher.addListener("rename", (eventType, filename: string) => {
try {
const stats = root.statSync(filename);
options.onAdd(filename, stats);
} catch (error) {
if (isErrnoException(error) && error.code === "ENOENT") {
options.onRemove(filename);
} else {
options.onError(error as Error);
}
}
});
watcher.addListener("change", (...args) => {
const [,filename] = args;
if (options.isDirectorySync) {
// For testing purposes just emit change events for all files
for (const entry of root.readdirSync(filename) as string[]) {
const path = joinPaths(filename, entry);
try {
const stats = root.statSync(path);
if (seenPaths.has(path)) {
options.onChange(path, stats);
} else {
seenPaths.add(path);
options.onAdd(path, stats);
}
} catch (error) {
options.onError(error as Error);
}
}
} else {
try {
const stats = root.statSync(filename);
if (seenPaths.has(filename)) {
options.onChange(filename, stats);
} else {
seenPaths.add(filename);
options.onAdd(filename, stats);
}
} catch (error) {
options.onError(error as Error);
}
}
});
return {
stop: () => {
watcher.close();
},
};
});
});
}
}; };
}; };