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:
parent
d08aeaa2e4
commit
7494121a86
@ -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,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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)();
|
||||||
|
|||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user