mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Fix cluster frame display issue (#5518)
* Fix cluster frame display issue - Add some defensive code to prevent this sort of infinite loop - Add some unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * fix unit tests Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix build Signed-off-by: Sebastian Malton <sebastian@malton.name> * Fix lint Signed-off-by: Sebastian Malton <sebastian@malton.name> * Factor out injectable for getting of cluster config data Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
1 parent
8c31e56462
commit
de43df1cca
40 files changed
+2126
-815
No files matched your search
@@ -7,14 +7,13 @@ import fs from "fs";
|
||||
import mockFs from "mock-fs";
|
||||
import path from "path";
|
||||
import fse from "fs-extra";
|
||||
import type { Cluster } from "../cluster/cluster";
|
||||
import type { ClusterStore } from "../cluster-store/cluster-store";
|
||||
import { Console } from "console";
|
||||
import { stdout, stderr } from "process";
|
||||
import getCustomKubeConfigDirectoryInjectable from "../app-paths/get-custom-kube-config-directory/get-custom-kube-config-directory.injectable";
|
||||
import clusterStoreInjectable from "../cluster-store/cluster-store.injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type { DiContainer } from "@ogre-tools/injectable";
|
||||
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
import directoryForUserDataInjectable from "../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||
import { getDiForUnitTesting } from "../../main/getDiForUnitTesting";
|
||||
@@ -25,17 +24,19 @@ import directoryForTempInjectable from "../app-paths/directory-for-temp/director
|
||||
import kubectlBinaryNameInjectable from "../../main/kubectl/binary-name.injectable";
|
||||
import kubectlDownloadingNormalizedArchInjectable from "../../main/kubectl/normalized-arch.injectable";
|
||||
import normalizedPlatformInjectable from "../vars/normalized-platform.injectable";
|
||||
import fsInjectable from "../fs/fs.injectable";
|
||||
|
||||
console = new Console(stdout, stderr);
|
||||
|
||||
const testDataIcon = fs.readFileSync(
|
||||
"test-data/cluster-store-migration-icon.png",
|
||||
);
|
||||
const clusterServerUrl = "https://localhost";
|
||||
const kubeconfig = `
|
||||
apiVersion: v1
|
||||
clusters:
|
||||
- cluster:
|
||||
server: https://localhost
|
||||
server: ${clusterServerUrl}
|
||||
name: test
|
||||
contexts:
|
||||
- context:
|
||||
@@ -78,7 +79,7 @@ jest.mock("electron", () => ({
|
||||
describe("cluster-store", () => {
|
||||
let mainDi: DiContainer;
|
||||
let clusterStore: ClusterStore;
|
||||
let createCluster: (model: ClusterModel) => Cluster;
|
||||
let createCluster: CreateCluster;
|
||||
|
||||
beforeEach(async () => {
|
||||
mainDi = getDiForUnitTesting({ doGeneralOverrides: true });
|
||||
@@ -94,6 +95,7 @@ describe("cluster-store", () => {
|
||||
mainDi.permitSideEffects(getConfigurationFileModelInjectable);
|
||||
mainDi.permitSideEffects(appVersionInjectable);
|
||||
mainDi.permitSideEffects(clusterStoreInjectable);
|
||||
mainDi.permitSideEffects(fsInjectable);
|
||||
|
||||
mainDi.unoverride(clusterStoreInjectable);
|
||||
});
|
||||
@@ -143,6 +145,8 @@ describe("cluster-store", () => {
|
||||
getCustomKubeConfigDirectory("foo"),
|
||||
kubeconfig,
|
||||
),
|
||||
}, {
|
||||
clusterServerUrl,
|
||||
});
|
||||
|
||||
clusterStore.addCluster(cluster);
|
||||
|
||||
@@ -78,12 +78,12 @@ describe("kube helpers", () => {
|
||||
describe("with default validation options", () => {
|
||||
describe("with valid kubeconfig", () => {
|
||||
it("does not return an error", () => {
|
||||
expect(validateKubeConfig(kc, "valid")).toBeUndefined();
|
||||
expect(validateKubeConfig(kc, "valid")).toBeDefined();
|
||||
});
|
||||
});
|
||||
describe("with invalid context object", () => {
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalid"))).toEqual(
|
||||
expect(validateKubeConfig(kc, "invalid").error?.toString()).toEqual(
|
||||
expect.stringContaining("No valid context object provided in kubeconfig for context 'invalid'"),
|
||||
);
|
||||
});
|
||||
@@ -91,7 +91,7 @@ describe("kube helpers", () => {
|
||||
|
||||
describe("with invalid cluster object", () => {
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalidCluster"))).toEqual(
|
||||
expect(validateKubeConfig(kc, "invalidCluster").error?.toString()).toEqual(
|
||||
expect.stringContaining("No valid cluster object provided in kubeconfig for context 'invalidCluster'"),
|
||||
);
|
||||
});
|
||||
@@ -99,7 +99,7 @@ describe("kube helpers", () => {
|
||||
|
||||
describe("with invalid user object", () => {
|
||||
it("returns an error", () => {
|
||||
expect(String(validateKubeConfig(kc, "invalidUser"))).toEqual(
|
||||
expect(validateKubeConfig(kc, "invalidUser").error?.toString()).toEqual(
|
||||
expect.stringContaining("No valid user object provided in kubeconfig for context 'invalidUser'"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { getInjectable } from "@ogre-tools/injectable";
|
||||
import { ClusterStore } from "./cluster-store";
|
||||
import { createClusterInjectionToken } from "../cluster/create-cluster-injection-token";
|
||||
import readClusterConfigSyncInjectable from "./read-cluster-config.injectable";
|
||||
|
||||
const clusterStoreInjectable = getInjectable({
|
||||
id: "cluster-store",
|
||||
@@ -14,6 +15,7 @@ const clusterStoreInjectable = getInjectable({
|
||||
|
||||
return ClusterStore.createInstance({
|
||||
createCluster: di.inject(createClusterInjectionToken),
|
||||
readClusterConfigSync: di.inject(readClusterConfigSyncInjectable),
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
@@ -16,13 +16,16 @@ import { disposer, toJS } from "../utils";
|
||||
import type { ClusterModel, ClusterId, ClusterState } from "../cluster-types";
|
||||
import { requestInitialClusterStates } from "../../renderer/ipc";
|
||||
import { clusterStates } from "../ipc/cluster";
|
||||
import type { CreateCluster } from "../cluster/create-cluster-injection-token";
|
||||
import type { ReadClusterConfigSync } from "./read-cluster-config.injectable";
|
||||
|
||||
export interface ClusterStoreModel {
|
||||
clusters?: ClusterModel[];
|
||||
}
|
||||
|
||||
interface Dependencies {
|
||||
createCluster: (model: ClusterModel) => Cluster;
|
||||
createCluster: CreateCluster;
|
||||
readClusterConfigSync: ReadClusterConfigSync;
|
||||
}
|
||||
|
||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
@@ -116,7 +119,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
|
||||
const cluster = clusterOrModel instanceof Cluster
|
||||
? clusterOrModel
|
||||
: this.dependencies.createCluster(clusterOrModel);
|
||||
: this.dependencies.createCluster(
|
||||
clusterOrModel,
|
||||
this.dependencies.readClusterConfigSync(clusterOrModel),
|
||||
);
|
||||
|
||||
this.clusters.set(cluster.id, cluster);
|
||||
|
||||
@@ -136,7 +142,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
if (cluster) {
|
||||
cluster.updateModel(clusterModel);
|
||||
} else {
|
||||
cluster = this.dependencies.createCluster(clusterModel);
|
||||
cluster = this.dependencies.createCluster(
|
||||
clusterModel,
|
||||
this.dependencies.readClusterConfigSync(clusterModel),
|
||||
);
|
||||
}
|
||||
newClusters.set(clusterModel.id, cluster);
|
||||
} catch (error) {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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 { ClusterConfigData, ClusterModel } from "../cluster-types";
|
||||
import readFileSyncInjectable from "../fs/read-file-sync.injectable";
|
||||
import { loadConfigFromString, validateKubeConfig } from "../kube-helpers";
|
||||
|
||||
export type ReadClusterConfigSync = (model: ClusterModel) => ClusterConfigData;
|
||||
|
||||
const readClusterConfigSyncInjectable = getInjectable({
|
||||
id: "read-cluster-config-sync",
|
||||
instantiate: (di): ReadClusterConfigSync => {
|
||||
const readFileSync = di.inject(readFileSyncInjectable);
|
||||
|
||||
return ({ kubeConfigPath, contextName }) => {
|
||||
const kubeConfigData = readFileSync(kubeConfigPath);
|
||||
const { config } = loadConfigFromString(kubeConfigData);
|
||||
const result = validateKubeConfig(config, contextName);
|
||||
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
|
||||
return { clusterServerUrl: result.cluster.server };
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export default readClusterConfigSyncInjectable;
|
||||
@@ -67,6 +67,15 @@ export interface ClusterModel {
|
||||
labels?: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* This data is retreived from the kubeconfig file before calling the cluster constructor.
|
||||
*
|
||||
* That is done to remove the external dependency on the construction of Cluster instances.
|
||||
*/
|
||||
export interface ClusterConfigData {
|
||||
clusterServerUrl: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* The complete set of cluster settings or preferences
|
||||
*/
|
||||
|
||||
@@ -10,13 +10,13 @@ import type { KubeConfig } from "@kubernetes/client-node";
|
||||
import { HttpError } from "@kubernetes/client-node";
|
||||
import type { Kubectl } from "../../main/kubectl/kubectl";
|
||||
import type { KubeconfigManager } from "../../main/kubeconfig-manager/kubeconfig-manager";
|
||||
import { loadConfigFromFile, loadConfigFromFileSync, validateKubeConfig } from "../kube-helpers";
|
||||
import { loadConfigFromFile } from "../kube-helpers";
|
||||
import type { KubeApiResource, KubeResource } from "../rbac";
|
||||
import { apiResourceRecord, apiResources } from "../rbac";
|
||||
import type { VersionDetector } from "../../main/cluster-detectors/version-detector";
|
||||
import type { DetectorRegistry } from "../../main/cluster-detectors/detector-registry";
|
||||
import plimit from "p-limit";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate } from "../cluster-types";
|
||||
import type { ClusterState, ClusterRefreshOptions, ClusterMetricsResourceType, ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences, ClusterPrometheusPreferences, UpdateClusterModel, KubeAuthUpdate, ClusterConfigData } from "../cluster-types";
|
||||
import { ClusterMetadataKey, initialNodeShellImage, ClusterStatus } from "../cluster-types";
|
||||
import { disposer, isDefined, isRequestError, toJS } from "../utils";
|
||||
import type { Response } from "request";
|
||||
@@ -236,27 +236,11 @@ export class Cluster implements ClusterModel, ClusterState {
|
||||
return this.preferences.defaultNamespace;
|
||||
}
|
||||
|
||||
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel) {
|
||||
constructor(private readonly dependencies: ClusterDependencies, model: ClusterModel, configData: ClusterConfigData) {
|
||||
makeObservable(this);
|
||||
this.id = model.id;
|
||||
this.updateModel(model);
|
||||
|
||||
const { config } = loadConfigFromFileSync(this.kubeConfigPath);
|
||||
const validationError = validateKubeConfig(config, this.contextName);
|
||||
|
||||
if (validationError) {
|
||||
throw validationError;
|
||||
}
|
||||
|
||||
const context = config.getContextObject(this.contextName);
|
||||
|
||||
assert(context);
|
||||
|
||||
const cluster = config.getCluster(context.cluster);
|
||||
|
||||
assert(cluster);
|
||||
|
||||
this.apiUrl = cluster.server;
|
||||
this.apiUrl = configData.clusterServerUrl;
|
||||
|
||||
// for the time being, until renderer gets its own cluster type
|
||||
this._contextHandler = this.dependencies.createContextHandler(this);
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectionToken } from "@ogre-tools/injectable";
|
||||
import type { ClusterModel } from "../cluster-types";
|
||||
import type { ClusterConfigData, ClusterModel } from "../cluster-types";
|
||||
import type { Cluster } from "./cluster";
|
||||
|
||||
export type CreateCluster = (model: ClusterModel) => Cluster;
|
||||
export type CreateCluster = (model: ClusterModel, configData: ClusterConfigData) => Cluster;
|
||||
|
||||
export const createClusterInjectionToken = getInjectionToken<CreateCluster>({
|
||||
id: "create-cluster-token",
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* 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 fsInjectable from "./fs.injectable";
|
||||
|
||||
export type ReadFileSync = (filePath: string) => string;
|
||||
|
||||
const readFileSyncInjectable = getInjectable({
|
||||
id: "read-file-sync",
|
||||
instantiate: (di): ReadFileSync => {
|
||||
const { readFileSync } = di.inject(fsInjectable);
|
||||
|
||||
return (filePath) => readFileSync(filePath, "utf-8");
|
||||
},
|
||||
});
|
||||
|
||||
export default readFileSyncInjectable;
|
||||
+30
-11
@@ -150,7 +150,7 @@ export function loadConfigFromString(content: string): ConfigResult {
|
||||
|
||||
export interface SplitConfigEntry {
|
||||
config: KubeConfig;
|
||||
error?: string;
|
||||
validationResult: ValidateKubeConfigResult;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -179,7 +179,7 @@ export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] {
|
||||
|
||||
return {
|
||||
config,
|
||||
error: validateKubeConfig(config, ctx.name)?.toString(),
|
||||
validationResult: validateKubeConfig(config, ctx.name),
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -243,25 +243,44 @@ export function dumpConfigYaml(kubeConfig: PartialDeep<KubeConfig>): string {
|
||||
return yaml.dump(config, { skipInvalid: true });
|
||||
}
|
||||
|
||||
export type ValidateKubeConfigResult = {
|
||||
error: Error;
|
||||
} | {
|
||||
error?: undefined;
|
||||
context: Context;
|
||||
cluster: Cluster;
|
||||
user: User;
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
||||
*
|
||||
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
|
||||
*/
|
||||
export function validateKubeConfig(config: KubeConfig, contextName: string): Error | undefined {
|
||||
const contextObject = config.getContextObject(contextName);
|
||||
export function validateKubeConfig(config: KubeConfig, contextName: string): ValidateKubeConfigResult {
|
||||
const context = config.getContextObject(contextName);
|
||||
|
||||
if (!contextObject) {
|
||||
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||
if (!context) {
|
||||
return {
|
||||
error: new Error(`No valid context object provided in kubeconfig for context '${contextName}'`),
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.getCluster(contextObject.cluster)) {
|
||||
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||
const cluster = config.getCluster(context.cluster);
|
||||
|
||||
if (!cluster) {
|
||||
return {
|
||||
error: new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`),
|
||||
};
|
||||
}
|
||||
|
||||
if (!config.getUser(contextObject.user)) {
|
||||
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||
const user = config.getUser(context.user);
|
||||
|
||||
if (!user) {
|
||||
return {
|
||||
error: new Error(`No valid user object provided in kubeconfig for context '${contextName}'`),
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
return { cluster, user, context };
|
||||
}
|
||||
Reference in new issue
Block a user