mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
commit
76f9f9ffaa
@ -2,7 +2,7 @@
|
|||||||
"name": "kontena-lens",
|
"name": "kontena-lens",
|
||||||
"productName": "Lens",
|
"productName": "Lens",
|
||||||
"description": "Lens - The Kubernetes IDE",
|
"description": "Lens - The Kubernetes IDE",
|
||||||
"version": "4.1.3",
|
"version": "4.1.4",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2020, Mirantis, Inc.",
|
"copyright": "© 2020, Mirantis, Inc.",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
|||||||
@ -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, contextName: string, validationOpts: KubeConfigValidationOpts = {}) {
|
||||||
export function validateKubeConfig (config: KubeConfig) {
|
|
||||||
// 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -31,7 +31,7 @@ export const apiResources: KubeApiResource[] = [
|
|||||||
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
|
{ kind: "PersistentVolume", apiName: "persistentvolumes" },
|
||||||
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
|
{ kind: "PersistentVolumeClaim", apiName: "persistentvolumeclaims" },
|
||||||
{ kind: "Pod", apiName: "pods" },
|
{ kind: "Pod", apiName: "pods" },
|
||||||
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets" },
|
{ kind: "PodDisruptionBudget", apiName: "poddisruptionbudgets", group: "policy" },
|
||||||
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
|
{ kind: "PodSecurityPolicy", apiName: "podsecuritypolicies" },
|
||||||
{ kind: "ResourceQuota", apiName: "resourcequotas" },
|
{ kind: "ResourceQuota", apiName: "resourcequotas" },
|
||||||
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
|
{ kind: "ReplicaSet", apiName: "replicasets", group: "apps" },
|
||||||
|
|||||||
@ -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);
|
||||||
const kubeconfig = this.getKubeconfig();
|
|
||||||
|
|
||||||
if (kubeconfig.getContextObject(this.contextName)) {
|
try {
|
||||||
|
const kubeconfig = this.getKubeconfig();
|
||||||
|
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -120,12 +120,18 @@ export class LensProxy {
|
|||||||
protected createProxy(): httpProxy {
|
protected createProxy(): httpProxy {
|
||||||
const proxy = httpProxy.createProxyServer();
|
const proxy = httpProxy.createProxyServer();
|
||||||
|
|
||||||
proxy.on("proxyRes", (proxyRes, req) => {
|
proxy.on("proxyRes", (proxyRes, req, res) => {
|
||||||
const retryCounterId = this.getRequestId(req);
|
const retryCounterId = this.getRequestId(req);
|
||||||
|
|
||||||
if (this.retryCounters.has(retryCounterId)) {
|
if (this.retryCounters.has(retryCounterId)) {
|
||||||
this.retryCounters.delete(retryCounterId);
|
this.retryCounters.delete(retryCounterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!res.headersSent && req.url) {
|
||||||
|
const url = new URL(req.url, "http://localhost");
|
||||||
|
|
||||||
|
if (url.searchParams.has("watch")) res.flushHeaders();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
proxy.on("error", (error, req, res, target) => {
|
proxy.on("error", (error, req, res, target) => {
|
||||||
|
|||||||
@ -110,6 +110,14 @@ export class ShellSession extends EventEmitter {
|
|||||||
env["SystemRoot"] = process.env.SystemRoot;
|
env["SystemRoot"] = process.env.SystemRoot;
|
||||||
env["PTYSHELL"] = process.env.SHELL || "powershell.exe";
|
env["PTYSHELL"] = process.env.SHELL || "powershell.exe";
|
||||||
env["PATH"] = pathStr;
|
env["PATH"] = pathStr;
|
||||||
|
env["LENS_SESSION"] = "true";
|
||||||
|
const lensWslEnv = "KUBECONFIG/up:LENS_SESSION/u";
|
||||||
|
|
||||||
|
if (process.env.WSLENV != undefined) {
|
||||||
|
env["WSLENV"] = `${process.env["WSLENV"]}:${lensWslEnv}`;
|
||||||
|
} else {
|
||||||
|
env["WSLENV"] = lensWslEnv;
|
||||||
|
}
|
||||||
} else if(typeof(process.env.SHELL) != "undefined") {
|
} else if(typeof(process.env.SHELL) != "undefined") {
|
||||||
env["PTYSHELL"] = process.env.SHELL;
|
env["PTYSHELL"] = process.env.SHELL;
|
||||||
env["PATH"] = pathStr;
|
env["PATH"] = pathStr;
|
||||||
|
|||||||
@ -187,7 +187,7 @@ export class HelmRelease implements ItemObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
const versions = this.chart.match(/(v?\d+)[^-].*$/);
|
const versions = this.chart.match(/(?<=-)(v?\d+)[^-].*$/);
|
||||||
|
|
||||||
if (versions) {
|
if (versions) {
|
||||||
return versions[0];
|
return versions[0];
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -1,12 +1,21 @@
|
|||||||
import { RouteProps } from "react-router";
|
import { RouteProps } from "react-router";
|
||||||
import { Config } from "./config";
|
|
||||||
import { IURLParams } from "../../../common/utils/buildUrl";
|
import { IURLParams } from "../../../common/utils/buildUrl";
|
||||||
import { configMapsURL } from "../+config-maps/config-maps.route";
|
import { configMapsRoute, configMapsURL } from "../+config-maps/config-maps.route";
|
||||||
|
import { hpaRoute } from "../+config-autoscalers";
|
||||||
|
import { limitRangesRoute } from "../+config-limit-ranges";
|
||||||
|
import { pdbRoute } from "../+config-pod-disruption-budgets";
|
||||||
|
import { resourceQuotaRoute } from "../+config-resource-quotas";
|
||||||
|
import { secretsRoute } from "../+config-secrets";
|
||||||
|
|
||||||
export const configRoute: RouteProps = {
|
export const configRoute: RouteProps = {
|
||||||
get path() {
|
path: [
|
||||||
return Config.tabRoutes.map(({ routePath }) => routePath).flat();
|
configMapsRoute,
|
||||||
}
|
secretsRoute,
|
||||||
|
resourceQuotaRoute,
|
||||||
|
limitRangesRoute,
|
||||||
|
hpaRoute,
|
||||||
|
pdbRoute
|
||||||
|
].map(route => route.path.toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
export const configURL = (params?: IURLParams) => configMapsURL(params);
|
export const configURL = (params?: IURLParams) => configMapsURL(params);
|
||||||
|
|||||||
@ -1,12 +1,17 @@
|
|||||||
import { RouteProps } from "react-router";
|
import { RouteProps } from "react-router";
|
||||||
import { Network } from "./network";
|
import { endpointRoute } from "../+network-endpoints";
|
||||||
import { servicesURL } from "../+network-services";
|
import { ingressRoute } from "../+network-ingresses";
|
||||||
|
import { networkPoliciesRoute } from "../+network-policies";
|
||||||
|
import { servicesRoute, servicesURL } from "../+network-services";
|
||||||
import { IURLParams } from "../../../common/utils/buildUrl";
|
import { IURLParams } from "../../../common/utils/buildUrl";
|
||||||
|
|
||||||
export const networkRoute: RouteProps = {
|
export const networkRoute: RouteProps = {
|
||||||
get path() {
|
path: [
|
||||||
return Network.tabRoutes.map(({ routePath }) => routePath).flat();
|
servicesRoute,
|
||||||
}
|
endpointRoute,
|
||||||
|
ingressRoute,
|
||||||
|
networkPoliciesRoute
|
||||||
|
].map(route => route.path.toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
export const networkURL = (params?: IURLParams) => servicesURL(params);
|
export const networkURL = (params?: IURLParams) => servicesURL(params);
|
||||||
|
|||||||
@ -1,3 +1,2 @@
|
|||||||
export * from "./pod-security-policies.route";
|
|
||||||
export * from "./pod-security-policies";
|
export * from "./pod-security-policies";
|
||||||
export * from "./pod-security-policy-details";
|
export * from "./pod-security-policy-details";
|
||||||
|
|||||||
@ -1,8 +0,0 @@
|
|||||||
import type { RouteProps } from "react-router";
|
|
||||||
import { buildURL } from "../../../common/utils/buildUrl";
|
|
||||||
|
|
||||||
export const podSecurityPoliciesRoute: RouteProps = {
|
|
||||||
path: "/pod-security-policies"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path);
|
|
||||||
@ -1,12 +1,15 @@
|
|||||||
import { RouteProps } from "react-router";
|
import { RouteProps } from "react-router";
|
||||||
import { volumeClaimsURL } from "../+storage-volume-claims";
|
import { storageClassesRoute } from "../+storage-classes";
|
||||||
import { Storage } from "./storage";
|
import { volumeClaimsRoute, volumeClaimsURL } from "../+storage-volume-claims";
|
||||||
|
import { volumesRoute } from "../+storage-volumes";
|
||||||
import { IURLParams } from "../../../common/utils/buildUrl";
|
import { IURLParams } from "../../../common/utils/buildUrl";
|
||||||
|
|
||||||
export const storageRoute: RouteProps = {
|
export const storageRoute: RouteProps = {
|
||||||
get path() {
|
path: [
|
||||||
return Storage.tabRoutes.map(({ routePath }) => routePath).flat();
|
volumeClaimsRoute,
|
||||||
}
|
volumesRoute,
|
||||||
|
storageClassesRoute
|
||||||
|
].map(route => route.path.toString())
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storageURL = (params?: IURLParams) => volumeClaimsURL(params);
|
export const storageURL = (params?: IURLParams) => volumeClaimsURL(params);
|
||||||
|
|||||||
@ -1,12 +1,5 @@
|
|||||||
import type { RouteProps } from "react-router";
|
import type { RouteProps } from "react-router";
|
||||||
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
||||||
import { UserManagement } from "./user-management";
|
|
||||||
|
|
||||||
export const usersManagementRoute: RouteProps = {
|
|
||||||
get path() {
|
|
||||||
return UserManagement.tabRoutes.map(({ routePath }) => routePath).flat();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
export const serviceAccountsRoute: RouteProps = {
|
export const serviceAccountsRoute: RouteProps = {
|
||||||
@ -18,6 +11,18 @@ export const rolesRoute: RouteProps = {
|
|||||||
export const roleBindingsRoute: RouteProps = {
|
export const roleBindingsRoute: RouteProps = {
|
||||||
path: "/role-bindings"
|
path: "/role-bindings"
|
||||||
};
|
};
|
||||||
|
export const podSecurityPoliciesRoute: RouteProps = {
|
||||||
|
path: "/pod-security-policies"
|
||||||
|
};
|
||||||
|
|
||||||
|
export const usersManagementRoute: RouteProps = {
|
||||||
|
path: [
|
||||||
|
serviceAccountsRoute,
|
||||||
|
roleBindingsRoute,
|
||||||
|
rolesRoute,
|
||||||
|
podSecurityPoliciesRoute
|
||||||
|
].map(route => route.path.toString())
|
||||||
|
};
|
||||||
|
|
||||||
// Route params
|
// Route params
|
||||||
export interface IServiceAccountsRouteParams {
|
export interface IServiceAccountsRouteParams {
|
||||||
@ -34,3 +39,4 @@ export const usersManagementURL = (params?: IURLParams) => serviceAccountsURL(pa
|
|||||||
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path);
|
export const serviceAccountsURL = buildURL<IServiceAccountsRouteParams>(serviceAccountsRoute.path);
|
||||||
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path);
|
export const roleBindingsURL = buildURL<IRoleBindingsRouteParams>(roleBindingsRoute.path);
|
||||||
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path);
|
export const rolesURL = buildURL<IRoleBindingsRouteParams>(rolesRoute.path);
|
||||||
|
export const podSecurityPoliciesURL = buildURL(podSecurityPoliciesRoute.path);
|
||||||
|
|||||||
@ -5,9 +5,9 @@ import { TabLayout, TabLayoutRoute } from "../layout/tab-layout";
|
|||||||
import { Roles } from "../+user-management-roles";
|
import { Roles } from "../+user-management-roles";
|
||||||
import { RoleBindings } from "../+user-management-roles-bindings";
|
import { RoleBindings } from "../+user-management-roles-bindings";
|
||||||
import { ServiceAccounts } from "../+user-management-service-accounts";
|
import { ServiceAccounts } from "../+user-management-service-accounts";
|
||||||
import { roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
|
import { podSecurityPoliciesRoute, podSecurityPoliciesURL, roleBindingsRoute, roleBindingsURL, rolesRoute, rolesURL, serviceAccountsRoute, serviceAccountsURL } from "./user-management.route";
|
||||||
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
import { namespaceUrlParam } from "../+namespaces/namespace.store";
|
||||||
import { PodSecurityPolicies, podSecurityPoliciesRoute, podSecurityPoliciesURL } from "../+pod-security-policies";
|
import { PodSecurityPolicies } from "../+pod-security-policies";
|
||||||
import { isAllowedResource } from "../../../common/rbac";
|
import { isAllowedResource } from "../../../common/rbac";
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
.PodDetailsSecrets {
|
.PodDetailsSecrets {
|
||||||
a {
|
> * {
|
||||||
display: block;
|
display: block;
|
||||||
margin-bottom: $margin;
|
margin-bottom: var(--margin);
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
|||||||
@ -13,33 +13,49 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class PodDetailsSecrets extends Component<Props> {
|
export class PodDetailsSecrets extends Component<Props> {
|
||||||
@observable secrets: Secret[] = [];
|
@observable secrets: Map<string, Secret> = observable.map<string, Secret>();
|
||||||
|
|
||||||
@disposeOnUnmount
|
@disposeOnUnmount
|
||||||
secretsLoader = autorun(async () => {
|
secretsLoader = autorun(async () => {
|
||||||
const { pod } = this.props;
|
const { pod } = this.props;
|
||||||
|
|
||||||
this.secrets = await Promise.all(
|
const secrets = await Promise.all(
|
||||||
pod.getSecrets().map(secretName => secretsApi.get({
|
pod.getSecrets().map(secretName => secretsApi.get({
|
||||||
name: secretName,
|
name: secretName,
|
||||||
namespace: pod.getNs(),
|
namespace: pod.getNs(),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
secrets.forEach(secret => secret && this.secrets.set(secret.getName(), secret));
|
||||||
});
|
});
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
const { pod } = this.props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="PodDetailsSecrets">
|
<div className="PodDetailsSecrets">
|
||||||
{
|
{
|
||||||
this.secrets.map(secret => {
|
pod.getSecrets().map(secretName => {
|
||||||
return (
|
const secret = this.secrets.get(secretName);
|
||||||
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
|
||||||
{secret.getName()}
|
if (secret) {
|
||||||
</Link>
|
return this.renderSecretLink(secret);
|
||||||
);
|
} else {
|
||||||
|
return (
|
||||||
|
<span key={secretName}>{secretName}</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected renderSecretLink(secret: Secret) {
|
||||||
|
return (
|
||||||
|
<Link key={secret.getId()} to={getDetailsUrl(secret.selfLink)}>
|
||||||
|
{secret.getName()}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,6 @@
|
|||||||
import type { RouteProps } from "react-router";
|
import type { RouteProps } from "react-router";
|
||||||
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
import { buildURL, IURLParams } from "../../../common/utils/buildUrl";
|
||||||
import { KubeResource } from "../../../common/rbac";
|
import { KubeResource } from "../../../common/rbac";
|
||||||
import { Workloads } from "./workloads";
|
|
||||||
|
|
||||||
export const workloadsRoute: RouteProps = {
|
|
||||||
get path() {
|
|
||||||
return Workloads.tabRoutes.map(({ routePath }) => routePath).flat();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
export const overviewRoute: RouteProps = {
|
export const overviewRoute: RouteProps = {
|
||||||
@ -35,6 +28,19 @@ export const cronJobsRoute: RouteProps = {
|
|||||||
path: "/cronjobs"
|
path: "/cronjobs"
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const workloadsRoute: RouteProps = {
|
||||||
|
path: [
|
||||||
|
overviewRoute,
|
||||||
|
podsRoute,
|
||||||
|
deploymentsRoute,
|
||||||
|
daemonSetsRoute,
|
||||||
|
statefulSetsRoute,
|
||||||
|
replicaSetsRoute,
|
||||||
|
jobsRoute,
|
||||||
|
cronJobsRoute
|
||||||
|
].map(route => route.path.toString())
|
||||||
|
};
|
||||||
|
|
||||||
// Route params
|
// Route params
|
||||||
export interface IWorkloadsOverviewRouteParams {
|
export interface IWorkloadsOverviewRouteParams {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -91,27 +91,15 @@ export class App extends React.Component {
|
|||||||
reaction(() => this.warningsTotal, (count: number) => {
|
reaction(() => this.warningsTotal, (count: number) => {
|
||||||
broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count);
|
broadcastMessage(`cluster-warning-event-count:${getHostedCluster().id}`, count);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
reaction(() => clusterPageMenuRegistry.getRootItems(), (rootItems) => {
|
|
||||||
this.generateExtensionTabLayoutRoutes(rootItems);
|
|
||||||
}, {
|
|
||||||
fireImmediately: true
|
|
||||||
})
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@observable startUrl = isAllowedResource(["events", "nodes", "pods"]) ? clusterURL() : workloadsURL();
|
||||||
|
|
||||||
@computed get warningsTotal(): number {
|
@computed get warningsTotal(): number {
|
||||||
return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
|
return nodesStore.getWarningsCount() + eventStore.getWarningsCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
get startURL() {
|
|
||||||
if (isAllowedResource(["events", "nodes", "pods"])) {
|
|
||||||
return clusterURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
return workloadsURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
getTabLayoutRoutes(menuItem: ClusterPageMenuRegistration) {
|
||||||
const routes: TabLayoutRoute[] = [];
|
const routes: TabLayoutRoute[] = [];
|
||||||
|
|
||||||
@ -152,38 +140,6 @@ export class App extends React.Component {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@observable extensionRoutes: Map<ClusterPageMenuRegistration, React.ReactNode> = new Map();
|
|
||||||
|
|
||||||
generateExtensionTabLayoutRoutes(rootItems: ClusterPageMenuRegistration[]) {
|
|
||||||
rootItems.forEach((menu, index) => {
|
|
||||||
let route = this.extensionRoutes.get(menu);
|
|
||||||
|
|
||||||
if (!route) {
|
|
||||||
const tabRoutes = this.getTabLayoutRoutes(menu);
|
|
||||||
|
|
||||||
if (tabRoutes.length > 0) {
|
|
||||||
const pageComponent = () => <TabLayout tabs={tabRoutes}/>;
|
|
||||||
|
|
||||||
route = <Route key={`extension-tab-layout-route-${index}`} component={pageComponent} path={tabRoutes.map((tab) => tab.routePath)}/>;
|
|
||||||
this.extensionRoutes.set(menu, route);
|
|
||||||
} else {
|
|
||||||
const page = clusterPageRegistry.getByPageTarget(menu.target);
|
|
||||||
|
|
||||||
if (page) {
|
|
||||||
route = <Route key={`extension-tab-layout-route-${index}`} path={page.url} component={page.components.Page}/>;
|
|
||||||
this.extensionRoutes.set(menu, route);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const menu of this.extensionRoutes.keys()) {
|
|
||||||
if (!rootItems.includes(menu)) {
|
|
||||||
this.extensionRoutes.delete(menu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
renderExtensionRoutes() {
|
renderExtensionRoutes() {
|
||||||
return clusterPageRegistry.getItems().map((page, index) => {
|
return clusterPageRegistry.getItems().map((page, index) => {
|
||||||
const menu = clusterPageMenuRegistry.getByPage(page);
|
const menu = clusterPageMenuRegistry.getByPage(page);
|
||||||
@ -195,8 +151,6 @@ export class App extends React.Component {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const cluster = getHostedCluster();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Router history={history}>
|
<Router history={history}>
|
||||||
<ErrorBoundary>
|
<ErrorBoundary>
|
||||||
@ -215,7 +169,7 @@ export class App extends React.Component {
|
|||||||
<Route component={Apps} {...appsRoute}/>
|
<Route component={Apps} {...appsRoute}/>
|
||||||
{this.renderExtensionTabLayoutRoutes()}
|
{this.renderExtensionTabLayoutRoutes()}
|
||||||
{this.renderExtensionRoutes()}
|
{this.renderExtensionRoutes()}
|
||||||
<Redirect exact from="/" to={this.startURL}/>
|
<Redirect exact from="/" to={this.startUrl}/>
|
||||||
<Route component={NotFound}/>
|
<Route component={NotFound}/>
|
||||||
</Switch>
|
</Switch>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
@ -228,7 +182,7 @@ export class App extends React.Component {
|
|||||||
<StatefulSetScaleDialog/>
|
<StatefulSetScaleDialog/>
|
||||||
<ReplicaSetScaleDialog/>
|
<ReplicaSetScaleDialog/>
|
||||||
<CronJobTriggerDialog/>
|
<CronJobTriggerDialog/>
|
||||||
<CommandContainer cluster={cluster}/>
|
<CommandContainer clusterId={getHostedCluster()?.id}/>
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,42 +0,0 @@
|
|||||||
import ChartJS from "chart.js";
|
|
||||||
import get from "lodash/get";
|
|
||||||
|
|
||||||
const defaultOptions = {
|
|
||||||
interval: 61,
|
|
||||||
coverBars: 3,
|
|
||||||
borderColor: "#44474A",
|
|
||||||
backgroundColor: "#00000033"
|
|
||||||
};
|
|
||||||
|
|
||||||
export const BackgroundBlock = {
|
|
||||||
options: {},
|
|
||||||
|
|
||||||
getOptions(chart: ChartJS) {
|
|
||||||
return get(chart, "options.plugins.BackgroundBlock");
|
|
||||||
},
|
|
||||||
|
|
||||||
afterInit(chart: ChartJS) {
|
|
||||||
this.options = {
|
|
||||||
...defaultOptions,
|
|
||||||
...this.getOptions(chart)
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
beforeDraw(chart: ChartJS) {
|
|
||||||
if (!chart.chartArea) return;
|
|
||||||
const { interval, coverBars, borderColor, backgroundColor } = this.options;
|
|
||||||
const { ctx, chartArea } = chart;
|
|
||||||
const { left, right, top, bottom } = chartArea;
|
|
||||||
const blockWidth = (right - left) / interval * coverBars;
|
|
||||||
|
|
||||||
ctx.save();
|
|
||||||
ctx.fillStyle = backgroundColor;
|
|
||||||
ctx.strokeStyle = borderColor;
|
|
||||||
ctx.fillRect(right - blockWidth, top, blockWidth, bottom - top);
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.moveTo(right - blockWidth + 1.5, top);
|
|
||||||
ctx.lineTo(right - blockWidth + 1.5, bottom);
|
|
||||||
ctx.stroke();
|
|
||||||
ctx.restore();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@ -1,45 +0,0 @@
|
|||||||
import moment from "moment";
|
|
||||||
import { useState, useEffect } from "react";
|
|
||||||
import { useInterval } from "../../hooks";
|
|
||||||
|
|
||||||
type IMetricValues = [number, string][];
|
|
||||||
type IChartData = { x: number; y: string }[];
|
|
||||||
|
|
||||||
const defaultParams = {
|
|
||||||
fetchInterval: 15,
|
|
||||||
updateInterval: 5
|
|
||||||
};
|
|
||||||
|
|
||||||
export function useRealTimeMetrics(metrics: IMetricValues, chartData: IChartData, params = defaultParams) {
|
|
||||||
const [index, setIndex] = useState(0);
|
|
||||||
const { fetchInterval, updateInterval } = params;
|
|
||||||
const rangeMetrics = metrics.slice(-updateInterval);
|
|
||||||
const steps = fetchInterval / updateInterval;
|
|
||||||
const data = [...chartData];
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setIndex(0);
|
|
||||||
}, [metrics]);
|
|
||||||
|
|
||||||
useInterval(() => {
|
|
||||||
if (index < steps + 1) {
|
|
||||||
setIndex(index + steps - 1);
|
|
||||||
}
|
|
||||||
}, updateInterval * 1000);
|
|
||||||
|
|
||||||
if (data.length && metrics.length) {
|
|
||||||
const lastTime = data[data.length - 1].x;
|
|
||||||
const values = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
values[i] = moment.unix(lastTime).add(i + 1, "m").unix();
|
|
||||||
}
|
|
||||||
data.push(
|
|
||||||
{ x: values[0], y: "0" },
|
|
||||||
{ x: values[1], y: parseFloat(rangeMetrics[index][1]).toFixed(3) },
|
|
||||||
{ x: values[2], y: "0" }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
@ -10,7 +10,6 @@ import { CommandDialog } from "./command-dialog";
|
|||||||
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
import { CommandRegistration, commandRegistry } from "../../../extensions/registries/command-registry";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
import { workspaceStore } from "../../../common/workspace-store";
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
|
|
||||||
export type CommandDialogEvent = {
|
export type CommandDialogEvent = {
|
||||||
component: React.ReactElement
|
component: React.ReactElement
|
||||||
@ -29,7 +28,7 @@ export class CommandOverlay {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class CommandContainer extends React.Component<{cluster?: Cluster}> {
|
export class CommandContainer extends React.Component<{ clusterId?: string }> {
|
||||||
@observable.ref commandComponent: React.ReactElement;
|
@observable.ref commandComponent: React.ReactElement;
|
||||||
|
|
||||||
private escHandler(event: KeyboardEvent) {
|
private escHandler(event: KeyboardEvent) {
|
||||||
@ -56,8 +55,8 @@ export class CommandContainer extends React.Component<{cluster?: Cluster}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
if (this.props.cluster) {
|
if (this.props.clusterId) {
|
||||||
subscribeToBroadcast(`command-palette:run-action:${this.props.cluster.id}`, (event, commandId: string) => {
|
subscribeToBroadcast(`command-palette:run-action:${this.props.clusterId}`, (event, commandId: string) => {
|
||||||
const command = this.findCommandById(commandId);
|
const command = this.findCommandById(commandId);
|
||||||
|
|
||||||
if (command) {
|
if (command) {
|
||||||
|
|||||||
@ -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 = (
|
||||||
|
|||||||
@ -89,7 +89,8 @@ const defaultProps: Partial<ItemListLayoutProps> = {
|
|||||||
filterItems: [],
|
filterItems: [],
|
||||||
hasDetailsView: true,
|
hasDetailsView: true,
|
||||||
onDetails: noop,
|
onDetails: noop,
|
||||||
virtual: true
|
virtual: true,
|
||||||
|
customizeTableRowProps: () => ({} as TableRowProps),
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ItemListLayoutUserSettings {
|
interface ItemListLayoutUserSettings {
|
||||||
@ -241,7 +242,7 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
sortItem={item}
|
sortItem={item}
|
||||||
selected={detailsItem && detailsItem.getId() === itemId}
|
selected={detailsItem && detailsItem.getId() === itemId}
|
||||||
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined}
|
onClick={hasDetailsView ? prevDefault(() => onDetails(item)) : undefined}
|
||||||
{...(customizeTableRowProps ? customizeTableRowProps(item) : {})}
|
{...customizeTableRowProps(item)}
|
||||||
>
|
>
|
||||||
{isSelectable && (
|
{isSelectable && (
|
||||||
<TableCell
|
<TableCell
|
||||||
@ -392,19 +393,21 @@ export class ItemListLayout extends React.Component<ItemListLayoutProps> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderTableHeader() {
|
renderTableHeader() {
|
||||||
const { renderTableHeader, isSelectable, isConfigurable, store } = this.props;
|
const { customizeTableRowProps, renderTableHeader, isSelectable, isConfigurable, store } = this.props;
|
||||||
|
|
||||||
if (!renderTableHeader) {
|
if (!renderTableHeader) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const enabledItems = this.items.filter(item => !customizeTableRowProps(item).disabled);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableHead showTopLine nowrap>
|
<TableHead showTopLine nowrap>
|
||||||
{isSelectable && (
|
{isSelectable && (
|
||||||
<TableCell
|
<TableCell
|
||||||
checkbox
|
checkbox
|
||||||
isChecked={store.isSelectedAll(this.items)}
|
isChecked={store.isSelectedAll(enabledItems)}
|
||||||
onClick={prevDefault(() => store.toggleSelectionAll(this.items))}
|
onClick={prevDefault(() => store.toggleSelectionAll(enabledItems))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{renderTableHeader.map((cellProps, index) => {
|
{renderTableHeader.map((cellProps, index) => {
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { observer } from "mobx-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { clusterSettingsURL } from "../+cluster-settings";
|
import { clusterSettingsURL } from "../+cluster-settings";
|
||||||
@ -11,7 +12,7 @@ interface Props {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MainLayoutHeader({ cluster, className }: Props) {
|
export const MainLayoutHeader = observer(({ cluster, className }: Props) => {
|
||||||
return (
|
return (
|
||||||
<header className={cssNames("flex gaps align-center justify-space-between", className)}>
|
<header className={cssNames("flex gaps align-center justify-space-between", className)}>
|
||||||
<span className="cluster">{cluster.name}</span>
|
<span className="cluster">{cluster.name}</span>
|
||||||
@ -29,4 +30,4 @@ export function MainLayoutHeader({ cluster, className }: Props) {
|
|||||||
/>
|
/>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -2,7 +2,18 @@
|
|||||||
|
|
||||||
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
Here you can find description of changes we've built into each release. While we try our best to make each upgrade automatic and as smooth as possible, there may be some cases where you might need to do something to ensure the application works smoothly. So please read through the release highlights!
|
||||||
|
|
||||||
## 4.1.3 (current version)
|
## 4.1.4 (current version)
|
||||||
|
|
||||||
|
- Ignore clusters with invalid kubeconfig
|
||||||
|
- Render only secret name on pod details without access to secrets
|
||||||
|
- Pass Lens wslenvs to terminal session on Windows
|
||||||
|
- Prevent top-level re-rendering on cluster refresh
|
||||||
|
- Extract chart version ignoring numbers in chart name
|
||||||
|
- The select all checkbox should not select disabled items
|
||||||
|
- Fix: Pdb should have policy group
|
||||||
|
- Fix: kubectl rollout not exiting properly on Lens terminal
|
||||||
|
|
||||||
|
## 4.1.3
|
||||||
|
|
||||||
- Don't reset selected namespaces to defaults in case of "All namespaces" on page reload
|
- Don't reset selected namespaces to defaults in case of "All namespaces" on page reload
|
||||||
- Fix loading all namespaces for users with limited cluster access
|
- Fix loading all namespaces for users with limited cluster access
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user