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:
parent
23c9255b9a
commit
e560baa2d0
@ -3,6 +3,9 @@ import mockFs from "mock-fs";
|
|||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import { Cluster } from "../../main/cluster";
|
import { Cluster } from "../../main/cluster";
|
||||||
import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
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 testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
||||||
const kubeconfig = `
|
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", () => {
|
describe("pre 2.0 config with an existing cluster", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ClusterStore.resetInstance();
|
ClusterStore.resetInstance();
|
||||||
@ -303,7 +313,7 @@ describe("pre 2.0 config with an existing cluster", () => {
|
|||||||
version: "1.0.0"
|
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 () => {
|
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
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: {
|
cluster1: {
|
||||||
kubeConfig: "foo",
|
kubeConfig: minimalValidKubeConfig,
|
||||||
icon: "icon_path",
|
icon: "icon_path",
|
||||||
preferences: {
|
preferences: {
|
||||||
terminalCWD: "/tmp"
|
terminalCWD: "/tmp"
|
||||||
@ -417,7 +427,7 @@ describe("for a pre 2.7.0-beta.0 config without a workspace", () => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
cluster1: {
|
cluster1: {
|
||||||
kubeConfig: "foo",
|
kubeConfig: minimalValidKubeConfig,
|
||||||
preferences: {
|
preferences: {
|
||||||
terminalCWD: "/tmp"
|
terminalCWD: "/tmp"
|
||||||
}
|
}
|
||||||
@ -451,7 +461,7 @@ describe("pre 3.6.0-beta.1 config with an existing cluster", () => {
|
|||||||
clusters: [
|
clusters: [
|
||||||
{
|
{
|
||||||
id: "cluster1",
|
id: "cluster1",
|
||||||
kubeConfig: "kubeconfig content",
|
kubeConfig: minimalValidKubeConfig,
|
||||||
contextName: "cluster",
|
contextName: "cluster",
|
||||||
preferences: {
|
preferences: {
|
||||||
icon: "store://icon_path",
|
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 () => {
|
it("migrates to modern format with kubeconfig in a file", async () => {
|
||||||
const config = clusterStore.clustersList[0].kubeConfigPath;
|
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 () => {
|
it("migrates to modern format with icon not in file", async () => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { validateKubeConfig } from "../kube-helpers";
|
import { validateKubeConfig, loadConfig } from "../kube-helpers";
|
||||||
|
|
||||||
const kubeconfig = `
|
const kubeconfig = `
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@ -40,61 +40,214 @@ users:
|
|||||||
|
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
describe("validateKubeconfig", () => {
|
interface kubeconfig {
|
||||||
beforeAll(() => {
|
apiVersion: string,
|
||||||
kc.loadFromString(kubeconfig);
|
clusters: [{
|
||||||
});
|
name: string,
|
||||||
describe("with default validation options", () => {
|
cluster: {
|
||||||
describe("with valid kubeconfig", () => {
|
server: string
|
||||||
it("does not raise exceptions", () => {
|
}
|
||||||
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
|
}],
|
||||||
});
|
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);
|
||||||
});
|
});
|
||||||
describe("with invalid context object", () => {
|
describe("with default validation options", () => {
|
||||||
it("it raises exception", () => {
|
describe("with valid kubeconfig", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
|
it("does not raise exceptions", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "valid");}).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe("with invalid context object", () => {
|
||||||
|
it("it raises exception", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalid");}).toThrow("No valid context object provided in kubeconfig for context 'invalid'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with invalid cluster object", () => {
|
||||||
|
it("it raises exception", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with invalid user object", () => {
|
||||||
|
it("it raises exception", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with invalid exec command", () => {
|
||||||
|
it("it raises exception", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid cluster object", () => {
|
describe("with validateCluster as false", () => {
|
||||||
it("it raises exception", () => {
|
describe("with invalid cluster object", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidCluster");}).toThrow("No valid cluster object provided in kubeconfig for context 'invalidCluster'");
|
it("does not raise exception", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid user object", () => {
|
describe("with validateUser as false", () => {
|
||||||
it("it raises exception", () => {
|
describe("with invalid user object", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidUser");}).toThrow("No valid user object provided in kubeconfig for context 'invalidUser'");
|
it("does not raise exceptions", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid exec command", () => {
|
describe("with validateExec as false", () => {
|
||||||
it("it raises exception", () => {
|
describe("with invalid exec object", () => {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidExec");}).toThrow("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig");
|
it("does not raise exceptions", () => {
|
||||||
|
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with validateCluster as false", () => {
|
describe("pre-validate context object in kubeconfig tests", () => {
|
||||||
describe("with invalid cluster object", () => {
|
beforeEach(() => {
|
||||||
it("does not raise exception", () => {
|
jest.clearAllMocks();
|
||||||
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
|
});
|
||||||
|
|
||||||
|
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("with validateUser as false", () => {
|
describe("Check valid kubeconfigs", () => {
|
||||||
describe("with invalid user object", () => {
|
beforeEach(() => {
|
||||||
it("does not raise excpetions", () => {
|
mockKubeConfig = {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
|
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("with validateExec as false", () => {
|
describe("Check invalid kubeconfigs", () => {
|
||||||
describe("with invalid exec object", () => {
|
beforeEach(() => {
|
||||||
it("does not raise excpetions", () => {
|
mockKubeConfig = {
|
||||||
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
|
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");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import yaml from "js-yaml";
|
|||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import commandExists from "command-exists";
|
import commandExists from "command-exists";
|
||||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
import { ExecValidationNotFoundError } from "./custom-errors";
|
||||||
|
import { newClusters, newContexts, newUsers } from "@kubernetes/client-node/dist/config_types";
|
||||||
|
|
||||||
export type KubeConfigValidationOpts = {
|
export type KubeConfigValidationOpts = {
|
||||||
validateCluster?: boolean;
|
validateCluster?: boolean;
|
||||||
@ -23,14 +24,36 @@ function resolveTilde(filePath: string) {
|
|||||||
return filePath;
|
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 {
|
export function loadConfig(pathOrContent?: string): KubeConfig {
|
||||||
|
const content = fse.pathExistsSync(pathOrContent) ? readResolvedPathSync(pathOrContent) : pathOrContent;
|
||||||
|
const options = loadToOptions(content);
|
||||||
const kc = new KubeConfig();
|
const kc = new KubeConfig();
|
||||||
|
|
||||||
if (fse.pathExistsSync(pathOrContent)) {
|
// need to load using the kubernetes client to generate a kubeconfig object
|
||||||
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
|
kc.loadFromOptions(options);
|
||||||
} else {
|
|
||||||
kc.loadFromString(pathOrContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
return kc;
|
return kc;
|
||||||
}
|
}
|
||||||
@ -146,7 +169,7 @@ export function podHasIssues(pod: V1Pod) {
|
|||||||
return (
|
return (
|
||||||
notReady ||
|
notReady ||
|
||||||
pod.status.phase !== "Running" ||
|
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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import type { ThemeId } from "../renderer/theme.store";
|
|||||||
import { app, remote } from "electron";
|
import { app, remote } from "electron";
|
||||||
import semver from "semver";
|
import semver from "semver";
|
||||||
import { readFile } from "fs-extra";
|
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 { BaseStore } from "./base-store";
|
||||||
import migrations from "../migrations/user-store";
|
import migrations from "../migrations/user-store";
|
||||||
import { getAppVersion } from "./utils/app-version";
|
import { getAppVersion } from "./utils/app-version";
|
||||||
@ -114,6 +114,10 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
|||||||
this.kubeConfigPath = kubeConfigDefaultPath;
|
this.kubeConfigPath = kubeConfigDefaultPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get isDefaultKubeConfigPath(): boolean {
|
||||||
|
return this.kubeConfigPath === kubeConfigDefaultPath;
|
||||||
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
async resetTheme() {
|
async resetTheme() {
|
||||||
await this.whenLoaded;
|
await this.whenLoaded;
|
||||||
|
|||||||
@ -62,9 +62,11 @@ export class AddCluster extends React.Component {
|
|||||||
this.kubeConfigPath = filePath;
|
this.kubeConfigPath = filePath;
|
||||||
userStore.kubeConfigPath = filePath; // save to store
|
userStore.kubeConfigPath = filePath; // save to store
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
Notifications.error(
|
if (!userStore.isDefaultKubeConfigPath) {
|
||||||
<div>Can't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
|
Notifications.error(
|
||||||
);
|
<div>Can't setup <code>{filePath}</code> as kubeconfig: {String(err)}</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (throwError) {
|
if (throwError) {
|
||||||
throw err;
|
throw err;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user