mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'extensions-api' into feature/telemetry-extension-improvements
This commit is contained in:
commit
835625ee97
@ -4,6 +4,7 @@ module.exports = {
|
|||||||
files: [
|
files: [
|
||||||
"src/renderer/**/*.js",
|
"src/renderer/**/*.js",
|
||||||
"build/**/*.js",
|
"build/**/*.js",
|
||||||
|
"extensions/**/*.js"
|
||||||
],
|
],
|
||||||
extends: [
|
extends: [
|
||||||
'eslint:recommended',
|
'eslint:recommended',
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -12,6 +12,6 @@ binaries/client/
|
|||||||
binaries/server/
|
binaries/server/
|
||||||
src/extensions/*/*.js
|
src/extensions/*/*.js
|
||||||
src/extensions/*/*.d.ts
|
src/extensions/*/*.d.ts
|
||||||
src/extensions/example-extension/src/**
|
|
||||||
types/extension-api.d.ts
|
types/extension-api.d.ts
|
||||||
types/extension-renderer-api.d.ts
|
types/extension-renderer-api.d.ts
|
||||||
|
extensions/*/dist
|
||||||
|
|||||||
@ -10,7 +10,7 @@ export default class ExampleExtension extends LensRendererExtension {
|
|||||||
registerPages(registry: Registry.PageRegistry) {
|
registerPages(registry: Registry.PageRegistry) {
|
||||||
this.disposers.push(
|
this.disposers.push(
|
||||||
registry.add({
|
registry.add({
|
||||||
type: Registry.DynamicPageType.CLUSTER,
|
type: Registry.PageRegistryType.CLUSTER,
|
||||||
path: "/extension-example",
|
path: "/extension-example",
|
||||||
title: "Example Extension",
|
title: "Example Extension",
|
||||||
components: {
|
components: {
|
||||||
|
|||||||
5
extensions/metrics-cluster-feature/Makefile
Normal file
5
extensions/metrics-cluster-feature/Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
3570
extensions/metrics-cluster-feature/package-lock.json
generated
Normal file
3570
extensions/metrics-cluster-feature/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
extensions/metrics-cluster-feature/package.json
Normal file
24
extensions/metrics-cluster-feature/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
extensions/metrics-cluster-feature/renderer.tsx
Normal file
25
extensions/metrics-cluster-feature/renderer.tsx
Normal 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()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
96
extensions/metrics-cluster-feature/src/metrics-feature.ts
Normal file
96
extensions/metrics-cluster-feature/src/metrics-feature.ts
Normal 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"}) }
|
||||||
|
}
|
||||||
27
extensions/metrics-cluster-feature/tsconfig.json
Normal file
27
extensions/metrics-cluster-feature/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
38
extensions/metrics-cluster-feature/webpack.config.js
Normal file
38
extensions/metrics-cluster-feature/webpack.config.js
Normal 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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
5
extensions/node-menu/Makefile
Normal file
5
extensions/node-menu/Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
3508
extensions/node-menu/package-lock.json
generated
Normal file
3508
extensions/node-menu/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
extensions/node-menu/package.json
Normal file
22
extensions/node-menu/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "lens-node-menu",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Lens node menu",
|
||||||
|
"renderer": "dist/renderer.js",
|
||||||
|
"lens": {
|
||||||
|
"metadata": {},
|
||||||
|
"styles": []
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config webpack.config.js",
|
||||||
|
"dev": "npm run build --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-loader": "^8.0.4",
|
||||||
|
"typescript": "^4.0.3",
|
||||||
|
"webpack": "^4.44.2",
|
||||||
|
"mobx": "^5.15.5",
|
||||||
|
"react": "^16.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
21
extensions/node-menu/renderer.tsx
Normal file
21
extensions/node-menu/renderer.tsx
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import { Registry, LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
import React from "react"
|
||||||
|
import { NodeMenu } from "./src/node-menu"
|
||||||
|
|
||||||
|
export default class NodeMenuRendererExtension extends LensRendererExtension {
|
||||||
|
async onActivate() {
|
||||||
|
console.log("node-menu extension activated")
|
||||||
|
}
|
||||||
|
|
||||||
|
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
|
||||||
|
this.disposers.push(
|
||||||
|
registry.add({
|
||||||
|
kind: "Node",
|
||||||
|
apiVersions: ["v1"],
|
||||||
|
components: {
|
||||||
|
MenuItem: (props) => <NodeMenu {...props} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
70
extensions/node-menu/src/node-menu.tsx
Normal file
70
extensions/node-menu/src/node-menu.tsx
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Component, K8sApi, Navigation} from "@k8slens/extensions"
|
||||||
|
|
||||||
|
export function NodeMenu(props: Component.KubeObjectMenuProps<K8sApi.Node>) {
|
||||||
|
const { object: node, toolbar } = props;
|
||||||
|
if (!node) return null;
|
||||||
|
const nodeName = node.getName();
|
||||||
|
|
||||||
|
const sendToTerminal = (command: string) => {
|
||||||
|
Component.terminalStore.sendCommand(command, {
|
||||||
|
enter: true,
|
||||||
|
newTab: true,
|
||||||
|
});
|
||||||
|
Navigation.hideDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
const shell = () => {
|
||||||
|
Component.createTerminalTab({
|
||||||
|
title: `Node: ${nodeName}`,
|
||||||
|
node: nodeName,
|
||||||
|
});
|
||||||
|
Navigation.hideDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
const cordon = () => {
|
||||||
|
sendToTerminal(`kubectl cordon ${nodeName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const unCordon = () => {
|
||||||
|
sendToTerminal(`kubectl uncordon ${nodeName}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const drain = () => {
|
||||||
|
const command = `kubectl drain ${nodeName} --delete-local-data --ignore-daemonsets --force`;
|
||||||
|
Component.ConfirmDialog.open({
|
||||||
|
ok: () => sendToTerminal(command),
|
||||||
|
labelOk: `Drain Node`,
|
||||||
|
message: (
|
||||||
|
<p>
|
||||||
|
Are you sure you want to drain <b>{nodeName}</b>?
|
||||||
|
</p>
|
||||||
|
),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Component.MenuItem onClick={shell}>
|
||||||
|
<Component.Icon svg="ssh" interactive={toolbar} title="Node shell"/>
|
||||||
|
<span className="title">Shell</span>
|
||||||
|
</Component.MenuItem>
|
||||||
|
{!node.isUnschedulable() && (
|
||||||
|
<Component.MenuItem onClick={cordon}>
|
||||||
|
<Component.Icon material="pause_circle_filled" title="Cordon" interactive={toolbar}/>
|
||||||
|
<span className="title">Cordon</span>
|
||||||
|
</Component.MenuItem>
|
||||||
|
)}
|
||||||
|
{node.isUnschedulable() && (
|
||||||
|
<Component.MenuItem onClick={unCordon}>
|
||||||
|
<Component.Icon material="play_circle_filled" title="Uncordon" interactive={toolbar}/>
|
||||||
|
<span className="title">Uncordon</span>
|
||||||
|
</Component.MenuItem>
|
||||||
|
)}
|
||||||
|
<Component.MenuItem onClick={drain}>
|
||||||
|
<Component.Icon material="delete_sweep" title="Drain" interactive={toolbar}/>
|
||||||
|
<span className="title">Drain</span>
|
||||||
|
</Component.MenuItem>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
27
extensions/node-menu/tsconfig.json
Normal file
27
extensions/node-menu/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
35
extensions/node-menu/webpack.config.js
Normal file
35
extensions/node-menu/webpack.config.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
5
extensions/pod-menu/Makefile
Normal file
5
extensions/pod-menu/Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
3508
extensions/pod-menu/package-lock.json
generated
Normal file
3508
extensions/pod-menu/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
22
extensions/pod-menu/package.json
Normal file
22
extensions/pod-menu/package.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"name": "lens-pod-menu",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Lens pod menu",
|
||||||
|
"renderer": "dist/renderer.js",
|
||||||
|
"lens": {
|
||||||
|
"metadata": {},
|
||||||
|
"styles": []
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack --config webpack.config.js",
|
||||||
|
"dev": "npm run build --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"ts-loader": "^8.0.4",
|
||||||
|
"typescript": "^4.0.3",
|
||||||
|
"webpack": "^4.44.2",
|
||||||
|
"mobx": "^5.15.5",
|
||||||
|
"react": "^16.13.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
31
extensions/pod-menu/renderer.tsx
Normal file
31
extensions/pod-menu/renderer.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import { Registry, LensRendererExtension } from "@k8slens/extensions";
|
||||||
|
import { PodShellMenu } from "./src/shell-menu"
|
||||||
|
import { PodLogsMenu } from "./src/logs-menu"
|
||||||
|
import React from "react"
|
||||||
|
|
||||||
|
export default class PodMenuRendererExtension extends LensRendererExtension {
|
||||||
|
async onActivate() {
|
||||||
|
console.log("pod-menu extension activated")
|
||||||
|
}
|
||||||
|
|
||||||
|
registerKubeObjectMenus(registry: Registry.KubeObjectMenuRegistry) {
|
||||||
|
this.disposers.push(
|
||||||
|
registry.add({
|
||||||
|
kind: "Pod",
|
||||||
|
apiVersions: ["v1"],
|
||||||
|
components: {
|
||||||
|
MenuItem: (props) => <PodShellMenu {...props} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
this.disposers.push(
|
||||||
|
registry.add({
|
||||||
|
kind: "Pod",
|
||||||
|
apiVersions: ["v1"],
|
||||||
|
components: {
|
||||||
|
MenuItem: (props) => <PodLogsMenu {...props} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
58
extensions/pod-menu/src/logs-menu.tsx
Normal file
58
extensions/pod-menu/src/logs-menu.tsx
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PodLogsMenu extends React.Component<Props> {
|
||||||
|
showLogs(container: K8sApi.IPodContainer) {
|
||||||
|
Navigation.hideDetails();
|
||||||
|
const pod = this.props.object;
|
||||||
|
Component.createPodLogsTab({
|
||||||
|
pod,
|
||||||
|
containers: pod.getContainers(),
|
||||||
|
initContainers: pod.getInitContainers(),
|
||||||
|
selectedContainer: container,
|
||||||
|
showTimestamps: false,
|
||||||
|
previous: false,
|
||||||
|
tailLines: 1000
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { object: pod, toolbar } = this.props
|
||||||
|
const containers = pod.getAllContainers();
|
||||||
|
const statuses = pod.getContainerStatuses();
|
||||||
|
if (!containers.length) return;
|
||||||
|
return (
|
||||||
|
<Component.MenuItem onClick={Util.prevDefault(() => this.showLogs(containers[0]))}>
|
||||||
|
<Component.Icon material="subject" title="Logs" interactive={toolbar}/>
|
||||||
|
<span className="title">Logs</span>
|
||||||
|
{containers.length > 1 && (
|
||||||
|
<>
|
||||||
|
<Component.Icon className="arrow" material="keyboard_arrow_right"/>
|
||||||
|
<Component.SubMenu>
|
||||||
|
{
|
||||||
|
containers.map(container => {
|
||||||
|
const { name } = container
|
||||||
|
const status = statuses.find(status => status.name === name);
|
||||||
|
const brick = status ? (
|
||||||
|
<Component.StatusBrick
|
||||||
|
className={Util.cssNames(Object.keys(status.state)[0], { ready: status.ready })}
|
||||||
|
/>
|
||||||
|
) : null
|
||||||
|
return (
|
||||||
|
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.showLogs(container))} className="flex align-center">
|
||||||
|
{brick}
|
||||||
|
{name}
|
||||||
|
</Component.MenuItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Component.SubMenu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Component.MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
64
extensions/pod-menu/src/shell-menu.tsx
Normal file
64
extensions/pod-menu/src/shell-menu.tsx
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { Component, K8sApi, Util, Navigation } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
interface Props extends Component.KubeObjectMenuProps<K8sApi.Pod> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PodShellMenu extends React.Component<Props> {
|
||||||
|
async execShell(container?: string) {
|
||||||
|
Navigation.hideDetails();
|
||||||
|
const { object: pod } = this.props
|
||||||
|
const containerParam = container ? `-c ${container}` : ""
|
||||||
|
let command = `kubectl exec -i -t -n ${pod.getNs()} ${pod.getName()} ${containerParam} "--"`
|
||||||
|
if (window.navigator.platform !== "Win32") {
|
||||||
|
command = `exec ${command}`
|
||||||
|
}
|
||||||
|
if (pod.getSelectedNodeOs() === "windows") {
|
||||||
|
command = `${command} powershell`
|
||||||
|
} else {
|
||||||
|
command = `${command} sh -c "clear; (bash || ash || sh)"`
|
||||||
|
}
|
||||||
|
|
||||||
|
const shell = Component.createTerminalTab({
|
||||||
|
title: `Pod: ${pod.getName()} (namespace: ${pod.getNs()})`
|
||||||
|
});
|
||||||
|
|
||||||
|
Component.terminalStore.sendCommand(command, {
|
||||||
|
enter: true,
|
||||||
|
tabId: shell.id
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { object, toolbar } = this.props
|
||||||
|
console.log(Object.keys(object))
|
||||||
|
const containers = object.getRunningContainers();
|
||||||
|
if (!containers.length) return;
|
||||||
|
return (
|
||||||
|
<Component.MenuItem onClick={Util.prevDefault(() => this.execShell(containers[0].name))}>
|
||||||
|
<Component.Icon svg="ssh" interactive={toolbar} title="Pod shell"/>
|
||||||
|
<span className="title">Shell</span>
|
||||||
|
{containers.length > 1 && (
|
||||||
|
<>
|
||||||
|
<Component.Icon className="arrow" material="keyboard_arrow_right"/>
|
||||||
|
<Component.SubMenu>
|
||||||
|
{
|
||||||
|
containers.map(container => {
|
||||||
|
const { name } = container;
|
||||||
|
return (
|
||||||
|
<Component.MenuItem key={name} onClick={Util.prevDefault(() => this.execShell(name))} className="flex align-center">
|
||||||
|
<Component.StatusBrick/>
|
||||||
|
{name}
|
||||||
|
</Component.MenuItem>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</Component.SubMenu>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Component.MenuItem>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
extensions/pod-menu/tsconfig.json
Normal file
27
extensions/pod-menu/tsconfig.json
Normal 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"
|
||||||
|
]
|
||||||
|
}
|
||||||
35
extensions/pod-menu/webpack.config.js
Normal file
35
extensions/pod-menu/webpack.config.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
5
extensions/support-page/Makefile
Normal file
5
extensions/support-page/Makefile
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
23
extensions/support-page/main.ts
Normal file
23
extensions/support-page/main.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import { LensMainExtension, Registry, windowManager } from "@k8slens/extensions";
|
||||||
|
import { supportPageURL } from "./src/support.route";
|
||||||
|
|
||||||
|
export default class SupportPageMainExtension extends LensMainExtension {
|
||||||
|
async onActivate() {
|
||||||
|
console.log("support page extension activated")
|
||||||
|
}
|
||||||
|
|
||||||
|
async registerAppMenus(registry: Registry.MenuRegistry) {
|
||||||
|
this.disposers.push(
|
||||||
|
registry.add({
|
||||||
|
parentId: "help",
|
||||||
|
label: "Support",
|
||||||
|
click() {
|
||||||
|
windowManager.navigate({
|
||||||
|
channel: "menu:navigate", // fixme: use windowManager.ensureMainWindow from Tray's PR
|
||||||
|
url: supportPageURL(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
3623
extensions/support-page/package-lock.json
generated
Normal file
3623
extensions/support-page/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
extensions/support-page/package.json
Normal file
24
extensions/support-page/package.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "lens-support-page",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"description": "Lens support page",
|
||||||
|
"main": "dist/main.js",
|
||||||
|
"renderer": "dist/renderer.js",
|
||||||
|
"scripts": {
|
||||||
|
"build": "webpack -p",
|
||||||
|
"dev": "webpack --watch"
|
||||||
|
},
|
||||||
|
"dependencies": {},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^14.11.11",
|
||||||
|
"@types/react": "^16.9.53",
|
||||||
|
"@types/react-router": "^5.1.8",
|
||||||
|
"@types/webpack": "^4.41.17",
|
||||||
|
"mobx": "^5.15.5",
|
||||||
|
"react": "^16.13.1",
|
||||||
|
"ts-loader": "^8.0.4",
|
||||||
|
"ts-node": "^9.0.0",
|
||||||
|
"typescript": "^4.0.3",
|
||||||
|
"webpack": "^4.44.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
37
extensions/support-page/renderer.tsx
Normal file
37
extensions/support-page/renderer.tsx
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Component, LensRendererExtension, Navigation, Registry } from "@k8slens/extensions";
|
||||||
|
import { supportPageRoute, supportPageURL } from "./src/support.route";
|
||||||
|
import { Support } from "./src/support";
|
||||||
|
|
||||||
|
export default class SupportPageRendererExtension extends LensRendererExtension {
|
||||||
|
async onActivate() {
|
||||||
|
console.log("support page extension activated")
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPages(registry: Registry.PageRegistry) {
|
||||||
|
this.disposers.push(
|
||||||
|
registry.add({
|
||||||
|
...supportPageRoute,
|
||||||
|
type: Registry.PageRegistryType.GLOBAL,
|
||||||
|
url: supportPageURL(),
|
||||||
|
components: {
|
||||||
|
Page: Support,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
registerStatusBarIcon(registry: Registry.StatusBarRegistry) {
|
||||||
|
this.disposers.push(
|
||||||
|
registry.add({
|
||||||
|
icon: (
|
||||||
|
<Component.Icon
|
||||||
|
material="support"
|
||||||
|
tooltip="Support"
|
||||||
|
onClick={() => Navigation.navigate(supportPageURL())}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
extensions/support-page/src/support.route.ts
Normal file
7
extensions/support-page/src/support.route.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import type { RouteProps } from "react-router";
|
||||||
|
|
||||||
|
export const supportPageRoute: RouteProps = {
|
||||||
|
path: "/support"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportPageURL = () => supportPageRoute.path.toString();
|
||||||
29
extensions/support-page/src/support.tsx
Normal file
29
extensions/support-page/src/support.tsx
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
// TODO: figure out how to consume styles / handle import "./support.scss"
|
||||||
|
// TODO: support localization / figure out how to extract / consume i18n strings
|
||||||
|
|
||||||
|
import React from "react"
|
||||||
|
import { observer } from "mobx-react"
|
||||||
|
import { CommonVars, Component } from "@k8slens/extensions";
|
||||||
|
|
||||||
|
@observer
|
||||||
|
export class Support extends React.Component {
|
||||||
|
render() {
|
||||||
|
const { PageLayout } = Component;
|
||||||
|
const { slackUrl, issuesTrackerUrl } = CommonVars;
|
||||||
|
return (
|
||||||
|
<PageLayout showOnTop className="Support" header={<h2>Support</h2>}>
|
||||||
|
<h2>Community Slack Channel</h2>
|
||||||
|
<p>
|
||||||
|
Ask a question, see what's being discussed, join the conversation <a href={slackUrl} target="_blank">here</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h2>Report an Issue</h2>
|
||||||
|
<p>
|
||||||
|
Review existing issues or open a new one <a href={issuesTrackerUrl} target="_blank">here</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/*<h2><Trans>Commercial Support</Trans></h2>*/}
|
||||||
|
</PageLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
30
extensions/support-page/tsconfig.json
Normal file
30
extensions/support-page/tsconfig.json
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"outDir": "dist",
|
||||||
|
"baseUrl": ".",
|
||||||
|
"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",
|
||||||
|
"paths": {
|
||||||
|
"*": [
|
||||||
|
"node_modules/*",
|
||||||
|
"../../types/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": [
|
||||||
|
"renderer.tsx",
|
||||||
|
"../../src/extensions/npm/**/*.d.ts",
|
||||||
|
"src/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
68
extensions/support-page/webpack.config.ts
Normal file
68
extensions/support-page/webpack.config.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import path from "path"
|
||||||
|
|
||||||
|
const outputPath = path.resolve(__dirname, 'dist');
|
||||||
|
|
||||||
|
// TODO: figure out how to share base TS and Webpack configs from Lens (npm, filesystem, etc?)
|
||||||
|
const lensExternals = {
|
||||||
|
"@k8slens/extensions": "var global.LensExtensions",
|
||||||
|
"react": "var global.React",
|
||||||
|
"mobx": "var global.Mobx",
|
||||||
|
"mobx-react": "var global.MobxReact",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
entry: './main.ts',
|
||||||
|
context: __dirname,
|
||||||
|
target: "electron-main",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
lensExternals,
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
libraryTarget: "commonjs2",
|
||||||
|
globalObject: "this",
|
||||||
|
filename: 'main.js',
|
||||||
|
path: outputPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entry: './renderer.tsx',
|
||||||
|
context: __dirname,
|
||||||
|
target: "electron-renderer",
|
||||||
|
mode: "production",
|
||||||
|
module: {
|
||||||
|
rules: [
|
||||||
|
{
|
||||||
|
test: /\.tsx?$/,
|
||||||
|
use: 'ts-loader',
|
||||||
|
exclude: /node_modules/,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
externals: [
|
||||||
|
lensExternals,
|
||||||
|
],
|
||||||
|
resolve: {
|
||||||
|
extensions: ['.tsx', '.ts', '.js'],
|
||||||
|
},
|
||||||
|
output: {
|
||||||
|
libraryTarget: "commonjs2",
|
||||||
|
globalObject: "this",
|
||||||
|
filename: 'renderer.js',
|
||||||
|
path: outputPath,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -9,8 +9,8 @@
|
|||||||
"styles": []
|
"styles": []
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "webpack --config webpack.config.js",
|
"build": "webpack -p",
|
||||||
"dev": "npm run build --watch"
|
"dev": "webpack --watch"
|
||||||
},
|
},
|
||||||
"dependencies": {},
|
"dependencies": {},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -26,5 +26,5 @@
|
|||||||
"renderer.ts",
|
"renderer.ts",
|
||||||
"../../src/extensions/npm/**/*.d.ts",
|
"../../src/extensions/npm/**/*.d.ts",
|
||||||
"src/**/*"
|
"src/**/*"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@ -174,7 +174,11 @@
|
|||||||
},
|
},
|
||||||
"lens": {
|
"lens": {
|
||||||
"extensions": [
|
"extensions": [
|
||||||
"telemetry"
|
"telemetry",
|
||||||
|
"pod-menu",
|
||||||
|
"node-menu",
|
||||||
|
"metrics-cluster-feature",
|
||||||
|
"support-page"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -312,7 +316,6 @@
|
|||||||
"jest": "^26.0.1",
|
"jest": "^26.0.1",
|
||||||
"jest-mock-extended": "^1.0.10",
|
"jest-mock-extended": "^1.0.10",
|
||||||
"make-plural": "^6.2.1",
|
"make-plural": "^6.2.1",
|
||||||
"material-design-icons": "^3.0.1",
|
|
||||||
"mini-css-extract-plugin": "^0.9.0",
|
"mini-css-extract-plugin": "^0.9.0",
|
||||||
"mobx-react": "^6.2.2",
|
"mobx-react": "^6.2.2",
|
||||||
"moment": "^2.26.0",
|
"moment": "^2.26.0",
|
||||||
|
|||||||
@ -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)
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,11 +23,6 @@ export const rendererDir = path.join(contextDir, "src/renderer");
|
|||||||
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
export const htmlTemplate = path.resolve(rendererDir, "template.html");
|
||||||
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
export const sassCommonVars = path.resolve(rendererDir, "components/vars.scss");
|
||||||
|
|
||||||
// Extensions
|
|
||||||
export const extensionsLibName = `${appName}-extensions.api`
|
|
||||||
export const extensionsRendererLibName = `${appName}-extensions-renderer.api`
|
|
||||||
export const extensionsDir = path.join(contextDir, "src/extensions");
|
|
||||||
|
|
||||||
// Special runtime paths
|
// Special runtime paths
|
||||||
defineGlobal("__static", {
|
defineGlobal("__static", {
|
||||||
get() {
|
get() {
|
||||||
|
|||||||
@ -1,27 +0,0 @@
|
|||||||
import { observable } from "mobx"
|
|
||||||
import React from "react"
|
|
||||||
|
|
||||||
export interface AppPreferenceComponents {
|
|
||||||
Hint: React.ComponentType<any>;
|
|
||||||
Input: React.ComponentType<any>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AppPreferenceRegistration {
|
|
||||||
title: string;
|
|
||||||
components: AppPreferenceComponents;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class AppPreferenceRegistry {
|
|
||||||
preferences = observable.array<AppPreferenceRegistration>([], { deep: false });
|
|
||||||
|
|
||||||
add(preference: AppPreferenceRegistration) {
|
|
||||||
this.preferences.push(preference)
|
|
||||||
return () => {
|
|
||||||
this.preferences.replace(
|
|
||||||
this.preferences.filter(c => c !== preference)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const appPreferenceRegistry = new AppPreferenceRegistry()
|
|
||||||
62
src/extensions/cluster-feature.ts
Normal file
62
src/extensions/cluster-feature.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
1
src/extensions/core-api/cluster-feature.ts
Normal file
1
src/extensions/core-api/cluster-feature.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
|
||||||
24
src/extensions/core-api/index.ts
Normal file
24
src/extensions/core-api/index.ts
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
// 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 EventBus from "./event-bus"
|
||||||
|
import * as Store from "./stores"
|
||||||
|
import * as Util from "./utils"
|
||||||
|
import * as Registry from "../registries"
|
||||||
|
import * as CommonVars from "../../common/vars";
|
||||||
|
import * as ClusterFeature from "./cluster-feature"
|
||||||
|
|
||||||
|
export let windowManager: WindowManager;
|
||||||
|
|
||||||
|
export {
|
||||||
|
EventBus,
|
||||||
|
ClusterFeature,
|
||||||
|
Store,
|
||||||
|
Util,
|
||||||
|
Registry,
|
||||||
|
CommonVars,
|
||||||
|
}
|
||||||
@ -1,2 +0,0 @@
|
|||||||
export type { DynamicPageType, PageRegistry } from "../page-registry"
|
|
||||||
export type { AppPreferenceRegistry } from "../app-preference-registry"
|
|
||||||
@ -1,3 +1,4 @@
|
|||||||
export { ExtensionStore } from "../extension-store"
|
export { ExtensionStore } from "../extension-store"
|
||||||
export { clusterStore, ClusterModel } from "../../common/cluster-store"
|
export { clusterStore, ClusterModel } from "../../common/cluster-store"
|
||||||
export { workspaceStore} from "../../common/workspace-store"
|
export { workspaceStore} from "../../common/workspace-store"
|
||||||
|
export type { Cluster } from "../../main/cluster"
|
||||||
|
|||||||
@ -1 +1,3 @@
|
|||||||
export { Singleton } from "../../common/utils"
|
export { Singleton } from "../../common/utils"
|
||||||
|
export { prevDefault, stopPropagation } from "../../renderer/utils/prevDefault"
|
||||||
|
export { cssNames } from "../../renderer/utils/cssNames"
|
||||||
|
|||||||
@ -9,9 +9,11 @@ import * as Store from "./core-api/stores"
|
|||||||
import * as Util from "./core-api/utils"
|
import * as Util from "./core-api/utils"
|
||||||
import * as App from "./core-api/app"
|
import * as App from "./core-api/app"
|
||||||
import * as Registry from "./core-api/registries"
|
import * as Registry from "./core-api/registries"
|
||||||
|
import * as ClusterFeature from "./core-api/cluster-feature"
|
||||||
|
|
||||||
export {
|
export {
|
||||||
App,
|
App,
|
||||||
|
ClusterFeature,
|
||||||
EventBus,
|
EventBus,
|
||||||
Registry,
|
Registry,
|
||||||
Store,
|
Store,
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { cssNames } from "../renderer/utils";
|
import { cssNames } from "../renderer/utils";
|
||||||
import { TabLayout } from "../renderer/components/layout/tab-layout";
|
import { TabLayout } from "../renderer/components/layout/tab-layout";
|
||||||
import { PageRegistration } from "./page-registry"
|
import { PageRegistration } from "./registries/page-registry"
|
||||||
|
|
||||||
export class DynamicPage extends React.Component<{ page: PageRegistration }> {
|
export class DynamicPage extends React.Component<{ page: PageRegistration }> {
|
||||||
render() {
|
render() {
|
||||||
|
|||||||
@ -1,2 +1,4 @@
|
|||||||
export * from "./core-extension-api"
|
// Extension-api types generation bundle (used by rollup.js)
|
||||||
export * from "./renderer-extension-api"
|
|
||||||
|
export * from "./core-api"
|
||||||
|
export * from "./renderer-api"
|
||||||
|
|||||||
@ -1,13 +1,12 @@
|
|||||||
import type { ExtensionId, LensExtension, ExtensionManifest, ExtensionModel } from "./lens-extension"
|
import type { ExtensionId, ExtensionManifest, ExtensionModel, LensExtension } from "./lens-extension"
|
||||||
|
import type { LensMainExtension } from "./lens-main-extension"
|
||||||
import type { LensRendererExtension } from "./lens-renderer-extension"
|
import type { LensRendererExtension } from "./lens-renderer-extension"
|
||||||
import { broadcastIpc } from "../common/ipc"
|
|
||||||
import type { LensExtensionRuntimeEnv } from "./lens-runtime"
|
|
||||||
import path from "path"
|
import path from "path"
|
||||||
|
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, remote, ipcRenderer } from "electron"
|
import { app, ipcRenderer, remote } from "electron"
|
||||||
import { pageRegistry } from "./page-registry";
|
import { appPreferenceRegistry, kubeObjectMenuRegistry, menuRegistry, pageRegistry, statusBarRegistry, clusterFeatureRegistry } from "./registries";
|
||||||
import { appPreferenceRegistry } from "./app-preference-registry"
|
|
||||||
|
|
||||||
export interface InstalledExtension extends ExtensionModel {
|
export interface InstalledExtension extends ExtensionModel {
|
||||||
manifestPath: string;
|
manifestPath: string;
|
||||||
@ -35,32 +34,35 @@ export class ExtensionLoader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnClusterRenderer(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
|
loadOnMain() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer')
|
logger.info('[EXTENSIONS-LOADER]: load on main')
|
||||||
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensRendererExtension) => {
|
this.autoloadExtensions((instance: LensMainExtension) => {
|
||||||
instance.registerPages(pageRegistry)
|
instance.registerAppMenus(menuRegistry);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnMainRenderer(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
|
loadOnClusterManagerRenderer() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on main renderer')
|
logger.info('[EXTENSIONS-LOADER]: load on main renderer (cluster manager)')
|
||||||
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensRendererExtension) => {
|
this.autoloadExtensions((instance: LensRendererExtension) => {
|
||||||
instance.registerPages(pageRegistry)
|
instance.registerPages(pageRegistry)
|
||||||
instance.registerAppPreferences(appPreferenceRegistry)
|
instance.registerAppPreferences(appPreferenceRegistry)
|
||||||
|
instance.registerClusterFeatures(clusterFeatureRegistry)
|
||||||
|
instance.registerStatusBarIcon(statusBarRegistry)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
loadOnMain(getLensRuntimeEnv: () => LensExtensionRuntimeEnv) {
|
loadOnClusterRenderer() {
|
||||||
logger.info('[EXTENSIONS-LOADER]: load on main')
|
logger.info('[EXTENSIONS-LOADER]: load on cluster renderer (dashboard)')
|
||||||
this.autoloadExtensions(getLensRuntimeEnv, (instance: LensExtension) => {
|
this.autoloadExtensions((instance: LensRendererExtension) => {
|
||||||
// todo
|
instance.registerPages(pageRegistry)
|
||||||
|
instance.registerKubeObjectMenus(kubeObjectMenuRegistry)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
protected autoloadExtensions(getLensRuntimeEnv: () => LensExtensionRuntimeEnv, 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.manifestPath)
|
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) {
|
||||||
@ -68,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(getLensRuntimeEnv())
|
try {
|
||||||
callback(instance)
|
instance.enable()
|
||||||
this.instances.set(ext.id, instance)
|
callback(instance)
|
||||||
|
} finally {
|
||||||
|
this.instances.set(ext.id, instance)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, {
|
}, {
|
||||||
@ -104,7 +109,9 @@ export class ExtensionLoader {
|
|||||||
const extension = this.getById(id);
|
const extension = this.getById(id);
|
||||||
if (extension) {
|
if (extension) {
|
||||||
const instance = this.instances.get(extension.id)
|
const instance = this.instances.get(extension.id)
|
||||||
if (instance) { await instance.disable() }
|
if (instance) {
|
||||||
|
await instance.disable()
|
||||||
|
}
|
||||||
this.extensions.delete(id);
|
this.extensions.delete(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import type { LensExtensionRuntimeEnv } from "./lens-runtime";
|
|
||||||
import { readJsonSync } from "fs-extra";
|
import { readJsonSync } from "fs-extra";
|
||||||
import { action, observable, toJS } from "mobx";
|
import { action, observable, toJS } from "mobx";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
@ -34,7 +33,6 @@ export class LensExtension implements ExtensionModel {
|
|||||||
@observable manifest: ExtensionManifest;
|
@observable manifest: ExtensionManifest;
|
||||||
@observable manifestPath: string;
|
@observable manifestPath: string;
|
||||||
@observable isEnabled = false;
|
@observable isEnabled = false;
|
||||||
@observable.ref runtime: LensExtensionRuntimeEnv;
|
|
||||||
|
|
||||||
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
|
constructor(model: ExtensionModel, manifest: ExtensionManifest) {
|
||||||
this.importModel(model, manifest);
|
this.importModel(model, manifest);
|
||||||
@ -56,9 +54,8 @@ export class LensExtension implements ExtensionModel {
|
|||||||
// mock
|
// mock
|
||||||
}
|
}
|
||||||
|
|
||||||
async enable(runtime: LensExtensionRuntimeEnv) {
|
async enable() {
|
||||||
this.isEnabled = true;
|
this.isEnabled = true;
|
||||||
this.runtime = runtime;
|
|
||||||
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
logger.info(`[EXTENSION]: enabled ${this.name}@${this.version}`);
|
||||||
this.onActivate();
|
this.onActivate();
|
||||||
}
|
}
|
||||||
@ -66,7 +63,6 @@ export class LensExtension implements ExtensionModel {
|
|||||||
async disable() {
|
async disable() {
|
||||||
this.onDeactivate();
|
this.onDeactivate();
|
||||||
this.isEnabled = false;
|
this.isEnabled = false;
|
||||||
this.runtime = null;
|
|
||||||
this.disposers.forEach(cleanUp => cleanUp());
|
this.disposers.forEach(cleanUp => cleanUp());
|
||||||
this.disposers.length = 0;
|
this.disposers.length = 0;
|
||||||
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
logger.info(`[EXTENSION]: disabled ${this.name}@${this.version}`);
|
||||||
|
|||||||
@ -1,11 +1,17 @@
|
|||||||
import { LensExtension } from "./lens-extension"
|
import { LensExtension } from "./lens-extension"
|
||||||
|
import type { MenuRegistry } from "./registries/menu-registry";
|
||||||
|
import type { StatusBarRegistry } from "./registries/status-bar-registry";
|
||||||
|
|
||||||
export class LensMainExtension extends LensExtension {
|
export class LensMainExtension extends LensExtension {
|
||||||
async registerAppMenus() {
|
registerAppMenus(registry: MenuRegistry) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
|
|
||||||
async registerPrometheusProviders(registry: any) {
|
registerStatusBarIcon(registry: StatusBarRegistry) {
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPrometheusProviders(registry: any) {
|
||||||
//
|
//
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,9 +1,7 @@
|
|||||||
import { LensExtension } from "./lens-extension"
|
import { LensExtension } from "./lens-extension"
|
||||||
import type { PageRegistry } from "./page-registry"
|
import type { PageRegistry, AppPreferenceRegistry, StatusBarRegistry, KubeObjectMenuRegistry, ClusterFeatureRegistry } from "./registries"
|
||||||
import type { AppPreferenceRegistry } from "./app-preference-registry";
|
|
||||||
|
|
||||||
export class LensRendererExtension extends LensExtension {
|
export class LensRendererExtension extends LensExtension {
|
||||||
|
|
||||||
registerPages(registry: PageRegistry) {
|
registerPages(registry: PageRegistry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -11,4 +9,16 @@ export class LensRendererExtension extends LensExtension {
|
|||||||
registerAppPreferences(registry: AppPreferenceRegistry) {
|
registerAppPreferences(registry: AppPreferenceRegistry) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
registerClusterFeatures(registry: ClusterFeatureRegistry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerStatusBarIcon(registry: StatusBarRegistry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
registerKubeObjectMenus(registry: KubeObjectMenuRegistry) {
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
// Lens extension runtime params available to extensions after activation
|
|
||||||
|
|
||||||
import logger from "../main/logger";
|
|
||||||
|
|
||||||
export interface LensExtensionRuntimeEnv {
|
|
||||||
logger: typeof logger;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getLensRuntime(): LensExtensionRuntimeEnv {
|
|
||||||
return {
|
|
||||||
logger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,52 +0,0 @@
|
|||||||
// Extensions-api -> Dynamic pages
|
|
||||||
|
|
||||||
import { computed, observable } from "mobx";
|
|
||||||
import React from "react";
|
|
||||||
import { RouteProps } from "react-router";
|
|
||||||
import { IconProps } from "../renderer/components/icon";
|
|
||||||
import { IClassName } from "../renderer/utils";
|
|
||||||
import { TabRoute } from "../renderer/components/layout/tab-layout";
|
|
||||||
|
|
||||||
export enum DynamicPageType {
|
|
||||||
GLOBAL = "lens-scope",
|
|
||||||
CLUSTER = "cluster-view-scope",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageRegistration extends RouteProps {
|
|
||||||
className?: IClassName;
|
|
||||||
url?: string; // initial url to be used for building menus and tabs, otherwise "path" applied by default
|
|
||||||
path: string; // route-path
|
|
||||||
title: React.ReactNode; // used in sidebar's & tabs-layout
|
|
||||||
type: DynamicPageType;
|
|
||||||
components: PageComponents;
|
|
||||||
subPages?: (PageRegistration & TabRoute)[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PageComponents {
|
|
||||||
Page: React.ComponentType<any>;
|
|
||||||
MenuIcon: React.ComponentType<IconProps>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class PageRegistry {
|
|
||||||
protected pages = observable.array<PageRegistration>([], { deep: false });
|
|
||||||
|
|
||||||
@computed get globalPages() {
|
|
||||||
return this.pages.filter(page => page.type === DynamicPageType.GLOBAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@computed get clusterPages() {
|
|
||||||
return this.pages.filter(page => page.type === DynamicPageType.CLUSTER);
|
|
||||||
}
|
|
||||||
|
|
||||||
// todo: verify paths to avoid collision with existing pages
|
|
||||||
add(params: PageRegistration) {
|
|
||||||
this.pages.push(params);
|
|
||||||
return () => {
|
|
||||||
this.pages.replace(
|
|
||||||
this.pages.filter(page => page.components !== params.components)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const pageRegistry = new PageRegistry();
|
|
||||||
17
src/extensions/registries/app-preference-registry.ts
Normal file
17
src/extensions/registries/app-preference-registry.ts
Normal file
@ -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()
|
||||||
17
src/extensions/registries/base-registry.ts
Normal file
17
src/extensions/registries/base-registry.ts
Normal file
@ -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};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
src/extensions/registries/cluster-feature-registry.ts
Normal file
16
src/extensions/registries/cluster-feature-registry.ts
Normal 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()
|
||||||
8
src/extensions/registries/index.ts
Normal file
8
src/extensions/registries/index.ts
Normal file
@ -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"
|
||||||
22
src/extensions/registries/kube-object-menu-registry.ts
Normal file
22
src/extensions/registries/kube-object-menu-registry.ts
Normal file
@ -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()
|
||||||
14
src/extensions/registries/menu-registry.ts
Normal file
14
src/extensions/registries/menu-registry.ts
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
// Extensions API -> Global menu customizations
|
||||||
|
|
||||||
|
import type { MenuTopId } from "../../main/menu";
|
||||||
|
import type { MenuItemConstructorOptions } from "electron";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface MenuRegistration extends MenuItemConstructorOptions {
|
||||||
|
parentId?: MenuTopId;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class MenuRegistry extends BaseRegistry<MenuRegistration> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const menuRegistry = new MenuRegistry();
|
||||||
40
src/extensions/registries/page-registry.ts
Normal file
40
src/extensions/registries/page-registry.ts
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
// Extensions-api -> Dynamic pages
|
||||||
|
|
||||||
|
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";
|
||||||
|
import { computed } from "mobx";
|
||||||
|
|
||||||
|
export enum PageRegistryType {
|
||||||
|
GLOBAL = "lens-scope",
|
||||||
|
CLUSTER = "cluster-view-scope",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageRegistration extends RouteProps {
|
||||||
|
type: PageRegistryType;
|
||||||
|
components: PageComponents;
|
||||||
|
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
|
||||||
|
subPages?: (PageRegistration & TabRoute)[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageComponents {
|
||||||
|
Page: React.ComponentType<any>;
|
||||||
|
MenuIcon?: React.ComponentType<IconProps>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PageRegistry extends BaseRegistry<PageRegistration> {
|
||||||
|
@computed get globalPages() {
|
||||||
|
return this.items.filter(page => page.type === PageRegistryType.GLOBAL);
|
||||||
|
}
|
||||||
|
|
||||||
|
@computed get clusterPages() {
|
||||||
|
return this.items.filter(page => page.type === PageRegistryType.CLUSTER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const pageRegistry = new PageRegistry();
|
||||||
13
src/extensions/registries/status-bar-registry.ts
Normal file
13
src/extensions/registries/status-bar-registry.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
// Extensions API -> Status bar customizations
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface StatusBarRegistration {
|
||||||
|
icon?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class StatusBarRegistry extends BaseRegistry<StatusBarRegistration> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const statusBarRegistry = new StatusBarRegistry();
|
||||||
@ -1,11 +1,21 @@
|
|||||||
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
|
// TODO: add more common re-usable UI components + refactor interfaces (Props -> ComponentProps)
|
||||||
|
|
||||||
export * from "../../renderer/components/icon"
|
export * from "../../renderer/components/icon"
|
||||||
export * from "../../renderer/components/checkbox"
|
export * from "../../renderer/components/checkbox"
|
||||||
export * from "../../renderer/components/tooltip"
|
export * from "../../renderer/components/tooltip"
|
||||||
export * from "../../renderer/components/button"
|
export * from "../../renderer/components/button"
|
||||||
export * from "../../renderer/components/tabs"
|
export * from "../../renderer/components/tabs"
|
||||||
export * from "../../renderer/components/badge"
|
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 { 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 { MenuItem, SubMenu } from "../../renderer/components/menu";
|
||||||
export { StatusBrick } from "../../renderer/components/status-brick";
|
export { StatusBrick } from "../../renderer/components/status-brick";
|
||||||
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
export { terminalStore, createTerminalTab } from "../../renderer/components/dock/terminal.store";
|
||||||
|
|||||||
12
src/extensions/renderer-api/index.ts
Normal file
12
src/extensions/renderer-api/index.ts
Normal file
@ -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,
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
|
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 } 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";
|
||||||
export { Deployment, deploymentApi } from "../../renderer/api/endpoints";
|
export { Deployment, deploymentApi } from "../../renderer/api/endpoints";
|
||||||
export { DaemonSet, daemonSetApi } from "../../renderer/api/endpoints";
|
export { DaemonSet, daemonSetApi } from "../../renderer/api/endpoints";
|
||||||
@ -9,7 +9,7 @@ export { StatefulSet, statefulSetApi } from "../../renderer/api/endpoints";
|
|||||||
export { Job, jobApi } from "../../renderer/api/endpoints";
|
export { Job, jobApi } from "../../renderer/api/endpoints";
|
||||||
export { CronJob, cronJobApi } from "../../renderer/api/endpoints";
|
export { CronJob, cronJobApi } from "../../renderer/api/endpoints";
|
||||||
export { ConfigMap, configMapApi } from "../../renderer/api/endpoints";
|
export { ConfigMap, configMapApi } from "../../renderer/api/endpoints";
|
||||||
export { Secret, secretsApi } from "../../renderer/api/endpoints";
|
export { Secret, secretsApi, ISecretRef } from "../../renderer/api/endpoints";
|
||||||
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
|
export { ResourceQuota, resourceQuotaApi } from "../../renderer/api/endpoints";
|
||||||
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
|
export { HorizontalPodAutoscaler, hpaApi } from "../../renderer/api/endpoints";
|
||||||
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
|
export { PodDisruptionBudget, pdbApi } from "../../renderer/api/endpoints";
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
export { navigate } from "../../renderer/navigation"
|
export { navigate, hideDetails, showDetails } from "../../renderer/navigation"
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
// APIs
|
|
||||||
import * as Component from "./renderer-api/components"
|
|
||||||
import * as K8sApi from "./renderer-api/k8s-api"
|
|
||||||
import * as Navigation from "./renderer-api/navigation"
|
|
||||||
|
|
||||||
export {
|
|
||||||
Component,
|
|
||||||
K8sApi,
|
|
||||||
Navigation,
|
|
||||||
}
|
|
||||||
@ -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,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
|
|
||||||
@ -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)
|
||||||
|
|||||||
@ -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,10 @@ export class Cluster implements ClusterModel {
|
|||||||
@observable eventCount = 0;
|
@observable eventCount = 0;
|
||||||
@observable preferences: ClusterPreferences = {};
|
@observable preferences: ClusterPreferences = {};
|
||||||
@observable metadata: ClusterMetadata = {};
|
@observable metadata: ClusterMetadata = {};
|
||||||
|
<<<<<<< HEAD
|
||||||
@observable features: FeatureStatusMap = {};
|
@observable features: FeatureStatusMap = {};
|
||||||
|
=======
|
||||||
|
>>>>>>> extensions-api
|
||||||
@observable allowedNamespaces: string[] = [];
|
@observable allowedNamespaces: string[] = [];
|
||||||
@observable allowedResources: string[] = [];
|
@observable allowedResources: string[] = [];
|
||||||
|
|
||||||
@ -194,12 +194,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 +245,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 +383,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,
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
import "../common/system-ca"
|
import "../common/system-ca"
|
||||||
import "../common/prometheus-providers"
|
import "../common/prometheus-providers"
|
||||||
|
import * as Mobx from "mobx"
|
||||||
|
import * as LensExtensions from "../extensions/core-api";
|
||||||
import { app, dialog } from "electron"
|
import { app, dialog } from "electron"
|
||||||
import { appName } from "../common/vars";
|
import { appName } from "../common/vars";
|
||||||
import path from "path"
|
import path from "path"
|
||||||
@ -17,17 +19,9 @@ import { clusterStore } from "../common/cluster-store"
|
|||||||
import { userStore } from "../common/user-store";
|
import { userStore } from "../common/user-store";
|
||||||
import { workspaceStore } from "../common/workspace-store";
|
import { workspaceStore } from "../common/workspace-store";
|
||||||
import { appEventBus } from "../common/event-bus"
|
import { appEventBus } from "../common/event-bus"
|
||||||
import * as LensExtensions from "../extensions/core-extension-api";
|
|
||||||
import { extensionManager } from "../extensions/extension-manager";
|
import { extensionManager } from "../extensions/extension-manager";
|
||||||
import { extensionLoader } from "../extensions/extension-loader";
|
import { extensionLoader } from "../extensions/extension-loader";
|
||||||
import { getLensRuntime } from "../extensions/lens-runtime";
|
|
||||||
import logger from "./logger"
|
import logger from "./logger"
|
||||||
import * as Mobx from "mobx"
|
|
||||||
|
|
||||||
export {
|
|
||||||
LensExtensions,
|
|
||||||
Mobx
|
|
||||||
}
|
|
||||||
|
|
||||||
const workingDir = path.join(app.getPath("appData"), appName);
|
const workingDir = path.join(app.getPath("appData"), appName);
|
||||||
app.setName(appName);
|
app.setName(appName);
|
||||||
@ -35,7 +29,6 @@ if (!process.env.CICD) {
|
|||||||
app.setPath("userData", workingDir);
|
app.setPath("userData", workingDir);
|
||||||
}
|
}
|
||||||
|
|
||||||
let windowManager: WindowManager;
|
|
||||||
let clusterManager: ClusterManager;
|
let clusterManager: ClusterManager;
|
||||||
let proxyServer: LensProxy;
|
let proxyServer: LensProxy;
|
||||||
|
|
||||||
@ -83,9 +76,9 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// create window manager and open app
|
// create window manager and open app
|
||||||
windowManager = new WindowManager(proxyPort);
|
LensExtensionsApi.windowManager = new WindowManager(proxyPort);
|
||||||
|
|
||||||
extensionLoader.loadOnMain(getLensRuntime)
|
extensionLoader.loadOnMain()
|
||||||
extensionLoader.extensions.replace(await extensionManager.load())
|
extensionLoader.extensions.replace(await extensionManager.load())
|
||||||
extensionLoader.broadcastExtensions()
|
extensionLoader.broadcastExtensions()
|
||||||
|
|
||||||
@ -102,3 +95,13 @@ app.on("will-quit", async (event) => {
|
|||||||
if (clusterManager) clusterManager.stop()
|
if (clusterManager) clusterManager.stop()
|
||||||
app.exit();
|
app.exit();
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Extensions-api runtime exports
|
||||||
|
export const LensExtensionsApi = {
|
||||||
|
...LensExtensions,
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
Mobx,
|
||||||
|
LensExtensionsApi as LensExtensions,
|
||||||
|
}
|
||||||
|
|||||||
@ -6,8 +6,11 @@ import { addClusterURL } from "../renderer/components/+add-cluster/add-cluster.r
|
|||||||
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
import { preferencesURL } from "../renderer/components/+preferences/preferences.route";
|
||||||
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
import { whatsNewURL } from "../renderer/components/+whats-new/whats-new.route";
|
||||||
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
import { clusterSettingsURL } from "../renderer/components/+cluster-settings/cluster-settings.route";
|
||||||
|
import { menuRegistry } from "../extensions/registries/menu-registry";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
|
export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"
|
||||||
|
|
||||||
export function initMenu(windowManager: WindowManager) {
|
export function initMenu(windowManager: WindowManager) {
|
||||||
autorun(() => buildMenu(windowManager), {
|
autorun(() => buildMenu(windowManager), {
|
||||||
delay: 100
|
delay: 100
|
||||||
@ -53,8 +56,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const mt: MenuItemConstructorOptions[] = [];
|
|
||||||
|
|
||||||
const macAppMenu: MenuItemConstructorOptions = {
|
const macAppMenu: MenuItemConstructorOptions = {
|
||||||
label: app.getName(),
|
label: app.getName(),
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -83,10 +84,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isMac) {
|
|
||||||
mt.push(macAppMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
const fileMenu: MenuItemConstructorOptions = {
|
const fileMenu: MenuItemConstructorOptions = {
|
||||||
label: "File",
|
label: "File",
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -124,7 +121,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
])
|
])
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
mt.push(fileMenu)
|
|
||||||
|
|
||||||
const editMenu: MenuItemConstructorOptions = {
|
const editMenu: MenuItemConstructorOptions = {
|
||||||
label: 'Edit',
|
label: 'Edit',
|
||||||
@ -140,7 +136,7 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ role: 'selectAll' },
|
{ role: 'selectAll' },
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
mt.push(editMenu)
|
|
||||||
const viewMenu: MenuItemConstructorOptions = {
|
const viewMenu: MenuItemConstructorOptions = {
|
||||||
label: 'View',
|
label: 'View',
|
||||||
submenu: [
|
submenu: [
|
||||||
@ -174,7 +170,6 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
{ role: 'togglefullscreen' }
|
{ role: 'togglefullscreen' }
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
mt.push(viewMenu)
|
|
||||||
|
|
||||||
const helpMenu: MenuItemConstructorOptions = {
|
const helpMenu: MenuItemConstructorOptions = {
|
||||||
role: 'help',
|
role: 'help',
|
||||||
@ -214,7 +209,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].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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -53,22 +53,6 @@ export class ApiManager {
|
|||||||
getStore(api: string | KubeApi): KubeObjectStore {
|
getStore(api: string | KubeApi): KubeObjectStore {
|
||||||
return this.stores.get(this.resolveApi(api));
|
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();
|
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 {
|
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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user