1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Allow extensions to define cluster features (#1125)

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2020-10-26 08:21:22 +02:00 committed by GitHub
parent f3a0059355
commit 62ae7771df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
81 changed files with 4069 additions and 510 deletions

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,24 @@
{
"name": "lens-metrics-cluster-feature",
"version": "0.1.0",
"description": "Lens metrics cluster feature",
"renderer": "dist/renderer.js",
"lens": {
"metadata": {},
"styles": []
},
"scripts": {
"build": "webpack --config webpack.config.js",
"dev": "npm run build --watch"
},
"dependencies": {
"semver": "^7.3.2"
},
"devDependencies": {
"ts-loader": "^8.0.4",
"typescript": "^4.0.3",
"webpack": "^4.44.2",
"mobx": "^5.15.5",
"react": "^16.13.1"
}
}

View File

@ -0,0 +1,25 @@
import { Registry, LensRendererExtension } from "@k8slens/extensions"
import { MetricsFeature } from "./src/metrics-feature"
import React from "react"
export default class ClusterMetricsFeatureExtension extends LensRendererExtension {
registerClusterFeatures(registry: Registry.ClusterFeatureRegistry) {
this.disposers.push(
registry.add({
title: "Metrics Stack",
components: {
Description: () => {
return (
<span>
Enable timeseries data visualization (Prometheus stack) for your cluster.
Install this only if you don't have existing Prometheus stack installed.
You can see preview of manifests <a href="https://github.com/lensapp/lens/tree/master/extensions/lens-metrics/resources" target="_blank">here</a>.
</span>
)
}
},
feature: new MetricsFeature()
})
)
}
}

View File

@ -0,0 +1,96 @@
import { ClusterFeature, Store, K8sApi } from "@k8slens/extensions"
import semver from "semver"
import * as path from "path"
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 ClusterFeature.Feature {
name = "metrics"
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: Store.Cluster): Promise<void> {
// Check if there are storageclasses
const storageClassApi = K8sApi.forCluster(cluster, K8sApi.StorageClass)
const scs = await storageClassApi.list()
this.config.persistence.enabled = scs.some(sc => (
sc.metadata?.annotations?.['storageclass.kubernetes.io/is-default-class'] === 'true' ||
sc.metadata?.annotations?.['storageclass.beta.kubernetes.io/is-default-class'] === 'true'
));
super.applyResources(cluster, super.renderTemplates(path.join(__dirname, "../resources/")))
}
async upgrade(cluster: Store.Cluster): Promise<void> {
return this.install(cluster)
}
async updateStatus(cluster: Store.Cluster): Promise<ClusterFeature.FeatureStatus> {
try {
const statefulSet = K8sApi.forCluster(cluster, K8sApi.StatefulSet)
const prometheus = await statefulSet.get({name: "prometheus", namespace: "lens-metrics"})
if (prometheus?.kind) {
this.status.installed = true;
this.status.currentVersion = prometheus.spec.template.spec.containers[0].image.split(":")[1];
this.status.canUpgrade = semver.lt(this.status.currentVersion, this.latestVersion, true);
} else {
this.status.installed = false
}
} catch(e) {
if (e?.error?.code === 404) {
this.status.installed = false
}
}
return this.status
}
async uninstall(cluster: Store.Cluster): Promise<void> {
const namespaceApi = K8sApi.forCluster(cluster, K8sApi.Namespace)
const clusterRoleBindingApi = K8sApi.forCluster(cluster, K8sApi.ClusterRoleBinding)
const clusterRoleApi = K8sApi.forCluster(cluster, K8sApi.ClusterRole)
await namespaceApi.delete({name: "lens-metrics"})
await clusterRoleBindingApi.delete({name: "lens-prometheus"})
await clusterRoleApi.delete({name: "lens-prometheus"}) }
}

View File

@ -0,0 +1,27 @@
{
"compilerOptions": {
"outDir": "dist",
"module": "CommonJS",
"target": "ES2017",
"lib": ["ESNext", "DOM", "DOM.Iterable"],
"moduleResolution": "Node",
"sourceMap": false,
"declaration": false,
"strict": false,
"noImplicitAny": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"jsx": "react"
},
"include": [
"../../src/extensions/npm/**/*.d.ts",
"./*.ts",
"./*.tsx"
],
"exclude": [
"node_modules",
"*.js"
]
}

View File

@ -0,0 +1,38 @@
const path = require('path');
module.exports = [
{
entry: './renderer.tsx',
context: __dirname,
target: "electron-renderer",
mode: "production",
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
externals: [
{
"@k8slens/extensions": "var global.LensExtensions",
"react": "var global.React",
"mobx": "var global.Mobx"
}
],
resolve: {
extensions: [ '.tsx', '.ts', '.js' ],
},
output: {
libraryTarget: "commonjs2",
globalObject: "this",
filename: 'renderer.js',
path: path.resolve(__dirname, 'dist'),
},
node: {
__dirname: false
}
},
];

View File

@ -0,0 +1,5 @@
install-deps:
npm install
build: install-deps
npm run build

View File

@ -23,7 +23,7 @@
} }
}, },
"include": [ "include": [
"renderer.ts", "renderer.tsx",
"../../src/extensions/npm/**/*.d.ts", "../../src/extensions/npm/**/*.d.ts",
"src/**/*" "src/**/*"
] ]

View File

@ -177,6 +177,7 @@
"telemetry", "telemetry",
"pod-menu", "pod-menu",
"node-menu", "node-menu",
"metrics-cluster-feature",
"support-page" "support-page"
] ]
}, },

View File

@ -2,6 +2,7 @@ import { createIpcChannel } from "./ipc";
import { ClusterId, clusterStore } from "./cluster-store"; import { ClusterId, clusterStore } from "./cluster-store";
import { extensionLoader } from "../extensions/extension-loader" import { extensionLoader } from "../extensions/extension-loader"
import { appEventBus } from "./event-bus" import { appEventBus } from "./event-bus"
import { ResourceApplier } from "../main/resource-applier";
export const clusterIpc = { export const clusterIpc = {
activate: createIpcChannel({ activate: createIpcChannel({
@ -42,32 +43,17 @@ export const clusterIpc = {
}, },
}), }),
installFeature: createIpcChannel({ kubectlApplyAll: createIpcChannel({
channel: "cluster:install-feature", channel: "cluster:kubectl-apply-all",
handle: async (clusterId: ClusterId, feature: string, config?: any) => { handle: (clusterId: ClusterId, resources: string[]) => {
appEventBus.emit({name: "cluster", action: "install", params: { feature: feature}}) appEventBus.emit({name: "cluster", action: "kubectl-apply-all"})
const cluster = clusterStore.getById(clusterId); const cluster = clusterStore.getById(clusterId);
if (cluster) { if (cluster) {
await cluster.installFeature(feature, config) const applier = new ResourceApplier(cluster)
applier.kubectlApplyAll(resources)
} else { } else {
throw `${clusterId} is not a valid cluster id`; throw `${clusterId} is not a valid cluster id`;
} }
} }
}), }),
uninstallFeature: createIpcChannel({
channel: "cluster:uninstall-feature",
handle: (clusterId: ClusterId, feature: string) => {
appEventBus.emit({name: "cluster", action: "uninstall", params: { feature: feature}})
return clusterStore.getById(clusterId)?.uninstallFeature(feature)
}
}),
upgradeFeature: createIpcChannel({
channel: "cluster:upgrade-feature",
handle: (clusterId: ClusterId, feature: string, config?: any) => {
appEventBus.emit({name: "cluster", action: "upgrade", params: { feature: feature}})
return clusterStore.getById(clusterId)?.upgradeFeature(feature, config)
}
}),
} }

View File

@ -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;
}
}

View File

@ -0,0 +1 @@
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"

View File

@ -10,11 +10,13 @@ import * as Store from "./stores"
import * as Util from "./utils" import * as Util from "./utils"
import * as Registry from "../registries" import * as Registry from "../registries"
import * as CommonVars from "../../common/vars"; import * as CommonVars from "../../common/vars";
import * as ClusterFeature from "./cluster-feature"
export let windowManager: WindowManager; export let windowManager: WindowManager;
export { export {
EventBus, EventBus,
ClusterFeature,
Store, Store,
Util, Util,
Registry, Registry,

View File

@ -1 +1,2 @@
export { ExtensionStore } from "../extension-store" export { ExtensionStore } from "../extension-store"
export type { Cluster } from "../../main/cluster"

View File

@ -0,0 +1,19 @@
// Lens-extensions api developer's kit
export type { LensExtensionRuntimeEnv } from "./lens-runtime";
export * from "./lens-main-extension"
export * from "./lens-renderer-extension"
// APIs
import * as EventBus from "./core-api/event-bus"
import * as Store from "./core-api/stores"
import * as Util from "./core-api/utils"
import * as Registry from "./core-api/registries"
import * as ClusterFeature from "./core-api/cluster-feature"
export {
ClusterFeature,
EventBus,
Registry,
Store,
Util
}

View File

@ -6,7 +6,7 @@ import { broadcastIpc } from "../common/ipc"
import { observable, reaction, toJS, } from "mobx" import { observable, reaction, toJS, } from "mobx"
import logger from "../main/logger" import logger from "../main/logger"
import { app, ipcRenderer, remote } from "electron" import { app, ipcRenderer, remote } from "electron"
import { appPreferenceRegistry, kubeObjectMenuRegistry, menuRegistry, pageRegistry, statusBarRegistry } from "./registries"; import { appPreferenceRegistry, kubeObjectMenuRegistry, menuRegistry, pageRegistry, statusBarRegistry, clusterFeatureRegistry } from "./registries";
export interface InstalledExtension extends ExtensionModel { export interface InstalledExtension extends ExtensionModel {
manifestPath: string; manifestPath: string;
@ -46,6 +46,7 @@ export class ExtensionLoader {
this.autoloadExtensions((instance: LensRendererExtension) => { this.autoloadExtensions((instance: LensRendererExtension) => {
instance.registerPages(pageRegistry) instance.registerPages(pageRegistry)
instance.registerAppPreferences(appPreferenceRegistry) instance.registerAppPreferences(appPreferenceRegistry)
instance.registerClusterFeatures(clusterFeatureRegistry)
instance.registerStatusBarIcon(statusBarRegistry) instance.registerStatusBarIcon(statusBarRegistry)
}) })
} }
@ -60,8 +61,8 @@ export class ExtensionLoader {
protected autoloadExtensions(callback: (instance: LensExtension) => void) { protected autoloadExtensions(callback: (instance: LensExtension) => void) {
return reaction(() => this.extensions.toJS(), (installedExtensions) => { return reaction(() => this.extensions.toJS(), (installedExtensions) => {
for (const [id, ext] of installedExtensions) { for(const [id, ext] of installedExtensions) {
let instance = this.instances.get(ext.name) let instance = this.instances.get(ext.id)
if (!instance) { if (!instance) {
const extensionModule = this.requireExtension(ext) const extensionModule = this.requireExtension(ext)
if (!extensionModule) { if (!extensionModule) {
@ -69,9 +70,12 @@ export class ExtensionLoader {
} }
const LensExtensionClass = extensionModule.default; const LensExtensionClass = extensionModule.default;
instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest); instance = new LensExtensionClass({ ...ext.manifest, manifestPath: ext.manifestPath, id: ext.manifestPath }, ext.manifest);
instance.enable(); try {
callback(instance) instance.enable()
this.instances.set(ext.name, instance) callback(instance)
} finally {
this.instances.set(ext.id, instance)
}
} }
} }
}, { }, {

View File

@ -1,5 +1,5 @@
import { LensExtension } from "./lens-extension" import { LensExtension } from "./lens-extension"
import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry } from "./registries" import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
export class LensRendererExtension extends LensExtension { export class LensRendererExtension extends LensExtension {
registerPages(registry: PageRegistry) { registerPages(registry: PageRegistry) {
@ -10,6 +10,10 @@ export class LensRendererExtension extends LensExtension {
return return
} }
registerClusterFeatures(registry: ClusterFeatureRegistry) {
return
}
registerStatusBarIcon(registry: StatusBarRegistry) { registerStatusBarIcon(registry: StatusBarRegistry) {
return return
} }

View File

@ -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()

View File

@ -5,3 +5,4 @@ export * from "./menu-registry"
export * from "./app-preference-registry" export * from "./app-preference-registry"
export * from "./status-bar-registry" export * from "./status-bar-registry"
export * from "./kube-object-menu-registry"; export * from "./kube-object-menu-registry";
export * from "./cluster-feature-registry"

View File

@ -1,6 +1,5 @@
export { apiManager } from "../../renderer/api/api-manager"; export { apiManager } from "../../renderer/api/api-manager";
export { KubeApi } from "../../renderer/api/kube-api"; export { KubeApi, forCluster, IKubeApiCluster } from "../../renderer/api/kube-api";
export { KubeObject } from "../../renderer/api/kube-object"; export { KubeObject } from "../../renderer/api/kube-object";
export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints"; export { Pod, podsApi, IPodContainer, IPodContainerStatus } from "../../renderer/api/endpoints";
export { Node, nodesApi } from "../../renderer/api/endpoints"; export { Node, nodesApi } from "../../renderer/api/endpoints";

View File

@ -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");
}
}

View File

@ -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");
}
}

View File

@ -1,11 +0,0 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: lens-user
rules:
- verbs:
- list
apiGroups:
- ''
resources:
- namespaces

View File

@ -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

View File

@ -49,6 +49,8 @@ export class ClusterManager {
// we need to swap path prefix so that request is proxied to kube api // we need to swap path prefix so that request is proxied to kube api
req.url = req.url.replace(`/${clusterId}`, apiKubePrefix) req.url = req.url.replace(`/${clusterId}`, apiKubePrefix)
} }
} else if (req.headers["x-cluster-id"]) {
cluster = clusterStore.getById(req.headers["x-cluster-id"].toString())
} else { } else {
const clusterId = getClusterIdFromHost(req.headers.host); const clusterId = getClusterIdFromHost(req.headers.host);
cluster = clusterStore.getById(clusterId) cluster = clusterStore.getById(clusterId)

View File

@ -1,7 +1,6 @@
import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store" import type { ClusterId, ClusterMetadata, ClusterModel, ClusterPreferences } from "../common/cluster-store"
import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api"; import type { IMetricsReqParams } from "../renderer/api/endpoints/metrics.api";
import type { WorkspaceId } from "../common/workspace-store"; import type { WorkspaceId } from "../common/workspace-store";
import type { FeatureStatusMap } from "./feature"
import { action, computed, observable, reaction, toJS, when } from "mobx"; import { action, computed, observable, reaction, toJS, when } from "mobx";
import { apiKubePrefix } from "../common/vars"; import { apiKubePrefix } from "../common/vars";
import { broadcastIpc } from "../common/ipc"; import { broadcastIpc } from "../common/ipc";
@ -10,7 +9,6 @@ import { AuthorizationV1Api, CoreV1Api, KubeConfig, V1ResourceAttributes } from
import { Kubectl } from "./kubectl"; import { Kubectl } from "./kubectl";
import { KubeconfigManager } from "./kubeconfig-manager" import { KubeconfigManager } from "./kubeconfig-manager"
import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers" import { getNodeWarningConditions, loadConfig, podHasIssues } from "../common/kube-helpers"
import { getFeatures, installFeature, uninstallFeature, upgradeFeature } from "./feature-manager";
import request, { RequestPromiseOptions } from "request-promise-native" import request, { RequestPromiseOptions } from "request-promise-native"
import { apiResources } from "../common/rbac"; import { apiResources } from "../common/rbac";
import logger from "./logger" import logger from "./logger"
@ -47,7 +45,6 @@ export interface ClusterState extends ClusterModel {
isAdmin: boolean; isAdmin: boolean;
allowedNamespaces: string[] allowedNamespaces: string[]
allowedResources: string[] allowedResources: string[]
features: FeatureStatusMap;
} }
export class Cluster implements ClusterModel { export class Cluster implements ClusterModel {
@ -78,7 +75,6 @@ export class Cluster implements ClusterModel {
@observable eventCount = 0; @observable eventCount = 0;
@observable preferences: ClusterPreferences = {}; @observable preferences: ClusterPreferences = {};
@observable metadata: ClusterMetadata = {}; @observable metadata: ClusterMetadata = {};
@observable features: FeatureStatusMap = {};
@observable allowedNamespaces: string[] = []; @observable allowedNamespaces: string[] = [];
@observable allowedResources: string[] = []; @observable allowedResources: string[] = [];
@ -194,12 +190,7 @@ export class Cluster implements ClusterModel {
await this.whenInitialized; await this.whenInitialized;
await this.refreshConnectionStatus(); await this.refreshConnectionStatus();
if (this.accessible) { if (this.accessible) {
const [features, isAdmin] = await Promise.all([ this.isAdmin = await this.isClusterAdmin();
getFeatures(this),
this.isClusterAdmin(),
]);
this.features = features;
this.isAdmin = isAdmin;
await Promise.all([ await Promise.all([
this.refreshEvents(), this.refreshEvents(),
this.refreshAllowedResources(), this.refreshAllowedResources(),
@ -250,18 +241,6 @@ export class Cluster implements ClusterModel {
return this.kubeconfigManager.getPath() 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> { protected async k8sRequest<T = any>(path: string, options: RequestPromiseOptions = {}): Promise<T> {
const apiUrl = this.kubeProxyUrl + path; const apiUrl = this.kubeProxyUrl + path;
return request(apiUrl, { return request(apiUrl, {
@ -400,7 +379,6 @@ export class Cluster implements ClusterModel {
accessible: this.accessible, accessible: this.accessible,
failureReason: this.failureReason, failureReason: this.failureReason,
isAdmin: this.isAdmin, isAdmin: this.isAdmin,
features: this.features,
eventCount: this.eventCount, eventCount: this.eventCount,
allowedNamespaces: this.allowedNamespaces, allowedNamespaces: this.allowedNamespaces,
allowedResources: this.allowedResources, allowedResources: this.allowedResources,

View File

@ -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)
}

View File

@ -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;
}
}

View File

@ -3,11 +3,10 @@ import { KubeApi } from "../kube-api";
export class ClusterRoleBinding extends RoleBinding { export class ClusterRoleBinding extends RoleBinding {
static kind = "ClusterRoleBinding" static kind = "ClusterRoleBinding"
static namespaced = false
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings"
} }
export const clusterRoleBindingApi = new KubeApi({ export const clusterRoleBindingApi = new KubeApi({
kind: ClusterRoleBinding.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterrolebindings",
isNamespaced: false,
objectConstructor: ClusterRoleBinding, objectConstructor: ClusterRoleBinding,
}); });

View File

@ -5,11 +5,10 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class ClusterRole extends Role { export class ClusterRole extends Role {
static kind = "ClusterRole" static kind = "ClusterRole"
static namespaced = false
static apiBase = "/apis/rbac.authorization.k8s.io/v1/clusterroles"
} }
export const clusterRoleApi = new KubeApi({ export const clusterRoleApi = new KubeApi({
kind: ClusterRole.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/clusterroles",
isNamespaced: false,
objectConstructor: ClusterRole, objectConstructor: ClusterRole,
}); });

View File

@ -3,6 +3,9 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class ClusterApi extends KubeApi<Cluster> { export class ClusterApi extends KubeApi<Cluster> {
static kind = "Cluster"
static namespaced = true
async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> { async getMetrics(nodeNames: string[], params?: IMetricsReqParams): Promise<IClusterMetrics> {
const nodes = nodeNames.join("|"); const nodes = nodeNames.join("|");
const opts = { category: "cluster", nodes: nodes } const opts = { category: "cluster", nodes: nodes }
@ -49,6 +52,7 @@ export interface IClusterMetrics<T = IMetrics> {
export class Cluster extends KubeObject { export class Cluster extends KubeObject {
static kind = "Cluster"; static kind = "Cluster";
static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"
spec: { spec: {
clusterNetwork?: { clusterNetwork?: {
@ -91,8 +95,5 @@ export class Cluster extends KubeObject {
} }
export const clusterApi = new ClusterApi({ export const clusterApi = new ClusterApi({
kind: Cluster.kind,
apiBase: "/apis/cluster.k8s.io/v1alpha1/clusters",
isNamespaced: true,
objectConstructor: Cluster, objectConstructor: Cluster,
}); });

View File

@ -9,6 +9,8 @@ export interface IComponentStatusCondition {
export class ComponentStatus extends KubeObject { export class ComponentStatus extends KubeObject {
static kind = "ComponentStatus" static kind = "ComponentStatus"
static namespaced = false
static apiBase = "/api/v1/componentstatuses"
conditions: IComponentStatusCondition[] conditions: IComponentStatusCondition[]
@ -18,8 +20,5 @@ export class ComponentStatus extends KubeObject {
} }
export const componentStatusApi = new KubeApi({ export const componentStatusApi = new KubeApi({
kind: ComponentStatus.kind,
apiBase: "/api/v1/componentstatuses",
isNamespaced: false,
objectConstructor: ComponentStatus, objectConstructor: ComponentStatus,
}); });

View File

@ -6,6 +6,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class ConfigMap extends KubeObject { export class ConfigMap extends KubeObject {
static kind = "ConfigMap"; static kind = "ConfigMap";
static namespaced = true;
static apiBase = "/api/v1/configmaps"
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
super(data); super(data);
@ -22,8 +24,5 @@ export class ConfigMap extends KubeObject {
} }
export const configMapApi = new KubeApi({ export const configMapApi = new KubeApi({
kind: ConfigMap.kind,
apiBase: "/api/v1/configmaps",
isNamespaced: true,
objectConstructor: ConfigMap, objectConstructor: ConfigMap,
}); });

View File

@ -19,6 +19,8 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
export class CustomResourceDefinition extends KubeObject { export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition"; static kind = "CustomResourceDefinition";
static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"
spec: { spec: {
group: string; group: string;
@ -145,9 +147,6 @@ export class CustomResourceDefinition extends KubeObject {
} }
export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({ export const crdApi = new VersionedKubeApi<CustomResourceDefinition>({
kind: CustomResourceDefinition.kind,
apiBase: "/apis/apiextensions.k8s.io/v1/customresourcedefinitions",
isNamespaced: false,
objectConstructor: CustomResourceDefinition objectConstructor: CustomResourceDefinition
}); });

View File

@ -8,6 +8,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class CronJob extends KubeObject { export class CronJob extends KubeObject {
static kind = "CronJob" static kind = "CronJob"
static namespaced = true
static apiBase = "/apis/batch/v1beta1/cronjobs"
kind: string kind: string
apiVersion: string apiVersion: string
@ -88,8 +90,5 @@ export class CronJob extends KubeObject {
} }
export const cronJobApi = new KubeApi({ export const cronJobApi = new KubeApi({
kind: CronJob.kind,
apiBase: "/apis/batch/v1beta1/cronjobs",
isNamespaced: true,
objectConstructor: CronJob, objectConstructor: CronJob,
}); });

View File

@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class DaemonSet extends WorkloadKubeObject { export class DaemonSet extends WorkloadKubeObject {
static kind = "DaemonSet" static kind = "DaemonSet"
static namespaced = true
static apiBase = "/apis/apps/v1/daemonsets"
spec: { spec: {
selector: { selector: {
@ -69,8 +71,5 @@ export class DaemonSet extends WorkloadKubeObject {
} }
export const daemonSetApi = new KubeApi({ export const daemonSetApi = new KubeApi({
kind: DaemonSet.kind,
apiBase: "/apis/apps/v1/daemonsets",
isNamespaced: true,
objectConstructor: DaemonSet, objectConstructor: DaemonSet,
}); });

View File

@ -28,6 +28,8 @@ export class DeploymentApi extends KubeApi<Deployment> {
@autobind() @autobind()
export class Deployment extends WorkloadKubeObject { export class Deployment extends WorkloadKubeObject {
static kind = "Deployment" static kind = "Deployment"
static namespaced = true
static apiBase = "/apis/apps/v1/deployments"
spec: { spec: {
replicas: number; replicas: number;
@ -164,8 +166,5 @@ export class Deployment extends WorkloadKubeObject {
} }
export const deploymentApi = new DeploymentApi({ export const deploymentApi = new DeploymentApi({
kind: Deployment.kind,
apiBase: "/apis/apps/v1/deployments",
isNamespaced: true,
objectConstructor: Deployment, objectConstructor: Deployment,
}); });

View File

@ -99,6 +99,8 @@ export class EndpointSubset implements IEndpointSubset {
@autobind() @autobind()
export class Endpoint extends KubeObject { export class Endpoint extends KubeObject {
static kind = "Endpoints" static kind = "Endpoints"
static namespaced = true
static apiBase = "/api/v1/endpoints"
subsets: IEndpointSubset[] subsets: IEndpointSubset[]
@ -118,8 +120,5 @@ export class Endpoint extends KubeObject {
} }
export const endpointApi = new KubeApi({ export const endpointApi = new KubeApi({
kind: Endpoint.kind,
apiBase: "/api/v1/endpoints",
isNamespaced: true,
objectConstructor: Endpoint, objectConstructor: Endpoint,
}); });

View File

@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class KubeEvent extends KubeObject { export class KubeEvent extends KubeObject {
static kind = "Event" static kind = "Event"
static namespaced = true
static apiBase = "/api/v1/events"
involvedObject: { involvedObject: {
kind: string; kind: string;
@ -52,8 +54,5 @@ export class KubeEvent extends KubeObject {
} }
export const eventApi = new KubeApi({ export const eventApi = new KubeApi({
kind: KubeEvent.kind,
apiBase: "/api/v1/events",
isNamespaced: true,
objectConstructor: KubeEvent, objectConstructor: KubeEvent,
}) })

View File

@ -40,6 +40,8 @@ export interface IHpaMetric {
export class HorizontalPodAutoscaler extends KubeObject { export class HorizontalPodAutoscaler extends KubeObject {
static kind = "HorizontalPodAutoscaler"; static kind = "HorizontalPodAutoscaler";
static namespaced = true;
static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"
spec: { spec: {
scaleTargetRef: { scaleTargetRef: {
@ -133,8 +135,5 @@ export class HorizontalPodAutoscaler extends KubeObject {
} }
export const hpaApi = new KubeApi({ export const hpaApi = new KubeApi({
kind: HorizontalPodAutoscaler.kind,
apiBase: "/apis/autoscaling/v2beta1/horizontalpodautoscalers",
isNamespaced: true,
objectConstructor: HorizontalPodAutoscaler, objectConstructor: HorizontalPodAutoscaler,
}); });

View File

@ -32,6 +32,8 @@ export interface ILoadBalancerIngress {
@autobind() @autobind()
export class Ingress extends KubeObject { export class Ingress extends KubeObject {
static kind = "Ingress" static kind = "Ingress"
static namespaced = true
static apiBase = "/apis/extensions/v1beta1/ingresses"
spec: { spec: {
tls: { tls: {
@ -110,8 +112,5 @@ export class Ingress extends KubeObject {
} }
export const ingressApi = new IngressApi({ export const ingressApi = new IngressApi({
kind: Ingress.kind,
apiBase: "/apis/extensions/v1beta1/ingresses",
isNamespaced: true,
objectConstructor: Ingress, objectConstructor: Ingress,
}); });

View File

@ -8,6 +8,8 @@ import { JsonApiParams } from "../json-api";
@autobind() @autobind()
export class Job extends WorkloadKubeObject { export class Job extends WorkloadKubeObject {
static kind = "Job" static kind = "Job"
static namespaced = true
static apiBase = "/apis/batch/v1/jobs"
spec: { spec: {
parallelism?: number; parallelism?: number;
@ -102,8 +104,5 @@ export class Job extends WorkloadKubeObject {
} }
export const jobApi = new KubeApi({ export const jobApi = new KubeApi({
kind: Job.kind,
apiBase: "/apis/batch/v1/jobs",
isNamespaced: true,
objectConstructor: Job, objectConstructor: Job,
}); });

View File

@ -10,6 +10,8 @@ export enum NamespaceStatus {
@autobind() @autobind()
export class Namespace extends KubeObject { export class Namespace extends KubeObject {
static kind = "Namespace"; static kind = "Namespace";
static namespaced = false;
static apiBase = "/api/v1/namespaces";
status?: { status?: {
phase: string; phase: string;
@ -21,8 +23,5 @@ export class Namespace extends KubeObject {
} }
export const namespacesApi = new KubeApi({ export const namespacesApi = new KubeApi({
kind: Namespace.kind,
apiBase: "/api/v1/namespaces",
isNamespaced: false,
objectConstructor: Namespace, objectConstructor: Namespace,
}); });

View File

@ -38,6 +38,8 @@ export interface IPolicyEgress {
@autobind() @autobind()
export class NetworkPolicy extends KubeObject { export class NetworkPolicy extends KubeObject {
static kind = "NetworkPolicy" static kind = "NetworkPolicy"
static namespaced = true
static apiBase = "/apis/networking.k8s.io/v1/networkpolicies"
spec: { spec: {
podSelector: { podSelector: {
@ -65,8 +67,5 @@ export class NetworkPolicy extends KubeObject {
} }
export const networkPolicyApi = new KubeApi({ export const networkPolicyApi = new KubeApi({
kind: NetworkPolicy.kind,
apiBase: "/apis/networking.k8s.io/v1/networkpolicies",
isNamespaced: true,
objectConstructor: NetworkPolicy, objectConstructor: NetworkPolicy,
}); });

View File

@ -31,6 +31,8 @@ export interface INodeMetrics<T = IMetrics> {
@autobind() @autobind()
export class Node extends KubeObject { export class Node extends KubeObject {
static kind = "Node" static kind = "Node"
static namespaced = false
static apiBase = "/api/v1/nodes"
spec: { spec: {
podCIDR: string; podCIDR: string;
@ -156,8 +158,5 @@ export class Node extends KubeObject {
} }
export const nodesApi = new NodesApi({ export const nodesApi = new NodesApi({
kind: Node.kind,
apiBase: "/api/v1/nodes",
isNamespaced: false,
objectConstructor: Node, objectConstructor: Node,
}); });

View File

@ -24,6 +24,8 @@ export interface IPvcMetrics<T = IMetrics> {
@autobind() @autobind()
export class PersistentVolumeClaim extends KubeObject { export class PersistentVolumeClaim extends KubeObject {
static kind = "PersistentVolumeClaim" static kind = "PersistentVolumeClaim"
static namespaced = true
static apiBase = "/api/v1/persistentvolumeclaims"
spec: { spec: {
accessModes: string[]; accessModes: string[];
@ -81,8 +83,5 @@ export class PersistentVolumeClaim extends KubeObject {
} }
export const pvcApi = new PersistentVolumeClaimsApi({ export const pvcApi = new PersistentVolumeClaimsApi({
kind: PersistentVolumeClaim.kind,
apiBase: "/api/v1/persistentvolumeclaims",
isNamespaced: true,
objectConstructor: PersistentVolumeClaim, objectConstructor: PersistentVolumeClaim,
}); });

View File

@ -6,6 +6,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class PersistentVolume extends KubeObject { export class PersistentVolume extends KubeObject {
static kind = "PersistentVolume" static kind = "PersistentVolume"
static namespaced = false
static apiBase = "/api/v1/persistentvolumes"
spec: { spec: {
capacity: { capacity: {
@ -64,8 +66,5 @@ export class PersistentVolume extends KubeObject {
} }
export const persistentVolumeApi = new KubeApi({ export const persistentVolumeApi = new KubeApi({
kind: PersistentVolume.kind,
apiBase: "/api/v1/persistentvolumes",
isNamespaced: false,
objectConstructor: PersistentVolume, objectConstructor: PersistentVolume,
}); });

View File

@ -2,6 +2,10 @@ import { KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api"; import { KubeApi } from "../kube-api";
export class PodMetrics extends KubeObject { export class PodMetrics extends KubeObject {
static kind = "Pod"
static namespaced = true
static apiBase = "/apis/metrics.k8s.io/v1beta1/pods"
timestamp: string timestamp: string
window: string window: string
containers: { containers: {
@ -14,8 +18,5 @@ export class PodMetrics extends KubeObject {
} }
export const podMetricsApi = new KubeApi({ export const podMetricsApi = new KubeApi({
kind: PodMetrics.kind,
apiBase: "/apis/metrics.k8s.io/v1beta1/pods",
isNamespaced: true,
objectConstructor: PodMetrics, objectConstructor: PodMetrics,
}); });

View File

@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class PodDisruptionBudget extends KubeObject { export class PodDisruptionBudget extends KubeObject {
static kind = "PodDisruptionBudget"; static kind = "PodDisruptionBudget";
static namespaced = true;
static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets";
spec: { spec: {
minAvailable: string; minAvailable: string;
@ -42,8 +44,5 @@ export class PodDisruptionBudget extends KubeObject {
} }
export const pdbApi = new KubeApi({ export const pdbApi = new KubeApi({
kind: PodDisruptionBudget.kind,
apiBase: "/apis/policy/v1beta1/poddisruptionbudgets",
isNamespaced: true,
objectConstructor: PodDisruptionBudget, objectConstructor: PodDisruptionBudget,
}); });

View File

@ -163,6 +163,8 @@ export interface IPodContainerStatus {
@autobind() @autobind()
export class Pod extends WorkloadKubeObject { export class Pod extends WorkloadKubeObject {
static kind = "Pod" static kind = "Pod"
static namespaced = true
static apiBase = "/api/v1/pods"
spec: { spec: {
volumes?: { volumes?: {
@ -419,8 +421,5 @@ export class Pod extends WorkloadKubeObject {
} }
export const podsApi = new PodsApi({ export const podsApi = new PodsApi({
kind: Pod.kind,
apiBase: "/api/v1/pods",
isNamespaced: true,
objectConstructor: Pod, objectConstructor: Pod,
}); });

View File

@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class PodSecurityPolicy extends KubeObject { export class PodSecurityPolicy extends KubeObject {
static kind = "PodSecurityPolicy" static kind = "PodSecurityPolicy"
static namespaced = false
static apiBase = "/apis/policy/v1beta1/podsecuritypolicies"
spec: { spec: {
allowPrivilegeEscalation?: boolean; allowPrivilegeEscalation?: boolean;
@ -87,8 +89,5 @@ export class PodSecurityPolicy extends KubeObject {
} }
export const pspApi = new KubeApi({ export const pspApi = new KubeApi({
kind: PodSecurityPolicy.kind,
apiBase: "/apis/policy/v1beta1/podsecuritypolicies",
isNamespaced: false,
objectConstructor: PodSecurityPolicy, objectConstructor: PodSecurityPolicy,
}); });

View File

@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class ReplicaSet extends WorkloadKubeObject { export class ReplicaSet extends WorkloadKubeObject {
static kind = "ReplicaSet" static kind = "ReplicaSet"
static namespaced = true
static apiBase = "/apis/apps/v1/replicasets"
spec: { spec: {
replicas?: number; replicas?: number;
@ -51,8 +53,5 @@ export class ReplicaSet extends WorkloadKubeObject {
} }
export const replicaSetApi = new KubeApi({ export const replicaSetApi = new KubeApi({
kind: ReplicaSet.kind,
apiBase: "/apis/apps/v1/replicasets",
isNamespaced: true,
objectConstructor: ReplicaSet, objectConstructor: ReplicaSet,
}); });

View File

@ -32,6 +32,8 @@ export interface IResourceQuotaValues {
export class ResourceQuota extends KubeObject { export class ResourceQuota extends KubeObject {
static kind = "ResourceQuota" static kind = "ResourceQuota"
static namespaced = true
static apiBase = "/api/v1/resourcequotas"
constructor(data: KubeJsonApiData) { constructor(data: KubeJsonApiData) {
super(data); super(data);
@ -61,8 +63,5 @@ export class ResourceQuota extends KubeObject {
} }
export const resourceQuotaApi = new KubeApi({ export const resourceQuotaApi = new KubeApi({
kind: ResourceQuota.kind,
apiBase: "/api/v1/resourcequotas",
isNamespaced: true,
objectConstructor: ResourceQuota, objectConstructor: ResourceQuota,
}); });

View File

@ -12,6 +12,8 @@ export interface IRoleBindingSubject {
@autobind() @autobind()
export class RoleBinding extends KubeObject { export class RoleBinding extends KubeObject {
static kind = "RoleBinding" static kind = "RoleBinding"
static namespaced = true
static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings"
subjects?: IRoleBindingSubject[] subjects?: IRoleBindingSubject[]
roleRef: { roleRef: {
@ -30,8 +32,5 @@ export class RoleBinding extends KubeObject {
} }
export const roleBindingApi = new KubeApi({ export const roleBindingApi = new KubeApi({
kind: RoleBinding.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/rolebindings",
isNamespaced: true,
objectConstructor: RoleBinding, objectConstructor: RoleBinding,
}); });

View File

@ -3,6 +3,8 @@ import { KubeApi } from "../kube-api";
export class Role extends KubeObject { export class Role extends KubeObject {
static kind = "Role" static kind = "Role"
static namespaced = true
static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles"
rules: { rules: {
verbs: string[]; verbs: string[];
@ -17,8 +19,5 @@ export class Role extends KubeObject {
} }
export const roleApi = new KubeApi({ export const roleApi = new KubeApi({
kind: Role.kind,
apiBase: "/apis/rbac.authorization.k8s.io/v1/roles",
isNamespaced: true,
objectConstructor: Role, objectConstructor: Role,
}); });

View File

@ -22,6 +22,8 @@ export interface ISecretRef {
@autobind() @autobind()
export class Secret extends KubeObject { export class Secret extends KubeObject {
static kind = "Secret" static kind = "Secret"
static namespaced = true
static apiBase = "/api/v1/secrets"
type: SecretType; type: SecretType;
data: { data: {
@ -44,8 +46,5 @@ export class Secret extends KubeObject {
} }
export const secretsApi = new KubeApi({ export const secretsApi = new KubeApi({
kind: Secret.kind,
apiBase: "/api/v1/secrets",
isNamespaced: true,
objectConstructor: Secret, objectConstructor: Secret,
}); });

View File

@ -22,6 +22,8 @@ export interface ISelfSubjectReviewRule {
export class SelfSubjectRulesReview extends KubeObject { export class SelfSubjectRulesReview extends KubeObject {
static kind = "SelfSubjectRulesReview" static kind = "SelfSubjectRulesReview"
static namespaced = false
static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"
spec: { spec: {
// todo: add more types from api docs // todo: add more types from api docs
@ -61,8 +63,5 @@ export class SelfSubjectRulesReview extends KubeObject {
} }
export const selfSubjectRulesReviewApi = new SelfSubjectRulesReviewApi({ export const selfSubjectRulesReviewApi = new SelfSubjectRulesReviewApi({
kind: SelfSubjectRulesReview.kind,
apiBase: "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews",
isNamespaced: false,
objectConstructor: SelfSubjectRulesReview, objectConstructor: SelfSubjectRulesReview,
}); });

View File

@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class ServiceAccount extends KubeObject { export class ServiceAccount extends KubeObject {
static kind = "ServiceAccount"; static kind = "ServiceAccount";
static namespaced = true;
static apiBase = "/api/v1/serviceaccounts"
secrets?: { secrets?: {
name: string; name: string;
@ -23,8 +25,5 @@ export class ServiceAccount extends KubeObject {
} }
export const serviceAccountsApi = new KubeApi<ServiceAccount>({ export const serviceAccountsApi = new KubeApi<ServiceAccount>({
kind: ServiceAccount.kind,
apiBase: "/api/v1/serviceaccounts",
isNamespaced: true,
objectConstructor: ServiceAccount, objectConstructor: ServiceAccount,
}); });

View File

@ -32,6 +32,8 @@ export class ServicePort implements IServicePort {
@autobind() @autobind()
export class Service extends KubeObject { export class Service extends KubeObject {
static kind = "Service" static kind = "Service"
static namespaced = true
static apiBase = "/api/v1/services"
spec: { spec: {
type: string; type: string;
@ -93,8 +95,5 @@ export class Service extends KubeObject {
} }
export const serviceApi = new KubeApi({ export const serviceApi = new KubeApi({
kind: Service.kind,
apiBase: "/api/v1/services",
isNamespaced: true,
objectConstructor: Service, objectConstructor: Service,
}); });

View File

@ -7,6 +7,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class StatefulSet extends WorkloadKubeObject { export class StatefulSet extends WorkloadKubeObject {
static kind = "StatefulSet" static kind = "StatefulSet"
static namespaced = true
static apiBase = "/apis/apps/v1/statefulsets"
spec: { spec: {
serviceName: string; serviceName: string;
@ -77,8 +79,5 @@ export class StatefulSet extends WorkloadKubeObject {
} }
export const statefulSetApi = new KubeApi({ export const statefulSetApi = new KubeApi({
kind: StatefulSet.kind,
apiBase: "/apis/apps/v1/statefulsets",
isNamespaced: true,
objectConstructor: StatefulSet, objectConstructor: StatefulSet,
}); });

View File

@ -5,6 +5,8 @@ import { KubeApi } from "../kube-api";
@autobind() @autobind()
export class StorageClass extends KubeObject { export class StorageClass extends KubeObject {
static kind = "StorageClass" static kind = "StorageClass"
static namespaced = false
static apiBase = "/apis/storage.k8s.io/v1/storageclasses"
provisioner: string; // e.g. "storage.k8s.io/v1" provisioner: string; // e.g. "storage.k8s.io/v1"
mountOptions?: string[]; mountOptions?: string[];
@ -32,8 +34,5 @@ export class StorageClass extends KubeObject {
} }
export const storageClassApi = new KubeApi({ export const storageClassApi = new KubeApi({
kind: StorageClass.kind,
apiBase: "/apis/storage.k8s.io/v1/storageclasses",
isNamespaced: false,
objectConstructor: StorageClass, objectConstructor: StorageClass,
}); });

View File

@ -8,13 +8,15 @@ import { apiKube } from "./index";
import { kubeWatchApi } from "./kube-watch-api"; import { kubeWatchApi } from "./kube-watch-api";
import { apiManager } from "./api-manager"; import { apiManager } from "./api-manager";
import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse";
import { apiKubePrefix, isDevelopment } from "../../common/vars";
import * as URL from "url"
export interface IKubeApiOptions<T extends KubeObject> { export interface IKubeApiOptions<T extends KubeObject> {
kind: string; // resource type within api-group, e.g. "Namespace" apiBase?: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
apiBase: string; // base api-path for listing all resources, e.g. "/api/v1/pods"
isNamespaced: boolean;
objectConstructor?: IKubeObjectConstructor<T>; objectConstructor?: IKubeObjectConstructor<T>;
request?: KubeJsonApi; request?: KubeJsonApi;
isNamespaced?: boolean;
kind?: string;
} }
export interface IKubeApiQueryParams { export interface IKubeApiQueryParams {
@ -25,6 +27,25 @@ export interface IKubeApiQueryParams {
continue?: string; // might be used with ?limit from second request continue?: string; // might be used with ?limit from second request
} }
export interface IKubeApiCluster {
id: string;
}
export function forCluster<T extends KubeObject>(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor<T>): KubeApi<T> {
const request = new KubeJsonApi({
apiBase: apiKubePrefix,
debug: isDevelopment,
}, {
headers: {
"X-Cluster-ID": cluster.id
}
});
return new KubeApi({
objectConstructor: kubeClass,
request: request
})
}
export class KubeApi<T extends KubeObject = any> { export class KubeApi<T extends KubeObject = any> {
static parseApi = parseKubeApi; static parseApi = parseKubeApi;
@ -48,11 +69,14 @@ export class KubeApi<T extends KubeObject = any> {
constructor(protected options: IKubeApiOptions<T>) { constructor(protected options: IKubeApiOptions<T>) {
const { const {
kind,
isNamespaced = false,
objectConstructor = KubeObject as IKubeObjectConstructor, objectConstructor = KubeObject as IKubeObjectConstructor,
request = apiKube request = apiKube,
kind = options.objectConstructor?.kind,
isNamespaced = options.objectConstructor?.namespaced
} = options || {}; } = options || {};
if (!options.apiBase) {
options.apiBase = objectConstructor.apiBase
}
const { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, resource } = KubeApi.parseApi(options.apiBase); const { apiBase, apiPrefix, apiGroup, apiVersion, apiVersionWithGroup, resource } = KubeApi.parseApi(options.apiBase);
this.kind = kind; this.kind = kind;
@ -174,4 +198,4 @@ export class KubeApi<T extends KubeObject = any> {
} }
} }
export * from "./kube-api-parse" export * from "./kube-api-parse"

View File

@ -10,6 +10,8 @@ import { resourceApplierApi } from "./endpoints/resource-applier.api";
export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & { export type IKubeObjectConstructor<T extends KubeObject = any> = (new (data: KubeJsonApiData | any) => T) & {
kind?: string; kind?: string;
namespaced?: boolean;
apiBase?: string;
}; };
export interface IKubeObjectMetadata { export interface IKubeObjectMetadata {
@ -43,6 +45,7 @@ export type IKubeMetaField = keyof IKubeObjectMetadata;
@autobind() @autobind()
export class KubeObject implements ItemObject { export class KubeObject implements ItemObject {
static readonly kind: string; static readonly kind: string;
static readonly namespaced: boolean;
static create(data: any) { static create(data: any) {
return new KubeObject(data); return new KubeObject(data);
@ -157,4 +160,4 @@ export class KubeObject implements ItemObject {
delete(params?: JsonApiParams) { delete(params?: JsonApiParams) {
return apiKube.del(this.selfLink, params); return apiKube.del(this.selfLink, params);
} }
} }

View File

@ -1,72 +1,87 @@
import React from "react"; import React from "react";
import { observable, reaction, comparer } from "mobx"; import { observable, reaction, comparer } from "mobx";
import { observer, disposeOnUnmount } from "mobx-react"; import { observer, disposeOnUnmount } from "mobx-react";
import { clusterIpc } from "../../../../common/cluster-ipc";
import { Cluster } from "../../../../main/cluster"; import { Cluster } from "../../../../main/cluster";
import { Button } from "../../button"; import { Button } from "../../button";
import { Notifications } from "../../notifications"; import { Notifications } from "../../notifications";
import { Spinner } from "../../spinner"; import { Spinner } from "../../spinner";
import { ClusterFeature } from "../../../../extensions/cluster-feature";
import { interval } from "../../../utils";
interface Props { interface Props {
cluster: Cluster cluster: Cluster
feature: string feature: ClusterFeature
} }
@observer @observer
export class InstallFeature extends React.Component<Props> { export class InstallFeature extends React.Component<Props> {
@observable loading = false; @observable loading = false;
@observable message = "";
componentDidMount() { componentDidMount() {
const feature = this.props.feature
const cluster = this.props.cluster
const statusUpdate = interval(20, () => {
feature.updateStatus(cluster)
})
statusUpdate.start(true)
disposeOnUnmount(this, () => {
statusUpdate.stop()
})
disposeOnUnmount(this, disposeOnUnmount(this,
reaction(() => this.props.cluster.features[this.props.feature], () => { reaction(() => feature.status.installed, () => {
this.loading = false; this.loading = false;
this.message = ""
}, { equals: comparer.structural }) }, { equals: comparer.structural })
); );
} }
getActionButtons() { getActionButtons() {
const { cluster, feature } = this.props; const { cluster, feature } = this.props;
const features = cluster.features[feature];
const disabled = !cluster.isAdmin || this.loading; const disabled = !cluster.isAdmin || this.loading;
const loadingIcon = this.loading ? <Spinner/> : null; const loadingIcon = this.loading ? <Spinner/> : null;
if (!features) return null;
return ( return (
<div className="flex gaps align-center"> <div className="flex gaps align-center">
{features.canUpgrade && {feature.status.canUpgrade &&
<Button <Button
primary primary
disabled={disabled} disabled={disabled}
onClick={this.runAction(() => onClick={this.runAction(() =>
clusterIpc.upgradeFeature.invokeFromRenderer(cluster.id, feature)) feature.upgrade(cluster)
} )}
> >
Upgrade Upgrade
</Button> </Button>
} }
{features.installed && {feature.status.installed &&
<Button <Button
accent accent
disabled={disabled} disabled={disabled}
onClick={this.runAction(() => onClick={this.runAction(async () => {
clusterIpc.uninstallFeature.invokeFromRenderer(cluster.id, feature)) this.message = "Uninstalling feature ..."
} feature.uninstall(cluster)
})}
> >
Uninstall Uninstall
</Button> </Button>
} }
{!features.installed && !features.canUpgrade && {!feature.status.installed && !feature.status.canUpgrade &&
<Button <Button
primary primary
disabled={disabled} disabled={disabled}
onClick={this.runAction(() => onClick={this.runAction(async () =>{
clusterIpc.installFeature.invokeFromRenderer(cluster.id, feature)) this.message = "Installing feature ..."
} feature.install(cluster)
})}
> >
Install Install
</Button> </Button>
} }
{loadingIcon} {loadingIcon}
{!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>} {!cluster.isAdmin && <span className='admin-note'>Actions can only be performed by admins.</span>}
{cluster.isAdmin && this.loading && this.message !== "" && <span className='admin-note'>{this.message}</span>}
</div> </div>
); );
} }
@ -90,4 +105,4 @@ export class InstallFeature extends React.Component<Props> {
</> </>
); );
} }
} }

View File

@ -2,8 +2,7 @@ import React from "react";
import { Cluster } from "../../../main/cluster"; import { Cluster } from "../../../main/cluster";
import { InstallFeature } from "./components/install-feature"; import { InstallFeature } from "./components/install-feature";
import { SubTitle } from "../layout/sub-title"; import { SubTitle } from "../layout/sub-title";
import { MetricsFeature } from "../../../features/metrics"; import { clusterFeatureRegistry } from "../../../extensions/registries/cluster-feature-registry";
import { UserModeFeature } from "../../../features/user-mode";
interface Props { interface Props {
cluster: Cluster; cluster: Cluster;
@ -12,31 +11,20 @@ interface Props {
export class Features extends React.Component<Props> { export class Features extends React.Component<Props> {
render() { render() {
const { cluster } = this.props; const { cluster } = this.props;
return ( return (
<div> <div>
<h2>Features</h2> <h2>Features</h2>
<InstallFeature cluster={cluster} feature={MetricsFeature.id}> { clusterFeatureRegistry.getItems().map((f) => {
<> return (
<SubTitle title="Metrics"/> <InstallFeature cluster={cluster} feature={f.feature}>
<p> <>
Enable timeseries data visualization (Prometheus stack) for your cluster. <SubTitle title={f.title}/>
Install this only if you don't have existing Prometheus stack installed. <p><f.components.Description /></p>
You can see preview of manifests{" "} </>
<a href="https://github.com/lensapp/lens/tree/master/src/features/metrics" target="_blank">here</a>. </InstallFeature>
</p> )
</> })}
</InstallFeature>
<InstallFeature cluster={cluster} feature={UserModeFeature.id}>
<>
<SubTitle title="User Mode"/>
<p>
User Mode feature enables non-admin users to see namespaces they have access to.{" "}
This is achieved by configuring RBAC rules so that every authenticated user is granted to list namespaces.
</p>
</>
</InstallFeature>
</div> </div>
); );
} }
} }