mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
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:
parent
e837e6f1db
commit
4f74b9aabe
@ -6,6 +6,29 @@ import { ClusterStore, getClusterIdFromHost } from "../cluster-store";
|
|||||||
import { workspaceStore } from "../workspace-store";
|
import { workspaceStore } from "../workspace-store";
|
||||||
|
|
||||||
const testDataIcon = fs.readFileSync("test-data/cluster-store-migration-icon.png");
|
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", () => {
|
jest.mock("electron", () => {
|
||||||
return {
|
return {
|
||||||
@ -47,13 +70,13 @@ describe("empty config", () => {
|
|||||||
clusterStore.addCluster(
|
clusterStore.addCluster(
|
||||||
new Cluster({
|
new Cluster({
|
||||||
id: "foo",
|
id: "foo",
|
||||||
contextName: "minikube",
|
contextName: "foo",
|
||||||
preferences: {
|
preferences: {
|
||||||
terminalCWD: "/tmp",
|
terminalCWD: "/tmp",
|
||||||
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
icon: "data:image/jpeg;base64, iVBORw0KGgoAAAANSUhEUgAAA1wAAAKoCAYAAABjkf5",
|
||||||
clusterName: "minikube"
|
clusterName: "minikube"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", "fancy foo config"),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("foo", kubeconfig),
|
||||||
workspace: workspaceStore.currentWorkspaceId
|
workspace: workspaceStore.currentWorkspaceId
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -91,20 +114,20 @@ describe("empty config", () => {
|
|||||||
clusterStore.addClusters(
|
clusterStore.addClusters(
|
||||||
new Cluster({
|
new Cluster({
|
||||||
id: "prod",
|
id: "prod",
|
||||||
contextName: "prod",
|
contextName: "foo",
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "prod"
|
clusterName: "prod"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", "fancy config"),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("prod", kubeconfig),
|
||||||
workspace: "workstation"
|
workspace: "workstation"
|
||||||
}),
|
}),
|
||||||
new Cluster({
|
new Cluster({
|
||||||
id: "dev",
|
id: "dev",
|
||||||
contextName: "dev",
|
contextName: "foo2",
|
||||||
preferences: {
|
preferences: {
|
||||||
clusterName: "dev"
|
clusterName: "dev"
|
||||||
},
|
},
|
||||||
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", "fancy config"),
|
kubeConfigPath: ClusterStore.embedCustomKubeConfig("dev", kubeconfig),
|
||||||
workspace: "workstation"
|
workspace: "workstation"
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@ -177,20 +200,20 @@ describe("config with existing clusters", () => {
|
|||||||
clusters: [
|
clusters: [
|
||||||
{
|
{
|
||||||
id: "cluster1",
|
id: "cluster1",
|
||||||
kubeConfig: "foo",
|
kubeConfigPath: kubeconfig,
|
||||||
contextName: "foo",
|
contextName: "foo",
|
||||||
preferences: { terminalCWD: "/foo" },
|
preferences: { terminalCWD: "/foo" },
|
||||||
workspace: "default"
|
workspace: "default"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cluster2",
|
id: "cluster2",
|
||||||
kubeConfig: "foo2",
|
kubeConfigPath: kubeconfig,
|
||||||
contextName: "foo2",
|
contextName: "foo2",
|
||||||
preferences: { terminalCWD: "/foo2" }
|
preferences: { terminalCWD: "/foo2" }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "cluster3",
|
id: "cluster3",
|
||||||
kubeConfig: "foo",
|
kubeConfigPath: kubeconfig,
|
||||||
contextName: "foo",
|
contextName: "foo",
|
||||||
preferences: { terminalCWD: "/foo" },
|
preferences: { terminalCWD: "/foo" },
|
||||||
workspace: "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", () => {
|
describe("pre 2.0 config with an existing cluster", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ClusterStore.resetInstance();
|
ClusterStore.resetInstance();
|
||||||
|
|||||||
101
src/common/__tests__/kube-helpers.test.ts
Normal file
101
src/common/__tests__/kube-helpers.test.ts
Normal 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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -323,7 +323,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
} else {
|
} else {
|
||||||
cluster = new Cluster(clusterModel);
|
cluster = new Cluster(clusterModel);
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
if (!cluster.isManaged && cluster.apiUrl) {
|
||||||
cluster.enabled = true;
|
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.clusters.replace(newClusters);
|
||||||
this.removedClusters.replace(removedClusters);
|
this.removedClusters.replace(removedClusters);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export * from "./ipc";
|
export * from "./ipc";
|
||||||
|
export * from "./invalid-kubeconfig";
|
||||||
export * from "./update-available";
|
export * from "./update-available";
|
||||||
export * from "./type-enforced-ipc";
|
export * from "./type-enforced-ipc";
|
||||||
|
|||||||
3
src/common/ipc/invalid-kubeconfig/index.ts
Normal file
3
src/common/ipc/invalid-kubeconfig/index.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export const InvalidKubeconfigChannel = "invalid-kubeconfig";
|
||||||
|
|
||||||
|
export type InvalidKubeConfigArgs = [clusterId: string];
|
||||||
@ -7,6 +7,12 @@ 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";
|
||||||
|
|
||||||
|
export type KubeConfigValidationOpts = {
|
||||||
|
validateCluster?: boolean;
|
||||||
|
validateUser?: boolean;
|
||||||
|
validateExec?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
|
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
|
||||||
|
|
||||||
function resolveTilde(filePath: string) {
|
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
|
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
||||||
* the User struct, specifically the command passed to the exec substructure.
|
|
||||||
*/
|
*/
|
||||||
export function validateKubeConfig (config: KubeConfig) {
|
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
|
// 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
|
// 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
|
// Validate the User Object
|
||||||
const user = config.getCurrentUser();
|
if (validateUser && !user) {
|
||||||
|
throw new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||||
|
}
|
||||||
|
|
||||||
if (user.exec) {
|
// Validate exec command if present
|
||||||
|
if (validateExec && user?.exec) {
|
||||||
const execCommand = user.exec["command"];
|
const execCommand = user.exec["command"];
|
||||||
// check if the command is absolute or not
|
// check if the command is absolute or not
|
||||||
const isAbsolute = path.isAbsolute(execCommand);
|
const isAbsolute = path.isAbsolute(execCommand);
|
||||||
|
|
||||||
// validate the exec struct in the user object, start with the command field
|
// 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)) {
|
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);
|
throw new ExecValidationNotFoundError(execCommand, isAbsolute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,12 +4,12 @@ import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
|||||||
import type { WorkspaceId } from "../common/workspace-store";
|
import type { WorkspaceId } from "../common/workspace-store";
|
||||||
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
import { action, comparer, computed, observable, reaction, toJS, when } from "mobx";
|
||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { broadcastMessage } from "../common/ipc";
|
import { broadcastMessage, InvalidKubeconfigChannel } from "../common/ipc";
|
||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from "@kubernetes/client-node";
|
||||||
import { Kubectl } from "./kubectl";
|
import { Kubectl } from "./kubectl";
|
||||||
import { KubeconfigManager } from "./kubeconfig-manager";
|
import { KubeconfigManager } from "./kubeconfig-manager";
|
||||||
import { loadConfig } from "../common/kube-helpers";
|
import { loadConfig, validateKubeConfig } from "../common/kube-helpers";
|
||||||
import request, { RequestPromiseOptions } from "request-promise-native";
|
import request, { RequestPromiseOptions } from "request-promise-native";
|
||||||
import { apiResources, KubeApiResource } from "../common/rbac";
|
import { apiResources, KubeApiResource } from "../common/rbac";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@ -177,6 +177,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @observable
|
* @observable
|
||||||
*/
|
*/
|
||||||
@observable isAdmin = false;
|
@observable isAdmin = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
|
* Global watch-api accessibility , e.g. "/api/v1/services?watch=1"
|
||||||
*
|
*
|
||||||
@ -256,10 +257,16 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
|
|
||||||
|
try {
|
||||||
const kubeconfig = this.getKubeconfig();
|
const kubeconfig = this.getKubeconfig();
|
||||||
|
|
||||||
if (kubeconfig.getContextObject(this.contextName)) {
|
validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false});
|
||||||
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
||||||
|
} catch(err) {
|
||||||
|
logger.error(err);
|
||||||
|
logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`);
|
||||||
|
broadcastMessage(InvalidKubeconfigChannel, model.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -147,7 +147,7 @@ export class AddCluster extends React.Component {
|
|||||||
try {
|
try {
|
||||||
const kubeConfig = this.kubeContexts.get(context);
|
const kubeConfig = this.kubeContexts.get(context);
|
||||||
|
|
||||||
validateKubeConfig(kubeConfig);
|
validateKubeConfig(kubeConfig, context);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@ -52,7 +52,7 @@ export class CreateResource extends React.Component<Props> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (errors.length) {
|
if (errors.length) {
|
||||||
errors.forEach(Notifications.error);
|
errors.forEach(error => Notifications.error(error));
|
||||||
if (!createdResources.length) throw errors[0];
|
if (!createdResources.length) throw errors[0];
|
||||||
}
|
}
|
||||||
const successMessage = (
|
const successMessage = (
|
||||||
|
|||||||
@ -21,11 +21,12 @@ export class Notifications extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static error(message: NotificationMessage) {
|
static error(message: NotificationMessage, customOpts: Partial<Notification> = {}) {
|
||||||
notificationsStore.add({
|
notificationsStore.add({
|
||||||
message,
|
message,
|
||||||
timeout: 10000,
|
timeout: 10000,
|
||||||
status: NotificationStatus.ERROR
|
status: NotificationStatus.ERROR,
|
||||||
|
...customOpts
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { Notifications, notificationsStore } from "../components/notifications";
|
|||||||
import { Button } from "../components/button";
|
import { Button } from "../components/button";
|
||||||
import { isMac } from "../../common/vars";
|
import { isMac } from "../../common/vars";
|
||||||
import * as uuid from "uuid";
|
import * as uuid from "uuid";
|
||||||
|
import { invalidKubeconfigHandler } from "./invalid-kubeconfig-handler";
|
||||||
|
|
||||||
function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
|
function sendToBackchannel(backchannel: string, notificationId: string, data: BackchannelArg): void {
|
||||||
notificationsStore.remove(notificationId);
|
notificationsStore.remove(notificationId);
|
||||||
@ -58,4 +59,5 @@ export function registerIpcHandlers() {
|
|||||||
listener: UpdateAvailableHandler,
|
listener: UpdateAvailableHandler,
|
||||||
verifier: areArgsUpdateAvailableFromMain,
|
verifier: areArgsUpdateAvailableFromMain,
|
||||||
});
|
});
|
||||||
|
onCorrect(invalidKubeconfigHandler);
|
||||||
}
|
}
|
||||||
|
|||||||
46
src/renderer/ipc/invalid-kubeconfig-handler.tsx
Normal file
46
src/renderer/ipc/invalid-kubeconfig-handler.tsx
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ipcRenderer, IpcRendererEvent, shell } from "electron";
|
||||||
|
import { clusterStore } from "../../common/cluster-store";
|
||||||
|
import { InvalidKubeConfigArgs, InvalidKubeconfigChannel } from "../../common/ipc/invalid-kubeconfig";
|
||||||
|
import { Notifications, notificationsStore } from "../components/notifications";
|
||||||
|
import { Button } from "../components/button";
|
||||||
|
|
||||||
|
export const invalidKubeconfigHandler = {
|
||||||
|
source: ipcRenderer,
|
||||||
|
channel: InvalidKubeconfigChannel,
|
||||||
|
listener: InvalidKubeconfigListener,
|
||||||
|
verifier: (args: [unknown]): args is InvalidKubeConfigArgs => {
|
||||||
|
return args.length === 1 && typeof args[0] === "string" && !!clusterStore.getById(args[0]);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function InvalidKubeconfigListener(event: IpcRendererEvent, ...[clusterId]: InvalidKubeConfigArgs): void {
|
||||||
|
const notificationId = `invalid-kubeconfig:${clusterId}`;
|
||||||
|
const cluster = clusterStore.getById(clusterId);
|
||||||
|
const contextName = cluster.name !== cluster.contextName ? `(context: ${cluster.contextName})` : "";
|
||||||
|
|
||||||
|
Notifications.error(
|
||||||
|
(
|
||||||
|
<div className="flex column gaps">
|
||||||
|
<b>Cluster with Invalid Kubeconfig Detected!</b>
|
||||||
|
<p>Cluster <b>{cluster.name}</b> has invalid kubeconfig {contextName} and cannot be displayed.
|
||||||
|
Please fix the <a href="#" onClick={(e) => { e.preventDefault(); shell.showItemInFolder(cluster.kubeConfigPath); }}>kubeconfig</a> manually and restart Lens
|
||||||
|
or remove the cluster.</p>
|
||||||
|
<p>Do you want to remove the cluster now?</p>
|
||||||
|
<div className="flex gaps row align-left box grow">
|
||||||
|
<Button active outlined label="Remove" onClick={()=> {
|
||||||
|
clusterStore.removeById(clusterId);
|
||||||
|
notificationsStore.remove(notificationId);
|
||||||
|
}} />
|
||||||
|
<Button active outlined label="Cancel" onClick={() => notificationsStore.remove(notificationId)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
{
|
||||||
|
id: notificationId,
|
||||||
|
timeout: 0
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Loading…
Reference in New Issue
Block a user