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:
Sebastian Malton authored and GitHub committed 2022-06-20 22:23:15 -04:00
1 parent 8c31e56462
commit de43df1cca
40 files changed
+2126 -815

No files matched your search

+8 -4
View File
@@ -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);
+4 -4
View File
@@ -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),
});
},
+12 -3
View File
@@ -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;
+9
View File
@@ -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
*/
+4 -20
View File
@@ -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
View File
@@ -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 };
}