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

fix kubeconfig-sync issues

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-05-03 13:39:24 +03:00
parent d08aeaa2e4
commit 7494121a86
5 changed files with 39 additions and 57 deletions

View File

@ -14,8 +14,8 @@ export class CatalogPusher {
init() { init() {
const disposers: Disposer[] = []; const disposers: Disposer[] = [];
disposers.push(reaction(() => this.catalog.items, (items) => { disposers.push(reaction(() => toJS(this.catalog.items, { recurseEverything: true }), (items) => {
broadcastMessage("catalog:items", toJS(items, { recurseEverything: true })); broadcastMessage("catalog:items", items);
}, { }, {
fireImmediately: true, fireImmediately: true,
})); }));

View File

@ -5,10 +5,12 @@ import { Cluster } from "../../cluster";
import { computeDiff, configToModels } from "../kubeconfig-sync"; import { computeDiff, configToModels } from "../kubeconfig-sync";
import mockFs from "mock-fs"; import mockFs from "mock-fs";
import fs from "fs"; import fs from "fs";
import { ClusterStore } from "../../../common/cluster-store";
describe("kubeconfig-sync.source tests", () => { describe("kubeconfig-sync.source tests", () => {
beforeEach(() => { beforeEach(() => {
mockFs(); mockFs();
ClusterStore.createInstance();
}); });
afterEach(() => { afterEach(() => {
@ -57,10 +59,9 @@ describe("kubeconfig-sync.source tests", () => {
it("should leave an empty source empty if there are no entries", () => { it("should leave an empty source empty if there are no entries", () => {
const contents = ""; const contents = "";
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const port = 0;
const filePath = "/bar"; const filePath = "/bar";
computeDiff(contents, rootSource, port, filePath); computeDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(0); expect(rootSource.size).toBe(0);
}); });
@ -93,12 +94,11 @@ describe("kubeconfig-sync.source tests", () => {
currentContext: "foobar" currentContext: "foobar"
}); });
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const port = 0;
const filePath = "/bar"; const filePath = "/bar";
fs.writeFileSync(filePath, contents); fs.writeFileSync(filePath, contents);
computeDiff(contents, rootSource, port, filePath); computeDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(1); expect(rootSource.size).toBe(1);
@ -137,12 +137,11 @@ describe("kubeconfig-sync.source tests", () => {
currentContext: "foobar" currentContext: "foobar"
}); });
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const port = 0;
const filePath = "/bar"; const filePath = "/bar";
fs.writeFileSync(filePath, contents); fs.writeFileSync(filePath, contents);
computeDiff(contents, rootSource, port, filePath); computeDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(1); expect(rootSource.size).toBe(1);
@ -151,7 +150,7 @@ describe("kubeconfig-sync.source tests", () => {
expect(c.kubeConfigPath).toBe("/bar"); expect(c.kubeConfigPath).toBe("/bar");
expect(c.contextName).toBe("context-name"); expect(c.contextName).toBe("context-name");
computeDiff("{}", rootSource, port, filePath); computeDiff("{}", rootSource, filePath);
expect(rootSource.size).toBe(0); expect(rootSource.size).toBe(0);
}); });
@ -192,12 +191,11 @@ describe("kubeconfig-sync.source tests", () => {
currentContext: "foobar" currentContext: "foobar"
}); });
const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>(); const rootSource = new ObservableMap<string, [Cluster, CatalogEntity]>();
const port = 0;
const filePath = "/bar"; const filePath = "/bar";
fs.writeFileSync(filePath, contents); fs.writeFileSync(filePath, contents);
computeDiff(contents, rootSource, port, filePath); computeDiff(contents, rootSource, filePath);
expect(rootSource.size).toBe(2); expect(rootSource.size).toBe(2);
@ -237,7 +235,7 @@ describe("kubeconfig-sync.source tests", () => {
currentContext: "foobar" currentContext: "foobar"
}); });
computeDiff(newContents, rootSource, port, filePath); computeDiff(newContents, rootSource, filePath);
expect(rootSource.size).toBe(1); expect(rootSource.size).toBe(1);

View File

@ -3,7 +3,6 @@ import { CatalogEntity, catalogEntityRegistry } from "../../common/catalog";
import { watch } from "chokidar"; import { watch } from "chokidar";
import fs from "fs"; import fs from "fs";
import fse from "fs-extra"; import fse from "fs-extra";
import * as uuid from "uuid";
import stream from "stream"; import stream from "stream";
import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils"; import { Disposer, ExtendedObservableMap, iter, Singleton } from "../../common/utils";
import logger from "../logger"; import logger from "../logger";
@ -13,6 +12,7 @@ import { Cluster } from "../cluster";
import { catalogEntityFromCluster } from "../cluster-manager"; import { catalogEntityFromCluster } from "../cluster-manager";
import { UserStore } from "../../common/user-store"; import { UserStore } from "../../common/user-store";
import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store"; import { ClusterStore, UpdateClusterModel } from "../../common/cluster-store";
import { createHash } from "crypto";
const logPrefix = "[KUBECONFIG-SYNC]:"; const logPrefix = "[KUBECONFIG-SYNC]:";
@ -24,7 +24,7 @@ export class KubeconfigSyncManager extends Singleton {
protected static readonly syncName = "lens:kube-sync"; protected static readonly syncName = "lens:kube-sync";
@action @action
startSync(port: number): void { startSync(): void {
if (this.syncing) { if (this.syncing) {
return; return;
} }
@ -41,16 +41,16 @@ export class KubeconfigSyncManager extends Singleton {
))); )));
// This must be done so that c&p-ed clusters are visible // This must be done so that c&p-ed clusters are visible
this.startNewSync(ClusterStore.storedKubeConfigFolder, port); this.startNewSync(ClusterStore.storedKubeConfigFolder);
for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) { for (const filePath of UserStore.getInstance().syncKubeconfigEntries.keys()) {
this.startNewSync(filePath, port); this.startNewSync(filePath);
} }
this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe(change => { this.syncListDisposer = UserStore.getInstance().syncKubeconfigEntries.observe(change => {
switch (change.type) { switch (change.type) {
case "add": case "add":
this.startNewSync(change.name, port); this.startNewSync(change.name);
break; break;
case "delete": case "delete":
this.stopOldSync(change.name); this.stopOldSync(change.name);
@ -72,14 +72,14 @@ export class KubeconfigSyncManager extends Singleton {
} }
@action @action
protected async startNewSync(filePath: string, port: number): Promise<void> { protected async startNewSync(filePath: string): Promise<void> {
if (this.sources.has(filePath)) { if (this.sources.has(filePath)) {
// don't start a new sync if we already have one // don't start a new sync if we already have one
return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath }); return void logger.debug(`${logPrefix} already syncing file/folder`, { filePath });
} }
try { try {
this.sources.set(filePath, await watchFileChanges(filePath, port)); this.sources.set(filePath, await watchFileChanges(filePath));
logger.info(`${logPrefix} starting sync of file/folder`, { 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()) }); logger.debug(`${logPrefix} ${this.sources.size} files/folders watched`, { files: Array.from(this.sources.keys()) });
@ -124,7 +124,7 @@ type RootSourceValue = [Cluster, CatalogEntity];
type RootSource = ObservableMap<string, RootSourceValue>; type RootSource = ObservableMap<string, RootSourceValue>;
// exported for testing // exported for testing
export function computeDiff(contents: string, source: RootSource, port: number, filePath: string): void { export function computeDiff(contents: string, source: RootSource, filePath: string): void {
runInAction(() => { runInAction(() => {
try { try {
const rawModels = configToModels(loadConfigFromString(contents), filePath); const rawModels = configToModels(loadConfigFromString(contents), filePath);
@ -156,14 +156,13 @@ export function computeDiff(contents: string, source: RootSource, port: number,
for (const [contextName, model] of models) { for (const [contextName, model] of models) {
// add new clusters to the source // add new clusters to the source
try { try {
const cluster = new Cluster({ ...model, id: uuid.v4() }); const clusterId = createHash("md5").update(`${filePath}:${contextName}`).digest("hex");
const cluster = ClusterStore.getInstance().getById(clusterId) || new Cluster({ ...model, id: clusterId});
if (!cluster.apiUrl) { if (!cluster.apiUrl) {
throw new Error("Cluster constructor failed, see above error"); throw new Error("Cluster constructor failed, see above error");
} }
cluster.init(port);
const entity = catalogEntityFromCluster(cluster); const entity = catalogEntityFromCluster(cluster);
entity.metadata.labels.file = filePath; entity.metadata.labels.file = filePath;
@ -181,7 +180,7 @@ export function computeDiff(contents: string, source: RootSource, port: number,
}); });
} }
function diffChangedConfig(filePath: string, source: RootSource, port: number): Disposer { function diffChangedConfig(filePath: string, source: RootSource): Disposer {
logger.debug(`${logPrefix} file changed`, { filePath }); logger.debug(`${logPrefix} file changed`, { filePath });
// TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out) // TODO: replace with an AbortController with fs.readFile when we upgrade to Node 16 (after it comes out)
@ -214,14 +213,14 @@ function diffChangedConfig(filePath: string, source: RootSource, port: number):
}) })
.on("end", () => { .on("end", () => {
if (!closed) { if (!closed) {
computeDiff(Buffer.concat(bufs).toString("utf-8"), source, port, filePath); computeDiff(Buffer.concat(bufs).toString("utf-8"), source, filePath);
} }
}); });
return cleanup; return cleanup;
} }
async function watchFileChanges(filePath: string, port: number): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> { async function watchFileChanges(filePath: string): Promise<[IComputedValue<CatalogEntity[]>, Disposer]> {
const stat = await fse.stat(filePath); // traverses symlinks, is a race condition const stat = await fse.stat(filePath); // traverses symlinks, is a race condition
const watcher = watch(filePath, { const watcher = watch(filePath, {
followSymlinks: true, followSymlinks: true,
@ -235,10 +234,10 @@ async function watchFileChanges(filePath: string, port: number): Promise<[ICompu
watcher watcher
.on("change", (childFilePath) => { .on("change", (childFilePath) => {
stoppers.get(childFilePath)(); stoppers.get(childFilePath)();
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath), port)); stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath)));
}) })
.on("add", (childFilePath) => { .on("add", (childFilePath) => {
stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath), port)); stoppers.set(childFilePath, diffChangedConfig(childFilePath, rootSource.getOrDefault(childFilePath)));
}) })
.on("unlink", (childFilePath) => { .on("unlink", (childFilePath) => {
stoppers.get(childFilePath)(); stoppers.get(childFilePath)();

View File

@ -1,24 +1,21 @@
import "../common/cluster-ipc"; import "../common/cluster-ipc";
import type http from "http"; import type http from "http";
import { ipcMain } from "electron"; import { ipcMain } from "electron";
import { action, autorun, observable, reaction, toJS } from "mobx"; import { action, autorun, reaction, toJS } from "mobx";
import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store"; import { ClusterStore, getClusterIdFromHost } from "../common/cluster-store";
import { Cluster } from "./cluster"; import { Cluster } from "./cluster";
import logger from "./logger"; import logger from "./logger";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { Singleton } from "../common/utils"; import { Singleton } from "../common/utils";
import { CatalogEntity, catalogEntityRegistry } from "../common/catalog"; import { catalogEntityRegistry } from "../common/catalog";
import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster"; import { KubernetesCluster } from "../common/catalog-entities/kubernetes-cluster";
const clusterOwnerRef = "ClusterManager"; const clusterOwnerRef = "ClusterManager";
export class ClusterManager extends Singleton { export class ClusterManager extends Singleton {
catalogSource = observable.array<CatalogEntity>([]);
constructor(public readonly port: number) { constructor(public readonly port: number) {
super(); super();
catalogEntityRegistry.addObservableSource("lens:kubernetes-clusters", this.catalogSource);
// auto-init clusters // auto-init clusters
reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => { reaction(() => ClusterStore.getInstance().enabledClustersList, (clusters) => {
clusters.forEach((cluster) => { clusters.forEach((cluster) => {
@ -31,7 +28,7 @@ export class ClusterManager extends Singleton {
}, { fireImmediately: true }); }, { fireImmediately: true });
reaction(() => toJS(ClusterStore.getInstance().enabledClustersList, { recurseEverything: true }), () => { reaction(() => toJS(ClusterStore.getInstance().enabledClustersList, { recurseEverything: true }), () => {
this.updateCatalogSource(ClusterStore.getInstance().enabledClustersList); this.updateCatalog(ClusterStore.getInstance().enabledClustersList);
}, { fireImmediately: true }); }, { fireImmediately: true });
reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => { reaction(() => catalogEntityRegistry.getItemsForApiKind<KubernetesCluster>("entity.k8slens.dev/v1alpha1", "KubernetesCluster"), (entities) => {
@ -58,31 +55,20 @@ export class ClusterManager extends Singleton {
ipcMain.on("network:online", () => { this.onNetworkOnline(); }); ipcMain.on("network:online", () => { this.onNetworkOnline(); });
} }
@action protected updateCatalogSource(clusters: Cluster[]) { @action protected updateCatalog(clusters: Cluster[]) {
this.catalogSource.replace(this.catalogSource.filter(entity => (
clusters.find((cluster) => entity.metadata.uid === cluster.id)
)));
for (const cluster of clusters) { for (const cluster of clusters) {
if (cluster.ownerRef) { const index = catalogEntityRegistry.items.findIndex((entity) => entity.metadata.uid === cluster.id);
continue;
}
const entityIndex = this.catalogSource.findIndex((entity) => entity.metadata.uid === cluster.id); if (index !== -1) {
const newEntity = catalogEntityFromCluster(cluster); const entity = catalogEntityRegistry.items[index];
if (entityIndex === -1) { entity.status.phase = cluster.disconnected ? "disconnected" : "connected";
this.catalogSource.push(newEntity); entity.status.active = !cluster.disconnected;
} else {
const oldEntity = this.catalogSource[entityIndex];
newEntity.status.phase = cluster.disconnected ? "disconnected" : "connected"; if (cluster.preferences?.clusterName) {
newEntity.status.active = !cluster.disconnected; entity.metadata.name = cluster.preferences.clusterName;
newEntity.metadata.labels = { }
...newEntity.metadata.labels, catalogEntityRegistry.items.splice(index, 1, entity);
...oldEntity.metadata.labels
};
this.catalogSource.splice(entityIndex, 1, newEntity);
} }
} }
} }
@ -109,7 +95,6 @@ export class ClusterManager extends Singleton {
} else { } else {
cluster.enabled = true; cluster.enabled = true;
cluster.ownerRef ||= clusterOwnerRef; cluster.ownerRef ||= clusterOwnerRef;
cluster.preferences.clusterName = entity.metadata.name;
cluster.kubeConfigPath = entity.spec.kubeconfigPath; cluster.kubeConfigPath = entity.spec.kubeconfigPath;
cluster.contextName = entity.spec.kubeconfigContext; cluster.contextName = entity.spec.kubeconfigContext;

View File

@ -133,7 +133,7 @@ app.on("ready", async () => {
const clusterManager = ClusterManager.getInstance(); const clusterManager = ClusterManager.getInstance();
// create kubeconfig sync manager // create kubeconfig sync manager
KubeconfigSyncManager.createInstance().startSync(clusterManager.port); KubeconfigSyncManager.createInstance().startSync();
// run proxy // run proxy
try { try {