Ignore clusters with invalid kubeconfig (#1956)

* Ignore clusters with invalid kubeconfig

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Improve error message

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Mark cluster as dead if kubeconfig loading fails

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Fix tests

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Validate cluster object in kubeconfig when constructing cluster

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Add unit tests for validateKubeConfig

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Refactor validateKubeconfig unit tests

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Extract ValidationOpts type

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Add default value to validationOpts param

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Change isDead to property

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Fix lint issues

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Add missing new line

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Update validateKubeConfig in-code documentation

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Remove isDead property

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>

* Display warning notification if invalid kubeconfig detected (#2233)

* Display warning notification if invalid kubeconfig detected

Signed-off-by: Lauri Nevala <lauri.nevala@gmail.com>
This commit is contained in:
Lauri Nevala authored and GitHub committed 2021-03-01 17:30:22 +02:00
1 parent e837e6f1db
commit 4f74b9aabe
12 files changed
+307 -30

No files matched your search

+104 -9
View File
@@ -6,6 +6,29 @@ import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
import { workspaceStore } from "../workspace-store";
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
const kubeconfig = `
apiVersion: v1
clusters:
- cluster:
server: https://localhost
name: test
contexts:
- context:
cluster: test
user: test
name: foo
- context:
cluster: test
user: test
name: foo2
current-context: test
kind: Config
preferences: {}
users:
- name: test
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
jest.mock("electron", () => {
return {
@@ -47,13 +70,13 @@ describe("empty config", () => {
clusterStore.addCluster(
new Cluster({
id: "foo",
contextName: "minikube",
contextName: "foo",
preferences: {
terminalCWD: "/tmp",
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
clusterName: "minikube"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"),
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig),
workspace: workspaceStore.currentWorkspaceId
})
);
@@ -91,20 +114,20 @@ describe("empty config", () => {
clusterStore.addClusters(
new Cluster({
id: "prod",
contextName: "prod",
contextName: "foo",
preferences: {
clusterName: "prod"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"),
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig),
workspace: "workstation"
}),
new Cluster({
id: "dev",
contextName: "dev",
contextName: "foo2",
preferences: {
clusterName: "dev"
},
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"),
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig),
workspace: "workstation"
})
);
@@ -177,20 +200,20 @@ describe("config with existing clusters", () => {
clusters: [
{
id: "cluster1",
kubeConfig: "foo",
kubeConfigPath: kubeconfig,
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "default"
},
{
id: "cluster2",
kubeConfig: "foo2",
kubeConfigPath: kubeconfig,
contextName: "foo2",
preferences: { terminalCWD: "/foo2" }
},
{
id: "cluster3",
kubeConfig: "foo",
kubeConfigPath: kubeconfig,
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "foo",
@@ -247,6 +270,78 @@ describe("config with existing clusters", () => {
});
});
describe("config with invalid cluster kubeconfig", () => {
beforeEach(() => {
const invalidKubeconfig = `
apiVersion: v1
clusters:
- cluster:
server: https://localhost
name: test2
contexts:
- context:
cluster: test
user: test
name: test
current-context: test
kind: Config
preferences: {}
users:
- name: test
user:
token: kubeconfig-user-q4lm4:xxxyyyy
`;
ClusterStore.resetInstance();
const mockOpts = {
"tmp": {
"lens-cluster-store.json": JSON.stringify({
__internal__: {
migrations: {
version: "99.99.99"
}
},
clusters: [
{
id: "cluster1",
kubeConfigPath: invalidKubeconfig,
contextName: "test",
preferences: { terminalCWD: "/foo" },
workspace: "foo",
},
{
id: "cluster2",
kubeConfigPath: kubeconfig,
contextName: "foo",
preferences: { terminalCWD: "/foo" },
workspace: "default"
},
]
})
}
};
mockFs(mockOpts);
clusterStore = ClusterStore.getInstance<ClusterStore>();
return clusterStore.load();
});
afterEach(() => {
mockFs.restore();
});
it("does not enable clusters with invalid kubeconfig", () => {
const storedClusters = clusterStore.clustersList;
expect(storedClusters.length).toBe(2);
expect(storedClusters[0].enabled).toBeFalsy;
expect(storedClusters[1].id).toBe("cluster2");
expect(storedClusters[1].enabled).toBeTruthy;
});
});
describe("pre 2.0 config with an existing cluster", () => {
beforeEach(() => {
ClusterStore.resetInstance();
+101
View File
@@ -0,0 +1,101 @@
import { KubeConfig } from "@kubernetes/client-node";
import { validateKubeConfig } from "../kube-helpers";
const kubeconfig = `
apiVersion: v1
clusters:
- cluster:
server: https://localhost
name: test
contexts:
- context:
cluster: test
user: test
name: valid
- context:
cluster: test2
user: test
name: invalidCluster
- context:
cluster: test
user: test2
name: invalidUser
- context:
cluster: test
user: invalidExec
name: invalidExec
current-context: test
kind: Config
preferences: {}
users:
- name: test
user:
exec:
command: echo
- name: invalidExec
user:
exec:
command: foo
`;
const kc = new KubeConfig();
describe("validateKubeconfig", () => {
beforeAll(() => {
kc.loadFromString(kubeconfig);
});
describe("with default validation options", () => {
describe("with valid kubeconfig", () => {
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 validateCluster as false", () => {
describe("with invalid cluster object", () => {
it("does not raise exception", () => {
expect(() => { validateKubeConfig(kc, "invalidCluster", { validateCluster: false });}).not.toThrow();
});
});
});
describe("with validateUser as false", () => {
describe("with invalid user object", () => {
it("does not raise excpetions", () => {
expect(() => { validateKubeConfig(kc, "invalidUser", { validateUser: false });}).not.toThrow();
});
});
});
describe("with validateExec as false", () => {
describe("with invalid exec object", () => {
it("does not raise excpetions", () => {
expect(() => { validateKubeConfig(kc, "invalidExec", { validateExec: false });}).not.toThrow();
});
});
});
});
+2 -2
View File
@@ -323,7 +323,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
} else {
cluster = new Cluster(clusterModel);
if (!cluster.isManaged) {
if (!cluster.isManaged && cluster.apiUrl) {
cluster.enabled = true;
}
}
@@ -337,7 +337,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
}
});
this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null;
this.clusters.replace(newClusters);
this.removedClusters.replace(removedClusters);
}
+1
View File
@@ -1,3 +1,4 @@
export * from "./ipc";
export * from "./invalid-kubeconfig";
export * from "./update-available";
export * from "./type-enforced-ipc";
@@ -0,0 +1,3 @@
export const InvalidKubeconfigChannel = "invalid-kubeconfig";
export type InvalidKubeConfigArgs = [clusterId: string];
+32 -11
View File
@@ -7,6 +7,12 @@ import logger from "../main/logger";
import commandExists from "command-exists";
import { ExecValidationNotFoundError } from "./custom-errors";
export type KubeConfigValidationOpts = {
validateCluster?: boolean;
validateUser?: boolean;
validateExec?: boolean;
};
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
function resolveTilde(filePath: string) {
@@ -151,27 +157,42 @@ export function getNodeWarningConditions(node: V1Node) {
}
/**
* Validates kubeconfig supplied in the add clusters screen. At present this will just validate
* the User struct, specifically the command passed to the exec substructure.
*/
export function validateKubeConfig (config: KubeConfig) {
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
*/
export function validateKubeConfig (config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}) {
// we only receive a single context, cluster & user object here so lets validate them as this
// will be called when we add a new cluster to Lens
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
const contextObject = config.getContextObject(contextName);
// Validate the Context Object
if (!contextObject) {
throw new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
}
// Validate the Cluster Object
if (validateCluster && !config.getCluster(contextObject.cluster)) {
throw new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
}
const user = config.getUser(contextObject.user);
// Validate the User Object
const user = config.getCurrentUser();
if (user.exec) {
if (validateUser && !user) {
throw new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
}
// Validate exec command if present
if (validateExec && user?.exec) {
const execCommand = user.exec["command"];
// check if the command is absolute or not
const isAbsolute = path.isAbsolute(execCommand);
// validate the exec struct in the user object, start with the command field
logger.debug(`validateKubeConfig: validating user exec command - ${JSON.stringify(execCommand)}`);
if (!commandExists.sync(execCommand)) {
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${config.currentContext} not found`);
logger.debug(`validateKubeConfig: exec command ${String(execCommand)} in kubeconfig ${contextName} not found`);
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
}
}