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

Pre-Validate kubeconfig before making available in Lens (#1078)

This commit is contained in:
steve richards 2021-04-16 16:41:18 +01:00 committed by Sebastian Malton
parent 23c9255b9a
commit e560baa2d0
5 changed files with 244 additions and 52 deletions

View File

@ -3,6 +3,9 @@ import mockFs from "mock-fs";
import yaml from "js-yaml";
import { Cluster } from "../../main/cluster";
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { Console } from "console";
console = new Console(process.stdout, process.stderr); // fix mockFS
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
const kubeconfig = `
@ -292,6 +295,13 @@ users:
});
});
const minimalValidKubeConfig = JSON.stringify({
apiVersion: "v1",
clusters: [],
users: [],
contexts: [],
});
describe("pre 2.0 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
@ -303,7 +313,7 @@ describe("pre 2.0 config with an existing cluster", () => {
version: "1.0.0"
}
},
cluster1: "kubeconfig content"
cluster1: minimalValidKubeConfig,
})
}
};
@ -321,7 +331,7 @@ describe("pre 2.0 config with an existing cluster", () => {
it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
expect(fs.readFileSync(config, "utf8")).toContain(`"contexts":[]`);
});
});
@ -375,7 +385,7 @@ describe("pre 2.6.0 config with a cluster icon", () => {
}
},
cluster1: {
kubeConfig: "foo",
kubeConfig: minimalValidKubeConfig,
icon: "icon_path",
preferences: {
terminalCWD: "/tmp"
@ -417,7 +427,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
}
},
cluster1: {
kubeConfig: "foo",
kubeConfig: minimalValidKubeConfig,
preferences: {
terminalCWD: "/tmp"
}
@ -451,7 +461,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
clusters: [
{
id: "cluster1",
kubeConfig: "kubeconfig content",
kubeConfig: minimalValidKubeConfig,
contextName: "cluster",
preferences: {
icon: "store://icon_path",
@ -476,7 +486,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
it("migrates to modern format with kubeconfig in a file", async () => {
const config = clusterStore.clustersList[0].kubeConfigPath;
expect(fs.readFileSync(config, "utf8")).toBe("kubeconfig content");
expect(fs.readFileSync(config, "utf8")).toBe(minimalValidKubeConfig);
});
it("migrates to modern format with icon not in file", async () => {

View File

@ -1,5 +1,5 @@
import { KubeConfig } from "@kubernetes/client-node";
import { validateKubeConfig } from "../kube-helpers";
import { validateKubeConfig, loadConfig } from "../kube-helpers";
const kubeconfig = `
apiVersion: v1
@ -40,7 +40,33 @@ users:
const kc = new KubeConfig();
describe("validateKubeconfig", () => {
interface kubeconfig {
apiVersion: string,
clusters: [{
name: string,
cluster: {
server: string
}
}],
contexts: [{
context: {
cluster: string,
user: string,
},
name: string
}],
users: [{
name: string
}],
kind: string,
"current-context": string,
preferences: {}
}
let mockKubeConfig: kubeconfig;
describe("kube helpers", () => {
describe("validateKubeconfig", () => {
beforeAll(() => {
kc.loadFromString(kubeconfig);
});
@ -85,7 +111,7 @@ describe("validateKubeconfig", () => {
describe("with validateUser as false", () => {
describe("with invalid user object", () => {
it("does not raise excpetions", () => {
it("does not raise exceptions", () => {
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
});
});
@ -93,9 +119,136 @@ describe("validateKubeconfig", () => {
describe("with validateExec as false", () => {
describe("with invalid exec object", () => {
it("does not raise excpetions", () => {
it("does not raise exceptions", () => {
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
});
});
});
});
describe("pre-validate context object in kubeconfig tests", () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe("Check logger.error() output", () => {
it("invalid yaml string", () => {
const invalidYAMLString = "fancy foo config";
expect(() => loadConfig(invalidYAMLString)).toThrowError("must be an object");
});
it("empty contexts", () => {
const emptyContexts = `apiVersion: v1\ncontexts:`;
expect(() => loadConfig(emptyContexts)).not.toThrow();
});
});
describe("Check valid kubeconfigs", () => {
beforeEach(() => {
mockKubeConfig = {
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
"current-context": "minikube",
preferences: {},
};
});
it("single context is ok", async () => {
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
});
it("multiple context is ok", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: "cluster-2"}, name: "cluster-2"});
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(2);
});
});
describe("Check invalid kubeconfigs", () => {
beforeEach(() => {
mockKubeConfig = {
apiVersion: "v1",
clusters: [{
name: "minikube",
cluster: {
server: "https://192.168.64.3:8443",
},
}],
contexts: [{
context: {
cluster: "minikube",
user: "minikube",
},
name: "minikube",
}],
users: [{
name: "minikube",
}],
kind: "Config",
"current-context": "minikube",
preferences: {},
};
});
it("empty name in context causes it to be removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: "cluster-2"}, name: ""});
expect(mockKubeConfig.contexts.length).toBe(2);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(1);
});
it("empty cluster in context causes it to be removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "", user: "cluster-2"}, name: "cluster-2"});
expect(mockKubeConfig.contexts.length).toBe(2);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(1);
});
it("empty user in context causes it to be removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: ""}, name: "cluster-2"});
expect(mockKubeConfig.contexts.length).toBe(2);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(1);
});
it("invalid context in between valid contexts is removed", async () => {
mockKubeConfig.contexts.push({context: {cluster: "cluster-2", user: ""}, name: "cluster-2"});
mockKubeConfig.contexts.push({context: {cluster: "cluster-3", user: "cluster-3"}, name: "cluster-3"});
expect(mockKubeConfig.contexts.length).toBe(3);
const kc:KubeConfig = loadConfig(JSON.stringify(mockKubeConfig));
expect(kc.getCurrentContext()).toBe("minikube");
expect(kc.contexts.length).toBe(2);
expect(kc.contexts[0].name).toBe("minikube");
expect(kc.contexts[1].name).toBe("cluster-3");
});
});
});
});

View File

@ -6,6 +6,7 @@ import yaml from "js-yaml";
import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types";
export type KubeConfigValidationOpts = {
validateCluster?: boolean;
@ -23,14 +24,36 @@ function resolveTilde(filePath: string) {
return filePath;
}
function readResolvedPathSync(filePath: string): string {
return fse.readFileSync(path.resolve(resolveTilde(filePath)), "utf8");
}
function checkRawContext(rawContext: any): boolean {
return rawContext.name && rawContext.context?.cluster && rawContext.context?.user;
}
function loadToOptions(rawYaml: string): any {
const obj = yaml.safeLoad(rawYaml);
if (typeof obj !== "object" || !obj) {
throw new TypeError("KubeConfig root entry must be an object");
}
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = obj;
const clusters = newClusters(rawClusters);
const users = newUsers(rawUsers);
const contexts = newContexts(rawContexts?.filter(checkRawContext));
return { clusters, users, contexts, currentContext };
}
export function loadConfig(pathOrContent?: string): KubeConfig {
const content = fse.pathExistsSync(pathOrContent) ? readResolvedPathSync(pathOrContent) : pathOrContent;
const options = loadToOptions(content);
const kc = new KubeConfig();
if (fse.pathExistsSync(pathOrContent)) {
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
} else {
kc.loadFromString(pathOrContent);
}
// need to load using the kubernetes client to generate a kubeconfig object
kc.loadFromOptions(options);
return kc;
}
@ -146,7 +169,7 @@ export function podHasIssues(pod: V1Pod) {
return (
notReady ||
pod.status.phase !== "Running" ||
pod.spec.priority > 500000 // We're interested in high prio pods events regardless of their running status
pod.spec.priority > 500000 // We're interested in high priority pods events regardless of their running status
);
}

View File

@ -2,7 +2,7 @@ import type { ThemeId } from "../renderer/theme.store";
import { app, remote } from "electron";
import semver from "semver";
import { readFile } from "fs-extra";
import { action, observable, reaction, toJS } from "mobx";
import { action, computed, observable, reaction, toJS } from "mobx";
import { BaseStore } from "./base-store";
import migrations from "../migrations/user-store";
import { getAppVersion } from "./utils/app-version";
@ -114,6 +114,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
this.kubeConfigPath = kubeConfigDefaultPath;
}
@computed get isDefaultKubeConfigPath(): boolean {
return this.kubeConfigPath === kubeConfigDefaultPath;
}
@action
async resetTheme() {
await this.whenLoaded;

View File

@ -62,9 +62,11 @@ export class AddCluster extends React.Component {
this.kubeConfigPath = filePath;
userStore.kubeConfigPath = filePath; // save to store
} catch (err) {
if (!userStore.isDefaultKubeConfigPath) {
Notifications.error(
<div>Can&apos;t setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
);
}
if (throwError) {
throw err;