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
230 files changed
+16386
-2611
No files matched your search
@@ -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',
|
||||||
|
|||||||
+1
-1
@@ -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: {
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
+3570
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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()
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -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"}) }
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
Generated
+3508
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
Generated
+3508
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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} />
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
install-deps:
|
||||||
|
npm install
|
||||||
|
|
||||||
|
build: install-deps
|
||||||
|
npm run build
|
||||||
@@ -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
File diff suppressed because it is too large.
Load diff
@@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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())}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { RouteProps } from "react-router";
|
||||||
|
|
||||||
|
export const supportPageRoute: RouteProps = {
|
||||||
|
path: "/support"
|
||||||
|
}
|
||||||
|
|
||||||
|
export const supportPageURL = () => supportPageRoute.path.toString();
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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/**/*"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -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/**/*"
|
||||||
],
|
]
|
||||||
}
|
}
|
||||||
+5
-2
@@ -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()
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path"
|
||||||
|
import hb from "handlebars"
|
||||||
|
import { observable } from "mobx"
|
||||||
|
import { ResourceApplier } from "../main/resource-applier"
|
||||||
|
import { Cluster } from "../main/cluster";
|
||||||
|
import logger from "../main/logger";
|
||||||
|
import { app } from "electron"
|
||||||
|
import { clusterIpc } from "../common/cluster-ipc"
|
||||||
|
|
||||||
|
export interface ClusterFeatureStatus {
|
||||||
|
currentVersion: string;
|
||||||
|
installed: boolean;
|
||||||
|
latestVersion: string;
|
||||||
|
canUpgrade: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export abstract class ClusterFeature {
|
||||||
|
name: string;
|
||||||
|
latestVersion: string;
|
||||||
|
config: any;
|
||||||
|
|
||||||
|
@observable status: ClusterFeatureStatus = {
|
||||||
|
currentVersion: null,
|
||||||
|
installed: false,
|
||||||
|
latestVersion: null,
|
||||||
|
canUpgrade: false
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract async install(cluster: Cluster): Promise<void>;
|
||||||
|
|
||||||
|
abstract async upgrade(cluster: Cluster): Promise<void>;
|
||||||
|
|
||||||
|
abstract async uninstall(cluster: Cluster): Promise<void>;
|
||||||
|
|
||||||
|
abstract async updateStatus(cluster: Cluster): Promise<ClusterFeatureStatus>;
|
||||||
|
|
||||||
|
protected async applyResources(cluster: Cluster, resources: string[]) {
|
||||||
|
if (app) {
|
||||||
|
await new ResourceApplier(cluster).kubectlApplyAll(resources)
|
||||||
|
} else {
|
||||||
|
await clusterIpc.kubectlApplyAll.invokeFromRenderer(cluster.id, resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected renderTemplates(folderPath: string): string[] {
|
||||||
|
const resources: string[] = [];
|
||||||
|
logger.info(`[FEATURE]: render templates from ${folderPath}`);
|
||||||
|
fs.readdirSync(folderPath).forEach(filename => {
|
||||||
|
const file = path.join(folderPath, filename);
|
||||||
|
const raw = fs.readFileSync(file);
|
||||||
|
if (filename.endsWith('.hb')) {
|
||||||
|
const template = hb.compile(raw.toString());
|
||||||
|
resources.push(template(this.config));
|
||||||
|
} else {
|
||||||
|
resources.push(raw.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return resources;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { ClusterFeature as Feature, ClusterFeatureStatus as FeatureStatus } from "../cluster-feature"
|
||||||
@@ -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();
|
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type React from "react"
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface AppPreferenceComponents {
|
||||||
|
Hint: React.ComponentType<any>;
|
||||||
|
Input: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppPreferenceRegistration {
|
||||||
|
title: string;
|
||||||
|
components: AppPreferenceComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AppPreferenceRegistry extends BaseRegistry<AppPreferenceRegistration> {
|
||||||
|
}
|
||||||
|
|
||||||
|
export const appPreferenceRegistry = new AppPreferenceRegistry()
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
// Base class for extensions-api registries
|
||||||
|
import { observable } from "mobx";
|
||||||
|
|
||||||
|
export class BaseRegistry<T = any> {
|
||||||
|
protected items = observable<T>([], { deep: false });
|
||||||
|
|
||||||
|
getItems(): T[] {
|
||||||
|
return this.items.toJS();
|
||||||
|
}
|
||||||
|
|
||||||
|
add(item: T) {
|
||||||
|
this.items.push(item);
|
||||||
|
return () => {
|
||||||
|
this.items.remove(item); // works because of {deep: false};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
import { ClusterFeature } from "../cluster-feature";
|
||||||
|
|
||||||
|
export interface ClusterFeatureComponents {
|
||||||
|
Description: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ClusterFeatureRegistration {
|
||||||
|
title: string;
|
||||||
|
components: ClusterFeatureComponents
|
||||||
|
feature: ClusterFeature
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClusterFeatureRegistry extends BaseRegistry<ClusterFeatureRegistration> {}
|
||||||
|
|
||||||
|
export const clusterFeatureRegistry = new ClusterFeatureRegistry()
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// All registries managed by extensions api
|
||||||
|
|
||||||
|
export * from "./page-registry"
|
||||||
|
export * from "./menu-registry"
|
||||||
|
export * from "./app-preference-registry"
|
||||||
|
export * from "./status-bar-registry"
|
||||||
|
export * from "./kube-object-menu-registry";
|
||||||
|
export * from "./cluster-feature-registry"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { BaseRegistry } from "./base-registry";
|
||||||
|
|
||||||
|
export interface KubeObjectMenuComponents {
|
||||||
|
MenuItem: React.ComponentType<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KubeObjectMenuRegistration {
|
||||||
|
kind: string;
|
||||||
|
apiVersions: string[];
|
||||||
|
components: KubeObjectMenuComponents;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class KubeObjectMenuRegistry extends BaseRegistry<KubeObjectMenuRegistration> {
|
||||||
|
getItemsForKind(kind: string, apiVersion: string) {
|
||||||
|
return this.items.filter((item) => {
|
||||||
|
return item.kind === kind && item.apiVersions.includes(apiVersion)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const kubeObjectMenuRegistry = new KubeObjectMenuRegistry()
|
||||||
@@ -0,0 +1,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();
|
||||||
@@ -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();
|
||||||
@@ -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";
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
+4
-22
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+14
-11
@@ -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,
|
||||||
|
}
|
||||||
+28
-11
@@ -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,
|
||||||
});
|
});
|
||||||
Loaded 100 of 230 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user