mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
mark uncreatable Cluster objects in cluster-store as dead
- Display a special error for each of the currently 2 error types from loading the kubeconfig - Special handling of dead clusters in proxy handler so that there are no unhandled promise rejections Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
238756be72
commit
b3a01fac2f
@ -21,9 +21,7 @@ module.exports = {
|
|||||||
sourceType: 'module',
|
sourceType: 'module',
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
"indent": ["error", 2, {
|
"indent": ["error", 2],
|
||||||
"SwitchCase": 1,
|
|
||||||
}],
|
|
||||||
"no-unused-vars": "off",
|
"no-unused-vars": "off",
|
||||||
"semi": ["error", "always"],
|
"semi": ["error", "always"],
|
||||||
"object-shorthand": "error",
|
"object-shorthand": "error",
|
||||||
|
|||||||
@ -9,7 +9,7 @@ import { comparer } from "mobx";
|
|||||||
export class Tracker extends Util.Singleton {
|
export class Tracker extends Util.Singleton {
|
||||||
static readonly GA_ID = "UA-159377374-1";
|
static readonly GA_ID = "UA-159377374-1";
|
||||||
static readonly SEGMENT_KEY = "YENwswyhlOgz8P7EFKUtIZ2MfON7Yxqb";
|
static readonly SEGMENT_KEY = "YENwswyhlOgz8P7EFKUtIZ2MfON7Yxqb";
|
||||||
protected eventHandlers: Array<(ev: EventBus.AppEvent ) => void> = [];
|
protected eventHandlers: Array<(ev: EventBus.AppEvent) => void> = [];
|
||||||
protected started = false;
|
protected started = false;
|
||||||
protected visitor: ua.Visitor;
|
protected visitor: ua.Visitor;
|
||||||
protected analytics: Analytics;
|
protected analytics: Analytics;
|
||||||
@ -124,30 +124,28 @@ export class Tracker extends Util.Singleton {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected resolveOS() {
|
protected resolveOS() {
|
||||||
let os = "";
|
|
||||||
if (App.isMac) {
|
if (App.isMac) {
|
||||||
os = "MacOS";
|
return "MacOS";
|
||||||
} else if(App.isWindows) {
|
|
||||||
os = "Windows";
|
|
||||||
} else if (App.isLinux) {
|
|
||||||
os = "Linux";
|
|
||||||
if (App.isSnap) {
|
|
||||||
os += "; Snap";
|
|
||||||
} else {
|
|
||||||
os += "; AppImage";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
os = "Unknown";
|
|
||||||
}
|
}
|
||||||
return os;
|
|
||||||
|
if (App.isWindows) {
|
||||||
|
return "Windows";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (App.isLinux) {
|
||||||
|
return `Linux; ${App.isSnap ? "Snap" : "AppImage"}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "Unknown";
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
protected async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
||||||
|
const allowed = await this.isTelemetryAllowed();
|
||||||
|
if (!allowed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const allowed = await this.isTelemetryAllowed();
|
|
||||||
if (!allowed) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.visitor.event({
|
this.visitor.event({
|
||||||
ec: eventCategory,
|
ec: eventCategory,
|
||||||
ea: eventAction,
|
ea: eventAction,
|
||||||
|
|||||||
@ -8,13 +8,14 @@ import { Cluster, ClusterState } from "../main/cluster";
|
|||||||
import migrations from "../migrations/cluster-store";
|
import migrations from "../migrations/cluster-store";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import { appEventBus } from "./event-bus";
|
import { appEventBus } from "./event-bus";
|
||||||
import { dumpConfigYaml } from "./kube-helpers";
|
import { dumpConfigYaml, LoadKubeError } from "./kube-helpers";
|
||||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc";
|
||||||
import _ from "lodash";
|
import _ from "lodash";
|
||||||
import move from "array-move";
|
import move from "array-move";
|
||||||
import type { WorkspaceId } from "./workspace-store";
|
import type { WorkspaceId } from "./workspace-store";
|
||||||
|
import { notificationsStore, NotificationStatus } from "./notifications.store";
|
||||||
|
|
||||||
export interface ClusterIconUpload {
|
export interface ClusterIconUpload {
|
||||||
clusterId: string;
|
clusterId: string;
|
||||||
@ -73,6 +74,14 @@ export interface ClusterPrometheusPreferences {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ClusterRenderInfo extends ClusterModel {
|
||||||
|
DeadError?: LoadKubeError; // this != undefined => dead
|
||||||
|
isAdmin: boolean;
|
||||||
|
name: string;
|
||||||
|
eventCount: number;
|
||||||
|
online: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||||
static getCustomKubeConfigPath(clusterId: ClusterId): string {
|
static getCustomKubeConfigPath(clusterId: ClusterId): string {
|
||||||
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
|
return path.resolve((app || remote.app).getPath("userData"), "kubeconfigs", clusterId);
|
||||||
@ -88,6 +97,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
@observable activeCluster: ClusterId;
|
@observable activeCluster: ClusterId;
|
||||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||||
|
@observable deadClusters = observable.map<ClusterId, [ClusterModel, LoadKubeError]>();
|
||||||
|
|
||||||
private constructor() {
|
private constructor() {
|
||||||
super({
|
super({
|
||||||
@ -137,6 +147,10 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return Array.from(this.clusters.values());
|
return Array.from(this.clusters.values());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed get deadClustersList(): [ClusterModel, LoadKubeError][] {
|
||||||
|
return Array.from(this.deadClusters.values());
|
||||||
|
}
|
||||||
|
|
||||||
@computed get enabledClustersList(): Cluster[] {
|
@computed get enabledClustersList(): Cluster[] {
|
||||||
return this.clustersList.filter((c) => c.enabled);
|
return this.clustersList.filter((c) => c.enabled);
|
||||||
}
|
}
|
||||||
@ -182,10 +196,32 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
return this.clusters.get(id);
|
return this.clusters.get(id);
|
||||||
}
|
}
|
||||||
|
|
||||||
getByWorkspaceId(workspaceId: string): Cluster[] {
|
getDeadById(id: ClusterId): [ClusterModel, LoadKubeError] {
|
||||||
const clusters = Array.from(this.clusters.values())
|
return this.deadClusters.get(id);
|
||||||
.filter(cluster => cluster.workspace === workspaceId);
|
}
|
||||||
return _.sortBy(clusters, cluster => cluster.preferences.iconOrder);
|
|
||||||
|
getByWorkspaceId(workspaceId: string): ClusterRenderInfo[] {
|
||||||
|
const aliveClusters: ClusterRenderInfo[] = this.clustersList.filter(c => c.workspace === workspaceId);
|
||||||
|
const deadClusters: ClusterRenderInfo[] = this.deadClustersList
|
||||||
|
.filter(([c]) => c.workspace === workspaceId)
|
||||||
|
.map(([cluster, error]) => ({
|
||||||
|
DeadError: error,
|
||||||
|
name: cluster.contextName,
|
||||||
|
isAdmin: false,
|
||||||
|
eventCount: 0,
|
||||||
|
online: false,
|
||||||
|
...cluster,
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
return _.sortBy([...aliveClusters, ...deadClusters], c => c.preferences?.iconOrder);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCountInWorkspace(workspaceId: string): number {
|
||||||
|
const aliveCount = this.clustersList.filter(c => c.workspace === workspaceId).length;
|
||||||
|
const deadCount = this.deadClustersList.filter(([c]) => c.workspace === workspaceId).length;
|
||||||
|
|
||||||
|
return aliveCount + deadCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
@ -241,6 +277,7 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
const currentClusters = this.clusters.toJS();
|
const currentClusters = this.clusters.toJS();
|
||||||
const newClusters = new Map<ClusterId, Cluster>();
|
const newClusters = new Map<ClusterId, Cluster>();
|
||||||
const removedClusters = new Map<ClusterId, Cluster>();
|
const removedClusters = new Map<ClusterId, Cluster>();
|
||||||
|
const deadClusters = new Map<ClusterId, [ClusterModel, LoadKubeError]>();
|
||||||
|
|
||||||
// update new clusters
|
// update new clusters
|
||||||
for (const clusterModel of clusters) {
|
for (const clusterModel of clusters) {
|
||||||
@ -248,7 +285,29 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
if (cluster) {
|
if (cluster) {
|
||||||
cluster.updateModel(clusterModel);
|
cluster.updateModel(clusterModel);
|
||||||
} else {
|
} else {
|
||||||
cluster = new Cluster(clusterModel);
|
try {
|
||||||
|
cluster = new Cluster(clusterModel);
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof LoadKubeError) {
|
||||||
|
deadClusters.set(clusterModel.id, [clusterModel, err]);
|
||||||
|
logger.error(`[CLUSTER-STORE]: marking cluster as dead`, {
|
||||||
|
err,
|
||||||
|
name: clusterModel.contextName,
|
||||||
|
id: clusterModel.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
notificationsStore.add({
|
||||||
|
message: `Cluster ${clusterModel.contextName} is reporting an error: ${err.toString()}`,
|
||||||
|
id: `${clusterModel.id}_IS_DEAD_NOTIFICATION`, // this **should** prevent double errors because we are loading the stores twice
|
||||||
|
status: NotificationStatus.ERROR,
|
||||||
|
timeout: 10000,
|
||||||
|
});
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
if (!cluster.isManaged) {
|
if (!cluster.isManaged) {
|
||||||
cluster.enabled = true;
|
cluster.enabled = true;
|
||||||
}
|
}
|
||||||
@ -264,14 +323,22 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
|
this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
|
||||||
|
this.deadClusters.replace(deadClusters);
|
||||||
this.clusters.replace(newClusters);
|
this.clusters.replace(newClusters);
|
||||||
this.removedClusters.replace(removedClusters);
|
this.removedClusters.replace(removedClusters);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed private get clustersAsJson(): ClusterModel[] {
|
||||||
|
const clusters = this.clustersList.map(c => c.toJSON());
|
||||||
|
const dead = Array.from(this.deadClusters.values()).map(([c]) => c);
|
||||||
|
|
||||||
|
return [...clusters, ...dead];
|
||||||
|
}
|
||||||
|
|
||||||
toJSON(): ClusterStoreModel {
|
toJSON(): ClusterStoreModel {
|
||||||
return toJS({
|
return toJS({
|
||||||
activeCluster: this.activeCluster,
|
activeCluster: this.activeCluster,
|
||||||
clusters: this.clustersList.map(cluster => cluster.toJSON()),
|
clusters: this.clustersAsJson,
|
||||||
}, {
|
}, {
|
||||||
recurseEverything: true
|
recurseEverything: true
|
||||||
});
|
});
|
||||||
|
|||||||
@ -16,13 +16,55 @@ function resolveTilde(filePath: string) {
|
|||||||
return filePath;
|
return filePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function loadConfig(pathOrContent?: string): KubeConfig {
|
export class LoadKubeError extends Error {
|
||||||
const kc = new KubeConfig();
|
public type: string;
|
||||||
|
}
|
||||||
|
|
||||||
if (fse.pathExistsSync(pathOrContent)) {
|
export class AccessError extends LoadKubeError {
|
||||||
kc.loadFromFile(path.resolve(resolveTilde(pathOrContent)));
|
type = "AccessError";
|
||||||
} else {
|
|
||||||
kc.loadFromString(pathOrContent);
|
constructor(public pathname: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Failed to loading the kube config. Permission denied.\n\n${this.pathname}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ExistError extends LoadKubeError {
|
||||||
|
type = "ExistError";
|
||||||
|
|
||||||
|
constructor(public pathname: string) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
toString() {
|
||||||
|
return `Failed to loading the kube config. No such file.\n\n${this.pathname}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(pathOrContent: string): KubeConfig {
|
||||||
|
const kc = new KubeConfig();
|
||||||
|
try {
|
||||||
|
kc.loadFromFile(pathOrContent);
|
||||||
|
} catch (err) {
|
||||||
|
switch (err.code) {
|
||||||
|
case "ENOENT": { // No such file or directory
|
||||||
|
const dir = path.dirname(pathOrContent);
|
||||||
|
const dirStat = fse.statSync(dir);
|
||||||
|
if (dirStat.isDirectory()) {
|
||||||
|
throw new ExistError(pathOrContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
kc.loadFromString(pathOrContent);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "EACCES":
|
||||||
|
throw new AccessError(pathOrContent);
|
||||||
|
default:
|
||||||
|
kc.loadFromString(pathOrContent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return kc;
|
return kc;
|
||||||
@ -147,7 +189,7 @@ export function getNodeWarningConditions(node: V1Node) {
|
|||||||
* Validates kubeconfig supplied in the add clusters screen. At present this will just validate
|
* 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.
|
* the User struct, specifically the command passed to the exec substructure.
|
||||||
*/
|
*/
|
||||||
export function validateKubeConfig (config: KubeConfig) {
|
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)}`);
|
logger.debug(`validateKubeConfig: validating kubeconfig - ${JSON.stringify(config)}`);
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { action, observable } from "mobx";
|
import { action, observable } from "mobx";
|
||||||
import { autobind } from "../../utils";
|
import { autobind, Singleton } from "./utils";
|
||||||
import uniqueId from "lodash/uniqueId";
|
import _ from "lodash";
|
||||||
import { JsonApiErrorParsed } from "../../api/json-api";
|
import { JsonApiErrorParsed } from "../renderer/api/json-api";
|
||||||
|
|
||||||
export type NotificationId = string | number;
|
export type NotificationId = string | number;
|
||||||
export type NotificationMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
|
export type NotificationMessage = React.ReactNode | React.ReactNode[] | JsonApiErrorParsed;
|
||||||
@ -21,10 +21,10 @@ export interface Notification {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
export class NotificationsStore {
|
export class NotificationsStore extends Singleton {
|
||||||
public notifications = observable.array<Notification>([], { deep: false });
|
public notifications = observable.array<Notification>([], { deep: false });
|
||||||
|
|
||||||
protected autoHideTimers = new Map<NotificationId, number>();
|
protected autoHideTimers = new Map<NotificationId, NodeJS.Timeout>();
|
||||||
|
|
||||||
getById(id: NotificationId): Notification | null {
|
getById(id: NotificationId): Notification | null {
|
||||||
return this.notifications.find(item => item.id === id) ?? null;
|
return this.notifications.find(item => item.id === id) ?? null;
|
||||||
@ -35,7 +35,7 @@ export class NotificationsStore {
|
|||||||
if (!notification) return;
|
if (!notification) return;
|
||||||
this.removeAutoHideTimer(id);
|
this.removeAutoHideTimer(id);
|
||||||
if (notification?.timeout) {
|
if (notification?.timeout) {
|
||||||
const timer = window.setTimeout(() => this.remove(id), notification.timeout);
|
const timer = setTimeout(() => this.remove(id), notification.timeout);
|
||||||
this.autoHideTimers.set(id, timer);
|
this.autoHideTimers.set(id, timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -50,7 +50,7 @@ export class NotificationsStore {
|
|||||||
@action
|
@action
|
||||||
add(notification: Notification): () => void {
|
add(notification: Notification): () => void {
|
||||||
const id = notification.id ?? (
|
const id = notification.id ?? (
|
||||||
notification.id = uniqueId("notification_")
|
notification.id = _.uniqueId("notification_")
|
||||||
);
|
);
|
||||||
const index = this.notifications.findIndex(item => item.id === id);
|
const index = this.notifications.findIndex(item => item.id === id);
|
||||||
if (index > -1) {
|
if (index > -1) {
|
||||||
@ -69,4 +69,4 @@ export class NotificationsStore {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const notificationsStore = new NotificationsStore();
|
export const notificationsStore = NotificationsStore.getInstance<NotificationsStore>();
|
||||||
@ -11,6 +11,10 @@ type Constructor<T = {}> = new (...args: any[]) => T;
|
|||||||
class Singleton {
|
class Singleton {
|
||||||
private static instances = new WeakMap<object, Singleton>();
|
private static instances = new WeakMap<object, Singleton>();
|
||||||
|
|
||||||
|
protected constructor() {
|
||||||
|
// to prevent constucting
|
||||||
|
}
|
||||||
|
|
||||||
// todo: improve types inferring
|
// todo: improve types inferring
|
||||||
static getInstance<T>(...args: ConstructorParameters<Constructor<T>>): T {
|
static getInstance<T>(...args: ConstructorParameters<Constructor<T>>): T {
|
||||||
if (!Singleton.instances.has(this)) {
|
if (!Singleton.instances.has(this)) {
|
||||||
|
|||||||
@ -27,6 +27,7 @@ jest.mock("../../common/ipc");
|
|||||||
jest.mock("child_process");
|
jest.mock("child_process");
|
||||||
jest.mock("tcp-port-used");
|
jest.mock("tcp-port-used");
|
||||||
|
|
||||||
|
import mockFs from "mock-fs";
|
||||||
import { Cluster } from "../cluster";
|
import { Cluster } from "../cluster";
|
||||||
import { KubeAuthProxy } from "../kube-auth-proxy";
|
import { KubeAuthProxy } from "../kube-auth-proxy";
|
||||||
import { getFreePort } from "../port";
|
import { getFreePort } from "../port";
|
||||||
@ -36,6 +37,10 @@ import { bundledKubectlPath, Kubectl } from "../kubectl";
|
|||||||
import { mock, MockProxy } from 'jest-mock-extended';
|
import { mock, MockProxy } from 'jest-mock-extended';
|
||||||
import { waitUntilUsed } from 'tcp-port-used';
|
import { waitUntilUsed } from 'tcp-port-used';
|
||||||
import { Readable } from "stream";
|
import { Readable } from "stream";
|
||||||
|
import { app, remote } from "electron";
|
||||||
|
import { Console } from "console";
|
||||||
|
|
||||||
|
console = new Console(process.stdout, process.stderr); // fix mockFS
|
||||||
|
|
||||||
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
|
const mockBroadcastIpc = broadcastMessage as jest.MockedFunction<typeof broadcastMessage>;
|
||||||
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
|
||||||
@ -44,6 +49,14 @@ const mockWaitUntilUsed = waitUntilUsed as jest.MockedFunction<typeof waitUntilU
|
|||||||
describe("kube auth proxy tests", () => {
|
describe("kube auth proxy tests", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
|
const mockOpts = {
|
||||||
|
'fake-path.yml': JSON.stringify({})
|
||||||
|
};
|
||||||
|
mockFs(mockOpts);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
mockFs.restore();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calling exit multiple times shouldn't throw", async () => {
|
it("calling exit multiple times shouldn't throw", async () => {
|
||||||
|
|||||||
@ -51,9 +51,9 @@ export interface ClusterState {
|
|||||||
export class Cluster implements ClusterModel, ClusterState {
|
export class Cluster implements ClusterModel, ClusterState {
|
||||||
public id: ClusterId;
|
public id: ClusterId;
|
||||||
public kubeCtl: Kubectl;
|
public kubeCtl: Kubectl;
|
||||||
public contextHandler: ContextHandler;
|
public contextHandler?: ContextHandler;
|
||||||
public ownerRef: string;
|
public ownerRef: string;
|
||||||
protected kubeconfigManager: KubeconfigManager;
|
protected kubeconfigManager?: KubeconfigManager;
|
||||||
protected eventDisposers: Function[] = [];
|
protected eventDisposers: Function[] = [];
|
||||||
protected activated = false;
|
protected activated = false;
|
||||||
|
|
||||||
@ -85,7 +85,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@computed get name() {
|
@computed get name() {
|
||||||
return this.preferences.clusterName || this.contextName;
|
return this.preferences.clusterName || this.contextName;
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
@computed get prometheusPreferences(): ClusterPrometheusPreferences {
|
||||||
@ -101,9 +101,12 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
|
|
||||||
constructor(model: ClusterModel) {
|
constructor(model: ClusterModel) {
|
||||||
this.updateModel(model);
|
this.updateModel(model);
|
||||||
|
|
||||||
const kubeconfig = this.getKubeconfig();
|
const kubeconfig = this.getKubeconfig();
|
||||||
if (kubeconfig.getContextObject(this.contextName)) {
|
const contextObj = kubeconfig.getContextObject(this.contextName);
|
||||||
this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server;
|
if (contextObj) {
|
||||||
|
const clusterObj = kubeconfig.getCluster(contextObj.cluster);
|
||||||
|
this.apiUrl = clusterObj.server;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -386,6 +389,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
online: this.online,
|
online: this.online,
|
||||||
accessible: this.accessible,
|
accessible: this.accessible,
|
||||||
disconnected: this.disconnected,
|
disconnected: this.disconnected,
|
||||||
|
activated: this.activated,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,8 @@ import { Router } from "./router";
|
|||||||
import { ClusterManager } from "./cluster-manager";
|
import { ClusterManager } from "./cluster-manager";
|
||||||
import { ContextHandler } from "./context-handler";
|
import { ContextHandler } from "./context-handler";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { clusterStore } from "../common/cluster-store";
|
||||||
|
|
||||||
export class LensProxy {
|
export class LensProxy {
|
||||||
protected origin: string;
|
protected origin: string;
|
||||||
@ -64,50 +66,55 @@ export class LensProxy {
|
|||||||
|
|
||||||
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
protected async handleProxyUpgrade(proxy: httpProxy, req: http.IncomingMessage, socket: net.Socket, head: Buffer) {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
if (cluster) {
|
if (!cluster) {
|
||||||
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
return;
|
||||||
const apiUrl = url.parse(cluster.apiUrl);
|
|
||||||
const pUrl = url.parse(proxyUrl);
|
|
||||||
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
|
|
||||||
const proxySocket = new net.Socket();
|
|
||||||
proxySocket.connect(connectOpts, () => {
|
|
||||||
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
|
||||||
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
|
||||||
for (let i = 0; i < req.rawHeaders.length; i += 2) {
|
|
||||||
const key = req.rawHeaders[i];
|
|
||||||
if (key !== "Host" && key !== "Authorization") {
|
|
||||||
proxySocket.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i+1]}\r\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
proxySocket.write("\r\n");
|
|
||||||
proxySocket.write(head);
|
|
||||||
});
|
|
||||||
|
|
||||||
proxySocket.setKeepAlive(true);
|
|
||||||
socket.setKeepAlive(true);
|
|
||||||
proxySocket.setTimeout(0);
|
|
||||||
socket.setTimeout(0);
|
|
||||||
|
|
||||||
proxySocket.on('data', function (chunk) {
|
|
||||||
socket.write(chunk);
|
|
||||||
});
|
|
||||||
proxySocket.on('end', function () {
|
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
proxySocket.on('error', function (err) {
|
|
||||||
socket.write("HTTP/" + req.httpVersion + " 500 Connection error\r\n\r\n");
|
|
||||||
socket.end();
|
|
||||||
});
|
|
||||||
socket.on('data', function (chunk) {
|
|
||||||
proxySocket.write(chunk);
|
|
||||||
});
|
|
||||||
socket.on('end', function () {
|
|
||||||
proxySocket.end();
|
|
||||||
});
|
|
||||||
socket.on('error', function () {
|
|
||||||
proxySocket.end();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, "");
|
||||||
|
const apiUrl = url.parse(cluster.apiUrl);
|
||||||
|
const pUrl = url.parse(proxyUrl);
|
||||||
|
const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname };
|
||||||
|
const proxySocket = new net.Socket();
|
||||||
|
proxySocket.connect(connectOpts, () => {
|
||||||
|
proxySocket.write(`${req.method} ${pUrl.path} HTTP/1.1\r\n`);
|
||||||
|
proxySocket.write(`Host: ${apiUrl.host}\r\n`);
|
||||||
|
|
||||||
|
for (const [key, value] of _.chunk(req.rawHeaders, 2)) {
|
||||||
|
if (["Host", "Authorization"].includes(key)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
proxySocket.write(`${key}: ${value}\r\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
proxySocket.write("\r\n");
|
||||||
|
proxySocket.write(head);
|
||||||
|
});
|
||||||
|
|
||||||
|
proxySocket.setKeepAlive(true);
|
||||||
|
socket.setKeepAlive(true);
|
||||||
|
proxySocket.setTimeout(0);
|
||||||
|
socket.setTimeout(0);
|
||||||
|
|
||||||
|
proxySocket.on('data', function (chunk) {
|
||||||
|
socket.write(chunk);
|
||||||
|
});
|
||||||
|
proxySocket.on('end', function () {
|
||||||
|
socket.end();
|
||||||
|
});
|
||||||
|
proxySocket.on('error', function (err) {
|
||||||
|
socket.write(`HTTP/${req.httpVersion} 500 Connection error\r\n\r\n`);
|
||||||
|
socket.end();
|
||||||
|
});
|
||||||
|
socket.on('data', function (chunk) {
|
||||||
|
proxySocket.write(chunk);
|
||||||
|
});
|
||||||
|
socket.on('end', function () {
|
||||||
|
proxySocket.end();
|
||||||
|
});
|
||||||
|
socket.on('error', function () {
|
||||||
|
proxySocket.end();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected createProxy(): httpProxy {
|
protected createProxy(): httpProxy {
|
||||||
@ -166,6 +173,10 @@ export class LensProxy {
|
|||||||
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) {
|
||||||
const cluster = this.clusterManager.getClusterForRequest(req);
|
const cluster = this.clusterManager.getClusterForRequest(req);
|
||||||
if (cluster) {
|
if (cluster) {
|
||||||
|
if (!cluster.initialized) {
|
||||||
|
return res.writeHead(404).end(`cluster: ${cluster.name} is not initialized`);
|
||||||
|
}
|
||||||
|
|
||||||
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler);
|
||||||
if (proxyTarget) {
|
if (proxyTarget) {
|
||||||
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
// allow to fetch apis in "clusterId.localhost:port" from "localhost:port"
|
||||||
|
|||||||
@ -1,12 +1,12 @@
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import packageInfo from "../../package.json";
|
import packageInfo from "../../package.json";
|
||||||
import { dialog, Menu, NativeImage, nativeTheme, Tray } from "electron";
|
import { dialog, Menu, MenuItemConstructorOptions, NativeImage, nativeTheme, Tray } from "electron";
|
||||||
import { autorun } from "mobx";
|
import { autorun } from "mobx";
|
||||||
import { showAbout } from "./menu";
|
import { showAbout } from "./menu";
|
||||||
import { AppUpdater } from "./app-updater";
|
import { AppUpdater } from "./app-updater";
|
||||||
import { WindowManager } from "./window-manager";
|
import { WindowManager } from "./window-manager";
|
||||||
import { clusterStore } from "../common/cluster-store";
|
import { ClusterRenderInfo, clusterStore } from "../common/cluster-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { Workspace, workspaceStore } from "../common/workspace-store";
|
||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
@ -33,7 +33,7 @@ export function initTray(windowManager: WindowManager) {
|
|||||||
const menu = createTrayMenu(windowManager);
|
const menu = createTrayMenu(windowManager);
|
||||||
buildTray(getTrayIcon(), menu);
|
buildTray(getTrayIcon(), menu);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error(`[TRAY]: building failed: ${err}`);
|
logger.error(`[TRAY]: building failed: `, err);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return () => {
|
return () => {
|
||||||
@ -82,25 +82,23 @@ export function createTrayMenu(windowManager: WindowManager): Menu {
|
|||||||
{
|
{
|
||||||
label: "Clusters",
|
label: "Clusters",
|
||||||
submenu: workspaceStore.enabledWorkspacesList
|
submenu: workspaceStore.enabledWorkspacesList
|
||||||
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
|
.map((workspace): [Workspace, ClusterRenderInfo[]] => [workspace, clusterStore.getByWorkspaceId(workspace.id)])
|
||||||
.map(workspace => {
|
.filter(([, clusters]) => clusters.length > 0)
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspace.id);
|
.map(([workspace, clusters]): MenuItemConstructorOptions => ({
|
||||||
return {
|
label: workspace.name,
|
||||||
label: workspace.name,
|
toolTip: workspace.description,
|
||||||
toolTip: workspace.description,
|
submenu: clusters.map(({ id: clusterId, name, online, workspace, DeadError }) => ({
|
||||||
submenu: clusters.map(cluster => {
|
label: name,
|
||||||
const { id: clusterId, name: label, online, workspace } = cluster;
|
enabled: !!!DeadError,
|
||||||
return {
|
checked: online,
|
||||||
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
|
toolTip: clusterId,
|
||||||
toolTip: clusterId,
|
type: "radio",
|
||||||
async click() {
|
async click() {
|
||||||
workspaceStore.setActive(workspace);
|
workspaceStore.setActive(workspace);
|
||||||
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
||||||
}
|
}
|
||||||
};
|
}))
|
||||||
})
|
})),
|
||||||
};
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Check for updates",
|
label: "Check for updates",
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { stringify } from "querystring";
|
import { stringify } from "querystring";
|
||||||
import { EventEmitter } from "../../common/event-emitter";
|
import { EventEmitter } from "../../common/event-emitter";
|
||||||
import { cancelableFetch } from "../utils/cancelableFetch";
|
import { cancelableFetch, CancelablePromise } from "../utils/cancelableFetch";
|
||||||
|
|
||||||
export interface JsonApiData {
|
export interface JsonApiData {
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
return this.request<T>(path, params, { ...reqInit, method: "delete" });
|
return this.request<T>(path, params, { ...reqInit, method: "delete" });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected request<D>(path: string, params?: P, init: RequestInit = {}) {
|
protected request<D>(path: string, params?: P, init: RequestInit = {}): CancelablePromise<D> {
|
||||||
let reqUrl = this.config.apiBase + path;
|
let reqUrl = this.config.apiBase + path;
|
||||||
const reqInit: RequestInit = { ...this.reqInit, ...init };
|
const reqInit: RequestInit = { ...this.reqInit, ...init };
|
||||||
const { data, query } = params || {} as P;
|
const { data, query } = params || {} as P;
|
||||||
@ -92,28 +92,27 @@ export class JsonApi<D = JsonApiData, P extends JsonApiParams = JsonApiParams> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseResponse<D>(res: Response, log: JsonApiLog): Promise<D> {
|
protected async parseResponse<D>(res: Response, log: JsonApiLog): Promise<D> {
|
||||||
const { status } = res;
|
const { status } = res;
|
||||||
return res.text().then(text => {
|
const text = await res.text();
|
||||||
let data;
|
let data;
|
||||||
try {
|
try {
|
||||||
data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body
|
data = text ? JSON.parse(text) : ""; // DELETE-requests might not have response-body
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
data = text;
|
data = text;
|
||||||
}
|
}
|
||||||
if (status >= 200 && status < 300) {
|
if (status >= 200 && status < 300) {
|
||||||
this.onData.emit(data, res);
|
this.onData.emit(data, res);
|
||||||
this.writeLog({ ...log, data });
|
this.writeLog({ ...log, data });
|
||||||
return data;
|
return data;
|
||||||
} else if (log.method === "GET" && res.status === 403) {
|
} else if (log.method === "GET" && res.status === 403) {
|
||||||
this.writeLog({ ...log, data });
|
this.writeLog({ ...log, data });
|
||||||
} else {
|
} else {
|
||||||
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
|
const error = new JsonApiErrorParsed(data, this.parseError(data, res));
|
||||||
this.onError.emit(error, res);
|
this.onError.emit(error, res);
|
||||||
this.writeLog({ ...log, error });
|
this.writeLog({ ...log, error });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected parseError(error: JsonApiError | string, res: Response): string[] {
|
protected parseError(error: JsonApiError | string, res: Response): string[] {
|
||||||
|
|||||||
@ -11,8 +11,7 @@ export class LandingPage extends React.Component {
|
|||||||
@observable showHint = true;
|
@observable showHint = true;
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const clusters = clusterStore.getByWorkspaceId(workspaceStore.currentWorkspaceId);
|
const noClustersInScope = clusterStore.getCountInWorkspace(workspaceStore.currentWorkspaceId) === 0;
|
||||||
const noClustersInScope = !clusters.length;
|
|
||||||
const showStartupHint = this.showHint && noClustersInScope;
|
const showStartupHint = this.showHint && noClustersInScope;
|
||||||
return (
|
return (
|
||||||
<div className="LandingPage flex">
|
<div className="LandingPage flex">
|
||||||
|
|||||||
@ -26,6 +26,15 @@
|
|||||||
height: var(--size);
|
height: var(--size);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Icon.dead-error {
|
||||||
|
position: absolute;
|
||||||
|
border-radius: 50%;
|
||||||
|
right: 0px;
|
||||||
|
top: 0px;
|
||||||
|
color: $colorError;
|
||||||
|
background-color: black;
|
||||||
|
}
|
||||||
|
|
||||||
.Badge {
|
.Badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
|
|||||||
@ -4,17 +4,16 @@ import React, { DOMAttributes } from "react";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Params as HashiconParams } from "@emeraldpay/hashicon";
|
import { Params as HashiconParams } from "@emeraldpay/hashicon";
|
||||||
import { Hashicon } from "@emeraldpay/hashicon-react";
|
import { Hashicon } from "@emeraldpay/hashicon-react";
|
||||||
import { Cluster } from "../../../main/cluster";
|
|
||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { Badge } from "../badge";
|
import { Badge } from "../badge";
|
||||||
import { Tooltip } from "../tooltip";
|
import { Tooltip } from "../tooltip";
|
||||||
import { eventStore } from "../+events/event.store";
|
import { subscribeToBroadcast } from "../../../common/ipc";
|
||||||
import { forCluster } from "../../api/kube-api";
|
import { observable } from "mobx";
|
||||||
import { subscribeToBroadcast, unsubscribeAllFromBroadcast } from "../../../common/ipc";
|
import { ClusterRenderInfo } from "../../../common/cluster-store";
|
||||||
import { observable, when } from "mobx";
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
interface Props extends DOMAttributes<HTMLElement> {
|
||||||
cluster: Cluster;
|
cluster: ClusterRenderInfo;
|
||||||
className?: IClassName;
|
className?: IClassName;
|
||||||
errorClass?: IClassName;
|
errorClass?: IClassName;
|
||||||
showErrors?: boolean;
|
showErrors?: boolean;
|
||||||
@ -51,24 +50,32 @@ export class ClusterIcon extends React.Component<Props> {
|
|||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const {
|
||||||
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive,
|
cluster, showErrors, showTooltip, errorClass, options, interactive, isActive, className,
|
||||||
children, ...elemProps
|
children, ...elemProps
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { name, preferences, id: clusterId } = cluster;
|
|
||||||
const eventCount = this.eventCount;
|
const eventCount = this.eventCount;
|
||||||
const { icon } = preferences;
|
const { name, preferences: { icon }, id: clusterId, DeadError } = cluster;
|
||||||
const clusterIconId = `cluster-icon-${clusterId}`;
|
const clusterIconId = `cluster-icon-${clusterId}`;
|
||||||
const className = cssNames("ClusterIcon flex inline", this.props.className, {
|
const isDead = !!DeadError;
|
||||||
interactive: interactive !== undefined ? interactive : !!this.props.onClick,
|
const classNames = cssNames("ClusterIcon flex inline", className, {
|
||||||
|
interactive: interactive ?? !!this.props.onClick,
|
||||||
active: isActive,
|
active: isActive,
|
||||||
});
|
});
|
||||||
return (
|
return (
|
||||||
<div {...elemProps} className={className} id={showTooltip ? clusterIconId : null}>
|
<div {...elemProps} className={classNames} id={showTooltip ? clusterIconId : null}>
|
||||||
{showTooltip && (
|
{showTooltip && (
|
||||||
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
|
<Tooltip targetId={clusterIconId}>{name}</Tooltip>
|
||||||
)}
|
)}
|
||||||
{icon && <img src={icon} alt={name}/>}
|
{
|
||||||
{!icon && <Hashicon value={clusterId} options={options}/>}
|
icon
|
||||||
|
? <img src={icon} alt={name} />
|
||||||
|
: <Hashicon value={clusterId} options={options} />
|
||||||
|
}
|
||||||
|
{
|
||||||
|
isDead && (
|
||||||
|
<Icon className="dead-error" material="error" />
|
||||||
|
)
|
||||||
|
}
|
||||||
{showErrors && eventCount > 0 && !isActive && (
|
{showErrors && eventCount > 0 && !isActive && (
|
||||||
<Badge
|
<Badge
|
||||||
className={cssNames("events-count", errorClass)}
|
className={cssNames("events-count", errorClass)}
|
||||||
|
|||||||
@ -0,0 +1,57 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
import { Trans } from "@lingui/macro";
|
||||||
|
import { observer } from "mobx-react";
|
||||||
|
import { ClusterModel } from "../../../common/cluster-store";
|
||||||
|
import { AccessError, ExistError, LoadKubeError } from "../../../common/kube-helpers";
|
||||||
|
import { cssNames } from "../../utils";
|
||||||
|
import { Icon } from "../icon";
|
||||||
|
import { isMac } from "../../../common/vars";
|
||||||
|
|
||||||
|
export interface ClusterDeadStatusProps {
|
||||||
|
cluster: ClusterModel,
|
||||||
|
error: LoadKubeError,
|
||||||
|
}
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class ClusterDeadStatus extends React.Component<ClusterDeadStatusProps> {
|
||||||
|
renderError(error: LoadKubeError) {
|
||||||
|
if (error instanceof AccessError) {
|
||||||
|
return <>
|
||||||
|
<p><Trans>The kube config file related to this cluster is no longer accessible by Lens.</Trans></p>
|
||||||
|
<p><Trans>Lens cannot connect to this cluster.</Trans></p>
|
||||||
|
{isMac && (
|
||||||
|
<p><Trans>This may have resulted in a recent macOS update locking down files.</Trans></p>
|
||||||
|
)}
|
||||||
|
<p><Trans>If you allow Lens to access your home directory, it may rectify the problem.</Trans></p>
|
||||||
|
<p className="monospace">{error.pathname}</p>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error instanceof ExistError) {
|
||||||
|
return <>
|
||||||
|
<p><Trans>The kube config file related to this cluster no longer exists.</Trans></p>
|
||||||
|
<p><Trans>Either it has been moved by another process or it was deleted.</Trans></p>
|
||||||
|
<p><Trans>To fix this error, either recreate the config file in the same location or delete and re-add this cluster.</Trans></p>
|
||||||
|
<p className="monospace">{error.pathname}</p>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<p><Trans>An unknown error of type "{error.type}" occured while loading this cluster's kube config.</Trans></p>
|
||||||
|
</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const classNames = cssNames("ClusterStatus flex column gaps box center align-center justify-center");
|
||||||
|
const { cluster, error } = this.props;
|
||||||
|
|
||||||
|
return <div className={classNames}>
|
||||||
|
<Icon material="cloud_off" className="error" />
|
||||||
|
<h2>
|
||||||
|
{cluster.preferences.clusterName || cluster.contextName}
|
||||||
|
</h2>
|
||||||
|
{this.renderError(error)}
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -4,7 +4,7 @@ import React from "react";
|
|||||||
import { Redirect, Route, Switch } from "react-router";
|
import { Redirect, Route, Switch } from "react-router";
|
||||||
import { comparer, reaction } from "mobx";
|
import { comparer, reaction } from "mobx";
|
||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { ClustersMenu } from "./clusters-menu";
|
import { ClusterMenu } from "./clusters-menu";
|
||||||
import { BottomBar } from "./bottom-bar";
|
import { BottomBar } from "./bottom-bar";
|
||||||
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
import { LandingPage, landingRoute, landingURL } from "../+landing-page";
|
||||||
import { Preferences, preferencesRoute } from "../+preferences";
|
import { Preferences, preferencesRoute } from "../+preferences";
|
||||||
@ -60,7 +60,7 @@ export class ClusterManager extends React.Component {
|
|||||||
return (
|
return (
|
||||||
<div className="ClusterManager">
|
<div className="ClusterManager">
|
||||||
<main>
|
<main>
|
||||||
<div id="lens-views"/>
|
<div id="lens-views" />
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route component={LandingPage} {...landingRoute} />
|
<Route component={LandingPage} {...landingRoute} />
|
||||||
<Route component={Preferences} {...preferencesRoute} />
|
<Route component={Preferences} {...preferencesRoute} />
|
||||||
@ -70,13 +70,13 @@ export class ClusterManager extends React.Component {
|
|||||||
<Route component={ClusterView} {...clusterViewRoute} />
|
<Route component={ClusterView} {...clusterViewRoute} />
|
||||||
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
<Route component={ClusterSettings} {...clusterSettingsRoute} />
|
||||||
{globalPageRegistry.getItems().map(({ routePath, exact, components: { Page } }) => {
|
{globalPageRegistry.getItems().map(({ routePath, exact, components: { Page } }) => {
|
||||||
return <Route key={routePath} path={routePath} component={Page} exact={exact}/>;
|
return <Route key={routePath} path={routePath} component={Page} exact={exact} />;
|
||||||
})}
|
})}
|
||||||
<Redirect exact to={this.startUrl}/>
|
<Redirect exact to={this.startUrl} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</main>
|
</main>
|
||||||
<ClustersMenu/>
|
<ClusterMenu />
|
||||||
<BottomBar/>
|
<BottomBar />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,4 +18,8 @@
|
|||||||
--size: 70px;
|
--size: 70px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p.monospace {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,7 +7,9 @@ import { IClusterViewRouteParams } from "./cluster-view.route";
|
|||||||
import { ClusterStatus } from "./cluster-status";
|
import { ClusterStatus } from "./cluster-status";
|
||||||
import { hasLoadedView } from "./lens-views";
|
import { hasLoadedView } from "./lens-views";
|
||||||
import { Cluster } from "../../../main/cluster";
|
import { Cluster } from "../../../main/cluster";
|
||||||
import { clusterStore } from "../../../common/cluster-store";
|
import { ClusterModel, clusterStore } from "../../../common/cluster-store";
|
||||||
|
import { LoadKubeError } from "../../../common/kube-helpers";
|
||||||
|
import { ClusterDeadStatus } from "./cluster-dead-status";
|
||||||
|
|
||||||
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
interface Props extends RouteComponentProps<IClusterViewRouteParams> {
|
||||||
}
|
}
|
||||||
@ -22,6 +24,10 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
return clusterStore.getById(this.clusterId);
|
return clusterStore.getById(this.clusterId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get deadCluster(): [ClusterModel, LoadKubeError] {
|
||||||
|
return clusterStore.getDeadById(this.clusterId);
|
||||||
|
}
|
||||||
|
|
||||||
async componentDidMount() {
|
async componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
reaction(() => this.clusterId, clusterId => clusterStore.setActive(clusterId), {
|
||||||
@ -31,13 +37,16 @@ export class ClusterView extends React.Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { cluster } = this;
|
const { cluster, deadCluster } = this;
|
||||||
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready);
|
const showStatus = cluster && (!cluster.available || !hasLoadedView(cluster.id) || !cluster.ready);
|
||||||
return (
|
return (
|
||||||
<div className="ClusterView flex align-center">
|
<div className="ClusterView flex align-center">
|
||||||
{showStatus && (
|
{showStatus
|
||||||
<ClusterStatus key={cluster.id} clusterId={cluster.id} className="box center"/>
|
? <ClusterStatus key={cluster.id} clusterId={cluster.id} className="box center" />
|
||||||
)}
|
: deadCluster && (
|
||||||
|
<ClusterDeadStatus cluster={deadCluster[0]} error={deadCluster[1]} />
|
||||||
|
)
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,13 +3,12 @@ import "./clusters-menu.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { remote } from "electron";
|
import { remote } from "electron";
|
||||||
import { requestMain } from "../../../common/ipc";
|
import { requestMain } from "../../../common/ipc";
|
||||||
import type { Cluster } from "../../../main/cluster";
|
|
||||||
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
import { DragDropContext, Draggable, DraggableProvided, Droppable, DroppableProvided, DropResult } from "react-beautiful-dnd";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { _i18n } from "../../i18n";
|
import { _i18n } from "../../i18n";
|
||||||
import { t, Trans } from "@lingui/macro";
|
import { t, Trans } from "@lingui/macro";
|
||||||
import { userStore } from "../../../common/user-store";
|
import { userStore } from "../../../common/user-store";
|
||||||
import { ClusterId, clusterStore } from "../../../common/cluster-store";
|
import { ClusterId, ClusterRenderInfo, clusterStore } from "../../../common/cluster-store";
|
||||||
import { workspaceStore } from "../../../common/workspace-store";
|
import { workspaceStore } from "../../../common/workspace-store";
|
||||||
import { ClusterIcon } from "../cluster-icon";
|
import { ClusterIcon } from "../cluster-icon";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
@ -30,7 +29,7 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ClustersMenu extends React.Component<Props> {
|
export class ClusterMenu extends React.Component<Props> {
|
||||||
showCluster = (clusterId: ClusterId) => {
|
showCluster = (clusterId: ClusterId) => {
|
||||||
navigate(clusterViewURL({ params: { clusterId } }));
|
navigate(clusterViewURL({ params: { clusterId } }));
|
||||||
};
|
};
|
||||||
@ -39,7 +38,7 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
navigate(addClusterURL());
|
navigate(addClusterURL());
|
||||||
};
|
};
|
||||||
|
|
||||||
showContextMenu = (cluster: Cluster) => {
|
showContextMenu = (cluster: ClusterRenderInfo) => {
|
||||||
const { Menu, MenuItem } = remote;
|
const { Menu, MenuItem } = remote;
|
||||||
const menu = new Menu();
|
const menu = new Menu();
|
||||||
|
|
||||||
@ -115,25 +114,22 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
<Droppable droppableId="cluster-menu" type="CLUSTER">
|
<Droppable droppableId="cluster-menu" type="CLUSTER">
|
||||||
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
|
{({ innerRef, droppableProps, placeholder }: DroppableProvided) => (
|
||||||
<div ref={innerRef} {...droppableProps}>
|
<div ref={innerRef} {...droppableProps}>
|
||||||
{clusters.map((cluster, index) => {
|
{clusters.map((cluster, index) => (
|
||||||
const isActive = cluster.id === activeClusterId;
|
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
||||||
return (
|
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
|
||||||
<Draggable draggableId={cluster.id} index={index} key={cluster.id}>
|
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
|
||||||
{({ draggableProps, dragHandleProps, innerRef }: DraggableProvided) => (
|
<ClusterIcon
|
||||||
<div ref={innerRef} {...draggableProps} {...dragHandleProps}>
|
key={cluster.id}
|
||||||
<ClusterIcon
|
showErrors={true}
|
||||||
key={cluster.id}
|
cluster={cluster}
|
||||||
showErrors={true}
|
isActive={cluster.id === activeClusterId}
|
||||||
cluster={cluster}
|
onClick={() => this.showCluster(cluster.id)}
|
||||||
isActive={isActive}
|
onContextMenu={() => this.showContextMenu(cluster)}
|
||||||
onClick={() => this.showCluster(cluster.id)}
|
/>
|
||||||
onContextMenu={() => this.showContextMenu(cluster)}
|
</div>
|
||||||
/>
|
)}
|
||||||
</div>
|
</Draggable>
|
||||||
)}
|
))}
|
||||||
</Draggable>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{placeholder}
|
{placeholder}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@ -144,9 +140,9 @@ export class ClustersMenu extends React.Component<Props> {
|
|||||||
<Tooltip targetId="add-cluster-icon">
|
<Tooltip targetId="add-cluster-icon">
|
||||||
<Trans>Add Cluster</Trans>
|
<Trans>Add Cluster</Trans>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Icon big material="add" id="add-cluster-icon" disabled={workspace.isManaged} onClick={this.addCluster}/>
|
<Icon big material="add" id="add-cluster-icon" disabled={workspace.isManaged} onClick={this.addCluster} />
|
||||||
{newContexts.size > 0 && (
|
{newContexts.size > 0 && (
|
||||||
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>}/>
|
<Badge className="counter" label={newContexts.size} tooltip={<Trans>new</Trans>} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="extensions">
|
<div className="extensions">
|
||||||
|
|||||||
@ -40,24 +40,23 @@ export class Icon extends React.PureComponent<IconProps> {
|
|||||||
if (this.props.disabled) {
|
if (this.props.disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.props.onClick) {
|
|
||||||
this.props.onClick(evt);
|
this.props.onClick?.(evt);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@autobind()
|
@autobind()
|
||||||
onKeyDown(evt: React.KeyboardEvent<any>) {
|
onKeyDown(evt: React.KeyboardEvent<any>) {
|
||||||
switch (evt.nativeEvent.code) {
|
switch (evt.nativeEvent.code) {
|
||||||
case "Space":
|
case "Space":
|
||||||
case "Enter":
|
case "Enter": {
|
||||||
const icon = findDOMNode(this) as HTMLElement;
|
const icon = findDOMNode(this) as HTMLElement;
|
||||||
setTimeout(() => icon.click());
|
setTimeout(() => icon.click());
|
||||||
evt.preventDefault();
|
evt.preventDefault();
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (this.props.onKeyDown) {
|
|
||||||
this.props.onKeyDown(evt);
|
this.props.onKeyDown?.(evt);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
@ -88,7 +87,7 @@ export class Icon extends React.PureComponent<IconProps> {
|
|||||||
// render as inline svg-icon
|
// render as inline svg-icon
|
||||||
if (svg) {
|
if (svg) {
|
||||||
const svgIconText = require("!!raw-loader!./" + svg + ".svg").default;
|
const svgIconText = require("!!raw-loader!./" + svg + ".svg").default;
|
||||||
iconContent = <span className="icon" dangerouslySetInnerHTML={{ __html: svgIconText }}/>;
|
iconContent = <span className="icon" dangerouslySetInnerHTML={{ __html: svgIconText }} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
// render as material-icon
|
// render as material-icon
|
||||||
@ -106,10 +105,10 @@ export class Icon extends React.PureComponent<IconProps> {
|
|||||||
|
|
||||||
// render icon type
|
// render icon type
|
||||||
if (link) {
|
if (link) {
|
||||||
return <NavLink {...iconProps} to={link}/>;
|
return <NavLink {...iconProps} to={link} />;
|
||||||
}
|
}
|
||||||
if (href) {
|
if (href) {
|
||||||
return <a {...iconProps} href={href}/>;
|
return <a {...iconProps} href={href} />;
|
||||||
}
|
}
|
||||||
return <i {...iconProps} />;
|
return <i {...iconProps} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,2 +1 @@
|
|||||||
export * from './notifications';
|
export * from './notifications';
|
||||||
export * from './notifications.store';
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { reaction } from "mobx";
|
|||||||
import { disposeOnUnmount, observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { JsonApiErrorParsed } from "../../api/json-api";
|
import { JsonApiErrorParsed } from "../../api/json-api";
|
||||||
import { cssNames, prevDefault } from "../../utils";
|
import { cssNames, prevDefault } from "../../utils";
|
||||||
import { Notification, NotificationMessage, notificationsStore, NotificationStatus } from "./notifications.store";
|
import { NotificationMessage, Notification, notificationsStore, NotificationStatus } from "../../../common/notifications.store";
|
||||||
import { Animate } from "../animate";
|
import { Animate } from "../animate";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
|
|
||||||
|
|||||||
@ -175,7 +175,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "ADDED":
|
case "ADDED":
|
||||||
case "MODIFIED":
|
case "MODIFIED": {
|
||||||
const newItem = new api.objectConstructor(object);
|
const newItem = new api.objectConstructor(object);
|
||||||
if (!item) {
|
if (!item) {
|
||||||
items.push(newItem);
|
items.push(newItem);
|
||||||
@ -183,6 +183,7 @@ export abstract class KubeObjectStore<T extends KubeObject = any> extends ItemSt
|
|||||||
items.splice(index, 1, newItem);
|
items.splice(index, 1, newItem);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "DELETED":
|
case "DELETED":
|
||||||
if (item) {
|
if (item) {
|
||||||
items.splice(index, 1);
|
items.splice(index, 1);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user