mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into logs-search
This commit is contained in:
commit
b0c0794200
352 files changed
+28437
-3271
No files matched your search
@@ -64,13 +64,13 @@ describe("empty config", () => {
|
||||
|
||||
it("sets active cluster", () => {
|
||||
clusterStore.setActive("foo");
|
||||
expect(clusterStore.activeCluster.id).toBe("foo");
|
||||
expect(clusterStore.active.id).toBe("foo");
|
||||
})
|
||||
})
|
||||
|
||||
describe("with prod and dev clusters added", () => {
|
||||
beforeEach(() => {
|
||||
clusterStore.addCluster(
|
||||
clusterStore.addClusters(
|
||||
new Cluster({
|
||||
id: "prod",
|
||||
contextName: "prod",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { appEventBus, AppEvent } from "../event-bus"
|
||||
|
||||
describe("event bus tests", () => {
|
||||
describe("emit", () => {
|
||||
it("emits an event", () => {
|
||||
let event: AppEvent = null
|
||||
appEventBus.addListener((data) => {
|
||||
event = data
|
||||
})
|
||||
|
||||
appEventBus.emit({name: "foo", action: "bar"})
|
||||
expect(event.name).toBe("foo")
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -10,7 +10,7 @@ jest.mock("electron", () => {
|
||||
}
|
||||
})
|
||||
|
||||
import { WorkspaceStore } from "../workspace-store"
|
||||
import { Workspace, WorkspaceStore } from "../workspace-store"
|
||||
|
||||
describe("workspace store tests", () => {
|
||||
describe("for an empty config", () => {
|
||||
@@ -35,16 +35,16 @@ describe("workspace store tests", () => {
|
||||
it("cannot remove the default workspace", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
expect(() => ws.removeWorkspace(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
|
||||
expect(() => ws.removeWorkspaceById(WorkspaceStore.defaultId)).toThrowError("Cannot remove");
|
||||
})
|
||||
|
||||
it("can update default workspace name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: WorkspaceStore.defaultId,
|
||||
name: "foobar",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(ws.currentWorkspace.name).toBe("foobar");
|
||||
})
|
||||
@@ -52,10 +52,10 @@ describe("workspace store tests", () => {
|
||||
it("can add workspaces", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "123",
|
||||
name: "foobar",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(ws.getById("123").name).toBe("foobar");
|
||||
})
|
||||
@@ -69,10 +69,10 @@ describe("workspace store tests", () => {
|
||||
it("can set a existent workspace to be active", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "abc",
|
||||
name: "foobar",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(() => ws.setActive("abc")).not.toThrowError();
|
||||
})
|
||||
@@ -80,15 +80,15 @@ describe("workspace store tests", () => {
|
||||
it("can remove a workspace", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "123",
|
||||
name: "foobar",
|
||||
});
|
||||
ws.saveWorkspace({
|
||||
}));
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "1234",
|
||||
name: "foobar 1",
|
||||
});
|
||||
ws.removeWorkspace("123");
|
||||
}));
|
||||
ws.removeWorkspaceById("123");
|
||||
|
||||
expect(ws.workspaces.size).toBe(2);
|
||||
})
|
||||
@@ -96,10 +96,10 @@ describe("workspace store tests", () => {
|
||||
it("cannot create workspace with existent name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "someid",
|
||||
name: "default",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
})
|
||||
@@ -107,10 +107,10 @@ describe("workspace store tests", () => {
|
||||
it("cannot create workspace with empty name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "random",
|
||||
name: "",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
})
|
||||
@@ -118,10 +118,10 @@ describe("workspace store tests", () => {
|
||||
it("cannot create workspace with ' ' name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "random",
|
||||
name: " ",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
})
|
||||
@@ -129,10 +129,10 @@ describe("workspace store tests", () => {
|
||||
it("trim workspace name", () => {
|
||||
const ws = WorkspaceStore.getInstance<WorkspaceStore>();
|
||||
|
||||
ws.saveWorkspace({
|
||||
ws.addWorkspace(new Workspace({
|
||||
id: "random",
|
||||
name: "default ",
|
||||
});
|
||||
}));
|
||||
|
||||
expect(ws.workspacesList.length).toBe(1); // default workspace only
|
||||
})
|
||||
@@ -169,4 +169,4 @@ describe("workspace store tests", () => {
|
||||
expect(ws.currentWorkspaceId).toBe("abc");
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -56,13 +56,17 @@ export class BaseStore<T = any> extends Singleton {
|
||||
...confOptions,
|
||||
projectName: "lens",
|
||||
projectVersion: getAppVersion(),
|
||||
cwd: (app || remote.app).getPath("userData"),
|
||||
cwd: this.storePath(),
|
||||
});
|
||||
logger.info(`[STORE]: LOADED from ${this.storeConfig.path}`);
|
||||
this.fromStore(this.storeConfig.store);
|
||||
this.isLoaded = true;
|
||||
}
|
||||
|
||||
protected storePath() {
|
||||
return (app || remote.app).getPath("userData")
|
||||
}
|
||||
|
||||
protected async saveToFile(model: T) {
|
||||
logger.info(`[STORE]: SAVING ${this.name}`);
|
||||
// todo: update when fixed https://github.com/sindresorhus/conf/issues/114
|
||||
@@ -86,13 +90,19 @@ export class BaseStore<T = any> extends Singleton {
|
||||
if (ipcRenderer) {
|
||||
const callback = (event: IpcRendererEvent, model: T) => {
|
||||
logger.silly(`[STORE]: SYNC ${this.name} from main`, { model });
|
||||
this.onSync(model);
|
||||
this.onSyncFromMain(model);
|
||||
};
|
||||
ipcRenderer.on(this.syncChannel, callback);
|
||||
this.syncDisposers.push(() => ipcRenderer.off(this.syncChannel, callback));
|
||||
}
|
||||
}
|
||||
|
||||
protected onSyncFromMain(model: T) {
|
||||
this.applyWithoutSync(() => {
|
||||
this.onSync(model)
|
||||
})
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
ipcRenderer.removeAllListeners(this.syncChannel)
|
||||
}
|
||||
|
||||
+12
-24
@@ -1,6 +1,8 @@
|
||||
import { createIpcChannel } from "./ipc";
|
||||
import { ClusterId, clusterStore } from "./cluster-store";
|
||||
import { tracker } from "./tracker";
|
||||
import { extensionLoader } from "../extensions/extension-loader"
|
||||
import { appEventBus } from "./event-bus"
|
||||
import { ResourceApplier } from "../main/resource-applier";
|
||||
|
||||
export const clusterIpc = {
|
||||
activate: createIpcChannel({
|
||||
@@ -19,6 +21,7 @@ export const clusterIpc = {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
if (frameId) cluster.frameId = frameId; // save cluster's webFrame.routingId to be able to send push-updates
|
||||
extensionLoader.broadcastExtensions(frameId)
|
||||
return cluster.pushState();
|
||||
}
|
||||
},
|
||||
@@ -28,44 +31,29 @@ export const clusterIpc = {
|
||||
channel: "cluster:refresh",
|
||||
handle: (clusterId: ClusterId) => {
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) return cluster.refresh();
|
||||
if (cluster) return cluster.refresh({ refreshMetadata: true })
|
||||
},
|
||||
}),
|
||||
|
||||
disconnect: createIpcChannel({
|
||||
channel: "cluster:disconnect",
|
||||
handle: (clusterId: ClusterId) => {
|
||||
tracker.event("cluster", "stop");
|
||||
appEventBus.emit({name: "cluster", action: "stop"});
|
||||
return clusterStore.getById(clusterId)?.disconnect();
|
||||
},
|
||||
}),
|
||||
|
||||
installFeature: createIpcChannel({
|
||||
channel: "cluster:install-feature",
|
||||
handle: async (clusterId: ClusterId, feature: string, config?: any) => {
|
||||
tracker.event("cluster", "install", feature);
|
||||
kubectlApplyAll: createIpcChannel({
|
||||
channel: "cluster:kubectl-apply-all",
|
||||
handle: (clusterId: ClusterId, resources: string[]) => {
|
||||
appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
|
||||
const cluster = clusterStore.getById(clusterId);
|
||||
if (cluster) {
|
||||
await cluster.installFeature(feature, config)
|
||||
const applier = new ResourceApplier(cluster)
|
||||
applier.kubectlApplyAll(resources)
|
||||
} else {
|
||||
throw `${clusterId} is not a valid cluster id`;
|
||||
}
|
||||
}
|
||||
}),
|
||||
|
||||
uninstallFeature: createIpcChannel({
|
||||
channel: "cluster:uninstall-feature",
|
||||
handle: (clusterId: ClusterId, feature: string) => {
|
||||
tracker.event("cluster", "uninstall", feature);
|
||||
return clusterStore.getById(clusterId)?.uninstallFeature(feature)
|
||||
}
|
||||
}),
|
||||
|
||||
upgradeFeature: createIpcChannel({
|
||||
channel: "cluster:upgrade-feature",
|
||||
handle: (clusterId: ClusterId, feature: string, config?: any) => {
|
||||
tracker.event("cluster", "upgrade", feature);
|
||||
return clusterStore.getById(clusterId)?.upgradeFeature(feature, config)
|
||||
}
|
||||
}),
|
||||
}
|
||||
+73
-25
@@ -1,18 +1,18 @@
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
import path from "path";
|
||||
import { app, ipcRenderer, remote, webFrame, webContents } from "electron";
|
||||
import { app, ipcRenderer, remote, webFrame } from "electron";
|
||||
import { unlink } from "fs-extra";
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import { Cluster, ClusterState } from "../main/cluster";
|
||||
import migrations from "../migrations/cluster-store"
|
||||
import logger from "../main/logger";
|
||||
import { tracker } from "./tracker";
|
||||
import { appEventBus } from "./event-bus"
|
||||
import { dumpConfigYaml } from "./kube-helpers";
|
||||
import { saveToAppFiles } from "./utils/saveToAppFiles";
|
||||
import { KubeConfig } from "@kubernetes/client-node";
|
||||
import _ from "lodash";
|
||||
import move from "array-move";
|
||||
import type { WorkspaceId } from "./workspace-store";
|
||||
|
||||
export interface ClusterIconUpload {
|
||||
clusterId: string;
|
||||
@@ -20,6 +20,10 @@ export interface ClusterIconUpload {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface ClusterMetadata {
|
||||
[key: string]: string | number | boolean;
|
||||
}
|
||||
|
||||
export interface ClusterStoreModel {
|
||||
activeCluster?: ClusterId; // last opened cluster
|
||||
clusters?: ClusterModel[]
|
||||
@@ -29,10 +33,12 @@ export type ClusterId = string;
|
||||
|
||||
export interface ClusterModel {
|
||||
id: ClusterId;
|
||||
kubeConfigPath: string;
|
||||
workspace?: WorkspaceId;
|
||||
contextName?: string;
|
||||
preferences?: ClusterPreferences;
|
||||
kubeConfigPath: string;
|
||||
metadata?: ClusterMetadata;
|
||||
ownerRef?: string;
|
||||
|
||||
/** @deprecated */
|
||||
kubeConfig?: string; // yaml
|
||||
@@ -67,25 +73,34 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
return filePath;
|
||||
}
|
||||
|
||||
@observable activeCluster: ClusterId;
|
||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||
|
||||
private constructor() {
|
||||
super({
|
||||
configName: "lens-cluster-store",
|
||||
accessPropertiesByDotNotation: false, // To make dots safe in cluster context names
|
||||
migrations: migrations,
|
||||
});
|
||||
|
||||
this.pushStateToViewsPeriodically()
|
||||
}
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
@observable removedClusters = observable.map<ClusterId, Cluster>();
|
||||
@observable clusters = observable.map<ClusterId, Cluster>();
|
||||
protected pushStateToViewsPeriodically() {
|
||||
if (!ipcRenderer) {
|
||||
// This is a bit of a hack, we need to do this because we might loose messages that are sent before a view is ready
|
||||
setInterval(() => {
|
||||
this.pushState()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcListener() {
|
||||
logger.info(`[CLUSTER-STORE] start to listen (${webFrame.routingId})`)
|
||||
ipcRenderer.on("cluster:state", (event, model: ClusterState) => {
|
||||
this.applyWithoutSync(() => {
|
||||
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, model);
|
||||
this.getById(model.id)?.updateModel(model);
|
||||
})
|
||||
ipcRenderer.on("cluster:state", (event, clusterId: string, state: ClusterState) => {
|
||||
logger.silly(`[CLUSTER-STORE]: received push-state at ${location.host} (${webFrame.routingId})`, clusterId, state);
|
||||
this.getById(clusterId)?.setState(state)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,21 +109,35 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
ipcRenderer.removeAllListeners("cluster:state")
|
||||
}
|
||||
|
||||
@computed get activeCluster(): Cluster | null {
|
||||
return this.getById(this.activeClusterId);
|
||||
pushState() {
|
||||
this.clusters.forEach((c) => {
|
||||
c.pushState()
|
||||
})
|
||||
}
|
||||
|
||||
get activeClusterId() {
|
||||
return this.activeCluster
|
||||
}
|
||||
|
||||
@computed get clustersList(): Cluster[] {
|
||||
return Array.from(this.clusters.values());
|
||||
}
|
||||
|
||||
@computed get enabledClustersList(): Cluster[] {
|
||||
return this.clustersList.filter((c) => c.enabled)
|
||||
}
|
||||
|
||||
@computed get active(): Cluster | null {
|
||||
return this.getById(this.activeCluster);
|
||||
}
|
||||
|
||||
isActive(id: ClusterId) {
|
||||
return this.activeClusterId === id;
|
||||
return this.activeCluster === id;
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id: ClusterId) {
|
||||
this.activeClusterId = id;
|
||||
this.activeCluster = this.clusters.has(id) ? id : null;
|
||||
}
|
||||
|
||||
@action
|
||||
@@ -140,22 +169,38 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
addCluster(...models: ClusterModel[]) {
|
||||
addClusters(...models: ClusterModel[]): Cluster[] {
|
||||
const clusters: Cluster[] = []
|
||||
models.forEach(model => {
|
||||
tracker.event("cluster", "add");
|
||||
const cluster = new Cluster(model);
|
||||
this.clusters.set(model.id, cluster);
|
||||
clusters.push(this.addCluster(model))
|
||||
})
|
||||
|
||||
return clusters
|
||||
}
|
||||
|
||||
@action
|
||||
addCluster(model: ClusterModel | Cluster ): Cluster {
|
||||
appEventBus.emit({name: "cluster", action: "add"})
|
||||
let cluster = model as Cluster;
|
||||
if (!(model instanceof Cluster)) {
|
||||
cluster = new Cluster(model)
|
||||
}
|
||||
this.clusters.set(model.id, cluster);
|
||||
return cluster
|
||||
}
|
||||
|
||||
async removeCluster(model: ClusterModel) {
|
||||
await this.removeById(model.id)
|
||||
}
|
||||
|
||||
@action
|
||||
async removeById(clusterId: ClusterId) {
|
||||
tracker.event("cluster", "remove");
|
||||
appEventBus.emit({name: "cluster", action: "remove"})
|
||||
const cluster = this.getById(clusterId);
|
||||
if (cluster) {
|
||||
this.clusters.delete(clusterId);
|
||||
if (this.activeClusterId === clusterId) {
|
||||
this.activeClusterId = null;
|
||||
if (this.activeCluster === clusterId) {
|
||||
this.setActive(null);
|
||||
}
|
||||
// remove only custom kubeconfigs (pasted as text)
|
||||
if (cluster.kubeConfigPath == ClusterStore.getCustomKubeConfigPath(clusterId)) {
|
||||
@@ -184,6 +229,9 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
cluster.updateModel(clusterModel);
|
||||
} else {
|
||||
cluster = new Cluster(clusterModel);
|
||||
if (!cluster.isManaged) {
|
||||
cluster.enabled = true
|
||||
}
|
||||
}
|
||||
newClusters.set(clusterModel.id, cluster);
|
||||
}
|
||||
@@ -195,14 +243,14 @@ export class ClusterStore extends BaseStore<ClusterStoreModel> {
|
||||
}
|
||||
});
|
||||
|
||||
this.activeClusterId = newClusters.has(activeCluster) ? activeCluster : null;
|
||||
this.activeCluster = newClusters.has(activeCluster) ? activeCluster : null;
|
||||
this.clusters.replace(newClusters);
|
||||
this.removedClusters.replace(removedClusters);
|
||||
}
|
||||
|
||||
toJSON(): ClusterStoreModel {
|
||||
return toJS({
|
||||
activeCluster: this.activeClusterId,
|
||||
activeCluster: this.activeCluster,
|
||||
clusters: this.clustersList.map(cluster => cluster.toJSON()),
|
||||
}, {
|
||||
recurseEverything: true
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import { EventEmitter } from "./event-emitter"
|
||||
|
||||
export type AppEvent = {
|
||||
name: string;
|
||||
action: string;
|
||||
params?: object;
|
||||
}
|
||||
|
||||
export const appEventBus = new EventEmitter<[AppEvent]>()
|
||||
File renamed without changes.
@@ -1,49 +0,0 @@
|
||||
import { app, App, remote } from "electron"
|
||||
import ua from "universal-analytics"
|
||||
import { machineIdSync } from "node-machine-id"
|
||||
import Singleton from "./utils/singleton";
|
||||
import { userStore } from "./user-store"
|
||||
import logger from "../main/logger";
|
||||
|
||||
export class Tracker extends Singleton {
|
||||
static readonly GA_ID = "UA-159377374-1"
|
||||
|
||||
protected visitor: ua.Visitor
|
||||
protected machineId: string = null;
|
||||
protected ip: string = null;
|
||||
protected appVersion: string;
|
||||
protected locale: string;
|
||||
protected electronUA: string;
|
||||
|
||||
private constructor(app: App) {
|
||||
super();
|
||||
try {
|
||||
this.visitor = ua(Tracker.GA_ID, machineIdSync(), { strictCidFormat: false })
|
||||
} catch (error) {
|
||||
this.visitor = ua(Tracker.GA_ID)
|
||||
}
|
||||
this.visitor.set("dl", "https://telemetry.k8slens.dev")
|
||||
}
|
||||
|
||||
protected async isTelemetryAllowed(): Promise<boolean> {
|
||||
return userStore.preferences.allowTelemetry;
|
||||
}
|
||||
|
||||
async event(eventCategory: string, eventAction: string, otherParams = {}) {
|
||||
try {
|
||||
const allowed = await this.isTelemetryAllowed();
|
||||
if (!allowed) {
|
||||
return;
|
||||
}
|
||||
this.visitor.event({
|
||||
ec: eventCategory,
|
||||
ea: eventAction,
|
||||
...otherParams,
|
||||
}).send()
|
||||
} catch (err) {
|
||||
logger.error(`Failed to track "${eventCategory}:${eventAction}"`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const tracker = Tracker.getInstance<Tracker>(app || remote.app);
|
||||
+27
-10
@@ -7,7 +7,7 @@ import { BaseStore } from "./base-store";
|
||||
import migrations from "../migrations/user-store"
|
||||
import { getAppVersion } from "./utils/app-version";
|
||||
import { kubeConfigDefaultPath, loadConfig } from "./kube-helpers";
|
||||
import { tracker } from "./tracker";
|
||||
import { appEventBus } from "./event-bus"
|
||||
import logger from "../main/logger";
|
||||
import path from 'path';
|
||||
|
||||
@@ -27,6 +27,7 @@ export interface UserPreferences {
|
||||
downloadKubectlBinaries?: boolean;
|
||||
downloadBinariesPath?: string;
|
||||
kubectlBinariesPath?: string;
|
||||
openAtLogin?: boolean;
|
||||
}
|
||||
|
||||
export class UserStore extends BaseStore<UserStoreModel> {
|
||||
@@ -38,14 +39,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
migrations: migrations,
|
||||
});
|
||||
|
||||
// track telemetry availability
|
||||
reaction(() => this.preferences.allowTelemetry, allowed => {
|
||||
tracker.event("telemetry", allowed ? "enabled" : "disabled");
|
||||
});
|
||||
|
||||
// refresh new contexts
|
||||
this.whenLoaded.then(this.refreshNewContexts);
|
||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
||||
this.handleOnLoad();
|
||||
}
|
||||
|
||||
@observable lastSeenAppVersion = "0.0.0"
|
||||
@@ -59,8 +53,31 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
colorTheme: UserStore.defaultTheme,
|
||||
downloadMirror: "default",
|
||||
downloadKubectlBinaries: true, // Download kubectl binaries matching cluster version
|
||||
openAtLogin: true,
|
||||
};
|
||||
|
||||
protected async handleOnLoad() {
|
||||
await this.whenLoaded;
|
||||
|
||||
// refresh new contexts
|
||||
this.refreshNewContexts();
|
||||
reaction(() => this.kubeConfigPath, this.refreshNewContexts);
|
||||
|
||||
if (app) {
|
||||
// track telemetry availability
|
||||
reaction(() => this.preferences.allowTelemetry, allowed => {
|
||||
appEventBus.emit({name: "telemetry", action: allowed ? "enabled" : "disabled"})
|
||||
});
|
||||
|
||||
// open at system start-up
|
||||
reaction(() => this.preferences.openAtLogin, open => {
|
||||
app.setLoginItemSettings({ openAtLogin: open });
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
get isNewVersion() {
|
||||
return semver.gt(getAppVersion(), this.lastSeenAppVersion);
|
||||
}
|
||||
@@ -77,7 +94,7 @@ export class UserStore extends BaseStore<UserStoreModel> {
|
||||
|
||||
@action
|
||||
saveLastSeenAppVersion() {
|
||||
tracker.event("app", "whats-new-seen")
|
||||
appEventBus.emit({name: "app", action: "whats-new-seen"})
|
||||
this.lastSeenAppVersion = getAppVersion();
|
||||
}
|
||||
|
||||
|
||||
@@ -7,3 +7,7 @@ export function getAppVersion(): string {
|
||||
export function getBundledKubectlVersion(): string {
|
||||
return packageInfo.config.bundledKubectlVersion;
|
||||
}
|
||||
|
||||
export function getBundledExtensions(): string[] {
|
||||
return packageInfo.lens?.extensions || []
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { compile } from "path-to-regexp"
|
||||
|
||||
export interface IURLParams<P extends object = {}, Q extends object = {}> {
|
||||
params?: P;
|
||||
query?: Q;
|
||||
}
|
||||
|
||||
export function buildURL<P extends object = {}, Q extends object = {}>(path: string | any) {
|
||||
const pathBuilder = compile(String(path));
|
||||
return function ({ params, query }: IURLParams<P, Q> = {}) {
|
||||
const queryParams = query ? new URLSearchParams(Object.entries(query)).toString() : ""
|
||||
return pathBuilder(params) + (queryParams ? `?${queryParams}` : "")
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import { defineGlobal } from "./utils/defineGlobal";
|
||||
|
||||
export const isMac = process.platform === "darwin"
|
||||
export const isWindows = process.platform === "win32"
|
||||
export const isLinux = process.platform === "linux"
|
||||
export const isDebugging = process.env.DEBUG === "true";
|
||||
export const isSnap = !!process.env["SNAP"]
|
||||
export const isProduction = process.env.NODE_ENV === "production"
|
||||
export const isTestEnv = !!process.env.JEST_WORKER_ID;
|
||||
export const isDevelopment = !isTestEnv && !isProduction;
|
||||
|
||||
+111
-18
@@ -1,20 +1,77 @@
|
||||
import { action, computed, observable, toJS } from "mobx";
|
||||
import { ipcRenderer } from "electron";
|
||||
import { action, computed, observable, toJS, reaction } from "mobx";
|
||||
import { BaseStore } from "./base-store";
|
||||
import { clusterStore } from "./cluster-store"
|
||||
import { landingURL } from "../renderer/components/+landing-page/landing-page.route";
|
||||
import { navigate } from "../renderer/navigation";
|
||||
import { appEventBus } from "./event-bus";
|
||||
import { broadcastIpc } from "../common/ipc";
|
||||
import logger from "../main/logger";
|
||||
|
||||
export type WorkspaceId = string;
|
||||
|
||||
export interface WorkspaceStoreModel {
|
||||
currentWorkspace?: WorkspaceId;
|
||||
workspaces: Workspace[]
|
||||
workspaces: WorkspaceModel[]
|
||||
}
|
||||
|
||||
export interface Workspace {
|
||||
export interface WorkspaceModel {
|
||||
id: WorkspaceId;
|
||||
name: string;
|
||||
description?: string;
|
||||
ownerRef?: string;
|
||||
}
|
||||
|
||||
export interface WorkspaceState {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
export class Workspace implements WorkspaceModel, WorkspaceState {
|
||||
@observable id: WorkspaceId
|
||||
@observable name: string
|
||||
@observable description?: string
|
||||
@observable ownerRef?: string
|
||||
@observable enabled: boolean
|
||||
|
||||
constructor(data: WorkspaceModel) {
|
||||
Object.assign(this, data)
|
||||
|
||||
if (!ipcRenderer) {
|
||||
reaction(() => this.getState(), () => {
|
||||
this.pushState()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
get isManaged(): boolean {
|
||||
return !!this.ownerRef
|
||||
}
|
||||
|
||||
getState(): WorkspaceState {
|
||||
return {
|
||||
enabled: this.enabled
|
||||
}
|
||||
}
|
||||
|
||||
pushState(state = this.getState()) {
|
||||
logger.silly("[WORKSPACE] pushing state", {...state, id: this.id})
|
||||
broadcastIpc({
|
||||
channel: "workspace:state",
|
||||
args: [this.id, toJS(state)],
|
||||
});
|
||||
}
|
||||
|
||||
@action
|
||||
setState(state: WorkspaceState) {
|
||||
Object.assign(this, state)
|
||||
}
|
||||
|
||||
toJSON(): WorkspaceModel {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
description: this.description,
|
||||
ownerRef: this.ownerRef
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
@@ -24,15 +81,33 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
super({
|
||||
configName: "lens-workspace-store",
|
||||
});
|
||||
|
||||
if (!ipcRenderer) {
|
||||
setInterval(() => {
|
||||
this.pushState()
|
||||
}, 5000)
|
||||
}
|
||||
}
|
||||
|
||||
registerIpcListener() {
|
||||
logger.info("[WORKSPACE-STORE] starting to listen state events")
|
||||
ipcRenderer.on("workspace:state", (event, workspaceId: string, state: WorkspaceState) => {
|
||||
this.getById(workspaceId)?.setState(state)
|
||||
})
|
||||
}
|
||||
|
||||
unregisterIpcListener() {
|
||||
super.unregisterIpcListener()
|
||||
ipcRenderer.removeAllListeners("workspace:state")
|
||||
}
|
||||
|
||||
@observable currentWorkspaceId = WorkspaceStore.defaultId;
|
||||
|
||||
@observable workspaces = observable.map<WorkspaceId, Workspace>({
|
||||
[WorkspaceStore.defaultId]: {
|
||||
[WorkspaceStore.defaultId]: new Workspace({
|
||||
id: WorkspaceStore.defaultId,
|
||||
name: "default"
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
@computed get currentWorkspace(): Workspace {
|
||||
@@ -43,6 +118,16 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
return Array.from(this.workspaces.values());
|
||||
}
|
||||
|
||||
@computed get enabledWorkspacesList() {
|
||||
return this.workspacesList.filter((w) => w.enabled);
|
||||
}
|
||||
|
||||
pushState() {
|
||||
this.workspaces.forEach((w) => {
|
||||
w.pushState()
|
||||
})
|
||||
}
|
||||
|
||||
isDefault(id: WorkspaceId) {
|
||||
return id === WorkspaceStore.defaultId;
|
||||
}
|
||||
@@ -56,22 +141,17 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
}
|
||||
|
||||
@action
|
||||
setActive(id = WorkspaceStore.defaultId, { redirectToLanding = true, resetActiveCluster = true } = {}) {
|
||||
setActive(id = WorkspaceStore.defaultId, reset = true) {
|
||||
if (id === this.currentWorkspaceId) return;
|
||||
if (!this.getById(id)) {
|
||||
throw new Error(`workspace ${id} doesn't exist`);
|
||||
}
|
||||
this.currentWorkspaceId = id;
|
||||
if (resetActiveCluster) {
|
||||
clusterStore.setActive(null)
|
||||
}
|
||||
if (redirectToLanding) {
|
||||
navigate(landingURL())
|
||||
}
|
||||
clusterStore.activeCluster = null; // fixme: handle previously selected cluster from current workspace
|
||||
}
|
||||
|
||||
@action
|
||||
saveWorkspace(workspace: Workspace) {
|
||||
addWorkspace(workspace: Workspace) {
|
||||
const { id, name } = workspace;
|
||||
const existingWorkspace = this.getById(id);
|
||||
if (!name.trim() || this.getByName(name.trim())) {
|
||||
@@ -79,13 +159,21 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
}
|
||||
if (existingWorkspace) {
|
||||
Object.assign(existingWorkspace, workspace);
|
||||
appEventBus.emit({name: "workspace", action: "update"})
|
||||
} else {
|
||||
appEventBus.emit({name: "workspace", action: "add"})
|
||||
}
|
||||
this.workspaces.set(id, workspace);
|
||||
return workspace;
|
||||
}
|
||||
|
||||
@action
|
||||
removeWorkspace(id: WorkspaceId) {
|
||||
removeWorkspace(workspace: Workspace) {
|
||||
this.removeWorkspaceById(workspace.id)
|
||||
}
|
||||
|
||||
@action
|
||||
removeWorkspaceById(id: WorkspaceId) {
|
||||
const workspace = this.getById(id);
|
||||
if (!workspace) return;
|
||||
if (this.isDefault(id)) {
|
||||
@@ -95,6 +183,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
this.currentWorkspaceId = WorkspaceStore.defaultId; // reset to default
|
||||
}
|
||||
this.workspaces.delete(id);
|
||||
appEventBus.emit({name: "workspace", action: "remove"})
|
||||
clusterStore.removeByWorkspaceId(id)
|
||||
}
|
||||
|
||||
@@ -105,7 +194,11 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
}
|
||||
if (workspaces.length) {
|
||||
this.workspaces.clear();
|
||||
workspaces.forEach(workspace => {
|
||||
workspaces.forEach(ws => {
|
||||
const workspace = new Workspace(ws)
|
||||
if (!workspace.isManaged) {
|
||||
workspace.enabled = true
|
||||
}
|
||||
this.workspaces.set(workspace.id, workspace)
|
||||
})
|
||||
}
|
||||
@@ -114,7 +207,7 @@ export class WorkspaceStore extends BaseStore<WorkspaceStoreModel> {
|
||||
toJSON(): WorkspaceStoreModel {
|
||||
return toJS({
|
||||
currentWorkspace: this.currentWorkspaceId,
|
||||
workspaces: this.workspacesList,
|
||||
workspaces: this.workspacesList.map((w) => w.toJSON()),
|
||||
}, {
|
||||
recurseEverything: true
|
||||
})
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import hb from "handlebars"
|
||||
import { observable } from "mobx"
|
||||
import { ResourceApplier } from "../main/resource-applier"
|
||||
import { Cluster } from "../main/cluster";
|
||||
import logger from "../main/logger";
|
||||
import { app } from "electron"
|
||||
import { clusterIpc } from "../common/cluster-ipc"
|
||||
|
||||
export interface ClusterFeatureStatus {
|
||||
currentVersion: string;
|
||||
installed: boolean;
|
||||
latestVersion: string;
|
||||
canUpgrade: boolean;
|
||||
}
|
||||
|
||||
export abstract class ClusterFeature {
|
||||
name: string;
|
||||
latestVersion: string;
|
||||
config: any;
|
||||
|
||||
@observable status: ClusterFeatureStatus = {
|
||||
currentVersion: null,
|
||||
installed: false,
|
||||
latestVersion: null,
|
||||
canUpgrade: false
|
||||
}
|
||||
|
||||
abstract async install(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||
|
||||
protected async applyResources(cluster: Cluster, resources: string[]) {
|
||||
if (app) {
|
||||
await new ResourceApplier(cluster).kubectlApplyAll(resources)
|
||||
} else {
|
||||
await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources)
|
||||
}
|
||||
}
|
||||
|
||||
protected renderTemplates(folderPath: string): string[] {
|
||||
const resources: string[] = [];
|
||||
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||
fs.readdirSync(folderPath).forEach(filename => {
|
||||
const file = path.join(folderPath, filename);
|
||||
const raw = fs.readFileSync(file);
|
||||
if (filename.endsWith('.hb')) {
|
||||
const template = hb.compile(raw.toString());
|
||||
resources.push(template(this.config));
|
||||
} else {
|
||||
resources.push(raw.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { getAppVersion } from "../../common/utils";
|
||||
|
||||
export const version = getAppVersion()
|
||||
export { isSnap, isWindows, isMac, isLinux, appName, slackUrl, issuesTrackerUrl } from "../../common/vars"
|
||||
@@ -0,0 +1 @@
|
||||
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
|
||||
@@ -0,0 +1,2 @@
|
||||
export { appEventBus } from "../../common/event-bus"
|
||||
export type { AppEvent } from "../../common/event-bus"
|
||||
@@ -0,0 +1,25 @@
|
||||
// Lens-extensions api developer's kit
|
||||
export * from "../lens-main-extension"
|
||||
export * from "../lens-renderer-extension"
|
||||
|
||||
import type { WindowManager } from "../../main/window-manager";
|
||||
|
||||
// APIs
|
||||
import * as App from "./app"
|
||||
import * as EventBus from "./event-bus"
|
||||
import * as Store from "./stores"
|
||||
import * as Util from "./utils"
|
||||
import * as Registry from "../registries"
|
||||
import * as ClusterFeature from "./cluster-feature"
|
||||
|
||||
// TODO: allow to expose windowManager.navigate() as Navigation.navigate() in runtime
|
||||
export let windowManager: WindowManager;
|
||||
|
||||
export {
|
||||
App,
|
||||
EventBus,
|
||||
ClusterFeature,
|
||||
Store,
|
||||
Util,
|
||||
Registry,
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
export { ExtensionStore } from "../extension-store"
|
||||
export { clusterStore, ClusterModel } from "../../common/cluster-store"
|
||||
export { Cluster } from "../../main/cluster"
|
||||
export { workspaceStore, Workspace, WorkspaceModel } from "../../common/workspace-store"
|
||||
@@ -0,0 +1,3 @@
|
||||
export { Singleton } from "../../common/utils"
|
||||
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"
|
||||
export { cssNames } from "../../renderer/utils/cssNames"
|
||||
@@ -0,0 +1,15 @@
|
||||
import React from "react";
|
||||
import { cssNames } from "../renderer/utils";
|
||||
import { TabLayout } from "../renderer/components/layout/tab-layout";
|
||||
import { PageRegistration } from "./registries/page-registry"
|
||||
|
||||
export class DynamicPage extends React.Component<{ page: PageRegistration }> {
|
||||
render() {
|
||||
const { className, components: { Page }, subPages = [] } = this.props.page;
|
||||
return (
|
||||
<TabLayout className={cssNames("ExtensionPage", className)} tabs={subPages}>
|
||||
<Page/>
|
||||
</TabLayout>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
// Extension-api types generation bundle (used by rollup.js)
|
||||
|
||||
export * from "./core-api"
|
||||
export * from "./renderer-api"
|
||||
@@ -0,0 +1,137 @@
|
||||
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
|
||||
import type { LensMainExtension } from "./lens-main-extension"
|
||||
import type { LensRendererExtension } from "./lens-renderer-extension"
|
||||
import path from "path"
|
||||
import { broadcastIpc } from "../common/ipc"
|
||||
import { observable, reaction, toJS, } from "mobx"
|
||||
import logger from "../main/logger"
|
||||
import { app, ipcRenderer, remote } from "electron"
|
||||
import { appPreferenceRegistry, clusterFeatureRegistry, clusterPageRegistry, globalPageRegistry, kubeObjectMenuRegistry, menuRegistry, statusBarRegistry } from "./registries";
|
||||
|
||||
export interface InstalledExtension extends ExtensionModel {
|
||||
manifestPath: string;
|
||||
manifest: ExtensionManifest;
|
||||
}
|
||||
|
||||
// lazy load so that we get correct userData
|
||||
export function extensionPackagesRoot() {
|
||||
return path.join((app || remote.app).getPath("userData"))
|
||||
}
|
||||
|
||||
export class ExtensionLoader {
|
||||
@observable extensions = observable.map<ExtensionId, InstalledExtension>([], { deep: false });
|
||||
@observable instances = observable.map<ExtensionId, LensExtension>([], { deep: false })
|
||||
|
||||
constructor() {
|
||||
if (ipcRenderer) {
|
||||
ipcRenderer.on("extensions:loaded", (event, extensions: InstalledExtension[]) => {
|
||||
extensions.forEach((ext) => {
|
||||
if (!this.getById(ext.manifestPath)) {
|
||||
this.extensions.set(ext.manifestPath, ext)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
loadOnMain() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on main')
|
||||
this.autoloadExtensions((extension: LensMainExtension) => {
|
||||
extension.registerTo(menuRegistry, extension.appMenus)
|
||||
})
|
||||
}
|
||||
|
||||
loadOnClusterManagerRenderer() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
||||
this.autoloadExtensions((extension: LensRendererExtension) => {
|
||||
extension.registerTo(globalPageRegistry, extension.globalPages)
|
||||
extension.registerTo(appPreferenceRegistry, extension.appPreferences)
|
||||
extension.registerTo(clusterFeatureRegistry, extension.clusterFeatures)
|
||||
extension.registerTo(statusBarRegistry, extension.statusBarItems)
|
||||
})
|
||||
}
|
||||
|
||||
loadOnClusterRenderer() {
|
||||
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
||||
this.autoloadExtensions((extension: LensRendererExtension) => {
|
||||
extension.registerTo(clusterPageRegistry, extension.clusterPages)
|
||||
extension.registerTo(kubeObjectMenuRegistry, extension.kubeObjectMenuItems)
|
||||
})
|
||||
}
|
||||
|
||||
protected autoloadExtensions(callback: (instance: LensExtension) => void) {
|
||||
return reaction(() => this.extensions.toJS(), (installedExtensions) => {
|
||||
for(const [id, ext] of installedExtensions) {
|
||||
let instance = this.instances.get(ext.id)
|
||||
if (!instance) {
|
||||
const extensionModule = this.requireExtension(ext)
|
||||
if (!extensionModule) {
|
||||
continue
|
||||
}
|
||||
const LensExtensionClass = extensionModule.default;
|
||||
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
|
||||
try {
|
||||
instance.enable()
|
||||
callback(instance)
|
||||
} finally {
|
||||
this.instances.set(ext.id, instance)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, {
|
||||
fireImmediately: true,
|
||||
delay: 0,
|
||||
})
|
||||
}
|
||||
|
||||
protected requireExtension(extension: InstalledExtension) {
|
||||
let extEntrypoint = ""
|
||||
try {
|
||||
if (ipcRenderer && extension.manifest.renderer) {
|
||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.renderer))
|
||||
} else if (!ipcRenderer && extension.manifest.main) {
|
||||
extEntrypoint = path.resolve(path.join(path.dirname(extension.manifestPath), extension.manifest.main))
|
||||
}
|
||||
if (extEntrypoint !== "") {
|
||||
return __non_webpack_require__(extEntrypoint)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[EXTENSION-LOADER]: can't load extension main at ${extEntrypoint}: ${err}`, { extension });
|
||||
console.trace(err)
|
||||
}
|
||||
}
|
||||
|
||||
getById(id: ExtensionId): InstalledExtension {
|
||||
return this.extensions.get(id);
|
||||
}
|
||||
|
||||
async removeById(id: ExtensionId) {
|
||||
const extension = this.getById(id);
|
||||
if (extension) {
|
||||
const instance = this.instances.get(extension.id)
|
||||
if (instance) {
|
||||
await instance.disable()
|
||||
}
|
||||
this.extensions.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
broadcastExtensions(frameId?: number) {
|
||||
broadcastIpc({
|
||||
channel: "extensions:loaded",
|
||||
frameId: frameId,
|
||||
frameOnly: !!frameId,
|
||||
args: [this.toJSON().extensions],
|
||||
})
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return toJS({
|
||||
extensions: Array.from(this.extensions).map(([id, instance]) => instance),
|
||||
}, {
|
||||
recurseEverything: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionLoader = new ExtensionLoader()
|
||||
@@ -0,0 +1,162 @@
|
||||
import type { ExtensionManifest } from "./lens-extension"
|
||||
import path from "path"
|
||||
import os from "os"
|
||||
import fs from "fs-extra"
|
||||
import logger from "../main/logger"
|
||||
import { extensionPackagesRoot, InstalledExtension } from "./extension-loader"
|
||||
import * as child_process from 'child_process';
|
||||
import { getBundledExtensions } from "../common/utils/app-version"
|
||||
|
||||
type Dependencies = {
|
||||
[name: string]: string;
|
||||
}
|
||||
|
||||
type PackageJson = {
|
||||
dependencies: Dependencies;
|
||||
}
|
||||
|
||||
export class ExtensionManager {
|
||||
|
||||
protected bundledFolderPath: string
|
||||
|
||||
protected packagesJson: PackageJson = {
|
||||
dependencies: {}
|
||||
}
|
||||
|
||||
get extensionPackagesRoot() {
|
||||
return extensionPackagesRoot()
|
||||
}
|
||||
|
||||
get inTreeTargetPath() {
|
||||
return path.join(this.extensionPackagesRoot, "extensions")
|
||||
}
|
||||
|
||||
get inTreeFolderPath(): string {
|
||||
return path.resolve(__static, "../extensions");
|
||||
}
|
||||
|
||||
get nodeModulesPath(): string {
|
||||
return path.join(this.extensionPackagesRoot, "node_modules")
|
||||
}
|
||||
|
||||
get localFolderPath(): string {
|
||||
return path.join(os.homedir(), ".k8slens", "extensions");
|
||||
}
|
||||
|
||||
get npmPath() {
|
||||
return __non_webpack_require__.resolve('npm/bin/npm-cli')
|
||||
}
|
||||
|
||||
get packageJsonPath() {
|
||||
return path.join(this.extensionPackagesRoot, "package.json")
|
||||
}
|
||||
|
||||
async load() {
|
||||
logger.info("[EXTENSION-MANAGER] loading extensions from " + this.extensionPackagesRoot)
|
||||
if (fs.existsSync(path.join(this.extensionPackagesRoot, "package-lock.json"))) {
|
||||
await fs.remove(path.join(this.extensionPackagesRoot, "package-lock.json"))
|
||||
}
|
||||
try {
|
||||
await fs.access(this.inTreeFolderPath, fs.constants.W_OK)
|
||||
this.bundledFolderPath = this.inTreeFolderPath
|
||||
} catch {
|
||||
// we need to copy in-tree extensions so that we can symlink them properly on "npm install"
|
||||
await fs.remove(this.inTreeTargetPath)
|
||||
await fs.ensureDir(this.inTreeTargetPath)
|
||||
await fs.copy(this.inTreeFolderPath, this.inTreeTargetPath)
|
||||
this.bundledFolderPath = this.inTreeTargetPath
|
||||
}
|
||||
await fs.ensureDir(this.nodeModulesPath)
|
||||
await fs.ensureDir(this.localFolderPath)
|
||||
return await this.loadExtensions();
|
||||
}
|
||||
|
||||
async getExtensionByManifest(manifestPath: string): Promise<InstalledExtension> {
|
||||
let manifestJson: ExtensionManifest;
|
||||
try {
|
||||
fs.accessSync(manifestPath, fs.constants.F_OK); // check manifest file for existence
|
||||
manifestJson = __non_webpack_require__(manifestPath)
|
||||
this.packagesJson.dependencies[manifestJson.name] = path.dirname(manifestPath)
|
||||
|
||||
logger.info("[EXTENSION-MANAGER] installed extension " + manifestJson.name)
|
||||
return {
|
||||
id: manifestJson.name,
|
||||
version: manifestJson.version,
|
||||
name: manifestJson.name,
|
||||
manifestPath: path.join(this.nodeModulesPath, manifestJson.name, "package.json"),
|
||||
manifest: manifestJson
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION-MANAGER]: can't install extension at ${manifestPath}: ${err}`, { manifestJson });
|
||||
}
|
||||
}
|
||||
|
||||
protected installPackages(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = child_process.fork(this.npmPath, ["install", "--silent", "--no-audit", "--only=prod", "--prefer-offline", "--no-package-lock"], {
|
||||
cwd: extensionPackagesRoot(),
|
||||
silent: true
|
||||
})
|
||||
child.on("close", () => {
|
||||
resolve()
|
||||
})
|
||||
child.on("error", (err) => {
|
||||
reject(err)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async loadExtensions() {
|
||||
const bundledExtensions = await this.loadBundledExtensions()
|
||||
const localExtensions = await this.loadFromFolder(this.localFolderPath)
|
||||
await fs.writeFile(path.join(this.packageJsonPath), JSON.stringify(this.packagesJson, null, 2), {mode: 0o600})
|
||||
await this.installPackages()
|
||||
const extensions = bundledExtensions.concat(localExtensions)
|
||||
return new Map(extensions.map(ext => [ext.id, ext]));
|
||||
}
|
||||
|
||||
async loadBundledExtensions() {
|
||||
const extensions: InstalledExtension[] = []
|
||||
const folderPath = this.bundledFolderPath
|
||||
const bundledExtensions = getBundledExtensions()
|
||||
const paths = await fs.readdir(folderPath);
|
||||
for (const fileName of paths) {
|
||||
if (!bundledExtensions.includes(fileName)) {
|
||||
continue
|
||||
}
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
const manifestPath = path.resolve(absPath, "package.json");
|
||||
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
|
||||
if (ext) {
|
||||
extensions.push(ext)
|
||||
}
|
||||
}
|
||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions
|
||||
}
|
||||
|
||||
async loadFromFolder(folderPath: string): Promise<InstalledExtension[]> {
|
||||
const bundledExtensions = getBundledExtensions()
|
||||
const extensions: InstalledExtension[] = []
|
||||
const paths = await fs.readdir(folderPath);
|
||||
for (const fileName of paths) {
|
||||
if (bundledExtensions.includes(fileName)) { // do no allow to override bundled extensions
|
||||
continue
|
||||
}
|
||||
const absPath = path.resolve(folderPath, fileName);
|
||||
if (!fs.existsSync(absPath) || !fs.lstatSync(absPath).isDirectory()) { // skip non-directories
|
||||
continue;
|
||||
}
|
||||
const manifestPath = path.resolve(absPath, "package.json");
|
||||
const ext = await this.getExtensionByManifest(manifestPath).catch(() => null)
|
||||
if (ext) {
|
||||
extensions.push(ext)
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`[EXTENSION-MANAGER]: ${extensions.length} extensions loaded`, { folderPath, extensions });
|
||||
return extensions;
|
||||
}
|
||||
}
|
||||
|
||||
export const extensionManager = new ExtensionManager()
|
||||
@@ -0,0 +1,21 @@
|
||||
import { BaseStore } from "../common/base-store"
|
||||
import * as path from "path"
|
||||
import { LensExtension } from "./lens-extension"
|
||||
|
||||
export class ExtensionStore<T = any> extends BaseStore<T> {
|
||||
protected extension: LensExtension
|
||||
|
||||
async loadExtension(extension: LensExtension) {
|
||||
this.extension = extension
|
||||
await super.load()
|
||||
}
|
||||
|
||||
async load() {
|
||||
if (!this.extension) { return }
|
||||
await super.load()
|
||||
}
|
||||
|
||||
protected storePath() {
|
||||
return path.join(super.storePath(), "extension-store", this.extension.name)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import { readJsonSync } from "fs-extra";
|
||||
import { action, observable, toJS } from "mobx";
|
||||
import logger from "../main/logger";
|
||||
import { BaseRegistry } from "./registries/base-registry";
|
||||
|
||||
export type ExtensionId = string | ExtensionPackageJsonPath;
|
||||
export type ExtensionPackageJsonPath = string;
|
||||
export type ExtensionVersion = string | number;
|
||||
|
||||
export interface ExtensionModel {
|
||||
id: ExtensionId;
|
||||
version: ExtensionVersion;
|
||||
name: string;
|
||||
manifestPath: string;
|
||||
description?: string;
|
||||
enabled?: boolean;
|
||||
updateUrl?: string;
|
||||
}
|
||||
|
||||
export interface ExtensionManifest extends ExtensionModel {
|
||||
main?: string;
|
||||
renderer?: string;
|
||||
description?: string; // todo: add more fields similar to package.json + some extra
|
||||
}
|
||||
|
||||
export class LensExtension implements ExtensionModel {
|
||||
public id: ExtensionId;
|
||||
public updateUrl: string;
|
||||
protected disposers: (() => void)[] = [];
|
||||
|
||||
@observable name = "";
|
||||
@observable description = "";
|
||||
@observable version: ExtensionVersion = "0.0.0";
|
||||
@observable manifest: ExtensionManifest;
|
||||
@observable manifestPath: string;
|
||||
@observable isEnabled = false;
|
||||
|
||||
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
|
||||
this.importModel(model, manifest);
|
||||
}
|
||||
|
||||
@action
|
||||
async importModel({ enabled, manifestPath, ...model }: ExtensionModel, manifest?: ExtensionManifest) {
|
||||
try {
|
||||
this.manifest = manifest || await readJsonSync(manifestPath, { throws: true })
|
||||
this.manifestPath = manifestPath;
|
||||
Object.assign(this, model);
|
||||
} catch (err) {
|
||||
logger.error(`[EXTENSION]: cannot read manifest at ${manifestPath}`, { ...model, err: String(err) })
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
||||
async migrate(appVersion: string) {
|
||||
// mock
|
||||
}
|
||||
|
||||
async enable() {
|
||||
this.isEnabled = true;
|
||||
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||
this.onActivate();
|
||||
}
|
||||
|
||||
async disable() {
|
||||
this.onDeactivate();
|
||||
this.isEnabled = false;
|
||||
this.disposers.forEach(cleanUp => cleanUp());
|
||||
this.disposers.length = 0;
|
||||
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
||||
}
|
||||
|
||||
// todo: add more hooks
|
||||
protected onActivate() {
|
||||
// mock
|
||||
}
|
||||
|
||||
protected onDeactivate() {
|
||||
// mock
|
||||
}
|
||||
|
||||
registerTo<T = any>(registry: BaseRegistry<T>, items: T[] = []) {
|
||||
const disposers = items.map(item => registry.add(item));
|
||||
this.disposers.push(...disposers);
|
||||
return () => {
|
||||
this.disposers = this.disposers.filter(disposer => !disposers.includes(disposer))
|
||||
};
|
||||
}
|
||||
|
||||
getMeta() {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
manifest: this.manifest,
|
||||
manifestPath: this.manifestPath,
|
||||
enabled: this.isEnabled
|
||||
}, {
|
||||
recurseEverything: true
|
||||
})
|
||||
}
|
||||
|
||||
toJSON(): ExtensionModel {
|
||||
return toJS({
|
||||
id: this.id,
|
||||
name: this.name,
|
||||
version: this.version,
|
||||
description: this.description,
|
||||
manifestPath: this.manifestPath,
|
||||
enabled: this.isEnabled,
|
||||
updateUrl: this.updateUrl,
|
||||
}, {
|
||||
recurseEverything: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { MenuRegistration } from "./registries/menu-registry";
|
||||
import { observable } from "mobx";
|
||||
import { LensExtension } from "./lens-extension"
|
||||
|
||||
export class LensMainExtension extends LensExtension {
|
||||
@observable.shallow appMenus: MenuRegistration[] = []
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { AppPreferenceRegistration, ClusterFeatureRegistration, KubeObjectMenuRegistration, PageRegistration, StatusBarRegistration } from "./registries"
|
||||
import { observable } from "mobx";
|
||||
import { LensExtension } from "./lens-extension"
|
||||
|
||||
export class LensRendererExtension extends LensExtension {
|
||||
@observable.shallow globalPages: PageRegistration[] = []
|
||||
@observable.shallow clusterPages: PageRegistration[] = []
|
||||
@observable.shallow appPreferences: AppPreferenceRegistration[] = []
|
||||
@observable.shallow clusterFeatures: ClusterFeatureRegistration[] = []
|
||||
@observable.shallow statusBarItems: StatusBarRegistration[] = []
|
||||
@observable.shallow kubeObjectMenuItems: KubeObjectMenuRegistration[] = []
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
api.d.ts
|
||||
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "@k8slens/extensions",
|
||||
"productName": "Lens extensions",
|
||||
"description": "Lens - The Kubernetes IDE: extensions",
|
||||
"version": "0.0.0",
|
||||
"copyright": "© 2020, Mirantis, Inc.",
|
||||
"license": "MIT",
|
||||
"types": "api.d.ts",
|
||||
"files": [
|
||||
"api.d.ts"
|
||||
],
|
||||
"author": {
|
||||
"name": "Mirantis, Inc.",
|
||||
"email": "info@k8slens.dev"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import type React from "react"
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface AppPreferenceComponents {
|
||||
Hint: React.ComponentType<any>;
|
||||
Input: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface AppPreferenceRegistration {
|
||||
title: string;
|
||||
components: AppPreferenceComponents;
|
||||
}
|
||||
|
||||
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration> {
|
||||
}
|
||||
|
||||
export const appPreferenceRegistry = new AppPreferenceRegistry()
|
||||
@@ -0,0 +1,17 @@
|
||||
// Base class for extensions-api registries
|
||||
import { observable } from "mobx";
|
||||
|
||||
export class BaseRegistry<T = any> {
|
||||
protected items = observable<T>([], { deep: false });
|
||||
|
||||
getItems(): T[] {
|
||||
return this.items.toJS();
|
||||
}
|
||||
|
||||
add(item: T) {
|
||||
this.items.push(item);
|
||||
return () => {
|
||||
this.items.remove(item); // works because of {deep: false};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
import { ClusterFeature } from "../cluster-feature";
|
||||
|
||||
export interface ClusterFeatureComponents {
|
||||
Description: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface ClusterFeatureRegistration {
|
||||
title: string;
|
||||
components: ClusterFeatureComponents
|
||||
feature: ClusterFeature
|
||||
}
|
||||
|
||||
export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> {}
|
||||
|
||||
export const clusterFeatureRegistry = new ClusterFeatureRegistry()
|
||||
@@ -0,0 +1,8 @@
|
||||
// All registries managed by extensions api
|
||||
|
||||
export * from "./page-registry"
|
||||
export * from "./menu-registry"
|
||||
export * from "./app-preference-registry"
|
||||
export * from "./status-bar-registry"
|
||||
export * from "./kube-object-menu-registry";
|
||||
export * from "./cluster-feature-registry"
|
||||
@@ -0,0 +1,22 @@
|
||||
import React from "react"
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface KubeObjectMenuComponents {
|
||||
MenuItem: React.ComponentType<any>;
|
||||
}
|
||||
|
||||
export interface KubeObjectMenuRegistration {
|
||||
kind: string;
|
||||
apiVersions: string[];
|
||||
components: KubeObjectMenuComponents;
|
||||
}
|
||||
|
||||
export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> {
|
||||
getItemsForKind(kind: string, apiVersion: string) {
|
||||
return this.items.filter((item) => {
|
||||
return item.kind === kind && item.apiVersions.includes(apiVersion)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry()
|
||||
@@ -0,0 +1,13 @@
|
||||
// Extensions API -> Global menu customizations
|
||||
|
||||
import type { MenuItemConstructorOptions } from "electron";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface MenuRegistration extends MenuItemConstructorOptions {
|
||||
parentId: string;
|
||||
}
|
||||
|
||||
export class MenuRegistry extends BaseRegistry<MenuRegistration> {
|
||||
}
|
||||
|
||||
export const menuRegistry = new MenuRegistry();
|
||||
@@ -0,0 +1,31 @@
|
||||
// Extensions-api -> Custom page registration
|
||||
|
||||
import type React from "react";
|
||||
import type { RouteProps } from "react-router";
|
||||
import type { IconProps } from "../../renderer/components/icon";
|
||||
import type { IClassName } from "../../renderer/utils";
|
||||
import type { TabRoute } from "../../renderer/components/layout/tab-layout";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface PageRegistration extends RouteProps {
|
||||
className?: IClassName;
|
||||
url?: string; // initial url to be used for building menus and tabs, otherwise "path" applied by default
|
||||
title?: React.ReactNode; // used in sidebar's & tabs-layout if provided
|
||||
hideInMenu?: boolean; // hide element within app's navigation menu
|
||||
subPages?: (PageRegistration & TabRoute)[];
|
||||
components: PageComponents;
|
||||
}
|
||||
|
||||
export interface PageComponents {
|
||||
Page: React.ComponentType<any>;
|
||||
MenuIcon?: React.ComponentType<IconProps>;
|
||||
}
|
||||
|
||||
export class GlobalPageRegistry extends BaseRegistry<PageRegistration> {
|
||||
}
|
||||
|
||||
export class ClusterPageRegistry extends BaseRegistry<PageRegistration> {
|
||||
}
|
||||
|
||||
export const globalPageRegistry = new GlobalPageRegistry();
|
||||
export const clusterPageRegistry = new ClusterPageRegistry();
|
||||
@@ -0,0 +1,13 @@
|
||||
// Extensions API -> Status bar customizations
|
||||
|
||||
import React from "react";
|
||||
import { BaseRegistry } from "./base-registry";
|
||||
|
||||
export interface StatusBarRegistration {
|
||||
item?: React.ReactNode;
|
||||
}
|
||||
|
||||
export class StatusBarRegistry extends BaseRegistry<StatusBarRegistration> {
|
||||
}
|
||||
|
||||
export const statusBarRegistry = new StatusBarRegistry();
|
||||
@@ -0,0 +1,22 @@
|
||||
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
|
||||
|
||||
export * from "../../renderer/components/icon"
|
||||
export * from "../../renderer/components/checkbox"
|
||||
export * from "../../renderer/components/tooltip"
|
||||
export * from "../../renderer/components/button"
|
||||
export * from "../../renderer/components/tabs"
|
||||
export * from "../../renderer/components/badge"
|
||||
export * from "../../renderer/components/layout/page-layout"
|
||||
export * from "../../renderer/components/drawer"
|
||||
|
||||
// kube helpers
|
||||
export { KubeObjectDetailsProps, KubeObjectMenuProps } from "../../renderer/components/kube-object"
|
||||
export { KubeObjectMeta } from "../../renderer/components/kube-object/kube-object-meta";
|
||||
export { KubeEventDetails } from "../../renderer/components/+events/kube-event-details"
|
||||
|
||||
// specific exports
|
||||
export { ConfirmDialog } from "../../renderer/components/confirm-dialog";
|
||||
export { MenuItem, SubMenu } from "../../renderer/components/menu";
|
||||
export { StatusBrick } from "../../renderer/components/status-brick";
|
||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
||||
export { createPodLogsTab } from "../../renderer/components/dock/pod-logs.store";
|
||||
@@ -0,0 +1,12 @@
|
||||
// Lens-extensions apis, required in renderer process runtime
|
||||
|
||||
// APIs
|
||||
import * as Component from "./components"
|
||||
import * as K8sApi from "./k8s-api"
|
||||
import * as Navigation from "./navigation"
|
||||
|
||||
export {
|
||||
Component,
|
||||
K8sApi,
|
||||
Navigation,
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
export { apiManager } from "../../renderer/api/api-manager";
|
||||
export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
|
||||
export { KubeObject } from "../../renderer/api/kube-object";
|
||||
export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
|
||||
export { Node, nodesApi } from "../../renderer/api/endpoints";
|
||||
export { Deployment, deploymentApi } from "../../renderer/api/endpoints";
|
||||
export { DaemonSet, daemonSetApi } from "../../renderer/api/endpoints";
|
||||
export { StatefulSet, statefulSetApi } from "../../renderer/api/endpoints";
|
||||
export { Job, jobApi } from "../../renderer/api/endpoints";
|
||||
export { CronJob, cronJobApi } from "../../renderer/api/endpoints";
|
||||
export { ConfigMap, configMapApi } from "../../renderer/api/endpoints";
|
||||
export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints";
|
||||
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
|
||||
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
|
||||
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
|
||||
export { Service, serviceApi } from "../../renderer/api/endpoints";
|
||||
export { Endpoint, endpointApi } from "../../renderer/api/endpoints";
|
||||
export { Ingress, ingressApi } from "../../renderer/api/endpoints";
|
||||
export { NetworkPolicy, networkPolicyApi } from "../../renderer/api/endpoints";
|
||||
export { PersistentVolume, persistentVolumeApi } from "../../renderer/api/endpoints";
|
||||
export { PersistentVolumeClaim, PersistentVolumeClaimsApi } from "../../renderer/api/endpoints";
|
||||
export { StorageClass, storageClassApi } from "../../renderer/api/endpoints";
|
||||
export { Namespace, namespacesApi } from "../../renderer/api/endpoints";
|
||||
export { KubeEvent, eventApi } from "../../renderer/api/endpoints";
|
||||
export { ServiceAccount, serviceAccountsApi } from "../../renderer/api/endpoints";
|
||||
export { Role, roleApi } from "../../renderer/api/endpoints";
|
||||
export { RoleBinding, roleBindingApi } from "../../renderer/api/endpoints";
|
||||
export { ClusterRole, clusterRoleApi } from "../../renderer/api/endpoints";
|
||||
export { ClusterRoleBinding, clusterRoleBindingApi } from "../../renderer/api/endpoints";
|
||||
@@ -0,0 +1 @@
|
||||
export { navigate, hideDetails, showDetails } from "../../renderer/navigation"
|
||||
@@ -0,0 +1,5 @@
|
||||
// Workaround for using Typescript in Rollup configutation
|
||||
// https://stackoverflow.com/questions/54711437/does-rollup-support-typescript-in-rollup-config-file
|
||||
|
||||
require('ts-node').register();
|
||||
module.exports = require('./rollup.config.ts');
|
||||
@@ -0,0 +1,57 @@
|
||||
// Generating declaration types for extensions-api
|
||||
// Rollup: https://rollupjs.org/guide/en/
|
||||
// Plugin docs: https://github.com/Swatinem/rollup-plugin-dts
|
||||
|
||||
import { OutputChunk, Plugin, RollupOptions } from 'rollup';
|
||||
import json from '@rollup/plugin-json';
|
||||
import dts from "rollup-plugin-dts";
|
||||
import ignoreImport from 'rollup-plugin-ignore-import'
|
||||
|
||||
const config: RollupOptions = {
|
||||
input: "src/extensions/extension-api.ts",
|
||||
output: [
|
||||
{ file: "src/extensions/npm/extensions/api.d.ts", format: "es", }
|
||||
],
|
||||
plugins: [
|
||||
dts(),
|
||||
dtsModuleWrap({ name: "@k8slens/extensions" }),
|
||||
ignoreImport({ extensions: ['.scss'] }),
|
||||
json(),
|
||||
],
|
||||
};
|
||||
|
||||
function dtsModuleWrap({ name }: { name: string }): Plugin {
|
||||
return {
|
||||
name,
|
||||
generateBundle: (options, bundle) => {
|
||||
const apiTypes = Object.values(bundle)[0] as OutputChunk; // extension-api.d.ts
|
||||
const typeRefs: string[] = []
|
||||
const declarations: string[] = []
|
||||
const apiLines = apiTypes.code.split("\n")
|
||||
let outputCode = ""
|
||||
|
||||
apiLines.forEach(line => {
|
||||
if (line.startsWith("///")) {
|
||||
typeRefs.push(line)
|
||||
} else {
|
||||
declarations.push(line)
|
||||
}
|
||||
})
|
||||
|
||||
// print external @types refs first
|
||||
if (typeRefs.length) {
|
||||
outputCode += typeRefs.join("\n") + "\n\n"
|
||||
}
|
||||
|
||||
// wrap declarations into global module definition
|
||||
outputCode += `declare module "${name}" {\n`
|
||||
outputCode += declarations.map(line => `\t${line}`).join("\n")
|
||||
outputCode += `\n}`
|
||||
|
||||
// save
|
||||
apiTypes.code = outputCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default [config];
|
||||
@@ -1,100 +0,0 @@
|
||||
import { Feature, FeatureStatus } from "../main/feature"
|
||||
import {KubeConfig, AppsV1Api, RbacAuthorizationV1Api} from "@kubernetes/client-node"
|
||||
import semver from "semver"
|
||||
import { Cluster } from "../main/cluster";
|
||||
import * as k8s from "@kubernetes/client-node"
|
||||
|
||||
export interface MetricsConfiguration {
|
||||
// Placeholder for Metrics config structure
|
||||
persistence: {
|
||||
enabled: boolean;
|
||||
storageClass: string;
|
||||
size: string;
|
||||
};
|
||||
nodeExporter: {
|
||||
enabled: boolean;
|
||||
};
|
||||
kubeStateMetrics: {
|
||||
enabled: boolean;
|
||||
};
|
||||
retention: {
|
||||
time: string;
|
||||
size: string;
|
||||
};
|
||||
alertManagers: string[];
|
||||
replicas: number;
|
||||
storageClass: string;
|
||||
}
|
||||
|
||||
export class MetricsFeature extends Feature {
|
||||
static id = 'metrics'
|
||||
name = MetricsFeature.id;
|
||||
latestVersion = "v2.17.2-lens1"
|
||||
|
||||
config: MetricsConfiguration = {
|
||||
persistence: {
|
||||
enabled: false,
|
||||
storageClass: null,
|
||||
size: "20G",
|
||||
},
|
||||
nodeExporter: {
|
||||
enabled: true,
|
||||
},
|
||||
retention: {
|
||||
time: "2d",
|
||||
size: "5GB",
|
||||
},
|
||||
kubeStateMetrics: {
|
||||
enabled: true,
|
||||
},
|
||||
alertManagers: null,
|
||||
replicas: 1,
|
||||
storageClass: null,
|
||||
};
|
||||
|
||||
async install(cluster: Cluster): Promise<void> {
|
||||
// Check if there are storageclasses
|
||||
const storageClient = cluster.getProxyKubeconfig().makeApiClient(k8s.StorageV1Api)
|
||||
const scs = await storageClient.listStorageClass();
|
||||
|
||||
this.config.persistence.enabled = scs.body.items.some(sc => (
|
||||
sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' ||
|
||||
sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true'
|
||||
));
|
||||
|
||||
return super.install(cluster)
|
||||
}
|
||||
|
||||
async upgrade(cluster: Cluster): Promise<void> {
|
||||
return this.install(cluster)
|
||||
}
|
||||
|
||||
async featureStatus(kc: KubeConfig): Promise<FeatureStatus> {
|
||||
const client = kc.makeApiClient(AppsV1Api)
|
||||
const status: FeatureStatus = {
|
||||
currentVersion: null,
|
||||
installed: false,
|
||||
latestVersion: this.latestVersion,
|
||||
canUpgrade: false, // Dunno yet
|
||||
};
|
||||
|
||||
try {
|
||||
const prometheus = (await client.readNamespacedStatefulSet('prometheus', 'lens-metrics')).body;
|
||||
status.installed = true;
|
||||
status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
|
||||
status.canUpgrade = semver.lt(status.currentVersion, this.latestVersion, true);
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
async uninstall(cluster: Cluster): Promise<void> {
|
||||
const rbacClient = cluster.getProxyKubeconfig().makeApiClient(RbacAuthorizationV1Api)
|
||||
|
||||
await this.deleteNamespace(cluster.getProxyKubeconfig(), "lens-metrics")
|
||||
await rbacClient.deleteClusterRole("lens-prometheus");
|
||||
await rbacClient.deleteClusterRoleBinding("lens-prometheus");
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: lens-metrics
|
||||
@@ -1,272 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: prometheus-config
|
||||
namespace: lens-metrics
|
||||
data:
|
||||
prometheus.yaml: |-
|
||||
# Global config
|
||||
global:
|
||||
scrape_interval: 15s
|
||||
|
||||
{{#if alertManagers}}
|
||||
# AlertManager
|
||||
alerting:
|
||||
alertmanagers:
|
||||
- static_configs:
|
||||
- targets:
|
||||
{{#each alertManagers}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
|
||||
# Scrape configs for running Prometheus on a Kubernetes cluster.
|
||||
# This uses separate scrape configs for cluster components (i.e. API server, node)
|
||||
# and services to allow each to use different authentication configs.
|
||||
#
|
||||
# Kubernetes labels will be added as Prometheus labels on metrics via the
|
||||
# `labelmap` relabeling action.
|
||||
scrape_configs:
|
||||
|
||||
# Scrape config for API servers.
|
||||
#
|
||||
# Kubernetes exposes API servers as endpoints to the default/kubernetes
|
||||
# service so this uses `endpoints` role and uses relabelling to only keep
|
||||
# the endpoints associated with the default/kubernetes service using the
|
||||
# default named port `https`. This works for single API server deployments as
|
||||
# well as HA API server deployments.
|
||||
- job_name: 'kubernetes-apiservers'
|
||||
kubernetes_sd_configs:
|
||||
- role: endpoints
|
||||
|
||||
scheme: https
|
||||
tls_config:
|
||||
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
# Using endpoints to discover kube-apiserver targets finds the pod IP
|
||||
# (host IP since apiserver uses host network) which is not used in
|
||||
# the server certificate.
|
||||
insecure_skip_verify: true
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
|
||||
# Keep only the default/kubernetes service endpoints for the https port. This
|
||||
# will add targets for each API server which Kubernetes adds an endpoint to
|
||||
# the default/kubernetes service.
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_namespace, __meta_kubernetes_service_name, __meta_kubernetes_endpoint_port_name]
|
||||
action: keep
|
||||
regex: default;kubernetes;https
|
||||
- replacement: apiserver
|
||||
action: replace
|
||||
target_label: job
|
||||
|
||||
# Scrape config for node (i.e. kubelet) /metrics (e.g. 'kubelet_'). Explore
|
||||
# metrics from a node by scraping kubelet (127.0.0.1:10250/metrics).
|
||||
- job_name: 'kubelet'
|
||||
kubernetes_sd_configs:
|
||||
- role: node
|
||||
|
||||
scheme: https
|
||||
tls_config:
|
||||
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
# Kubelet certs don't have any fixed IP SANs
|
||||
insecure_skip_verify: true
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
|
||||
relabel_configs:
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_node_label_(.+)
|
||||
- replacement: 'lens-metrics'
|
||||
target_label: kubernetes_namespace
|
||||
|
||||
metric_relabel_configs:
|
||||
- source_labels:
|
||||
- namespace
|
||||
action: replace
|
||||
regex: (.+)
|
||||
target_label: kubernetes_namespace
|
||||
|
||||
# Scrape config for Kubelet cAdvisor. Explore metrics from a node by
|
||||
# scraping kubelet (127.0.0.1:10250/metrics/cadvisor).
|
||||
- job_name: 'kubernetes-cadvisor'
|
||||
kubernetes_sd_configs:
|
||||
- role: node
|
||||
|
||||
scheme: https
|
||||
metrics_path: /metrics/cadvisor
|
||||
tls_config:
|
||||
# Kubelet certs don't have any fixed IP SANs
|
||||
insecure_skip_verify: true
|
||||
ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
|
||||
bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
|
||||
|
||||
relabel_configs:
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_node_label_(.+)
|
||||
metric_relabel_configs:
|
||||
- source_labels:
|
||||
- namespace
|
||||
action: replace
|
||||
target_label: kubernetes_namespace
|
||||
- source_labels:
|
||||
- pod
|
||||
regex: (.*)
|
||||
replacement: $1
|
||||
action: replace
|
||||
target_label: pod_name
|
||||
- source_labels:
|
||||
- container
|
||||
regex: (.*)
|
||||
replacement: $1
|
||||
action: replace
|
||||
target_label: container_name
|
||||
|
||||
# Scrap etcd metrics from masters via etcd-scraper-proxy
|
||||
- job_name: 'etcd'
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
scheme: http
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_namespace]
|
||||
action: keep
|
||||
regex: 'kube-system'
|
||||
- source_labels: [__meta_kubernetes_pod_label_component]
|
||||
action: keep
|
||||
regex: 'etcd-scraper-proxy'
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_pod_label_(.+)
|
||||
|
||||
# Scrape config for service endpoints.
|
||||
#
|
||||
# The relabeling allows the actual service scrape endpoint to be configured
|
||||
# via the following annotations:
|
||||
#
|
||||
# * `prometheus.io/scrape`: Only scrape services that have a value of `true`
|
||||
# * `prometheus.io/scheme`: If the metrics endpoint is secured then you will need
|
||||
# to set this to `https` & most likely set the `tls_config` of the scrape config.
|
||||
# * `prometheus.io/path`: If the metrics path is not `/metrics` override this.
|
||||
# * `prometheus.io/port`: If the metrics are exposed on a different port to the
|
||||
# service then set this appropriately.
|
||||
- job_name: 'kubernetes-service-endpoints'
|
||||
|
||||
kubernetes_sd_configs:
|
||||
- role: endpoints
|
||||
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scrape]
|
||||
action: keep
|
||||
regex: true
|
||||
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_scheme]
|
||||
action: replace
|
||||
target_label: __scheme__
|
||||
regex: (https?)
|
||||
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_path]
|
||||
action: replace
|
||||
target_label: __metrics_path__
|
||||
regex: (.+)
|
||||
- source_labels: [__address__, __meta_kubernetes_service_annotation_prometheus_io_port]
|
||||
action: replace
|
||||
target_label: __address__
|
||||
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||
replacement: $1:$2
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_service_label_(.+)
|
||||
- source_labels: [__meta_kubernetes_service_name]
|
||||
action: replace
|
||||
target_label: job
|
||||
- action: replace
|
||||
source_labels:
|
||||
- __meta_kubernetes_pod_node_name
|
||||
target_label: kubernetes_node
|
||||
- source_labels: [__meta_kubernetes_namespace]
|
||||
action: replace
|
||||
target_label: kubernetes_namespace
|
||||
metric_relabel_configs:
|
||||
- source_labels:
|
||||
- namespace
|
||||
action: replace
|
||||
regex: (.+)
|
||||
target_label: kubernetes_namespace
|
||||
|
||||
# Example scrape config for probing services via the Blackbox Exporter.
|
||||
#
|
||||
# The relabeling allows the actual service scrape endpoint to be configured
|
||||
# via the following annotations:
|
||||
#
|
||||
# * `prometheus.io/probe`: Only probe services that have a value of `true`
|
||||
- job_name: 'kubernetes-services'
|
||||
|
||||
metrics_path: /probe
|
||||
params:
|
||||
module: [http_2xx]
|
||||
|
||||
kubernetes_sd_configs:
|
||||
- role: service
|
||||
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_service_annotation_prometheus_io_probe]
|
||||
action: keep
|
||||
regex: true
|
||||
- source_labels: [__address__]
|
||||
target_label: __param_target
|
||||
- target_label: __address__
|
||||
replacement: blackbox
|
||||
- source_labels: [__param_target]
|
||||
target_label: instance
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_service_label_(.+)
|
||||
- source_labels: [__meta_kubernetes_service_name]
|
||||
target_label: job
|
||||
metric_relabel_configs:
|
||||
- source_labels:
|
||||
- namespace
|
||||
action: replace
|
||||
regex: (.+)
|
||||
target_label: kubernetes_namespace
|
||||
|
||||
# Example scrape config for pods
|
||||
#
|
||||
# The relabeling allows the actual pod scrape endpoint to be configured via the
|
||||
# following annotations:
|
||||
#
|
||||
# * `prometheus.io/scrape`: Only scrape pods that have a value of `true`
|
||||
# * `prometheus.io/path`: If the metrics path is not `/metrics` override this.
|
||||
# * `prometheus.io/port`: Scrape the pod on the indicated port instead of the
|
||||
# pod's declared ports (default is a port-free target if none are declared).
|
||||
- job_name: 'kubernetes-pods'
|
||||
|
||||
kubernetes_sd_configs:
|
||||
- role: pod
|
||||
|
||||
relabel_configs:
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]
|
||||
action: keep
|
||||
regex: true
|
||||
- source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_path]
|
||||
action: replace
|
||||
target_label: __metrics_path__
|
||||
regex: (.+)
|
||||
- source_labels: [__address__, __meta_kubernetes_pod_annotation_prometheus_io_port]
|
||||
action: replace
|
||||
regex: ([^:]+)(?::\d+)?;(\d+)
|
||||
replacement: $1:$2
|
||||
target_label: __address__
|
||||
- action: labelmap
|
||||
regex: __meta_kubernetes_pod_label_(.+)
|
||||
- source_labels: [__meta_kubernetes_namespace]
|
||||
action: replace
|
||||
target_label: kubernetes_namespace
|
||||
- source_labels: [__meta_kubernetes_pod_name]
|
||||
action: replace
|
||||
target_label: kubernetes_pod_name
|
||||
metric_relabel_configs:
|
||||
- source_labels:
|
||||
- namespace
|
||||
action: replace
|
||||
regex: (.+)
|
||||
target_label: kubernetes_namespace
|
||||
|
||||
# Rule files
|
||||
rule_files:
|
||||
- "/etc/prometheus/rules/*.rules"
|
||||
- "/etc/prometheus/rules/*.yaml"
|
||||
- "/etc/prometheus/rules/*.yml"
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: prometheus
|
||||
namespace: lens-metrics
|
||||
@@ -1,16 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: prometheus
|
||||
namespace: lens-metrics
|
||||
annotations:
|
||||
prometheus.io/scrape: 'true'
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
name: prometheus
|
||||
ports:
|
||||
- name: web
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 9090
|
||||
@@ -1,116 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: prometheus
|
||||
namespace: lens-metrics
|
||||
spec:
|
||||
replicas: {{replicas}}
|
||||
serviceName: prometheus
|
||||
selector:
|
||||
matchLabels:
|
||||
name: prometheus
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: prometheus
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- matchExpressions:
|
||||
- key: beta.kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
# <%- if config.node_selector -%>
|
||||
# nodeSelector:
|
||||
# <%- node_selector.to_h.each do |key, value| -%>
|
||||
# <%= key %>: <%= value %>
|
||||
# <%- end -%>
|
||||
# <%- end -%>
|
||||
# <%- unless config.tolerations.empty? -%>
|
||||
# tolerations:
|
||||
# <%- config.tolerations.each do |t| -%>
|
||||
# -
|
||||
# <%- t.each do |k,v| -%>
|
||||
# <%= k %>: <%= v %>
|
||||
# <%- end -%>
|
||||
# <%- end -%>
|
||||
# <%- end -%>
|
||||
serviceAccountName: prometheus
|
||||
initContainers:
|
||||
- name: chown
|
||||
image: docker.io/alpine:3.9
|
||||
command: ["chown", "-R", "65534:65534", "/var/lib/prometheus"]
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /var/lib/prometheus
|
||||
containers:
|
||||
- name: prometheus
|
||||
image: docker.io/prom/prometheus:v2.17.2
|
||||
args:
|
||||
- --web.listen-address=0.0.0.0:9090
|
||||
- --config.file=/etc/prometheus/prometheus.yaml
|
||||
- --storage.tsdb.path=/var/lib/prometheus
|
||||
- --storage.tsdb.retention.time={{retention.time}}
|
||||
- --storage.tsdb.retention.size={{retention.size}}
|
||||
- --storage.tsdb.min-block-duration=2h
|
||||
- --storage.tsdb.max-block-duration=2h
|
||||
ports:
|
||||
- name: web
|
||||
containerPort: 9090
|
||||
volumeMounts:
|
||||
- name: config
|
||||
mountPath: /etc/prometheus
|
||||
- name: rules
|
||||
mountPath: /etc/prometheus/rules
|
||||
- name: data
|
||||
mountPath: /var/lib/prometheus
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /-/ready
|
||||
port: 9090
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 10
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /-/healthy
|
||||
port: 9090
|
||||
initialDelaySeconds: 10
|
||||
timeoutSeconds: 10
|
||||
resources:
|
||||
requests:
|
||||
cpu: 100m
|
||||
memory: 512Mi
|
||||
terminationGracePeriodSeconds: 30
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: prometheus-config
|
||||
- name: rules
|
||||
configMap:
|
||||
name: prometheus-rules
|
||||
{{#unless persistence.enabled}}
|
||||
- name: data
|
||||
emptyDir: {}
|
||||
{{/unless}}
|
||||
{{#if persistence.enabled}}
|
||||
volumeClaimTemplates:
|
||||
- metadata:
|
||||
name: data
|
||||
spec:
|
||||
accessModes:
|
||||
- ReadWriteOnce
|
||||
{{#if persistence.storageClass}}
|
||||
storageClassName: "{{persistence.storageClass}}"
|
||||
{{/if}}
|
||||
resources:
|
||||
requests:
|
||||
storage: {{persistence.size}}
|
||||
{{/if}}
|
||||
@@ -1,514 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: prometheus-rules
|
||||
namespace: lens-metrics
|
||||
data:
|
||||
alertmanager.rules.yaml: |
|
||||
groups:
|
||||
- name: alertmanager.rules
|
||||
rules:
|
||||
- alert: AlertmanagerConfigInconsistent
|
||||
expr: count_values("config_hash", alertmanager_config_hash) BY (service) / ON(service)
|
||||
GROUP_LEFT() label_replace(prometheus_operator_alertmanager_spec_replicas, "service",
|
||||
"alertmanager-$1", "alertmanager", "(.*)") != 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: The configuration of the instances of the Alertmanager cluster
|
||||
`{{$labels.service}}` are out of sync.
|
||||
- alert: AlertmanagerDownOrMissing
|
||||
expr: label_replace(prometheus_operator_alertmanager_spec_replicas, "job", "alertmanager-$1",
|
||||
"alertmanager", "(.*)") / ON(job) GROUP_RIGHT() sum(up) BY (job) != 1
|
||||
for: 5m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: An unexpected number of Alertmanagers are scraped or Alertmanagers
|
||||
disappeared from discovery.
|
||||
- alert: AlertmanagerFailedReload
|
||||
expr: alertmanager_config_last_reload_successful == 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Reloading Alertmanager's configuration has failed for {{ $labels.namespace
|
||||
}}/{{ $labels.pod}}.
|
||||
etcd3.rules.yaml: |
|
||||
groups:
|
||||
- name: ./etcd3.rules
|
||||
rules:
|
||||
- alert: InsufficientMembers
|
||||
expr: count(up{job="etcd"} == 0) > (count(up{job="etcd"}) / 2 - 1)
|
||||
for: 3m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: If one more etcd member goes down the cluster will be unavailable
|
||||
summary: etcd cluster insufficient members
|
||||
- alert: NoLeader
|
||||
expr: etcd_server_has_leader{job="etcd"} == 0
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: etcd member {{ $labels.instance }} has no leader
|
||||
summary: etcd member has no leader
|
||||
- alert: HighNumberOfLeaderChanges
|
||||
expr: increase(etcd_server_leader_changes_seen_total{job="etcd"}[1h]) > 3
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: etcd instance {{ $labels.instance }} has seen {{ $value }} leader
|
||||
changes within the last hour
|
||||
summary: a high number of leader changes within the etcd cluster are happening
|
||||
- alert: GRPCRequestsSlow
|
||||
expr: histogram_quantile(0.99, sum(rate(grpc_server_handling_seconds_bucket{job="etcd",grpc_type="unary"}[5m])) by (grpc_service, grpc_method, le))
|
||||
> 0.15
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: on etcd instance {{ $labels.instance }} gRPC requests to {{ $labels.grpc_method
|
||||
}} are slow
|
||||
summary: slow gRPC requests
|
||||
- alert: HighNumberOfFailedHTTPRequests
|
||||
expr: sum(rate(etcd_http_failed_total{job="etcd"}[5m])) BY (method) / sum(rate(etcd_http_received_total{job="etcd"}[5m]))
|
||||
BY (method) > 0.01
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{ $value }}% of requests for {{ $labels.method }} failed on etcd
|
||||
instance {{ $labels.instance }}'
|
||||
summary: a high number of HTTP requests are failing
|
||||
- alert: HighNumberOfFailedHTTPRequests
|
||||
expr: sum(rate(etcd_http_failed_total{job="etcd"}[5m])) BY (method) / sum(rate(etcd_http_received_total{job="etcd"}[5m]))
|
||||
BY (method) > 0.05
|
||||
for: 5m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: '{{ $value }}% of requests for {{ $labels.method }} failed on etcd
|
||||
instance {{ $labels.instance }}'
|
||||
summary: a high number of HTTP requests are failing
|
||||
- alert: HTTPRequestsSlow
|
||||
expr: histogram_quantile(0.99, rate(etcd_http_successful_duration_seconds_bucket[5m]))
|
||||
> 0.15
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: on etcd instance {{ $labels.instance }} HTTP requests to {{ $labels.method
|
||||
}} are slow
|
||||
summary: slow HTTP requests
|
||||
- alert: EtcdMemberCommunicationSlow
|
||||
expr: histogram_quantile(0.99, rate(etcd_network_peer_round_trip_time_seconds_bucket[5m]))
|
||||
> 0.15
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: etcd instance {{ $labels.instance }} member communication with
|
||||
{{ $labels.To }} is slow
|
||||
summary: etcd member communication is slow
|
||||
- alert: HighNumberOfFailedProposals
|
||||
expr: increase(etcd_server_proposals_failed_total{job="etcd"}[1h]) > 5
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: etcd instance {{ $labels.instance }} has seen {{ $value }} proposal
|
||||
failures within the last hour
|
||||
summary: a high number of proposals within the etcd cluster are failing
|
||||
- alert: HighFsyncDurations
|
||||
expr: histogram_quantile(0.99, rate(etcd_disk_wal_fsync_duration_seconds_bucket[5m]))
|
||||
> 0.5
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: etcd instance {{ $labels.instance }} fync durations are high
|
||||
summary: high fsync durations
|
||||
- alert: HighCommitDurations
|
||||
expr: histogram_quantile(0.99, rate(etcd_disk_backend_commit_duration_seconds_bucket[5m]))
|
||||
> 0.25
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: etcd instance {{ $labels.instance }} commit durations are high
|
||||
summary: high commit durations
|
||||
general.rules.yaml: |
|
||||
groups:
|
||||
- name: general.rules
|
||||
rules:
|
||||
- alert: TargetDown
|
||||
expr: 100 * (count(up == 0) BY (job) / count(up) BY (job)) > 10
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{ $value }}% of {{ $labels.job }} targets are down.'
|
||||
summary: Targets are down
|
||||
- record: fd_utilization
|
||||
expr: process_open_fds / process_max_fds
|
||||
- alert: FdExhaustionClose
|
||||
expr: predict_linear(fd_utilization[1h], 3600 * 4) > 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{ $labels.job }}: {{ $labels.namespace }}/{{ $labels.pod }} instance
|
||||
will exhaust in file/socket descriptors within the next 4 hours'
|
||||
summary: file descriptors soon exhausted
|
||||
- alert: FdExhaustionClose
|
||||
expr: predict_linear(fd_utilization[10m], 3600) > 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: '{{ $labels.job }}: {{ $labels.namespace }}/{{ $labels.pod }} instance
|
||||
will exhaust in file/socket descriptors within the next hour'
|
||||
summary: file descriptors soon exhausted
|
||||
kube-state-metrics.rules.yaml: |
|
||||
groups:
|
||||
- name: kube-state-metrics.rules
|
||||
rules:
|
||||
- alert: DeploymentGenerationMismatch
|
||||
expr: kube_deployment_status_observed_generation != kube_deployment_metadata_generation
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Observed deployment generation does not match expected one for
|
||||
deployment {{$labels.namespaces}}/{{$labels.deployment}}
|
||||
summary: Deployment is outdated
|
||||
- alert: DeploymentReplicasNotUpdated
|
||||
expr: ((kube_deployment_status_replicas_updated != kube_deployment_spec_replicas)
|
||||
or (kube_deployment_status_replicas_available != kube_deployment_spec_replicas))
|
||||
unless (kube_deployment_spec_paused == 1)
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Replicas are not updated and available for deployment {{$labels.namespaces}}/{{$labels.deployment}}
|
||||
summary: Deployment replicas are outdated
|
||||
- alert: DaemonSetRolloutStuck
|
||||
expr: kube_daemonset_status_number_ready / kube_daemonset_status_desired_number_scheduled
|
||||
* 100 < 100
|
||||
for: 15m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Only {{$value}}% of desired pods scheduled and ready for daemon
|
||||
set {{$labels.namespaces}}/{{$labels.daemonset}}
|
||||
summary: DaemonSet is missing pods
|
||||
- alert: K8SDaemonSetsNotScheduled
|
||||
expr: kube_daemonset_status_desired_number_scheduled - kube_daemonset_status_current_number_scheduled
|
||||
> 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: A number of daemonsets are not scheduled.
|
||||
summary: Daemonsets are not scheduled correctly
|
||||
- alert: DaemonSetsMissScheduled
|
||||
expr: kube_daemonset_status_number_misscheduled > 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: A number of daemonsets are running where they are not supposed
|
||||
to run.
|
||||
summary: Daemonsets are not scheduled correctly
|
||||
- alert: PodFrequentlyRestarting
|
||||
expr: increase(kube_pod_container_status_restarts_total[1h]) > 5
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Pod {{$labels.namespaces}}/{{$labels.pod}} restarted {{$value}}
|
||||
times within the last hour
|
||||
summary: Pod is restarting frequently
|
||||
kubelet.rules.yaml: |
|
||||
groups:
|
||||
- name: kubelet.rules
|
||||
rules:
|
||||
- alert: K8SNodeNotReady
|
||||
expr: kube_node_status_condition{condition="Ready",status="true"} == 0
|
||||
for: 1h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: The Kubelet on {{ $labels.node }} has not checked in with the API,
|
||||
or has set itself to NotReady, for more than an hour
|
||||
summary: Node status is NotReady
|
||||
- alert: K8SManyNodesNotReady
|
||||
expr: count(kube_node_status_condition{condition="Ready",status="true"} == 0)
|
||||
> 1 and (count(kube_node_status_condition{condition="Ready",status="true"} ==
|
||||
0) / count(kube_node_status_condition{condition="Ready",status="true"})) > 0.2
|
||||
for: 1m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: '{{ $value }}% of Kubernetes nodes are not ready'
|
||||
- alert: K8SKubeletDown
|
||||
expr: count(up{job="kubelet"} == 0) / count(up{job="kubelet"}) * 100 > 3
|
||||
for: 1h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Prometheus failed to scrape {{ $value }}% of kubelets.
|
||||
- alert: K8SKubeletDown
|
||||
expr: (absent(up{job="kubelet"} == 1) or count(up{job="kubelet"} == 0) / count(up{job="kubelet"}))
|
||||
* 100 > 10
|
||||
for: 1h
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: Prometheus failed to scrape {{ $value }}% of kubelets, or all Kubelets
|
||||
have disappeared from service discovery.
|
||||
summary: Many Kubelets cannot be scraped
|
||||
- alert: K8SKubeletTooManyPods
|
||||
expr: kubelet_running_pod_count > 100
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Kubelet {{$labels.instance}} is running {{$value}} pods, close
|
||||
to the limit of 110
|
||||
summary: Kubelet is close to pod limit
|
||||
kubernetes.rules.yaml: |
|
||||
groups:
|
||||
- name: kubernetes.rules
|
||||
rules:
|
||||
- record: pod_name:container_memory_usage_bytes:sum
|
||||
expr: sum(container_memory_usage_bytes{container_name!="POD",pod_name!=""}) BY
|
||||
(pod_name)
|
||||
- record: pod_name:container_spec_cpu_shares:sum
|
||||
expr: sum(container_spec_cpu_shares{container_name!="POD",pod_name!=""}) BY (pod_name)
|
||||
- record: pod_name:container_cpu_usage:sum
|
||||
expr: sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name!=""}[5m]))
|
||||
BY (pod_name)
|
||||
- record: pod_name:container_fs_usage_bytes:sum
|
||||
expr: sum(container_fs_usage_bytes{container_name!="POD",pod_name!=""}) BY (pod_name)
|
||||
- record: namespace:container_memory_usage_bytes:sum
|
||||
expr: sum(container_memory_usage_bytes{container_name!=""}) BY (namespace)
|
||||
- record: namespace:container_spec_cpu_shares:sum
|
||||
expr: sum(container_spec_cpu_shares{container_name!=""}) BY (namespace)
|
||||
- record: namespace:container_cpu_usage:sum
|
||||
expr: sum(rate(container_cpu_usage_seconds_total{container_name!="POD"}[5m]))
|
||||
BY (namespace)
|
||||
- record: cluster:memory_usage:ratio
|
||||
expr: sum(container_memory_usage_bytes{container_name!="POD",pod_name!=""}) BY
|
||||
(cluster) / sum(machine_memory_bytes) BY (cluster)
|
||||
- record: cluster:container_spec_cpu_shares:ratio
|
||||
expr: sum(container_spec_cpu_shares{container_name!="POD",pod_name!=""}) / 1000
|
||||
/ sum(machine_cpu_cores)
|
||||
- record: cluster:container_cpu_usage:ratio
|
||||
expr: sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name!=""}[5m]))
|
||||
/ sum(machine_cpu_cores)
|
||||
- record: apiserver_latency_seconds:quantile
|
||||
expr: histogram_quantile(0.99, rate(apiserver_request_latencies_bucket[5m])) /
|
||||
1e+06
|
||||
labels:
|
||||
quantile: "0.99"
|
||||
- record: apiserver_latency:quantile_seconds
|
||||
expr: histogram_quantile(0.9, rate(apiserver_request_latencies_bucket[5m])) /
|
||||
1e+06
|
||||
labels:
|
||||
quantile: "0.9"
|
||||
- record: apiserver_latency_seconds:quantile
|
||||
expr: histogram_quantile(0.5, rate(apiserver_request_latencies_bucket[5m])) /
|
||||
1e+06
|
||||
labels:
|
||||
quantile: "0.5"
|
||||
- alert: APIServerLatencyHigh
|
||||
expr: apiserver_latency_seconds:quantile{quantile="0.99",subresource!="log",verb!~"^(?:WATCH|WATCHLIST|PROXY|CONNECT)$"}
|
||||
> 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: the API server has a 99th percentile latency of {{ $value }} seconds
|
||||
for {{$labels.verb}} {{$labels.resource}}
|
||||
- alert: APIServerLatencyHigh
|
||||
expr: apiserver_latency_seconds:quantile{quantile="0.99",subresource!="log",verb!~"^(?:WATCH|WATCHLIST|PROXY|CONNECT)$"}
|
||||
> 4
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: the API server has a 99th percentile latency of {{ $value }} seconds
|
||||
for {{$labels.verb}} {{$labels.resource}}
|
||||
- alert: APIServerErrorsHigh
|
||||
expr: rate(apiserver_request_count{code=~"^(?:5..)$"}[5m]) / rate(apiserver_request_count[5m])
|
||||
* 100 > 2
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: API server returns errors for {{ $value }}% of requests
|
||||
- alert: APIServerErrorsHigh
|
||||
expr: rate(apiserver_request_count{code=~"^(?:5..)$"}[5m]) / rate(apiserver_request_count[5m])
|
||||
* 100 > 5
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: API server returns errors for {{ $value }}% of requests
|
||||
- alert: K8SApiserverDown
|
||||
expr: absent(up{job="apiserver"} == 1)
|
||||
for: 20m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: No API servers are reachable or all have disappeared from service
|
||||
discovery
|
||||
|
||||
- alert: K8sCertificateExpirationNotice
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Kubernetes API Certificate is expiring soon (less than 7 days)
|
||||
expr: sum(apiserver_client_certificate_expiration_seconds_bucket{le="604800"}) > 0
|
||||
|
||||
- alert: K8sCertificateExpirationNotice
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: Kubernetes API Certificate is expiring in less than 1 day
|
||||
expr: sum(apiserver_client_certificate_expiration_seconds_bucket{le="86400"}) > 0
|
||||
node.rules.yaml: |
|
||||
groups:
|
||||
- name: node.rules
|
||||
rules:
|
||||
- record: instance:node_cpu:rate:sum
|
||||
expr: sum(rate(node_cpu{mode!="idle",mode!="iowait",mode!~"^(?:guest.*)$"}[3m]))
|
||||
BY (instance)
|
||||
- record: instance:node_filesystem_usage:sum
|
||||
expr: sum((node_filesystem_size{mountpoint="/"} - node_filesystem_free{mountpoint="/"}))
|
||||
BY (instance)
|
||||
- record: instance:node_network_receive_bytes:rate:sum
|
||||
expr: sum(rate(node_network_receive_bytes[3m])) BY (instance)
|
||||
- record: instance:node_network_transmit_bytes:rate:sum
|
||||
expr: sum(rate(node_network_transmit_bytes[3m])) BY (instance)
|
||||
- record: instance:node_cpu:ratio
|
||||
expr: sum(rate(node_cpu{mode!="idle"}[5m])) WITHOUT (cpu, mode) / ON(instance)
|
||||
GROUP_LEFT() count(sum(node_cpu) BY (instance, cpu)) BY (instance)
|
||||
- record: cluster:node_cpu:sum_rate5m
|
||||
expr: sum(rate(node_cpu{mode!="idle"}[5m]))
|
||||
- record: cluster:node_cpu:ratio
|
||||
expr: cluster:node_cpu:rate5m / count(sum(node_cpu) BY (instance, cpu))
|
||||
- alert: NodeExporterDown
|
||||
expr: absent(up{job="node-exporter"} == 1)
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Prometheus could not scrape a node-exporter for more than 10m,
|
||||
or node-exporters have disappeared from discovery
|
||||
- alert: NodeDiskRunningFull
|
||||
expr: predict_linear(node_filesystem_free[6h], 3600 * 24) < 0
|
||||
for: 30m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: device {{$labels.device}} on node {{$labels.instance}} is running
|
||||
full within the next 24 hours (mounted at {{$labels.mountpoint}})
|
||||
- alert: NodeDiskRunningFull
|
||||
expr: predict_linear(node_filesystem_free[30m], 3600 * 2) < 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: device {{$labels.device}} on node {{$labels.instance}} is running
|
||||
full within the next 2 hours (mounted at {{$labels.mountpoint}})
|
||||
- alert: InactiveRAIDDisk
|
||||
expr: node_md_disks - node_md_disks_active > 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{$value}} RAID disk(s) on node {{$labels.instance}} are inactive'
|
||||
prometheus.rules.yaml: |
|
||||
groups:
|
||||
- name: prometheus.rules
|
||||
rules:
|
||||
- alert: PrometheusConfigReloadFailed
|
||||
expr: prometheus_config_last_reload_successful == 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Reloading Prometheus' configuration has failed for {{$labels.namespace}}/{{$labels.pod}}
|
||||
- alert: PrometheusNotificationQueueRunningFull
|
||||
expr: predict_linear(prometheus_notifications_queue_length[5m], 60 * 30) > prometheus_notifications_queue_capacity
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Prometheus' alert notification queue is running full for {{$labels.namespace}}/{{
|
||||
$labels.pod}}
|
||||
- alert: PrometheusErrorSendingAlerts
|
||||
expr: rate(prometheus_notifications_errors_total[5m]) / rate(prometheus_notifications_sent_total[5m])
|
||||
> 0.01
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Errors while sending alerts from Prometheus {{$labels.namespace}}/{{
|
||||
$labels.pod}} to Alertmanager {{$labels.Alertmanager}}
|
||||
- alert: PrometheusErrorSendingAlerts
|
||||
expr: rate(prometheus_notifications_errors_total[5m]) / rate(prometheus_notifications_sent_total[5m])
|
||||
> 0.03
|
||||
for: 10m
|
||||
labels:
|
||||
severity: critical
|
||||
annotations:
|
||||
description: Errors while sending alerts from Prometheus {{$labels.namespace}}/{{
|
||||
$labels.pod}} to Alertmanager {{$labels.Alertmanager}}
|
||||
- alert: PrometheusNotConnectedToAlertmanagers
|
||||
expr: prometheus_notifications_alertmanagers_discovered < 1
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: Prometheus {{ $labels.namespace }}/{{ $labels.pod}} is not connected
|
||||
to any Alertmanagers
|
||||
- alert: PrometheusTSDBReloadsFailing
|
||||
expr: increase(prometheus_tsdb_reloads_failures_total[2h]) > 0
|
||||
for: 12h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{$labels.job}} at {{$labels.instance}} had {{$value | humanize}}
|
||||
reload failures over the last four hours.'
|
||||
summary: Prometheus has issues reloading data blocks from disk
|
||||
- alert: PrometheusTSDBCompactionsFailing
|
||||
expr: increase(prometheus_tsdb_compactions_failed_total[2h]) > 0
|
||||
for: 12h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{$labels.job}} at {{$labels.instance}} had {{$value | humanize}}
|
||||
compaction failures over the last four hours.'
|
||||
summary: Prometheus has issues compacting sample blocks
|
||||
- alert: PrometheusTSDBWALCorruptions
|
||||
expr: tsdb_wal_corruptions_total > 0
|
||||
for: 4h
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: '{{$labels.job}} at {{$labels.instance}} has a corrupted write-ahead
|
||||
log (WAL).'
|
||||
summary: Prometheus write-ahead log is corrupted
|
||||
- alert: PrometheusNotIngestingSamples
|
||||
expr: rate(prometheus_tsdb_head_samples_appended_total[5m]) <= 0
|
||||
for: 10m
|
||||
labels:
|
||||
severity: warning
|
||||
annotations:
|
||||
description: "Prometheus {{ $labels.namespace }}/{{ $labels.pod}} isn't ingesting samples."
|
||||
summary: "Prometheus isn't ingesting samples"
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: lens-prometheus
|
||||
rules:
|
||||
- apiGroups: [""]
|
||||
resources:
|
||||
- nodes
|
||||
- nodes/proxy
|
||||
- nodes/metrics
|
||||
- services
|
||||
- endpoints
|
||||
- pods
|
||||
- ingresses
|
||||
- configmaps
|
||||
verbs: ["get", "list", "watch"]
|
||||
- nonResourceURLs: ["/metrics"]
|
||||
verbs: ["get"]
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: lens-prometheus
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: lens-prometheus
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: prometheus
|
||||
namespace: lens-metrics
|
||||
@@ -1,84 +0,0 @@
|
||||
{{#if nodeExporter.enabled}}
|
||||
apiVersion: apps/v1
|
||||
kind: DaemonSet
|
||||
metadata:
|
||||
name: node-exporter
|
||||
namespace: lens-metrics
|
||||
spec:
|
||||
updateStrategy:
|
||||
type: RollingUpdate
|
||||
rollingUpdate:
|
||||
maxUnavailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
name: node-exporter
|
||||
phase: prod
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: node-exporter
|
||||
phase: prod
|
||||
annotations:
|
||||
seccomp.security.alpha.kubernetes.io/pod: 'docker/default'
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- matchExpressions:
|
||||
- key: beta.kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
securityContext:
|
||||
runAsNonRoot: true
|
||||
runAsUser: 65534
|
||||
hostPID: true
|
||||
containers:
|
||||
- name: node-exporter
|
||||
image: docker.io/prom/node-exporter:v1.0.0-rc.0
|
||||
args:
|
||||
- --path.procfs=/host/proc
|
||||
- --path.sysfs=/host/sys
|
||||
- --path.rootfs=/host/root
|
||||
- --collector.filesystem.ignored-mount-points=^/(dev|proc|sys|var/lib/docker|var/lib/containers/.+)($|/)
|
||||
- --collector.filesystem.ignored-fs-types=^(autofs|binfmt_misc|cgroup|configfs|debugfs|devpts|devtmpfs|fusectl|hugetlbfs|mqueue|overlay|proc|procfs|pstore|rpc_pipefs|securityfs|sysfs|tracefs)$
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: 9100
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 24Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 100Mi
|
||||
volumeMounts:
|
||||
- name: proc
|
||||
mountPath: /host/proc
|
||||
readOnly: true
|
||||
- name: sys
|
||||
mountPath: /host/sys
|
||||
readOnly: true
|
||||
- name: root
|
||||
mountPath: /host/root
|
||||
readOnly: true
|
||||
tolerations:
|
||||
- effect: NoSchedule
|
||||
operator: Exists
|
||||
volumes:
|
||||
- name: proc
|
||||
hostPath:
|
||||
path: /proc
|
||||
- name: sys
|
||||
hostPath:
|
||||
path: /sys
|
||||
- name: root
|
||||
hostPath:
|
||||
path: /
|
||||
{{/if}}
|
||||
@@ -1,18 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: node-exporter
|
||||
namespace: lens-metrics
|
||||
annotations:
|
||||
prometheus.io/scrape: 'true'
|
||||
spec:
|
||||
type: ClusterIP
|
||||
clusterIP: None
|
||||
selector:
|
||||
name: node-exporter
|
||||
phase: prod
|
||||
ports:
|
||||
- name: metrics
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: 9100
|
||||
@@ -1,114 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: lens-kube-state-metrics
|
||||
rules:
|
||||
- apiGroups:
|
||||
- ""
|
||||
resources:
|
||||
- configmaps
|
||||
- secrets
|
||||
- nodes
|
||||
- pods
|
||||
- services
|
||||
- resourcequotas
|
||||
- replicationcontrollers
|
||||
- limitranges
|
||||
- persistentvolumeclaims
|
||||
- persistentvolumes
|
||||
- namespaces
|
||||
- endpoints
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- extensions
|
||||
resources:
|
||||
- daemonsets
|
||||
- deployments
|
||||
- replicasets
|
||||
- ingresses
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- apps
|
||||
resources:
|
||||
- statefulsets
|
||||
- daemonsets
|
||||
- deployments
|
||||
- replicasets
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- batch
|
||||
resources:
|
||||
- cronjobs
|
||||
- jobs
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- autoscaling
|
||||
resources:
|
||||
- horizontalpodautoscalers
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- authentication.k8s.io
|
||||
resources:
|
||||
- tokenreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- authorization.k8s.io
|
||||
resources:
|
||||
- subjectaccessreviews
|
||||
verbs:
|
||||
- create
|
||||
- apiGroups:
|
||||
- policy
|
||||
resources:
|
||||
- poddisruptionbudgets
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- certificates.k8s.io
|
||||
resources:
|
||||
- certificatesigningrequests
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- storage.k8s.io
|
||||
resources:
|
||||
- storageclasses
|
||||
- volumeattachments
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- admissionregistration.k8s.io
|
||||
resources:
|
||||
- mutatingwebhookconfigurations
|
||||
- validatingwebhookconfigurations
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- networking.k8s.io
|
||||
resources:
|
||||
- networkpolicies
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
- apiGroups:
|
||||
- coordination.k8s.io
|
||||
resources:
|
||||
- leases
|
||||
verbs:
|
||||
- list
|
||||
- watch
|
||||
@@ -1,5 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: kube-state-metrics
|
||||
namespace: lens-metrics
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: lens-kube-state-metrics
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: lens-kube-state-metrics
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: kube-state-metrics
|
||||
namespace: lens-metrics
|
||||
@@ -1,59 +0,0 @@
|
||||
{{#if kubeStateMetrics.enabled}}
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: kube-state-metrics
|
||||
namespace: lens-metrics
|
||||
spec:
|
||||
selector:
|
||||
matchLabels:
|
||||
name: kube-state-metrics
|
||||
replicas: 1
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
name: kube-state-metrics
|
||||
spec:
|
||||
affinity:
|
||||
nodeAffinity:
|
||||
requiredDuringSchedulingIgnoredDuringExecution:
|
||||
nodeSelectorTerms:
|
||||
- matchExpressions:
|
||||
- key: kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- key: kubernetes.io/arch
|
||||
operator: In
|
||||
values:
|
||||
- amd64
|
||||
- matchExpressions:
|
||||
- key: beta.kubernetes.io/os
|
||||
operator: In
|
||||
values:
|
||||
- linux
|
||||
- key: beta.kubernetes.io/arch
|
||||
operator: In
|
||||
values:
|
||||
- amd64
|
||||
serviceAccountName: kube-state-metrics
|
||||
containers:
|
||||
- name: kube-state-metrics
|
||||
image: quay.io/coreos/kube-state-metrics:v1.9.5
|
||||
ports:
|
||||
- name: metrics
|
||||
containerPort: 8080
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
timeoutSeconds: 5
|
||||
resources:
|
||||
requests:
|
||||
cpu: 10m
|
||||
memory: 150Mi
|
||||
limits:
|
||||
cpu: 200m
|
||||
memory: 150Mi
|
||||
{{/if}}
|
||||
@@ -1,17 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: kube-state-metrics
|
||||
namespace: lens-metrics
|
||||
labels:
|
||||
name: kube-state-metrics
|
||||
annotations:
|
||||
prometheus.io/scrape: 'true'
|
||||
spec:
|
||||
ports:
|
||||
- name: metrics
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
selector:
|
||||
name: kube-state-metrics
|
||||
@@ -1,44 +0,0 @@
|
||||
import { Feature, FeatureStatus } from "../main/feature"
|
||||
import {KubeConfig, RbacAuthorizationV1Api} from "@kubernetes/client-node"
|
||||
import { Cluster } from "../main/cluster"
|
||||
|
||||
export class UserModeFeature extends Feature {
|
||||
static id = 'user-mode'
|
||||
name = UserModeFeature.id;
|
||||
latestVersion = "v2.0.0"
|
||||
|
||||
async install(cluster: Cluster): Promise<void> {
|
||||
return super.install(cluster)
|
||||
}
|
||||
|
||||
async upgrade(cluster: Cluster): Promise<void> {
|
||||
return;
|
||||
}
|
||||
|
||||
async featureStatus(kc: KubeConfig): Promise<FeatureStatus> {
|
||||
const client = kc.makeApiClient(RbacAuthorizationV1Api)
|
||||
const status: FeatureStatus = {
|
||||
currentVersion: null,
|
||||
installed: false,
|
||||
latestVersion: this.latestVersion,
|
||||
canUpgrade: false, // Dunno yet
|
||||
};
|
||||
|
||||
try {
|
||||
await client.readClusterRoleBinding("lens-user")
|
||||
status.installed = true;
|
||||
status.currentVersion = this.latestVersion;
|
||||
status.canUpgrade = false;
|
||||
} catch {
|
||||
// ignore error
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
async uninstall(cluster: Cluster): Promise<void> {
|
||||
const rbacClient = cluster.getProxyKubeconfig().makeApiClient(RbacAuthorizationV1Api)
|
||||
await rbacClient.deleteClusterRole("lens-user");
|
||||
await rbacClient.deleteClusterRoleBinding("lens-user");
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRole
|
||||
metadata:
|
||||
name: lens-user
|
||||
rules:
|
||||
- verbs:
|
||||
- list
|
||||
apiGroups:
|
||||
- ''
|
||||
resources:
|
||||
- namespaces
|
||||
@@ -1,12 +0,0 @@
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: ClusterRoleBinding
|
||||
metadata:
|
||||
name: lens-user
|
||||
subjects:
|
||||
- kind: Group
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
name: 'system:authenticated'
|
||||
roleRef:
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
kind: ClusterRole
|
||||
name: lens-user
|
||||
@@ -38,6 +38,7 @@ import { getFreePort } from "../port";
|
||||
import { V1ResourceAttributes } from "@kubernetes/client-node";
|
||||
import { apiResources } from "../../common/rbac";
|
||||
import request from "request-promise-native"
|
||||
import { Kubectl } from "../kubectl";
|
||||
|
||||
const mockedRequest = request as jest.MockedFunction<typeof request>
|
||||
|
||||
@@ -73,6 +74,7 @@ describe("create clusters", () => {
|
||||
})
|
||||
}
|
||||
mockFs(mockOpts)
|
||||
jest.spyOn(Kubectl.prototype, "ensureKubectl").mockReturnValue(Promise.resolve(true))
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
@@ -116,7 +118,7 @@ describe("create clusters", () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jest.spyOn(Cluster.prototype, "isClusterAdmin").mockReturnValue(Promise.resolve(true))
|
||||
jest.spyOn(Cluster.prototype, "canI")
|
||||
.mockImplementationOnce((attr: V1ResourceAttributes): Promise<boolean> => {
|
||||
expect(attr.namespace).toBe("default")
|
||||
@@ -159,7 +161,7 @@ describe("create clusters", () => {
|
||||
expect(c.accessible).toBe(true)
|
||||
expect(c.allowedNamespaces.length).toBe(1)
|
||||
expect(c.allowedResources.length).toBe(apiResources.length)
|
||||
|
||||
c.disconnect()
|
||||
jest.resetAllMocks()
|
||||
})
|
||||
})
|
||||
@@ -1,19 +1,19 @@
|
||||
import { autoUpdater } from "electron-updater"
|
||||
import logger from "./logger"
|
||||
|
||||
export default class AppUpdater {
|
||||
export class AppUpdater {
|
||||
static readonly defaultUpdateIntervalMs = 1000 * 60 * 60 * 24 // once a day
|
||||
|
||||
protected updateInterval: number = (1000 * 60 * 60 * 24) // once a day
|
||||
static checkForUpdates() {
|
||||
return autoUpdater.checkForUpdatesAndNotify()
|
||||
}
|
||||
|
||||
constructor() {
|
||||
constructor(protected updateInterval = AppUpdater.defaultUpdateIntervalMs) {
|
||||
autoUpdater.logger = logger
|
||||
}
|
||||
|
||||
public start() {
|
||||
setInterval(() => {
|
||||
autoUpdater.checkForUpdatesAndNotify()
|
||||
}, this.updateInterval)
|
||||
|
||||
return autoUpdater.checkForUpdatesAndNotify()
|
||||
setInterval(AppUpdater.checkForUpdates, this.updateInterval)
|
||||
return AppUpdater.checkForUpdates();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||
import { Cluster } from "../cluster";
|
||||
|
||||
export type ClusterDetectionResult = {
|
||||
value: string | number | boolean
|
||||
accuracy: number
|
||||
}
|
||||
|
||||
export class BaseClusterDetector {
|
||||
cluster: Cluster
|
||||
key: string
|
||||
|
||||
constructor(cluster: Cluster) {
|
||||
this.cluster = cluster
|
||||
}
|
||||
|
||||
detect(): Promise<ClusterDetectionResult> {
|
||||
return null
|
||||
}
|
||||
|
||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
const apiUrl = this.cluster.kubeProxyUrl + path;
|
||||
return request(apiUrl, {
|
||||
json: true,
|
||||
timeout: 30000,
|
||||
...options,
|
||||
headers: {
|
||||
Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest()
|
||||
...(options.headers || {}),
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { createHash } from "crypto"
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class ClusterIdDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.CLUSTER_ID
|
||||
|
||||
public async detect() {
|
||||
let id: string
|
||||
try {
|
||||
id = await this.getDefaultNamespaceId()
|
||||
} catch(_) {
|
||||
id = this.cluster.apiUrl
|
||||
}
|
||||
const value = createHash("sha256").update(id).digest("hex")
|
||||
return { value: value, accuracy: 100 }
|
||||
}
|
||||
|
||||
protected async getDefaultNamespaceId() {
|
||||
const response = await this.k8sRequest("/api/v1/namespaces/default")
|
||||
return response.metadata.uid
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { observable } from "mobx";
|
||||
import { ClusterMetadata } from "../../common/cluster-store";
|
||||
import { Cluster } from "../cluster";
|
||||
import { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector";
|
||||
import { ClusterIdDetector } from "./cluster-id-detector";
|
||||
import { DistributionDetector } from "./distribution-detector";
|
||||
import { LastSeenDetector } from "./last-seen-detector";
|
||||
import { NodesCountDetector } from "./nodes-count-detector";
|
||||
import { VersionDetector } from "./version-detector";
|
||||
|
||||
export class DetectorRegistry {
|
||||
registry = observable.array<typeof BaseClusterDetector>([], { deep: false });
|
||||
|
||||
add(detectorClass: typeof BaseClusterDetector) {
|
||||
this.registry.push(detectorClass)
|
||||
}
|
||||
|
||||
async detectForCluster(cluster: Cluster): Promise<ClusterMetadata> {
|
||||
const results: {[key: string]: ClusterDetectionResult } = {}
|
||||
for (const detectorClass of this.registry) {
|
||||
const detector = new detectorClass(cluster)
|
||||
try {
|
||||
const data = await detector.detect()
|
||||
if (!data) continue;
|
||||
const existingValue = results[detector.key]
|
||||
if (existingValue && existingValue.accuracy > data.accuracy) continue; // previous value exists and is more accurate
|
||||
results[detector.key] = data
|
||||
} catch (e) {
|
||||
// detector raised error, do nothing
|
||||
}
|
||||
}
|
||||
const metadata: ClusterMetadata = {}
|
||||
for (const [key, result] of Object.entries(results)) {
|
||||
metadata[key] = result.value
|
||||
}
|
||||
return metadata
|
||||
}
|
||||
}
|
||||
|
||||
export const detectorRegistry = new DetectorRegistry()
|
||||
detectorRegistry.add(ClusterIdDetector)
|
||||
detectorRegistry.add(LastSeenDetector)
|
||||
detectorRegistry.add(VersionDetector)
|
||||
detectorRegistry.add(DistributionDetector)
|
||||
detectorRegistry.add(NodesCountDetector)
|
||||
@@ -0,0 +1,80 @@
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class DistributionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.DISTRIBUTION
|
||||
version: string
|
||||
|
||||
public async detect() {
|
||||
this.version = await this.getKubernetesVersion()
|
||||
if (await this.isRancher()) {
|
||||
return { value: "rancher", accuracy: 80}
|
||||
}
|
||||
if (this.isGKE()) {
|
||||
return { value: "gke", accuracy: 80}
|
||||
}
|
||||
if (this.isEKS()) {
|
||||
return { value: "eks", accuracy: 80}
|
||||
}
|
||||
if (this.isIKS()) {
|
||||
return { value: "iks", accuracy: 80}
|
||||
}
|
||||
if (this.isAKS()) {
|
||||
return { value: "aks", accuracy: 80}
|
||||
}
|
||||
if (this.isDigitalOcean()) {
|
||||
return { value: "digitalocean", accuracy: 90}
|
||||
}
|
||||
if (this.isMinikube()) {
|
||||
return { value: "minikube", accuracy: 80}
|
||||
}
|
||||
if (this.isCustom()) {
|
||||
return { value: "custom", accuracy: 10}
|
||||
}
|
||||
return { value: "vanilla", accuracy: 10}
|
||||
}
|
||||
|
||||
public async getKubernetesVersion() {
|
||||
if (this.cluster.version) return this.cluster.version
|
||||
|
||||
const response = await this.k8sRequest("/version")
|
||||
return response.gitVersion
|
||||
}
|
||||
|
||||
protected isGKE() {
|
||||
return this.version.includes("gke")
|
||||
}
|
||||
|
||||
protected isEKS() {
|
||||
return this.version.includes("eks")
|
||||
}
|
||||
|
||||
protected isIKS() {
|
||||
return this.version.includes("IKS")
|
||||
}
|
||||
|
||||
protected isAKS() {
|
||||
return this.cluster.apiUrl.endsWith("azmk8s.io")
|
||||
}
|
||||
|
||||
protected isDigitalOcean() {
|
||||
return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com")
|
||||
}
|
||||
|
||||
protected isMinikube() {
|
||||
return this.cluster.contextName.startsWith("minikube")
|
||||
}
|
||||
|
||||
protected isCustom() {
|
||||
return this.version.includes("+")
|
||||
}
|
||||
|
||||
protected async isRancher() {
|
||||
try {
|
||||
const response = await this.k8sRequest("")
|
||||
return response.data.find((api: any) => api?.apiVersion?.group === "meta.cattle.io") !== undefined
|
||||
} catch (e) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class LastSeenDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.LAST_SEEN
|
||||
|
||||
public async detect() {
|
||||
if (!this.cluster.accessible) return null;
|
||||
|
||||
await this.k8sRequest("/version")
|
||||
return { value: new Date().toJSON(), accuracy: 100 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class NodesCountDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.NODES_COUNT
|
||||
|
||||
public async detect() {
|
||||
const nodeCount = await this.getNodeCount()
|
||||
return { value: nodeCount, accuracy: 100}
|
||||
}
|
||||
|
||||
protected async getNodeCount(): Promise<number> {
|
||||
if (!this.cluster.accessible) return null;
|
||||
|
||||
const response = await this.k8sRequest("/api/v1/nodes")
|
||||
return response.items.length
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { BaseClusterDetector } from "./base-cluster-detector";
|
||||
import { ClusterMetadataKey } from "../cluster";
|
||||
|
||||
export class VersionDetector extends BaseClusterDetector {
|
||||
key = ClusterMetadataKey.VERSION
|
||||
value: string
|
||||
|
||||
public async detect() {
|
||||
const version = await this.getKubernetesVersion()
|
||||
return { value: version, accuracy: 100}
|
||||
}
|
||||
|
||||
public async getKubernetesVersion() {
|
||||
const response = await this.k8sRequest("/version")
|
||||
return response.gitVersion
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ export class ClusterManager {
|
||||
constructor(public readonly port: number) {
|
||||
// auto-init clusters
|
||||
autorun(() => {
|
||||
clusterStore.clusters.forEach(cluster => {
|
||||
clusterStore.enabledClustersList.forEach(cluster => {
|
||||
if (!cluster.initialized) {
|
||||
logger.info(`[CLUSTER-MANAGER]: init cluster`, cluster.getMeta());
|
||||
cluster.init(port);
|
||||
@@ -49,6 +49,8 @@ export class ClusterManager {
|
||||
// we need to swap path prefix so that request is proxied to kube api
|
||||
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
|
||||
}
|
||||
} else if (req.headers["x-cluster-id"]) {
|
||||
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString())
|
||||
} else {
|
||||
const clusterId = getClusterIdFromHost(req.headers.host);
|
||||
cluster = clusterStore.getById(clusterId)
|
||||
|
||||
+67
-73
@@ -1,7 +1,7 @@
|
||||
import type { ClusterId, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||
import { ipcMain } from "electron"
|
||||
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
|
||||
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
|
||||
import type { WorkspaceId } from "../common/workspace-store";
|
||||
import type { FeatureStatusMap } from "./feature"
|
||||
import { action, computed, observable, reaction, toJS, when } from "mobx";
|
||||
import { apiKubePrefix } from "../common/vars";
|
||||
import { broadcastIpc } from "../common/ipc";
|
||||
@@ -10,10 +10,11 @@ import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from
|
||||
import { Kubectl } from "./kubectl";
|
||||
import { KubeconfigManager } from "./kubeconfig-manager"
|
||||
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
|
||||
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
|
||||
import request, { RequestPromiseOptions } from "request-promise-native"
|
||||
import { apiResources } from "../common/rbac";
|
||||
import logger from "./logger"
|
||||
import { VersionDetector } from "./cluster-detectors/version-detector";
|
||||
import { detectorRegistry } from "./cluster-detectors/detector-registry";
|
||||
|
||||
export enum ClusterStatus {
|
||||
AccessGranted = 2,
|
||||
@@ -21,7 +22,19 @@ export enum ClusterStatus {
|
||||
Offline = 0
|
||||
}
|
||||
|
||||
export interface ClusterState extends ClusterModel {
|
||||
export enum ClusterMetadataKey {
|
||||
VERSION = "version",
|
||||
CLUSTER_ID = "id",
|
||||
DISTRIBUTION = "distribution",
|
||||
NODES_COUNT = "nodes",
|
||||
LAST_SEEN = "lastSeen"
|
||||
}
|
||||
|
||||
export type ClusterRefreshOptions = {
|
||||
refreshMetadata?: boolean
|
||||
}
|
||||
|
||||
export interface ClusterState {
|
||||
initialized: boolean;
|
||||
apiUrl: string;
|
||||
online: boolean;
|
||||
@@ -29,21 +42,18 @@ export interface ClusterState extends ClusterModel {
|
||||
accessible: boolean;
|
||||
ready: boolean;
|
||||
failureReason: string;
|
||||
nodes: number;
|
||||
eventCount: number;
|
||||
version: string;
|
||||
distribution: string;
|
||||
isAdmin: boolean;
|
||||
allowedNamespaces: string[]
|
||||
allowedResources: string[]
|
||||
features: FeatureStatusMap;
|
||||
}
|
||||
|
||||
export class Cluster implements ClusterModel {
|
||||
export class Cluster implements ClusterModel, ClusterState {
|
||||
public id: ClusterId;
|
||||
public frameId: number;
|
||||
public kubeCtl: Kubectl
|
||||
public contextHandler: ContextHandler;
|
||||
public ownerRef: string;
|
||||
protected kubeconfigManager: KubeconfigManager;
|
||||
protected eventDisposers: Function[] = [];
|
||||
protected activated = false;
|
||||
@@ -57,19 +67,17 @@ export class Cluster implements ClusterModel {
|
||||
@observable kubeConfigPath: string;
|
||||
@observable apiUrl: string; // cluster server url
|
||||
@observable kubeProxyUrl: string; // lens-proxy to kube-api url
|
||||
@observable enabled = false;
|
||||
@observable online = false;
|
||||
@observable accessible = false;
|
||||
@observable ready = false;
|
||||
@observable reconnecting = false;
|
||||
@observable disconnected = true;
|
||||
@observable failureReason: string;
|
||||
@observable nodes = 0;
|
||||
@observable version: string;
|
||||
@observable distribution = "unknown";
|
||||
@observable isAdmin = false;
|
||||
@observable eventCount = 0;
|
||||
@observable preferences: ClusterPreferences = {};
|
||||
@observable features: FeatureStatusMap = {};
|
||||
@observable metadata: ClusterMetadata = {};
|
||||
@observable allowedNamespaces: string[] = [];
|
||||
@observable allowedResources: string[] = [];
|
||||
|
||||
@@ -77,6 +85,10 @@ export class Cluster implements ClusterModel {
|
||||
return this.accessible && !this.disconnected;
|
||||
}
|
||||
|
||||
get version(): string {
|
||||
return String(this.metadata?.version) || ""
|
||||
}
|
||||
|
||||
constructor(model: ClusterModel) {
|
||||
this.updateModel(model);
|
||||
const kubeconfig = this.getKubeconfig()
|
||||
@@ -85,6 +97,10 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
}
|
||||
|
||||
get isManaged(): boolean {
|
||||
return !!this.ownerRef
|
||||
}
|
||||
|
||||
@action
|
||||
updateModel(model: ClusterModel) {
|
||||
Object.assign(this, model);
|
||||
@@ -113,11 +129,17 @@ export class Cluster implements ClusterModel {
|
||||
protected bindEvents() {
|
||||
logger.info(`[CLUSTER]: bind events`, this.getMeta());
|
||||
const refreshTimer = setInterval(() => !this.disconnected && this.refresh(), 30000); // every 30s
|
||||
const refreshMetadataTimer = setInterval(() => !this.disconnected && this.refreshMetadata(), 900000); // every 15 minutes
|
||||
|
||||
this.eventDisposers.push(
|
||||
reaction(this.getState, this.pushState),
|
||||
() => clearInterval(refreshTimer),
|
||||
);
|
||||
if (ipcMain) {
|
||||
this.eventDisposers.push(
|
||||
reaction(() => this.getState(), () => this.pushState()),
|
||||
() => {
|
||||
clearInterval(refreshTimer);
|
||||
clearInterval(refreshMetadataTimer);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
protected unbindEvents() {
|
||||
@@ -142,6 +164,7 @@ export class Cluster implements ClusterModel {
|
||||
await this.refreshConnectionStatus()
|
||||
if (this.accessible) {
|
||||
await this.refreshAllowedResources()
|
||||
this.isAdmin = await this.isClusterAdmin()
|
||||
this.ready = true
|
||||
this.kubeCtl = new Kubectl(this.version)
|
||||
this.kubeCtl.ensureKubectl() // download kubectl in background, so it's not blocking dashboard
|
||||
@@ -172,29 +195,32 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
|
||||
@action
|
||||
async refresh() {
|
||||
async refresh(opts: ClusterRefreshOptions = {}) {
|
||||
logger.info(`[CLUSTER]: refresh`, this.getMeta());
|
||||
await this.whenInitialized;
|
||||
await this.refreshConnectionStatus();
|
||||
if (this.accessible) {
|
||||
this.distribution = this.detectKubernetesDistribution(this.version)
|
||||
const [features, isAdmin, nodesCount] = await Promise.all([
|
||||
getFeatures(this),
|
||||
this.isClusterAdmin(),
|
||||
this.getNodeCount(),
|
||||
]);
|
||||
this.features = features;
|
||||
this.isAdmin = isAdmin;
|
||||
this.nodes = nodesCount;
|
||||
this.isAdmin = await this.isClusterAdmin();
|
||||
await Promise.all([
|
||||
this.refreshEvents(),
|
||||
this.refreshAllowedResources(),
|
||||
]);
|
||||
if (opts.refreshMetadata) {
|
||||
this.refreshMetadata()
|
||||
}
|
||||
this.ready = true
|
||||
}
|
||||
this.pushState();
|
||||
}
|
||||
|
||||
@action
|
||||
async refreshMetadata() {
|
||||
logger.info(`[CLUSTER]: refreshMetadata`, this.getMeta());
|
||||
const metadata = await detectorRegistry.detectForCluster(this)
|
||||
const existingMetadata = this.metadata
|
||||
this.metadata = Object.assign(existingMetadata, metadata)
|
||||
}
|
||||
|
||||
@action
|
||||
async refreshConnectionStatus() {
|
||||
const connectionStatus = await this.getConnectionStatus();
|
||||
@@ -225,18 +251,6 @@ export class Cluster implements ClusterModel {
|
||||
return this.kubeconfigManager.getPath()
|
||||
}
|
||||
|
||||
async installFeature(name: string, config: any) {
|
||||
return installFeature(name, this, config)
|
||||
}
|
||||
|
||||
async upgradeFeature(name: string, config: any) {
|
||||
return upgradeFeature(name, this, config)
|
||||
}
|
||||
|
||||
async uninstallFeature(name: string) {
|
||||
return uninstallFeature(name, this)
|
||||
}
|
||||
|
||||
protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
|
||||
const apiUrl = this.kubeProxyUrl + path;
|
||||
return request(apiUrl, {
|
||||
@@ -263,9 +277,9 @@ export class Cluster implements ClusterModel {
|
||||
|
||||
protected async getConnectionStatus(): Promise<ClusterStatus> {
|
||||
try {
|
||||
const response = await this.k8sRequest("/version")
|
||||
this.version = response.gitVersion
|
||||
this.failureReason = null
|
||||
const versionDetector = new VersionDetector(this)
|
||||
const versionData = await versionDetector.detect()
|
||||
this.metadata.version = versionData.value
|
||||
return ClusterStatus.AccessGranted;
|
||||
} catch (error) {
|
||||
logger.error(`Failed to connect cluster "${this.contextName}": ${error}`)
|
||||
@@ -314,27 +328,6 @@ export class Cluster implements ClusterModel {
|
||||
})
|
||||
}
|
||||
|
||||
protected detectKubernetesDistribution(kubernetesVersion: string): string {
|
||||
if (kubernetesVersion.includes("gke")) return "gke"
|
||||
if (kubernetesVersion.includes("eks")) return "eks"
|
||||
if (kubernetesVersion.includes("IKS")) return "iks"
|
||||
if (this.apiUrl.endsWith("azmk8s.io")) return "aks"
|
||||
if (this.apiUrl.endsWith("k8s.ondigitalocean.com")) return "digitalocean"
|
||||
if (this.contextName.startsWith("minikube")) return "minikube"
|
||||
if (kubernetesVersion.includes("+")) return "custom"
|
||||
return "vanilla"
|
||||
}
|
||||
|
||||
protected async getNodeCount(): Promise<number> {
|
||||
try {
|
||||
const response = await this.k8sRequest("/api/v1/nodes")
|
||||
return response.items.length
|
||||
} catch (error) {
|
||||
logger.debug(`failed to request node list: ${error.message}`)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
protected async getEventCount(): Promise<number> {
|
||||
if (!this.isAdmin) {
|
||||
return 0;
|
||||
@@ -377,6 +370,8 @@ export class Cluster implements ClusterModel {
|
||||
kubeConfigPath: this.kubeConfigPath,
|
||||
workspace: this.workspace,
|
||||
preferences: this.preferences,
|
||||
metadata: this.metadata,
|
||||
ownerRef: this.ownerRef
|
||||
};
|
||||
return toJS(model, {
|
||||
recurseEverything: true
|
||||
@@ -384,9 +379,8 @@ export class Cluster implements ClusterModel {
|
||||
}
|
||||
|
||||
// serializable cluster-state used for sync btw main <-> renderer
|
||||
getState = (): ClusterState => {
|
||||
getState(): ClusterState {
|
||||
const state: ClusterState = {
|
||||
...this.toJSON(),
|
||||
initialized: this.initialized,
|
||||
apiUrl: this.apiUrl,
|
||||
online: this.online,
|
||||
@@ -394,11 +388,7 @@ export class Cluster implements ClusterModel {
|
||||
disconnected: this.disconnected,
|
||||
accessible: this.accessible,
|
||||
failureReason: this.failureReason,
|
||||
nodes: this.nodes,
|
||||
version: this.version,
|
||||
distribution: this.distribution,
|
||||
isAdmin: this.isAdmin,
|
||||
features: this.features,
|
||||
eventCount: this.eventCount,
|
||||
allowedNamespaces: this.allowedNamespaces,
|
||||
allowedResources: this.allowedResources,
|
||||
@@ -408,14 +398,18 @@ export class Cluster implements ClusterModel {
|
||||
})
|
||||
}
|
||||
|
||||
pushState = (state = this.getState()): ClusterState => {
|
||||
@action
|
||||
setState(state: ClusterState) {
|
||||
Object.assign(this, state)
|
||||
}
|
||||
|
||||
pushState(state = this.getState()) {
|
||||
logger.silly(`[CLUSTER]: push-state`, state);
|
||||
broadcastIpc({
|
||||
channel: "cluster:state",
|
||||
frameId: this.frameId,
|
||||
args: [state],
|
||||
});
|
||||
return state;
|
||||
args: [this.id, state],
|
||||
})
|
||||
}
|
||||
|
||||
// get cluster system meta, e.g. use in "logger"
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import { KubeConfig } from "@kubernetes/client-node"
|
||||
import logger from "./logger";
|
||||
import { Cluster } from "./cluster";
|
||||
import { Feature, FeatureStatusMap, FeatureMap } from "./feature"
|
||||
import { MetricsFeature } from "../features/metrics"
|
||||
import { UserModeFeature } from "../features/user-mode"
|
||||
|
||||
const ALL_FEATURES: Map<string, Feature> = new Map([
|
||||
[MetricsFeature.id, new MetricsFeature(null)],
|
||||
[UserModeFeature.id, new UserModeFeature(null)],
|
||||
]);
|
||||
|
||||
export async function getFeatures(cluster: Cluster): Promise<FeatureStatusMap> {
|
||||
const result: FeatureStatusMap = {};
|
||||
logger.debug(`features for ${cluster.contextName}`);
|
||||
|
||||
for (const [key, feature] of ALL_FEATURES) {
|
||||
logger.debug(`feature ${key}`);
|
||||
logger.debug("getting feature status...");
|
||||
|
||||
const kc = new KubeConfig();
|
||||
kc.loadFromFile(cluster.getProxyKubeconfigPath());
|
||||
|
||||
result[feature.name] = await feature.featureStatus(kc);
|
||||
}
|
||||
|
||||
logger.debug(`getFeatures resolving with features: ${JSON.stringify(result)}`);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
export async function installFeature(name: string, cluster: Cluster, config: any): Promise<void> {
|
||||
// TODO Figure out how to handle config stuff
|
||||
return ALL_FEATURES.get(name).install(cluster)
|
||||
}
|
||||
|
||||
export async function upgradeFeature(name: string, cluster: Cluster, config: any): Promise<void> {
|
||||
// TODO Figure out how to handle config stuff
|
||||
return ALL_FEATURES.get(name).upgrade(cluster)
|
||||
}
|
||||
|
||||
export async function uninstallFeature(name: string, cluster: Cluster): Promise<void> {
|
||||
return ALL_FEATURES.get(name).uninstall(cluster)
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import fs from "fs";
|
||||
import path from "path"
|
||||
import hb from "handlebars"
|
||||
import { ResourceApplier } from "./resource-applier"
|
||||
import { CoreV1Api, KubeConfig, Watch } from "@kubernetes/client-node"
|
||||
import { Cluster } from "./cluster";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment } from "../common/vars";
|
||||
|
||||
export type FeatureStatusMap = Record<string, FeatureStatus>
|
||||
export type FeatureMap = Record<string, Feature>
|
||||
|
||||
export interface FeatureInstallRequest {
|
||||
clusterId: string;
|
||||
name: string;
|
||||
config?: any;
|
||||
}
|
||||
|
||||
export interface FeatureStatus {
|
||||
currentVersion: string;
|
||||
installed: boolean;
|
||||
latestVersion: string;
|
||||
canUpgrade: boolean;
|
||||
}
|
||||
|
||||
export abstract class Feature {
|
||||
public name: string;
|
||||
public latestVersion: string;
|
||||
|
||||
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||
|
||||
abstract async featureStatus(kc: KubeConfig): Promise<FeatureStatus>;
|
||||
|
||||
constructor(public config: any) {
|
||||
}
|
||||
|
||||
get folderPath() {
|
||||
if (isDevelopment) {
|
||||
return path.resolve(__static, "../src/features", this.name);
|
||||
}
|
||||
return path.resolve(__static, "../features", this.name);
|
||||
}
|
||||
|
||||
async install(cluster: Cluster): Promise<void> {
|
||||
const resources = this.renderTemplates();
|
||||
try {
|
||||
await new ResourceApplier(cluster).kubectlApplyAll(resources);
|
||||
} catch (err) {
|
||||
logger.error("Installing feature error", { err, cluster });
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
protected async deleteNamespace(kc: KubeConfig, name: string) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
const client = kc.makeApiClient(CoreV1Api)
|
||||
const result = await client.deleteNamespace("lens-metrics", 'false', undefined, undefined, undefined, "Foreground");
|
||||
const nsVersion = result.body.metadata.resourceVersion;
|
||||
const nsWatch = new Watch(kc);
|
||||
const query: Record<string, string> = {
|
||||
resourceVersion: nsVersion,
|
||||
fieldSelector: "metadata.name=lens-metrics",
|
||||
}
|
||||
const req = await nsWatch.watch('/api/v1/namespaces', query,
|
||||
(phase, obj) => {
|
||||
if (phase === 'DELETED') {
|
||||
logger.debug(`namespace ${name} finally gone`)
|
||||
req.abort();
|
||||
resolve()
|
||||
}
|
||||
},
|
||||
(err?: any) => {
|
||||
if (err) reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
protected renderTemplates(): string[] {
|
||||
const folderPath = this.folderPath;
|
||||
const resources: string[] = [];
|
||||
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||
fs.readdirSync(folderPath).forEach(filename => {
|
||||
const file = path.join(folderPath, filename);
|
||||
const raw = fs.readFileSync(file);
|
||||
if (filename.endsWith('.hb')) {
|
||||
const template = hb.compile(raw.toString());
|
||||
resources.push(template(this.config));
|
||||
} else {
|
||||
resources.push(raw.toString());
|
||||
}
|
||||
});
|
||||
|
||||
return resources;
|
||||
}
|
||||
}
|
||||
@@ -37,12 +37,12 @@ export class HelmRepoManager extends Singleton {
|
||||
|
||||
async loadAvailableRepos(): Promise<HelmRepo[]> {
|
||||
const res = await customRequestPromise({
|
||||
uri: "https://hub.helm.sh/assets/js/repos.json",
|
||||
uri: "https://github.com/lensapp/artifact-hub-repositories/releases/download/latest/repositories.json",
|
||||
json: true,
|
||||
resolveWithFullResponse: true,
|
||||
timeout: 10000,
|
||||
});
|
||||
return orderBy<HelmRepo>(res.body.data, repo => repo.name);
|
||||
return orderBy<HelmRepo>(res.body, repo => repo.name);
|
||||
}
|
||||
|
||||
async init() {
|
||||
|
||||
+56
-29
@@ -2,13 +2,15 @@
|
||||
|
||||
import "../common/system-ca"
|
||||
import "../common/prometheus-providers"
|
||||
import * as Mobx from "mobx"
|
||||
import * as LensExtensions from "../extensions/core-api";
|
||||
import { app, dialog } from "electron"
|
||||
import { appName } from "../common/vars";
|
||||
import path from "path"
|
||||
import { LensProxy } from "./lens-proxy"
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { ClusterManager } from "./cluster-manager";
|
||||
import AppUpdater from "./app-updater"
|
||||
import { AppUpdater } from "./app-updater"
|
||||
import { shellSync } from "./shell-sync"
|
||||
import { getFreePort } from "./port"
|
||||
import { mangleProxyEnv } from "./proxy-env"
|
||||
@@ -16,51 +18,52 @@ import { registerFileProtocol } from "../common/register-protocol";
|
||||
import { clusterStore } from "../common/cluster-store"
|
||||
import { userStore } from "../common/user-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { tracker } from "../common/tracker";
|
||||
import { appEventBus } from "../common/event-bus"
|
||||
import { extensionManager } from "../extensions/extension-manager";
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import logger from "./logger"
|
||||
|
||||
const workingDir = path.join(app.getPath("appData"), appName);
|
||||
let proxyPort: number;
|
||||
let proxyServer: LensProxy;
|
||||
let clusterManager: ClusterManager;
|
||||
let windowManager: WindowManager;
|
||||
|
||||
app.setName(appName);
|
||||
if (!process.env.CICD) {
|
||||
app.setPath("userData", workingDir);
|
||||
}
|
||||
|
||||
let windowManager: WindowManager;
|
||||
let clusterManager: ClusterManager;
|
||||
let proxyServer: LensProxy;
|
||||
|
||||
mangleProxyEnv()
|
||||
if (app.commandLine.getSwitchValue("proxy-server") !== "") {
|
||||
process.env.HTTPS_PROXY = app.commandLine.getSwitchValue("proxy-server")
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await shellSync();
|
||||
app.on("ready", async () => {
|
||||
logger.info(`🚀 Starting Lens from "${workingDir}"`)
|
||||
await shellSync();
|
||||
|
||||
tracker.event("app", "start");
|
||||
const updater = new AppUpdater()
|
||||
updater.start();
|
||||
|
||||
registerFileProtocol("static", __static);
|
||||
|
||||
// find free port
|
||||
let proxyPort: number
|
||||
try {
|
||||
proxyPort = await getFreePort()
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
|
||||
app.quit();
|
||||
}
|
||||
|
||||
// preload configuration from stores
|
||||
// preload isomorphic stores
|
||||
await Promise.all([
|
||||
userStore.load(),
|
||||
clusterStore.load(),
|
||||
workspaceStore.load(),
|
||||
]);
|
||||
|
||||
// find free port
|
||||
try {
|
||||
proxyPort = await getFreePort()
|
||||
} catch (error) {
|
||||
logger.error(error)
|
||||
dialog.showErrorBox("Lens Error", "Could not find a free port for the cluster proxy")
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// create cluster manager
|
||||
clusterManager = new ClusterManager(proxyPort);
|
||||
|
||||
@@ -70,18 +73,42 @@ async function main() {
|
||||
} catch (error) {
|
||||
logger.error(`Could not start proxy (127.0.0:${proxyPort}): ${error.message}`)
|
||||
dialog.showErrorBox("Lens Error", `Could not start proxy (127.0.0:${proxyPort}): ${error.message || "unknown error"}`)
|
||||
app.quit();
|
||||
app.exit();
|
||||
}
|
||||
|
||||
// create window manager and open app
|
||||
windowManager = new WindowManager(proxyPort);
|
||||
}
|
||||
|
||||
app.on("ready", main);
|
||||
LensExtensionsApi.windowManager = windowManager; // expose to extensions
|
||||
extensionLoader.loadOnMain()
|
||||
extensionLoader.extensions.replace(await extensionManager.load())
|
||||
extensionLoader.broadcastExtensions()
|
||||
|
||||
app.on("will-quit", async (event) => {
|
||||
event.preventDefault(); // To allow mixpanel sending to be executed
|
||||
if (proxyServer) proxyServer.close()
|
||||
if (clusterManager) clusterManager.stop()
|
||||
app.exit();
|
||||
setTimeout(() => {
|
||||
appEventBus.emit({ name: "app", action: "start" })
|
||||
}, 1000)
|
||||
});
|
||||
|
||||
app.on("activate", (event, hasVisibleWindows) => {
|
||||
logger.info('APP:ACTIVATE', { hasVisibleWindows })
|
||||
if (!hasVisibleWindows) {
|
||||
windowManager.initMainWindow();
|
||||
}
|
||||
});
|
||||
|
||||
// Quit app on Cmd+Q (MacOS)
|
||||
app.on("will-quit", (event) => {
|
||||
logger.info('APP:QUIT');
|
||||
event.preventDefault(); // prevent app's default shutdown (e.g. required for telemetry, etc.)
|
||||
clusterManager?.stop(); // close cluster connections
|
||||
return; // skip exit to make tray work, to quit go to app's global menu or tray's menu
|
||||
})
|
||||
|
||||
// Extensions-api runtime exports
|
||||
export const LensExtensionsApi = {
|
||||
...LensExtensions,
|
||||
};
|
||||
|
||||
export {
|
||||
Mobx,
|
||||
LensExtensionsApi as LensExtensions,
|
||||
}
|
||||
+66
-56
@@ -1,19 +1,38 @@
|
||||
import { app, BrowserWindow, dialog, Menu, MenuItem, MenuItemConstructorOptions, shell, webContents } from "electron"
|
||||
import { autorun } from "mobx";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { appName, isMac, issuesTrackerUrl, isWindows, slackUrl } from "../common/vars";
|
||||
import { appName, isMac, isWindows } from "../common/vars";
|
||||
import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.route";
|
||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
||||
import { menuRegistry } from "../extensions/registries/menu-registry";
|
||||
import logger from "./logger";
|
||||
|
||||
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
|
||||
|
||||
export function initMenu(windowManager: WindowManager) {
|
||||
autorun(() => buildMenu(windowManager), {
|
||||
return autorun(() => buildMenu(windowManager), {
|
||||
delay: 100
|
||||
});
|
||||
}
|
||||
|
||||
export function showAbout(browserWindow: BrowserWindow) {
|
||||
const appInfo = [
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Copyright 2020 Mirantis, Inc.`,
|
||||
]
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
detail: appInfo.join("\r\n")
|
||||
})
|
||||
}
|
||||
|
||||
export function buildMenu(windowManager: WindowManager) {
|
||||
function ignoreOnMac(menuItems: MenuItemConstructorOptions[]) {
|
||||
if (isMac) return [];
|
||||
@@ -29,32 +48,11 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
return menuItems;
|
||||
}
|
||||
|
||||
function navigate(url: string) {
|
||||
async function navigate(url: string) {
|
||||
logger.info(`[MENU]: navigating to ${url}`);
|
||||
windowManager.navigate({
|
||||
channel: "menu:navigate",
|
||||
url: url,
|
||||
})
|
||||
await windowManager.navigate(url);
|
||||
}
|
||||
|
||||
function showAbout(browserWindow: BrowserWindow) {
|
||||
const appInfo = [
|
||||
`${appName}: ${app.getVersion()}`,
|
||||
`Electron: ${process.versions.electron}`,
|
||||
`Chrome: ${process.versions.chrome}`,
|
||||
`Copyright 2020 Mirantis, Inc.`,
|
||||
]
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
title: `${isWindows ? " ".repeat(2) : ""}${appName}`,
|
||||
type: "info",
|
||||
buttons: ["Close"],
|
||||
message: `Lens`,
|
||||
detail: appInfo.join("\r\n")
|
||||
})
|
||||
}
|
||||
|
||||
const mt: MenuItemConstructorOptions[] = [];
|
||||
|
||||
const macAppMenu: MenuItemConstructorOptions = {
|
||||
label: app.getName(),
|
||||
submenu: [
|
||||
@@ -67,7 +65,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Preferences',
|
||||
accelerator: 'Cmd+,',
|
||||
accelerator: 'CmdOrCtrl+,',
|
||||
click() {
|
||||
navigate(preferencesURL())
|
||||
}
|
||||
@@ -79,14 +77,16 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'hideOthers' },
|
||||
{ role: 'unhide' },
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
{
|
||||
label: 'Quit',
|
||||
accelerator: 'Cmd+Q',
|
||||
click() {
|
||||
app.exit(); // force quit since might be blocked within app.on("will-quit")
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
if (isMac) {
|
||||
mt.push(macAppMenu);
|
||||
}
|
||||
|
||||
const fileMenu: MenuItemConstructorOptions = {
|
||||
label: "File",
|
||||
submenu: [
|
||||
@@ -121,10 +121,11 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{ role: 'quit' }
|
||||
])
|
||||
]),
|
||||
{ type: 'separator' },
|
||||
{ role: 'close' } // close current window
|
||||
]
|
||||
};
|
||||
mt.push(fileMenu)
|
||||
|
||||
const editMenu: MenuItemConstructorOptions = {
|
||||
label: 'Edit',
|
||||
@@ -140,7 +141,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'selectAll' },
|
||||
]
|
||||
};
|
||||
mt.push(editMenu)
|
||||
|
||||
const viewMenu: MenuItemConstructorOptions = {
|
||||
label: 'View',
|
||||
submenu: [
|
||||
@@ -162,7 +163,7 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
label: 'Reload',
|
||||
accelerator: 'CmdOrCtrl+R',
|
||||
click() {
|
||||
windowManager.reload({ channel: "menu:reload" });
|
||||
windowManager.reload();
|
||||
}
|
||||
},
|
||||
{ role: 'toggleDevTools' },
|
||||
@@ -174,35 +175,22 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
{ role: 'togglefullscreen' }
|
||||
]
|
||||
};
|
||||
mt.push(viewMenu)
|
||||
|
||||
const helpMenu: MenuItemConstructorOptions = {
|
||||
role: 'help',
|
||||
submenu: [
|
||||
{
|
||||
label: "License",
|
||||
click: async () => {
|
||||
shell.openExternal('https://k8slens.dev/licenses/eula.md');
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Community Slack",
|
||||
click: async () => {
|
||||
shell.openExternal(slackUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Report an Issue',
|
||||
click: async () => {
|
||||
shell.openExternal(issuesTrackerUrl);
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "What's new?",
|
||||
click() {
|
||||
navigate(whatsNewURL())
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "License",
|
||||
click: async () => {
|
||||
shell.openExternal('https://k8slens.dev/licenses/eula.md');
|
||||
},
|
||||
},
|
||||
...ignoreOnMac([
|
||||
{
|
||||
label: "About Lens",
|
||||
@@ -214,7 +202,29 @@ export function buildMenu(windowManager: WindowManager) {
|
||||
]
|
||||
};
|
||||
|
||||
mt.push(helpMenu)
|
||||
// Prepare menu items order
|
||||
const appMenu: Record<MenuTopId, MenuItemConstructorOptions> = {
|
||||
mac: macAppMenu,
|
||||
file: fileMenu,
|
||||
edit: editMenu,
|
||||
view: viewMenu,
|
||||
help: helpMenu,
|
||||
}
|
||||
|
||||
Menu.setApplicationMenu(Menu.buildFromTemplate(mt));
|
||||
// Modify menu from extensions-api
|
||||
menuRegistry.getItems().forEach(({ parentId, ...menuItem }) => {
|
||||
try {
|
||||
const topMenu = appMenu[parentId as MenuTopId].submenu as MenuItemConstructorOptions[];
|
||||
topMenu.push(menuItem);
|
||||
} catch (err) {
|
||||
logger.error(`[MENU]: can't register menu item, parentId=${parentId}`, { menuItem })
|
||||
}
|
||||
})
|
||||
|
||||
if (!isMac) {
|
||||
delete appMenu.mac
|
||||
}
|
||||
|
||||
const menu = Menu.buildFromTemplate(Object.values(appMenu));
|
||||
Menu.setApplicationMenu(menu);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import * as k8s from "@kubernetes/client-node"
|
||||
import { KubeConfig } from "@kubernetes/client-node"
|
||||
import { Cluster } from "./cluster"
|
||||
import logger from "./logger";
|
||||
import { tracker } from "../common/tracker";
|
||||
import { appEventBus } from "../common/event-bus"
|
||||
|
||||
export class NodeShellSession extends ShellSession {
|
||||
protected nodeName: string;
|
||||
@@ -44,7 +44,7 @@ export class NodeShellSession extends ShellSession {
|
||||
this.closeWebsocketOnProcessExit()
|
||||
this.exitProcessOnWebsocketClose()
|
||||
|
||||
tracker.event("node-shell", "open")
|
||||
appEventBus.emit({name: "node-shell", action: "open"})
|
||||
}
|
||||
|
||||
protected exit(code = 1000) {
|
||||
|
||||
@@ -6,7 +6,7 @@ import * as yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import * as tempy from "tempy";
|
||||
import logger from "./logger"
|
||||
import { tracker } from "../common/tracker";
|
||||
import { appEventBus } from "../common/event-bus"
|
||||
import { cloneJsonObject } from "../common/utils";
|
||||
|
||||
export class ResourceApplier {
|
||||
@@ -15,7 +15,7 @@ export class ResourceApplier {
|
||||
|
||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||
resource = this.sanitizeObject(resource);
|
||||
tracker.event("resource", "apply")
|
||||
appEventBus.emit({name: "resource", action: "apply"})
|
||||
return await this.kubectlApply(yaml.safeDump(resource));
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import { Cluster } from "./cluster"
|
||||
import { ClusterPreferences } from "../common/cluster-store";
|
||||
import { helmCli } from "./helm/helm-cli"
|
||||
import { isWindows } from "../common/vars";
|
||||
import { tracker } from "../common/tracker";
|
||||
import { appEventBus } from "../common/event-bus"
|
||||
import { userStore } from "../common/user-store";
|
||||
|
||||
export class ShellSession extends EventEmitter {
|
||||
@@ -58,7 +58,7 @@ export class ShellSession extends EventEmitter {
|
||||
this.closeWebsocketOnProcessExit()
|
||||
this.exitProcessOnWebsocketClose()
|
||||
|
||||
tracker.event("shell", "open")
|
||||
appEventBus.emit({name: "shell", action: "open"})
|
||||
}
|
||||
|
||||
protected cwd(): string {
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import path from "path"
|
||||
import packageInfo from "../../package.json"
|
||||
import { app, dialog, Menu, NativeImage, nativeTheme, Tray } from "electron"
|
||||
import { autorun } from "mobx";
|
||||
import { showAbout } from "./menu";
|
||||
import { AppUpdater } from "./app-updater";
|
||||
import { WindowManager } from "./window-manager";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { workspaceStore } from "../common/workspace-store";
|
||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||
import { clusterViewURL } from "../renderer/components/cluster-manager/cluster-view.route";
|
||||
import logger from "./logger";
|
||||
import { isDevelopment } from "../common/vars";
|
||||
|
||||
// note: instance of Tray should be saved somewhere, otherwise it disappears
|
||||
export let tray: Tray;
|
||||
|
||||
// refresh icon when MacOS dark/light theme has changed
|
||||
nativeTheme.on("updated", () => tray?.setImage(getTrayIcon()));
|
||||
|
||||
export function getTrayIcon(isDark = nativeTheme.shouldUseDarkColors): string {
|
||||
return path.resolve(
|
||||
__static,
|
||||
isDevelopment ? "../build/tray" : "icons", // copied within electron-builder extras
|
||||
`tray_icon${isDark ? "_dark" : ""}.png`
|
||||
)
|
||||
}
|
||||
|
||||
export function initTray(windowManager: WindowManager) {
|
||||
const dispose = autorun(() => {
|
||||
try {
|
||||
const menu = createTrayMenu(windowManager);
|
||||
buildTray(getTrayIcon(), menu);
|
||||
} catch (err) {
|
||||
logger.error(`[TRAY]: building failed: ${err}`);
|
||||
}
|
||||
})
|
||||
return () => {
|
||||
dispose();
|
||||
tray?.destroy();
|
||||
tray = null;
|
||||
}
|
||||
}
|
||||
|
||||
export function buildTray(icon: string | NativeImage, menu: Menu) {
|
||||
if (!tray) {
|
||||
tray = new Tray(icon)
|
||||
tray.setToolTip(packageInfo.description)
|
||||
tray.setIgnoreDoubleClickEvents(true);
|
||||
}
|
||||
|
||||
tray.setImage(icon);
|
||||
tray.setContextMenu(menu);
|
||||
|
||||
return tray;
|
||||
}
|
||||
|
||||
export function createTrayMenu(windowManager: WindowManager): Menu {
|
||||
return Menu.buildFromTemplate([
|
||||
{
|
||||
label: "About Lens",
|
||||
async click() {
|
||||
// note: argument[1] (browserWindow) not available when app is not focused / hidden
|
||||
const browserWindow = await windowManager.ensureMainWindow();
|
||||
showAbout(browserWindow);
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: "Open Lens",
|
||||
async click() {
|
||||
await windowManager.ensureMainWindow()
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Preferences",
|
||||
click() {
|
||||
windowManager.navigate(preferencesURL());
|
||||
},
|
||||
},
|
||||
{
|
||||
label: "Clusters",
|
||||
submenu: workspaceStore.enabledWorkspacesList
|
||||
.filter(workspace => clusterStore.getByWorkspaceId(workspace.id).length > 0) // hide empty workspaces
|
||||
.map(workspace => {
|
||||
const clusters = clusterStore.getByWorkspaceId(workspace.id);
|
||||
return {
|
||||
label: workspace.name,
|
||||
toolTip: workspace.description,
|
||||
submenu: clusters.map(cluster => {
|
||||
const { id: clusterId, preferences: { clusterName: label }, online, workspace } = cluster;
|
||||
return {
|
||||
label: `${online ? '✓' : '\x20'.repeat(3)/*offset*/}${label}`,
|
||||
toolTip: clusterId,
|
||||
async click() {
|
||||
workspaceStore.setActive(workspace);
|
||||
clusterStore.setActive(clusterId);
|
||||
windowManager.navigate(clusterViewURL({ params: { clusterId } }));
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}),
|
||||
},
|
||||
{
|
||||
label: "Check for updates",
|
||||
async click() {
|
||||
const result = await AppUpdater.checkForUpdates();
|
||||
if (!result) {
|
||||
const browserWindow = await windowManager.ensureMainWindow();
|
||||
dialog.showMessageBoxSync(browserWindow, {
|
||||
message: "No updates available",
|
||||
type: "info",
|
||||
})
|
||||
}
|
||||
},
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: 'Quit App',
|
||||
click() {
|
||||
app.exit();
|
||||
}
|
||||
}
|
||||
]);
|
||||
}
|
||||
+110
-49
@@ -1,84 +1,140 @@
|
||||
import type { ClusterId } from "../common/cluster-store";
|
||||
import { clusterStore } from "../common/cluster-store";
|
||||
import { BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { observable } from "mobx";
|
||||
import { app, BrowserWindow, dialog, ipcMain, shell, webContents } from "electron"
|
||||
import windowStateKeeper from "electron-window-state"
|
||||
import { extensionLoader } from "../extensions/extension-loader";
|
||||
import { appEventBus } from "../common/event-bus"
|
||||
import { initMenu } from "./menu";
|
||||
import { initTray } from "./tray";
|
||||
|
||||
export class WindowManager {
|
||||
protected mainView: BrowserWindow;
|
||||
protected mainWindow: BrowserWindow;
|
||||
protected splashWindow: BrowserWindow;
|
||||
protected windowState: windowStateKeeper.State;
|
||||
protected disposers: Record<string, Function> = {};
|
||||
|
||||
@observable activeClusterId: ClusterId;
|
||||
|
||||
constructor(protected proxyPort: number) {
|
||||
this.bindEvents();
|
||||
this.initMenu();
|
||||
this.initTray();
|
||||
this.initMainWindow();
|
||||
}
|
||||
|
||||
get mainUrl() {
|
||||
return `http://localhost:${this.proxyPort}`
|
||||
}
|
||||
|
||||
async initMainWindow(showSplash = true) {
|
||||
// Manage main window size and position with state persistence
|
||||
this.windowState = windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
if (!this.windowState) {
|
||||
this.windowState = windowStateKeeper({
|
||||
defaultHeight: 900,
|
||||
defaultWidth: 1440,
|
||||
});
|
||||
}
|
||||
if (!this.mainWindow) {
|
||||
// show icon in dock (mac-os only)
|
||||
app.dock?.show();
|
||||
|
||||
const { width, height, x, y } = this.windowState;
|
||||
this.mainView = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainView);
|
||||
const { width, height, x, y } = this.windowState;
|
||||
this.mainWindow = new BrowserWindow({
|
||||
x, y, width, height,
|
||||
show: false,
|
||||
minWidth: 700, // accommodate 800 x 600 display minimum
|
||||
minHeight: 500, // accommodate 800 x 600 display minimum
|
||||
titleBarStyle: "hidden",
|
||||
backgroundColor: "#1e2124",
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
nodeIntegrationInSubFrames: true,
|
||||
enableRemoteModule: true,
|
||||
},
|
||||
});
|
||||
this.windowState.manage(this.mainWindow);
|
||||
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
this.mainView.webContents.on("new-window", (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
// open external links in default browser (target=_blank, window.open)
|
||||
this.mainWindow.webContents.on("new-window", (event, url) => {
|
||||
event.preventDefault();
|
||||
shell.openExternal(url);
|
||||
});
|
||||
this.mainWindow.webContents.on("dom-ready", () => {
|
||||
extensionLoader.broadcastExtensions()
|
||||
})
|
||||
this.mainWindow.on("focus", () => {
|
||||
appEventBus.emit({name: "app", action: "focus"})
|
||||
})
|
||||
this.mainWindow.on("blur", () => {
|
||||
appEventBus.emit({name: "app", action: "blur"})
|
||||
})
|
||||
|
||||
// clean up
|
||||
this.mainWindow.on("closed", () => {
|
||||
this.windowState.unmanage();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
app.dock?.hide(); // hide icon in dock (mac-os)
|
||||
})
|
||||
}
|
||||
try {
|
||||
if (showSplash) await this.showSplash();
|
||||
await this.mainWindow.loadURL(this.mainUrl);
|
||||
this.mainWindow.show();
|
||||
this.splashWindow?.close();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox("ERROR!", err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
protected async initMenu() {
|
||||
this.disposers.menuAutoUpdater = initMenu(this);
|
||||
}
|
||||
|
||||
protected initTray() {
|
||||
this.disposers.trayAutoUpdater = initTray(this);
|
||||
}
|
||||
|
||||
protected bindEvents() {
|
||||
// track visible cluster from ui
|
||||
ipcMain.on("cluster-view:current-id", (event, clusterId: ClusterId) => {
|
||||
this.activeClusterId = clusterId;
|
||||
});
|
||||
|
||||
// load & show app
|
||||
this.showMain();
|
||||
initMenu(this);
|
||||
}
|
||||
|
||||
navigate({ url, channel, frameId }: { url: string, channel: string, frameId?: number }) {
|
||||
async ensureMainWindow(): Promise<BrowserWindow> {
|
||||
if (!this.mainWindow) await this.initMainWindow();
|
||||
this.mainWindow.show();
|
||||
return this.mainWindow;
|
||||
}
|
||||
|
||||
sendToView({ channel, frameId, data = [] }: { channel: string, frameId?: number, data?: any[] }) {
|
||||
if (frameId) {
|
||||
this.mainView.webContents.sendToFrame(frameId, channel, url);
|
||||
this.mainWindow.webContents.sendToFrame(frameId, channel, ...data);
|
||||
} else {
|
||||
this.mainView.webContents.send(channel, url);
|
||||
this.mainWindow.webContents.send(channel, ...data);
|
||||
}
|
||||
}
|
||||
|
||||
reload({ channel }: { channel: string }) {
|
||||
async navigate(url: string, frameId?: number) {
|
||||
await this.ensureMainWindow();
|
||||
this.sendToView({
|
||||
channel: "menu:navigate",
|
||||
frameId: frameId,
|
||||
data: [url],
|
||||
})
|
||||
}
|
||||
|
||||
reload() {
|
||||
const frameId = clusterStore.getById(this.activeClusterId)?.frameId;
|
||||
if (frameId) {
|
||||
this.mainView.webContents.sendToFrame(frameId, channel);
|
||||
this.sendToView({ channel: "menu:reload", frameId });
|
||||
} else {
|
||||
webContents.getFocusedWebContents()?.reload();
|
||||
}
|
||||
}
|
||||
|
||||
async showMain() {
|
||||
try {
|
||||
await this.showSplash();
|
||||
await this.mainView.loadURL(`http://localhost:${this.proxyPort}`)
|
||||
this.mainView.show();
|
||||
this.splashWindow.close();
|
||||
} catch (err) {
|
||||
dialog.showErrorBox("ERROR!", err.toString())
|
||||
}
|
||||
}
|
||||
|
||||
async showSplash() {
|
||||
if (!this.splashWindow) {
|
||||
this.splashWindow = new BrowserWindow({
|
||||
@@ -99,8 +155,13 @@ export class WindowManager {
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.windowState.unmanage();
|
||||
this.mainWindow.destroy();
|
||||
this.splashWindow.destroy();
|
||||
this.mainView.destroy();
|
||||
this.mainWindow = null;
|
||||
this.splashWindow = null;
|
||||
Object.entries(this.disposers).forEach(([name, dispose]) => {
|
||||
dispose();
|
||||
delete this.disposers[name]
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -53,22 +53,6 @@ export class ApiManager {
|
||||
getStore(api: string | KubeApi): KubeObjectStore {
|
||||
return this.stores.get(this.resolveApi(api));
|
||||
}
|
||||
|
||||
registerViews(api: KubeApi | KubeApi[], views: ApiComponents) {
|
||||
if (Array.isArray(api)) {
|
||||
api.forEach(api => this.registerViews(api, views));
|
||||
return;
|
||||
}
|
||||
const currentViews = this.views.get(api) || {};
|
||||
this.views.set(api, {
|
||||
...currentViews,
|
||||
...views,
|
||||
});
|
||||
}
|
||||
|
||||
getViews(api: string | KubeApi): ApiComponents {
|
||||
return this.views.get(this.resolveApi(api)) || {}
|
||||
}
|
||||
}
|
||||
|
||||
export const apiManager = new ApiManager();
|
||||
@@ -1,262 +0,0 @@
|
||||
// Kubernetes certificate management controller apis
|
||||
// Reference: https://docs.cert-manager.io/en/latest/reference/index.html
|
||||
// API docs: https://docs.cert-manager.io/en/latest/reference/api-docs/index.html
|
||||
|
||||
import { KubeObject } from "../kube-object";
|
||||
import { ISecretRef, secretsApi } from "./secret.api";
|
||||
import { getDetailsUrl } from "../../navigation";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class Certificate extends KubeObject {
|
||||
static kind = "Certificate"
|
||||
|
||||
spec: {
|
||||
secretName: string;
|
||||
commonName?: string;
|
||||
dnsNames?: string[];
|
||||
organization?: string[];
|
||||
ipAddresses?: string[];
|
||||
duration?: string;
|
||||
renewBefore?: string;
|
||||
isCA?: boolean;
|
||||
keySize?: number;
|
||||
keyAlgorithm?: "rsa" | "ecdsa";
|
||||
issuerRef: {
|
||||
kind?: string;
|
||||
name: string;
|
||||
};
|
||||
acme?: {
|
||||
config: {
|
||||
domains: string[];
|
||||
http01: {
|
||||
ingress?: string;
|
||||
ingressClass?: string;
|
||||
};
|
||||
dns01?: {
|
||||
provider: string;
|
||||
};
|
||||
}[];
|
||||
};
|
||||
}
|
||||
status: {
|
||||
conditions?: {
|
||||
lastTransitionTime: string; // 2019-06-04T07:35:58Z,
|
||||
message: string; // Certificate is up to date and has not expired,
|
||||
reason: string; // Ready,
|
||||
status: string; // True,
|
||||
type: string; // Ready
|
||||
}[];
|
||||
notAfter: string; // 2019-11-01T05:36:27Z
|
||||
lastFailureTime?: string;
|
||||
}
|
||||
|
||||
getType(): string {
|
||||
const { isCA, acme } = this.spec;
|
||||
if (isCA) return "CA"
|
||||
if (acme) return "ACME"
|
||||
}
|
||||
|
||||
getCommonName() {
|
||||
return this.spec.commonName || ""
|
||||
}
|
||||
|
||||
getIssuerName() {
|
||||
return this.spec.issuerRef.name;
|
||||
}
|
||||
|
||||
getSecretName() {
|
||||
return this.spec.secretName;
|
||||
}
|
||||
|
||||
getIssuerDetailsUrl() {
|
||||
return getDetailsUrl(issuersApi.getUrl({
|
||||
namespace: this.getNs(),
|
||||
name: this.getIssuerName(),
|
||||
}))
|
||||
}
|
||||
|
||||
getSecretDetailsUrl() {
|
||||
return getDetailsUrl(secretsApi.getUrl({
|
||||
namespace: this.getNs(),
|
||||
name: this.getSecretName(),
|
||||
}))
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
const { conditions = [] } = this.status;
|
||||
return conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class Issuer extends KubeObject {
|
||||
static kind = "Issuer"
|
||||
|
||||
spec: {
|
||||
acme?: {
|
||||
email: string;
|
||||
server: string;
|
||||
skipTLSVerify?: boolean;
|
||||
privateKeySecretRef: ISecretRef;
|
||||
solvers?: {
|
||||
dns01?: {
|
||||
cnameStrategy: string;
|
||||
acmedns?: {
|
||||
host: string;
|
||||
accountSecretRef: ISecretRef;
|
||||
};
|
||||
akamai?: {
|
||||
accessTokenSecretRef: ISecretRef;
|
||||
clientSecretSecretRef: ISecretRef;
|
||||
clientTokenSecretRef: ISecretRef;
|
||||
serviceConsumerDomain: string;
|
||||
};
|
||||
azuredns?: {
|
||||
clientID: string;
|
||||
clientSecretSecretRef: ISecretRef;
|
||||
hostedZoneName: string;
|
||||
resourceGroupName: string;
|
||||
subscriptionID: string;
|
||||
tenantID: string;
|
||||
};
|
||||
clouddns?: {
|
||||
project: string;
|
||||
serviceAccountSecretRef: ISecretRef;
|
||||
};
|
||||
cloudflare?: {
|
||||
email: string;
|
||||
apiKeySecretRef: ISecretRef;
|
||||
};
|
||||
digitalocean?: {
|
||||
tokenSecretRef: ISecretRef;
|
||||
};
|
||||
rfc2136?: {
|
||||
nameserver: string;
|
||||
tsigAlgorithm: string;
|
||||
tsigKeyName: string;
|
||||
tsigSecretSecretRef: ISecretRef;
|
||||
};
|
||||
route53?: {
|
||||
accessKeyID: string;
|
||||
hostedZoneID: string;
|
||||
region: string;
|
||||
secretAccessKeySecretRef: ISecretRef;
|
||||
};
|
||||
webhook?: {
|
||||
config: object; // arbitrary json
|
||||
groupName: string;
|
||||
solverName: string;
|
||||
};
|
||||
};
|
||||
http01?: {
|
||||
ingress: {
|
||||
class: string;
|
||||
name: string;
|
||||
serviceType: string;
|
||||
};
|
||||
};
|
||||
selector?: {
|
||||
dnsNames: string[];
|
||||
matchLabels: {
|
||||
[label: string]: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
};
|
||||
ca?: {
|
||||
secretName: string;
|
||||
};
|
||||
vault?: {
|
||||
path: string;
|
||||
server: string;
|
||||
caBundle: string; // <base64 encoded caBundle PEM file>
|
||||
auth: {
|
||||
appRole: {
|
||||
path: string;
|
||||
roleId: string;
|
||||
secretRef: ISecretRef;
|
||||
};
|
||||
};
|
||||
};
|
||||
selfSigned?: {};
|
||||
venafi?: {
|
||||
zone: string;
|
||||
cloud?: {
|
||||
apiTokenSecretRef: ISecretRef;
|
||||
};
|
||||
tpp?: {
|
||||
url: string;
|
||||
caBundle: string; // <base64 encoded caBundle PEM file>
|
||||
credentialsRef: {
|
||||
name: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
status: {
|
||||
acme?: {
|
||||
uri: string;
|
||||
};
|
||||
conditions?: {
|
||||
lastTransitionTime: string; // 2019-06-05T07:10:42Z,
|
||||
message: string; // The ACME account was registered with the ACME server,
|
||||
reason: string; // ACMEAccountRegistered,
|
||||
status: string; // True,
|
||||
type: string; // Ready
|
||||
}[];
|
||||
}
|
||||
|
||||
getType() {
|
||||
const { acme, ca, selfSigned, vault, venafi } = this.spec;
|
||||
if (acme) return "ACME"
|
||||
if (ca) return "CA"
|
||||
if (selfSigned) return "SelfSigned"
|
||||
if (vault) return "Vault"
|
||||
if (venafi) return "Venafi"
|
||||
}
|
||||
|
||||
getConditions() {
|
||||
if (!this.status?.conditions) return [];
|
||||
const { conditions = [] } = this.status;
|
||||
return conditions.map(condition => {
|
||||
const { message, reason, lastTransitionTime, status } = condition;
|
||||
return {
|
||||
...condition,
|
||||
isReady: status === "True",
|
||||
tooltip: `${message || reason} (${lastTransitionTime})`,
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class ClusterIssuer extends Issuer {
|
||||
static kind = "ClusterIssuer"
|
||||
}
|
||||
|
||||
export const certificatesApi = new KubeApi({
|
||||
kind: Certificate.kind,
|
||||
apiBase: "/apis/cert-manager.io/v1alpha2/certificates",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Certificate,
|
||||
});
|
||||
|
||||
export const issuersApi = new KubeApi({
|
||||
kind: Issuer.kind,
|
||||
apiBase: "/apis/cert-manager.io/v1alpha2/issuers",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Issuer,
|
||||
});
|
||||
|
||||
export const clusterIssuersApi = new KubeApi({
|
||||
kind: ClusterIssuer.kind,
|
||||
apiBase: "/apis/cert-manager.io/v1alpha2/clusterissuers",
|
||||
isNamespaced: false,
|
||||
objectConstructor: ClusterIssuer,
|
||||
});
|
||||
@@ -3,11 +3,10 @@ import { KubeApi } from "../kube-api";
|
||||
|
||||
export class ClusterRoleBinding extends RoleBinding {
|
||||
static kind = "ClusterRoleBinding"
|
||||
static namespaced = false
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"
|
||||
}
|
||||
|
||||
export const clusterRoleBindingApi = new KubeApi({
|
||||
kind: ClusterRoleBinding.kind,
|
||||
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings",
|
||||
isNamespaced: false,
|
||||
objectConstructor: ClusterRoleBinding,
|
||||
});
|
||||
@@ -5,11 +5,10 @@ import { KubeApi } from "../kube-api";
|
||||
@autobind()
|
||||
export class ClusterRole extends Role {
|
||||
static kind = "ClusterRole"
|
||||
static namespaced = false
|
||||
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles"
|
||||
}
|
||||
|
||||
export const clusterRoleApi = new KubeApi({
|
||||
kind: ClusterRole.kind,
|
||||
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterroles",
|
||||
isNamespaced: false,
|
||||
objectConstructor: ClusterRole,
|
||||
});
|
||||
@@ -3,6 +3,9 @@ import { KubeObject } from "../kube-object";
|
||||
import { KubeApi } from "../kube-api";
|
||||
|
||||
export class ClusterApi extends KubeApi<Cluster> {
|
||||
static kind = "Cluster"
|
||||
static namespaced = true
|
||||
|
||||
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
|
||||
const nodes = nodeNames.join("|");
|
||||
const opts = { category: "cluster", nodes: nodes }
|
||||
@@ -49,6 +52,7 @@ export interface IClusterMetrics<T = IMetrics> {
|
||||
|
||||
export class Cluster extends KubeObject {
|
||||
static kind = "Cluster";
|
||||
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"
|
||||
|
||||
spec: {
|
||||
clusterNetwork?: {
|
||||
@@ -91,8 +95,5 @@ export class Cluster extends KubeObject {
|
||||
}
|
||||
|
||||
export const clusterApi = new ClusterApi({
|
||||
kind: Cluster.kind,
|
||||
apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Cluster,
|
||||
});
|
||||
@@ -9,6 +9,8 @@ export interface IComponentStatusCondition {
|
||||
|
||||
export class ComponentStatus extends KubeObject {
|
||||
static kind = "ComponentStatus"
|
||||
static namespaced = false
|
||||
static apiBase = "/api/v1/componentstatuses"
|
||||
|
||||
conditions: IComponentStatusCondition[]
|
||||
|
||||
@@ -18,8 +20,5 @@ export class ComponentStatus extends KubeObject {
|
||||
}
|
||||
|
||||
export const componentStatusApi = new KubeApi({
|
||||
kind: ComponentStatus.kind,
|
||||
apiBase: "/api/v1/componentstatuses",
|
||||
isNamespaced: false,
|
||||
objectConstructor: ComponentStatus,
|
||||
});
|
||||
@@ -6,6 +6,8 @@ import { KubeApi } from "../kube-api";
|
||||
@autobind()
|
||||
export class ConfigMap extends KubeObject {
|
||||
static kind = "ConfigMap";
|
||||
static namespaced = true;
|
||||
static apiBase = "/api/v1/configmaps"
|
||||
|
||||
constructor(data: KubeJsonApiData) {
|
||||
super(data);
|
||||
@@ -22,8 +24,5 @@ export class ConfigMap extends KubeObject {
|
||||
}
|
||||
|
||||
export const configMapApi = new KubeApi({
|
||||
kind: ConfigMap.kind,
|
||||
apiBase: "/api/v1/configmaps",
|
||||
isNamespaced: true,
|
||||
objectConstructor: ConfigMap,
|
||||
});
|
||||
@@ -19,6 +19,8 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
||||
|
||||
export class CustomResourceDefinition extends KubeObject {
|
||||
static kind = "CustomResourceDefinition";
|
||||
static namespaced = false;
|
||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"
|
||||
|
||||
spec: {
|
||||
group: string;
|
||||
@@ -145,9 +147,6 @@ export class CustomResourceDefinition extends KubeObject {
|
||||
}
|
||||
|
||||
export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({
|
||||
kind: CustomResourceDefinition.kind,
|
||||
apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions",
|
||||
isNamespaced: false,
|
||||
objectConstructor: CustomResourceDefinition
|
||||
});
|
||||
|
||||
@@ -8,6 +8,8 @@ import { KubeApi } from "../kube-api";
|
||||
@autobind()
|
||||
export class CronJob extends KubeObject {
|
||||
static kind = "CronJob"
|
||||
static namespaced = true
|
||||
static apiBase = "/apis/batch/v1beta1/cronjobs"
|
||||
|
||||
kind: string
|
||||
apiVersion: string
|
||||
@@ -88,8 +90,5 @@ export class CronJob extends KubeObject {
|
||||
}
|
||||
|
||||
export const cronJobApi = new KubeApi({
|
||||
kind: CronJob.kind,
|
||||
apiBase: "/apis/batch/v1beta1/cronjobs",
|
||||
isNamespaced: true,
|
||||
objectConstructor: CronJob,
|
||||
});
|
||||
@@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
|
||||
@autobind()
|
||||
export class DaemonSet extends WorkloadKubeObject {
|
||||
static kind = "DaemonSet"
|
||||
static namespaced = true
|
||||
static apiBase = "/apis/apps/v1/daemonsets"
|
||||
|
||||
spec: {
|
||||
selector: {
|
||||
@@ -69,8 +71,5 @@ export class DaemonSet extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
export const daemonSetApi = new KubeApi({
|
||||
kind: DaemonSet.kind,
|
||||
apiBase: "/apis/apps/v1/daemonsets",
|
||||
isNamespaced: true,
|
||||
objectConstructor: DaemonSet,
|
||||
});
|
||||
@@ -28,6 +28,8 @@ export class DeploymentApi extends KubeApi<Deployment> {
|
||||
@autobind()
|
||||
export class Deployment extends WorkloadKubeObject {
|
||||
static kind = "Deployment"
|
||||
static namespaced = true
|
||||
static apiBase = "/apis/apps/v1/deployments"
|
||||
|
||||
spec: {
|
||||
replicas: number;
|
||||
@@ -164,8 +166,5 @@ export class Deployment extends WorkloadKubeObject {
|
||||
}
|
||||
|
||||
export const deploymentApi = new DeploymentApi({
|
||||
kind: Deployment.kind,
|
||||
apiBase: "/apis/apps/v1/deployments",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Deployment,
|
||||
});
|
||||
@@ -99,6 +99,8 @@ export class EndpointSubset implements IEndpointSubset {
|
||||
@autobind()
|
||||
export class Endpoint extends KubeObject {
|
||||
static kind = "Endpoints"
|
||||
static namespaced = true
|
||||
static apiBase = "/api/v1/endpoints"
|
||||
|
||||
subsets: IEndpointSubset[]
|
||||
|
||||
@@ -118,8 +120,5 @@ export class Endpoint extends KubeObject {
|
||||
}
|
||||
|
||||
export const endpointApi = new KubeApi({
|
||||
kind: Endpoint.kind,
|
||||
apiBase: "/api/v1/endpoints",
|
||||
isNamespaced: true,
|
||||
objectConstructor: Endpoint,
|
||||
});
|
||||
@@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
|
||||
@autobind()
|
||||
export class KubeEvent extends KubeObject {
|
||||
static kind = "Event"
|
||||
static namespaced = true
|
||||
static apiBase = "/api/v1/events"
|
||||
|
||||
involvedObject: {
|
||||
kind: string;
|
||||
@@ -52,8 +54,5 @@ export class KubeEvent extends KubeObject {
|
||||
}
|
||||
|
||||
export const eventApi = new KubeApi({
|
||||
kind: KubeEvent.kind,
|
||||
apiBase: "/api/v1/events",
|
||||
isNamespaced: true,
|
||||
objectConstructor: KubeEvent,
|
||||
})
|
||||
@@ -141,7 +141,7 @@ export class HelmRelease implements ItemObject {
|
||||
chart: string
|
||||
status: string
|
||||
updated: string
|
||||
revision: number
|
||||
revision: string
|
||||
|
||||
getId() {
|
||||
return this.namespace + this.name;
|
||||
@@ -165,7 +165,7 @@ export class HelmRelease implements ItemObject {
|
||||
}
|
||||
|
||||
getRevision() {
|
||||
return this.revision;
|
||||
return parseInt(this.revision, 10);
|
||||
}
|
||||
|
||||
getStatus() {
|
||||
|
||||
@@ -40,6 +40,8 @@ export interface IHpaMetric {
|
||||
|
||||
export class HorizontalPodAutoscaler extends KubeObject {
|
||||
static kind = "HorizontalPodAutoscaler";
|
||||
static namespaced = true;
|
||||
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"
|
||||
|
||||
spec: {
|
||||
scaleTargetRef: {
|
||||
@@ -133,8 +135,5 @@ export class HorizontalPodAutoscaler extends KubeObject {
|
||||
}
|
||||
|
||||
export const hpaApi = new KubeApi({
|
||||
kind: HorizontalPodAutoscaler.kind,
|
||||
apiBase: "/apis/autoscaling/v2beta1/horizontalpodautoscalers",
|
||||
isNamespaced: true,
|
||||
objectConstructor: HorizontalPodAutoscaler,
|
||||
});
|
||||
Loaded 100 of 352 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user