mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Merge branch 'master' into catalog-entity-pin-icons
This commit is contained in:
commit
62b45875c4
16
Makefile
16
Makefile
@ -113,19 +113,17 @@ docs:
|
|||||||
|
|
||||||
.PHONY: clean-extensions
|
.PHONY: clean-extensions
|
||||||
clean-extensions:
|
clean-extensions:
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/dist)
|
rm -rf $(EXTENSIONS_DIR)/*/{dist,node_modules,*.tgz}
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm -rf $(dir)/node_modules)
|
|
||||||
$(foreach dir, $(wildcard $(EXTENSIONS_DIR)/*), rm $(dir)/*.tgz || true)
|
|
||||||
|
|
||||||
.PHONY: clean-npm
|
.PHONY: clean-npm
|
||||||
clean-npm:
|
clean-npm:
|
||||||
rm -rf src/extensions/npm/extensions/dist
|
rm -rf src/extensions/npm/extensions/{dist,__mocks__,node_modules}
|
||||||
rm -rf src/extensions/npm/extensions/__mocks__
|
|
||||||
rm -rf src/extensions/npm/extensions/node_modules
|
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: clean-npm clean-extensions
|
clean: clean-npm clean-extensions
|
||||||
rm -rf binaries/client
|
rm -rf binaries/client
|
||||||
rm -rf dist/*
|
rm -rf dist
|
||||||
rm -rf static/build/*
|
rm -rf static/build
|
||||||
rm -rf node_modules/
|
rm -rf node_modules
|
||||||
|
rm -rf site
|
||||||
|
rm -rf docs/extensions/api
|
||||||
|
|||||||
@ -2,6 +2,6 @@
|
|||||||
|
|
||||||
## APIs
|
## APIs
|
||||||
|
|
||||||
- [Common](modules/common.md)
|
- [Common](modules/Common.md)
|
||||||
- [Main](modules/main.md)
|
- [Main](modules/Main.md)
|
||||||
- [Renderer](modules/renderer.md)
|
- [Renderer](modules/Renderer.md)
|
||||||
|
|||||||
@ -33,7 +33,7 @@ describe("preferences page tests", () => {
|
|||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
let app: ElectronApplication;
|
let app: ElectronApplication;
|
||||||
|
|
||||||
({ window, cleanup, app } = await utils.start());
|
({ window, cleanup, app } = await utils.start());
|
||||||
await utils.clickWelcomeButton(window);
|
await utils.clickWelcomeButton(window);
|
||||||
|
|
||||||
@ -74,7 +74,7 @@ describe("preferences page tests", () => {
|
|||||||
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
|
utils.itIf(process.platform !== "win32")("ensures helm repos", async () => {
|
||||||
await window.click("[data-testid=kubernetes-tab]");
|
await window.click("[data-testid=kubernetes-tab]");
|
||||||
await window.waitForSelector("[data-testid=repository-name]", {
|
await window.waitForSelector("[data-testid=repository-name]", {
|
||||||
timeout: 100_000,
|
timeout: 140_000,
|
||||||
});
|
});
|
||||||
await window.click("#HelmRepoSelect");
|
await window.click("#HelmRepoSelect");
|
||||||
await window.waitForSelector("div.Select__option");
|
await window.waitForSelector("div.Select__option");
|
||||||
|
|||||||
@ -93,7 +93,7 @@ export async function start() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function clickWelcomeButton(window: Page) {
|
export async function clickWelcomeButton(window: Page) {
|
||||||
await window.click("#hotbarIcon-catalog-entity .Icon");
|
await window.click("[data-testid=welcome-menu-container] li a");
|
||||||
}
|
}
|
||||||
|
|
||||||
function minikubeEntityId() {
|
function minikubeEntityId() {
|
||||||
|
|||||||
33
package.json
33
package.json
@ -3,7 +3,7 @@
|
|||||||
"productName": "OpenLens",
|
"productName": "OpenLens",
|
||||||
"description": "OpenLens - Open Source IDE for Kubernetes",
|
"description": "OpenLens - Open Source IDE for Kubernetes",
|
||||||
"homepage": "https://github.com/lensapp/lens",
|
"homepage": "https://github.com/lensapp/lens",
|
||||||
"version": "5.3.0-alpha.0",
|
"version": "5.3.0-alpha.2",
|
||||||
"main": "static/build/main.js",
|
"main": "static/build/main.js",
|
||||||
"copyright": "© 2021 OpenLens Authors",
|
"copyright": "© 2021 OpenLens Authors",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@ -179,7 +179,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@electron/remote": "^1.2.1",
|
"@electron/remote": "^1.2.2",
|
||||||
"@hapi/call": "^8.0.1",
|
"@hapi/call": "^8.0.1",
|
||||||
"@hapi/subtext": "^7.0.3",
|
"@hapi/subtext": "^7.0.3",
|
||||||
"@kubernetes/client-node": "^0.15.1",
|
"@kubernetes/client-node": "^0.15.1",
|
||||||
@ -193,8 +193,7 @@
|
|||||||
"byline": "^5.0.0",
|
"byline": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"chokidar": "^3.4.3",
|
"chokidar": "^3.4.3",
|
||||||
"command-exists": "1.2.9",
|
"conf": "^7.1.2",
|
||||||
"conf": "^7.0.1",
|
|
||||||
"crypto-js": "^4.1.1",
|
"crypto-js": "^4.1.1",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-updater": "^4.6.0",
|
"electron-updater": "^4.6.0",
|
||||||
@ -251,7 +250,7 @@
|
|||||||
"winston": "^3.3.3",
|
"winston": "^3.3.3",
|
||||||
"winston-console-format": "^1.0.8",
|
"winston-console-format": "^1.0.8",
|
||||||
"winston-transport-browserconsole": "^1.0.5",
|
"winston-transport-browserconsole": "^1.0.5",
|
||||||
"ws": "^7.4.6"
|
"ws": "^7.5.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emeraldpay/hashicon-react": "^0.4.0",
|
"@emeraldpay/hashicon-react": "^0.4.0",
|
||||||
@ -259,13 +258,13 @@
|
|||||||
"@material-ui/icons": "^4.11.2",
|
"@material-ui/icons": "^4.11.2",
|
||||||
"@material-ui/lab": "^4.0.0-alpha.60",
|
"@material-ui/lab": "^4.0.0-alpha.60",
|
||||||
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
"@pmmmwh/react-refresh-webpack-plugin": "^0.4.3",
|
||||||
"@sentry/react": "^6.8.0",
|
"@sentry/react": "^6.13.3",
|
||||||
"@sentry/types": "^6.8.0",
|
"@sentry/types": "^6.8.0",
|
||||||
"@testing-library/dom": "^8.2.0",
|
"@testing-library/dom": "^8.9.0",
|
||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^11.2.6",
|
"@testing-library/react": "^11.2.6",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
"@types/byline": "^4.2.32",
|
"@types/byline": "^4.2.33",
|
||||||
"@types/chart.js": "^2.9.34",
|
"@types/chart.js": "^2.9.34",
|
||||||
"@types/color": "^3.0.2",
|
"@types/color": "^3.0.2",
|
||||||
"@types/crypto-js": "^3.1.47",
|
"@types/crypto-js": "^3.1.47",
|
||||||
@ -279,7 +278,7 @@
|
|||||||
"@types/http-proxy": "^1.17.7",
|
"@types/http-proxy": "^1.17.7",
|
||||||
"@types/jest": "^26.0.24",
|
"@types/jest": "^26.0.24",
|
||||||
"@types/js-yaml": "^3.12.4",
|
"@types/js-yaml": "^3.12.4",
|
||||||
"@types/jsdom": "^16.2.4",
|
"@types/jsdom": "^16.2.13",
|
||||||
"@types/jsonpath": "^0.2.0",
|
"@types/jsonpath": "^0.2.0",
|
||||||
"@types/lodash": "^4.14.155",
|
"@types/lodash": "^4.14.155",
|
||||||
"@types/marked": "^2.0.4",
|
"@types/marked": "^2.0.4",
|
||||||
@ -287,13 +286,13 @@
|
|||||||
"@types/mini-css-extract-plugin": "^0.9.1",
|
"@types/mini-css-extract-plugin": "^0.9.1",
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/module-alias": "^2.0.1",
|
"@types/module-alias": "^2.0.1",
|
||||||
"@types/node": "14.17.14",
|
"@types/node": "14.17.26",
|
||||||
"@types/node-fetch": "^2.5.12",
|
"@types/node-fetch": "^2.5.12",
|
||||||
"@types/npm": "^2.0.32",
|
"@types/npm": "^2.0.32",
|
||||||
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
"@types/progress-bar-webpack-plugin": "^2.1.2",
|
||||||
"@types/proper-lockfile": "^4.1.2",
|
"@types/proper-lockfile": "^4.1.2",
|
||||||
"@types/randomcolor": "^0.5.6",
|
"@types/randomcolor": "^0.5.6",
|
||||||
"@types/react": "^17.0.0",
|
"@types/react": "^17.0.29",
|
||||||
"@types/react-beautiful-dnd": "^13.1.1",
|
"@types/react-beautiful-dnd": "^13.1.1",
|
||||||
"@types/react-dom": "^17.0.9",
|
"@types/react-dom": "^17.0.9",
|
||||||
"@types/react-router-dom": "^5.3.1",
|
"@types/react-router-dom": "^5.3.1",
|
||||||
@ -301,7 +300,7 @@
|
|||||||
"@types/react-table": "^7.7.6",
|
"@types/react-table": "^7.7.6",
|
||||||
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
"@types/react-virtualized-auto-sizer": "^1.0.1",
|
||||||
"@types/react-window": "^1.8.5",
|
"@types/react-window": "^1.8.5",
|
||||||
"@types/readable-stream": "^2.3.9",
|
"@types/readable-stream": "^2.3.11",
|
||||||
"@types/request": "^2.48.7",
|
"@types/request": "^2.48.7",
|
||||||
"@types/request-promise-native": "^1.0.18",
|
"@types/request-promise-native": "^1.0.18",
|
||||||
"@types/semver": "^7.2.0",
|
"@types/semver": "^7.2.0",
|
||||||
@ -327,7 +326,7 @@
|
|||||||
"concurrently": "^5.2.0",
|
"concurrently": "^5.2.0",
|
||||||
"css-loader": "^5.2.7",
|
"css-loader": "^5.2.7",
|
||||||
"deepdash": "^5.3.9",
|
"deepdash": "^5.3.9",
|
||||||
"dompurify": "^2.3.1",
|
"dompurify": "^2.3.3",
|
||||||
"electron": "^13.5.1",
|
"electron": "^13.5.1",
|
||||||
"electron-builder": "^22.11.11",
|
"electron-builder": "^22.11.11",
|
||||||
"electron-notarize": "^0.3.0",
|
"electron-notarize": "^0.3.0",
|
||||||
@ -335,7 +334,7 @@
|
|||||||
"esbuild-loader": "^2.15.1",
|
"esbuild-loader": "^2.15.1",
|
||||||
"eslint": "^7.32.0",
|
"eslint": "^7.32.0",
|
||||||
"eslint-plugin-header": "^3.1.1",
|
"eslint-plugin-header": "^3.1.1",
|
||||||
"eslint-plugin-react": "^7.24.0",
|
"eslint-plugin-react": "^7.26.1",
|
||||||
"eslint-plugin-react-hooks": "^4.2.0",
|
"eslint-plugin-react-hooks": "^4.2.0",
|
||||||
"eslint-plugin-unused-imports": "^1.0.1",
|
"eslint-plugin-unused-imports": "^1.0.1",
|
||||||
"file-loader": "^6.2.0",
|
"file-loader": "^6.2.0",
|
||||||
@ -353,7 +352,7 @@
|
|||||||
"mini-css-extract-plugin": "^1.6.2",
|
"mini-css-extract-plugin": "^1.6.2",
|
||||||
"node-gyp": "7.1.2",
|
"node-gyp": "7.1.2",
|
||||||
"node-loader": "^1.0.3",
|
"node-loader": "^1.0.3",
|
||||||
"nodemon": "^2.0.12",
|
"nodemon": "^2.0.13",
|
||||||
"playwright": "^1.14.0",
|
"playwright": "^1.14.0",
|
||||||
"postcss": "^8.3.6",
|
"postcss": "^8.3.6",
|
||||||
"postcss-loader": "4.3.0",
|
"postcss-loader": "4.3.0",
|
||||||
@ -373,14 +372,14 @@
|
|||||||
"sass-loader": "^8.0.2",
|
"sass-loader": "^8.0.2",
|
||||||
"sharp": "^0.29.1",
|
"sharp": "^0.29.1",
|
||||||
"style-loader": "^2.0.0",
|
"style-loader": "^2.0.0",
|
||||||
"tailwindcss": "^2.2.4",
|
"tailwindcss": "^2.2.17",
|
||||||
"ts-jest": "26.5.6",
|
"ts-jest": "26.5.6",
|
||||||
"ts-loader": "^7.0.5",
|
"ts-loader": "^7.0.5",
|
||||||
"ts-node": "^10.2.1",
|
"ts-node": "^10.2.1",
|
||||||
"type-fest": "^1.0.2",
|
"type-fest": "^1.0.2",
|
||||||
"typed-emitter": "^1.3.1",
|
"typed-emitter": "^1.3.1",
|
||||||
"typedoc": "0.22.5",
|
"typedoc": "0.22.5",
|
||||||
"typedoc-plugin-markdown": "^3.9.0",
|
"typedoc-plugin-markdown": "^3.11.3",
|
||||||
"typeface-roboto": "^1.1.13",
|
"typeface-roboto": "^1.1.13",
|
||||||
"typescript": "^4.4.3",
|
"typescript": "^4.4.3",
|
||||||
"typescript-plugin-css-modules": "^3.4.0",
|
"typescript-plugin-css-modules": "^3.4.0",
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { KubeConfig } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import { validateKubeConfig, loadConfigFromString, getNodeWarningConditions } from "../kube-helpers";
|
import { validateKubeConfig, loadConfigFromString } from "../kube-helpers";
|
||||||
|
|
||||||
const kubeconfig = `
|
const kubeconfig = `
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
@ -120,38 +120,6 @@ describe("kube helpers", () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with invalid exec command", () => {
|
|
||||||
it("returns an error", () => {
|
|
||||||
expect(String(validateKubeConfig(kc, "invalidExec"))).toEqual(
|
|
||||||
expect.stringContaining("User Exec command \"foo\" not found on host. Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig")
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with validateCluster as false", () => {
|
|
||||||
describe("with invalid cluster object", () => {
|
|
||||||
it("does not return an error", () => {
|
|
||||||
expect(validateKubeConfig(kc, "invalidCluster", { validateCluster: false })).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with validateUser as false", () => {
|
|
||||||
describe("with invalid user object", () => {
|
|
||||||
it("does not return an error", () => {
|
|
||||||
expect(validateKubeConfig(kc, "invalidUser", { validateUser: false })).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("with validateExec as false", () => {
|
|
||||||
describe("with invalid exec object", () => {
|
|
||||||
it("does not return an error", () => {
|
|
||||||
expect(validateKubeConfig(kc, "invalidExec", { validateExec: false })).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -280,43 +248,4 @@ describe("kube helpers", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getNodeWarningConditions", () => {
|
|
||||||
it("should return an empty array if no status or no conditions", () => {
|
|
||||||
expect(getNodeWarningConditions({}).length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should return an empty array if all conditions are good", () => {
|
|
||||||
expect(getNodeWarningConditions({
|
|
||||||
status: {
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
type: "Ready",
|
|
||||||
status: "foobar"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}).length).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should all not ready conditions", () => {
|
|
||||||
const conds = getNodeWarningConditions({
|
|
||||||
status: {
|
|
||||||
conditions: [
|
|
||||||
{
|
|
||||||
type: "Ready",
|
|
||||||
status: "foobar"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "NotReady",
|
|
||||||
status: "true"
|
|
||||||
},
|
|
||||||
]
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(conds.length).toBe(1);
|
|
||||||
expect(conds[0].type).toBe("NotReady");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@ -54,15 +54,24 @@ export interface KubernetesClusterSpec extends CatalogEntitySpec {
|
|||||||
accessibleNamespaces?: string[];
|
accessibleNamespaces?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum LensKubernetesClusterStatus {
|
||||||
|
DELETING = "deleting",
|
||||||
|
CONNECTING = "connecting",
|
||||||
|
CONNECTED = "connected",
|
||||||
|
DISCONNECTED = "disconnected"
|
||||||
|
}
|
||||||
|
|
||||||
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
export interface KubernetesClusterMetadata extends CatalogEntityMetadata {
|
||||||
distro?: string;
|
distro?: string;
|
||||||
kubeVersion?: string;
|
kubeVersion?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated This is no longer used as it is incorrect. Other sources can add more values
|
||||||
|
*/
|
||||||
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
export type KubernetesClusterStatusPhase = "connected" | "connecting" | "disconnected" | "deleting";
|
||||||
|
|
||||||
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
export interface KubernetesClusterStatus extends CatalogEntityStatus {
|
||||||
phase: KubernetesClusterStatusPhase;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata, KubernetesClusterStatus, KubernetesClusterSpec> {
|
||||||
@ -110,15 +119,15 @@ export class KubernetesCluster extends CatalogEntity<KubernetesClusterMetadata,
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch (this.status.phase) {
|
switch (this.status.phase) {
|
||||||
case "connected":
|
case LensKubernetesClusterStatus.CONNECTED:
|
||||||
case "connecting":
|
case LensKubernetesClusterStatus.CONNECTING:
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Disconnect",
|
title: "Disconnect",
|
||||||
icon: "link_off",
|
icon: "link_off",
|
||||||
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
|
onClick: () => requestMain(clusterDisconnectHandler, this.metadata.uid)
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "disconnected":
|
case LensKubernetesClusterStatus.DISCONNECTED:
|
||||||
context.menuItems.push({
|
context.menuItems.push({
|
||||||
title: "Connect",
|
title: "Connect",
|
||||||
icon: "link",
|
icon: "link",
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
/**
|
|
||||||
* Copyright (c) 2021 OpenLens Authors
|
|
||||||
*
|
|
||||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
|
||||||
* this software and associated documentation files (the "Software"), to deal in
|
|
||||||
* the Software without restriction, including without limitation the rights to
|
|
||||||
* use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
|
||||||
* the Software, and to permit persons to whom the Software is furnished to do so,
|
|
||||||
* subject to the following conditions:
|
|
||||||
*
|
|
||||||
* The above copyright notice and this permission notice shall be included in all
|
|
||||||
* copies or substantial portions of the Software.
|
|
||||||
*
|
|
||||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
|
||||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
|
||||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
|
||||||
* IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
|
||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export class ExecValidationNotFoundError extends Error {
|
|
||||||
constructor(execPath: string, isAbsolute: boolean) {
|
|
||||||
super(`User Exec command "${execPath}" not found on host.`);
|
|
||||||
let message = `User Exec command "${execPath}" not found on host.`;
|
|
||||||
|
|
||||||
if (!isAbsolute) {
|
|
||||||
message += ` Please ensure binary is found in PATH or use absolute path to binary in Kubeconfig`;
|
|
||||||
}
|
|
||||||
this.message = message;
|
|
||||||
this.name = this.constructor.name;
|
|
||||||
Error.captureStackTrace(this, this.constructor);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -20,92 +20,108 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { CustomResourceDefinition } from "../endpoints";
|
import { CustomResourceDefinition } from "../endpoints";
|
||||||
import type { KubeObjectMetadata } from "../kube-object";
|
|
||||||
|
|
||||||
describe("Crds", () => {
|
describe("Crds", () => {
|
||||||
describe("getVersion", () => {
|
describe("getVersion", () => {
|
||||||
it("should get the first version name from the list of versions", () => {
|
it("should throw if none of the versions are served", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as KubeObjectMetadata,
|
metadata: {
|
||||||
|
name: "foo",
|
||||||
|
resourceVersion: "12345",
|
||||||
|
uid: "12345",
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "123",
|
||||||
|
served: false,
|
||||||
|
storage: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1234",
|
||||||
|
served: false,
|
||||||
|
storage: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
expect(() => crd.getVersion()).toThrowError("Failed to find a version for CustomResourceDefinition foo");
|
||||||
versions: [
|
});
|
||||||
{
|
|
||||||
name: "123",
|
it("should should get the version that is both served and stored", () => {
|
||||||
served: false,
|
const crd = new CustomResourceDefinition({
|
||||||
storage: false,
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
}
|
kind: "CustomResourceDefinition",
|
||||||
]
|
metadata: {
|
||||||
} as any;
|
name: "foo",
|
||||||
|
resourceVersion: "12345",
|
||||||
|
uid: "12345",
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "123",
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1234",
|
||||||
|
served: false,
|
||||||
|
storage: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
expect(crd.getVersion()).toBe("123");
|
expect(crd.getVersion()).toBe("123");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the first version name from the list of versions (length 2)", () => {
|
it("should should get the version that is both served and stored even with version field", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "apiextensions.k8s.io/v1",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as KubeObjectMetadata,
|
metadata: {
|
||||||
|
name: "foo",
|
||||||
|
resourceVersion: "12345",
|
||||||
|
uid: "12345",
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
version: "abc",
|
||||||
|
versions: [
|
||||||
|
{
|
||||||
|
name: "123",
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "1234",
|
||||||
|
served: false,
|
||||||
|
storage: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
|
||||||
versions: [
|
|
||||||
{
|
|
||||||
name: "123",
|
|
||||||
served: false,
|
|
||||||
storage: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1234",
|
|
||||||
served: false,
|
|
||||||
storage: false,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
expect(crd.getVersion()).toBe("123");
|
expect(crd.getVersion()).toBe("123");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should get the first version name from the list of versions (length 2) even with version field", () => {
|
it("should get the version name from the version field", () => {
|
||||||
const crd = new CustomResourceDefinition({
|
const crd = new CustomResourceDefinition({
|
||||||
apiVersion: "foo",
|
apiVersion: "apiextensions.k8s.io/v1beta1",
|
||||||
kind: "CustomResourceDefinition",
|
kind: "CustomResourceDefinition",
|
||||||
metadata: {} as KubeObjectMetadata,
|
metadata: {
|
||||||
|
name: "foo",
|
||||||
|
resourceVersion: "12345",
|
||||||
|
uid: "12345",
|
||||||
|
},
|
||||||
|
spec: {
|
||||||
|
version: "abc",
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
crd.spec = {
|
|
||||||
version: "abc",
|
|
||||||
versions: [
|
|
||||||
{
|
|
||||||
name: "123",
|
|
||||||
served: false,
|
|
||||||
storage: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "1234",
|
|
||||||
served: false,
|
|
||||||
storage: false,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
expect(crd.getVersion()).toBe("123");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should get the first version name from the version field", () => {
|
|
||||||
const crd = new CustomResourceDefinition({
|
|
||||||
apiVersion: "foo",
|
|
||||||
kind: "CustomResourceDefinition",
|
|
||||||
metadata: {} as KubeObjectMetadata,
|
|
||||||
});
|
|
||||||
|
|
||||||
crd.spec = {
|
|
||||||
version: "abc"
|
|
||||||
} as any;
|
|
||||||
|
|
||||||
expect(crd.getVersion()).toBe("abc");
|
expect(crd.getVersion()).toBe("abc");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,10 +19,11 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { KubeObject } from "../kube-object";
|
import { KubeCreationError, KubeObject } from "../kube-object";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import { crdResourcesURL } from "../../routes";
|
import { crdResourcesURL } from "../../routes";
|
||||||
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
|
||||||
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
|
|
||||||
type AdditionalPrinterColumnsCommon = {
|
type AdditionalPrinterColumnsCommon = {
|
||||||
name: string;
|
name: string;
|
||||||
@ -39,10 +40,21 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
|
|||||||
JSONPath: string;
|
JSONPath: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export interface CRDVersion {
|
||||||
|
name: string;
|
||||||
|
served: boolean;
|
||||||
|
storage: boolean;
|
||||||
|
schema?: object; // required in v1 but not present in v1beta
|
||||||
|
additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface CustomResourceDefinition {
|
export interface CustomResourceDefinition {
|
||||||
spec: {
|
spec: {
|
||||||
group: string;
|
group: string;
|
||||||
version?: string; // deprecated in v1 api
|
/**
|
||||||
|
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||||
|
*/
|
||||||
|
version?: string;
|
||||||
names: {
|
names: {
|
||||||
plural: string;
|
plural: string;
|
||||||
singular: string;
|
singular: string;
|
||||||
@ -50,19 +62,19 @@ export interface CustomResourceDefinition {
|
|||||||
listKind: string;
|
listKind: string;
|
||||||
};
|
};
|
||||||
scope: "Namespaced" | "Cluster" | string;
|
scope: "Namespaced" | "Cluster" | string;
|
||||||
validation?: any;
|
/**
|
||||||
versions?: {
|
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||||
name: string;
|
*/
|
||||||
served: boolean;
|
validation?: object;
|
||||||
storage: boolean;
|
versions?: CRDVersion[];
|
||||||
schema?: unknown; // required in v1 but not present in v1beta
|
|
||||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1[]
|
|
||||||
}[];
|
|
||||||
conversion: {
|
conversion: {
|
||||||
strategy?: string;
|
strategy?: string;
|
||||||
webhook?: any;
|
webhook?: any;
|
||||||
};
|
};
|
||||||
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
|
/**
|
||||||
|
* @deprecated for apiextensions.k8s.io/v1 but used previously
|
||||||
|
*/
|
||||||
|
additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
|
||||||
};
|
};
|
||||||
status: {
|
status: {
|
||||||
conditions: {
|
conditions: {
|
||||||
@ -83,11 +95,23 @@ export interface CustomResourceDefinition {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CRDApiData extends KubeJsonApiData {
|
||||||
|
spec: object; // TODO: make better
|
||||||
|
}
|
||||||
|
|
||||||
export class CustomResourceDefinition extends KubeObject {
|
export class CustomResourceDefinition extends KubeObject {
|
||||||
static kind = "CustomResourceDefinition";
|
static kind = "CustomResourceDefinition";
|
||||||
static namespaced = false;
|
static namespaced = false;
|
||||||
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
|
||||||
|
|
||||||
|
constructor(data: CRDApiData) {
|
||||||
|
super(data);
|
||||||
|
|
||||||
|
if (!data.spec || typeof data.spec !== "object") {
|
||||||
|
throw new KubeCreationError("Cannot create a CustomResourceDefinition from an object without spec", data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getResourceUrl() {
|
getResourceUrl() {
|
||||||
return crdResourcesURL({
|
return crdResourcesURL({
|
||||||
params: {
|
params: {
|
||||||
@ -125,9 +149,36 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
return this.spec.scope;
|
return this.spec.scope;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getPreferedVersion(): CRDVersion {
|
||||||
|
// Prefer the modern `versions` over the legacy `version`
|
||||||
|
if (this.spec.versions) {
|
||||||
|
for (const version of this.spec.versions) {
|
||||||
|
/**
|
||||||
|
* If the version is not served then 404 errors will occur
|
||||||
|
* We should also prefer the storage version
|
||||||
|
*/
|
||||||
|
if (version.served && version.storage) {
|
||||||
|
return version;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (this.spec.version) {
|
||||||
|
const { additionalPrinterColumns: apc } = this.spec;
|
||||||
|
const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc}) => ({ ...apc, jsonPath: JSONPath }));
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: this.spec.version,
|
||||||
|
served: true,
|
||||||
|
storage: true,
|
||||||
|
schema: this.spec.validation,
|
||||||
|
additionalPrinterColumns,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
getVersion() {
|
getVersion() {
|
||||||
// v1 has removed the spec.version property, if it is present it must match the first version
|
return this.getPreferedVersion().name;
|
||||||
return this.spec.versions?.[0]?.name ?? this.spec.version;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isNamespaced() {
|
isNamespaced() {
|
||||||
@ -147,17 +198,14 @@ export class CustomResourceDefinition extends KubeObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] {
|
||||||
const columns = this.spec.versions?.find(a => this.getVersion() == a.name)?.additionalPrinterColumns
|
const columns = this.getPreferedVersion().additionalPrinterColumns ?? [];
|
||||||
?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape
|
|
||||||
?? [];
|
|
||||||
|
|
||||||
return columns
|
return columns
|
||||||
.filter(column => column.name != "Age")
|
.filter(column => column.name != "Age" && (ignorePriority || !column.priority));
|
||||||
.filter(column => ignorePriority ? true : !column.priority);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
getValidation() {
|
getValidation() {
|
||||||
return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2);
|
return JSON.stringify(this.getPreferedVersion().schema, null, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
getConditions() {
|
getConditions() {
|
||||||
|
|||||||
@ -56,6 +56,21 @@ export interface INodeMetrics<T = IMetrics> {
|
|||||||
fsSize: T;
|
fsSize: T;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface NodeTaint {
|
||||||
|
key: string;
|
||||||
|
value?: string;
|
||||||
|
effect: string;
|
||||||
|
timeAdded: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatNodeTaint(taint: NodeTaint): string {
|
||||||
|
if (taint.value) {
|
||||||
|
return `${taint.key}=${taint.value}:${taint.effect}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${taint.key}:${taint.effect}`;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Node {
|
export interface Node {
|
||||||
spec: {
|
spec: {
|
||||||
podCIDR?: string;
|
podCIDR?: string;
|
||||||
@ -65,12 +80,7 @@ export interface Node {
|
|||||||
* @deprecated see https://issues.k8s.io/61966
|
* @deprecated see https://issues.k8s.io/61966
|
||||||
*/
|
*/
|
||||||
externalID?: string;
|
externalID?: string;
|
||||||
taints?: {
|
taints?: NodeTaint[];
|
||||||
key: string;
|
|
||||||
value: string;
|
|
||||||
effect: string;
|
|
||||||
timeAdded: string;
|
|
||||||
}[];
|
|
||||||
unschedulable?: boolean;
|
unschedulable?: boolean;
|
||||||
};
|
};
|
||||||
status: {
|
status: {
|
||||||
|
|||||||
@ -27,8 +27,7 @@ import { stringify } from "querystring";
|
|||||||
import { EventEmitter } from "../../common/event-emitter";
|
import { EventEmitter } from "../../common/event-emitter";
|
||||||
import logger from "../../common/logger";
|
import logger from "../../common/logger";
|
||||||
|
|
||||||
export interface JsonApiData {
|
export interface JsonApiData {}
|
||||||
}
|
|
||||||
|
|
||||||
export interface JsonApiError {
|
export interface JsonApiError {
|
||||||
code?: number;
|
code?: number;
|
||||||
|
|||||||
@ -19,24 +19,16 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { KubeConfig, V1Node, V1Pod } from "@kubernetes/client-node";
|
import { KubeConfig } from "@kubernetes/client-node";
|
||||||
import fse from "fs-extra";
|
import fse from "fs-extra";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import yaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import logger from "../main/logger";
|
import logger from "../main/logger";
|
||||||
import commandExists from "command-exists";
|
|
||||||
import { ExecValidationNotFoundError } from "./custom-errors";
|
|
||||||
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
import { Cluster, Context, newClusters, newContexts, newUsers, User } from "@kubernetes/client-node/dist/config_types";
|
||||||
import { resolvePath } from "./utils";
|
import { resolvePath } from "./utils";
|
||||||
import Joi from "joi";
|
import Joi from "joi";
|
||||||
|
|
||||||
export type KubeConfigValidationOpts = {
|
|
||||||
validateCluster?: boolean;
|
|
||||||
validateUser?: boolean;
|
|
||||||
validateExec?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
|
export const kubeConfigDefaultPath = path.join(os.homedir(), ".kube", "config");
|
||||||
|
|
||||||
export function loadConfigFromFileSync(filePath: string): ConfigResult {
|
export function loadConfigFromFileSync(filePath: string): ConfigResult {
|
||||||
@ -86,35 +78,34 @@ const contextSchema = Joi.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
const kubeConfigSchema = Joi
|
const kubeConfigSchema = Joi.object({
|
||||||
.object({
|
users: Joi
|
||||||
users: Joi
|
.array()
|
||||||
.array()
|
.items(userSchema)
|
||||||
.items(userSchema)
|
.optional(),
|
||||||
.optional(),
|
clusters: Joi
|
||||||
clusters: Joi
|
.array()
|
||||||
.array()
|
.items(clusterSchema)
|
||||||
.items(clusterSchema)
|
.optional(),
|
||||||
.optional(),
|
contexts: Joi
|
||||||
contexts: Joi
|
.array()
|
||||||
.array()
|
.items(contextSchema)
|
||||||
.items(contextSchema)
|
.optional(),
|
||||||
.optional(),
|
"current-context": Joi
|
||||||
"current-context": Joi
|
.string()
|
||||||
.string()
|
.min(1)
|
||||||
.min(1)
|
.optional(),
|
||||||
.optional(),
|
})
|
||||||
})
|
|
||||||
.required();
|
.required();
|
||||||
|
|
||||||
export interface KubeConfigOptions {
|
interface KubeConfigOptions {
|
||||||
clusters: Cluster[];
|
clusters: Cluster[];
|
||||||
users: User[];
|
users: User[];
|
||||||
contexts: Context[];
|
contexts: Context[];
|
||||||
currentContext?: string;
|
currentContext?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface OptionsResult {
|
interface OptionsResult {
|
||||||
options: KubeConfigOptions;
|
options: KubeConfigOptions;
|
||||||
error: Joi.ValidationError;
|
error: Joi.ValidationError;
|
||||||
}
|
}
|
||||||
@ -132,7 +123,12 @@ function loadToOptions(rawYaml: string): OptionsResult {
|
|||||||
arrays: true,
|
arrays: true,
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const { clusters: rawClusters, users: rawUsers, contexts: rawContexts, "current-context": currentContext } = value ?? {};
|
const {
|
||||||
|
clusters: rawClusters,
|
||||||
|
users: rawUsers,
|
||||||
|
contexts: rawContexts,
|
||||||
|
"current-context": currentContext,
|
||||||
|
} = value ?? {};
|
||||||
const clusters = newClusters(rawClusters);
|
const clusters = newClusters(rawClusters);
|
||||||
const users = newUsers(rawUsers);
|
const users = newUsers(rawUsers);
|
||||||
const contexts = newContexts(rawContexts);
|
const contexts = newContexts(rawContexts);
|
||||||
@ -175,66 +171,78 @@ export interface SplitConfigEntry {
|
|||||||
* Breaks kube config into several configs. Each context as it own KubeConfig object
|
* Breaks kube config into several configs. Each context as it own KubeConfig object
|
||||||
*/
|
*/
|
||||||
export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] {
|
export function splitConfig(kubeConfig: KubeConfig): SplitConfigEntry[] {
|
||||||
const { contexts = [] } = kubeConfig;
|
return kubeConfig.getContexts().map(ctx => {
|
||||||
|
|
||||||
return contexts.map(context => {
|
|
||||||
const config = new KubeConfig();
|
const config = new KubeConfig();
|
||||||
|
const cluster = kubeConfig.getCluster(ctx.cluster);
|
||||||
|
const user = kubeConfig.getUser(ctx.user);
|
||||||
|
const context = kubeConfig.getContextObject(ctx.name);
|
||||||
|
|
||||||
config.clusters = [kubeConfig.getCluster(context.cluster)].filter(Boolean);
|
if (cluster) {
|
||||||
config.users = [kubeConfig.getUser(context.user)].filter(Boolean);
|
config.addCluster(cluster);
|
||||||
config.contexts = [kubeConfig.getContextObject(context.name)].filter(Boolean);
|
}
|
||||||
config.setCurrentContext(context.name);
|
|
||||||
|
if (user) {
|
||||||
|
config.addUser(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context) {
|
||||||
|
config.addContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
config.setCurrentContext(ctx.name);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
config,
|
config,
|
||||||
error: validateKubeConfig(config, context.name)?.toString(),
|
error: validateKubeConfig(config, ctx.name)?.toString(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pretty format the object as human readable yaml, such as would be on the filesystem
|
||||||
|
* @param kubeConfig The kubeconfig object to format as pretty yaml
|
||||||
|
* @returns The yaml representation of the kubeconfig object
|
||||||
|
*/
|
||||||
export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
|
export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
|
||||||
|
const clusters = kubeConfig.clusters.map(cluster => ({
|
||||||
|
name: cluster.name,
|
||||||
|
cluster: {
|
||||||
|
"certificate-authority-data": cluster.caData,
|
||||||
|
"certificate-authority": cluster.caFile,
|
||||||
|
server: cluster.server,
|
||||||
|
"insecure-skip-tls-verify": cluster.skipTLSVerify
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const contexts = kubeConfig.contexts.map(context => ({
|
||||||
|
name: context.name,
|
||||||
|
context: {
|
||||||
|
cluster: context.cluster,
|
||||||
|
user: context.user,
|
||||||
|
namespace: context.namespace
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
const users = kubeConfig.users.map(user => ({
|
||||||
|
name: user.name,
|
||||||
|
user: {
|
||||||
|
"client-certificate-data": user.certData,
|
||||||
|
"client-certificate": user.certFile,
|
||||||
|
"client-key-data": user.keyData,
|
||||||
|
"client-key": user.keyFile,
|
||||||
|
"auth-provider": user.authProvider,
|
||||||
|
exec: user.exec,
|
||||||
|
token: user.token,
|
||||||
|
username: user.username,
|
||||||
|
password: user.password
|
||||||
|
}
|
||||||
|
}));
|
||||||
const config = {
|
const config = {
|
||||||
apiVersion: "v1",
|
apiVersion: "v1",
|
||||||
kind: "Config",
|
kind: "Config",
|
||||||
preferences: {},
|
preferences: {},
|
||||||
"current-context": kubeConfig.currentContext,
|
"current-context": kubeConfig.currentContext,
|
||||||
clusters: kubeConfig.clusters.map(cluster => {
|
clusters,
|
||||||
return {
|
contexts,
|
||||||
name: cluster.name,
|
users,
|
||||||
cluster: {
|
|
||||||
"certificate-authority-data": cluster.caData,
|
|
||||||
"certificate-authority": cluster.caFile,
|
|
||||||
server: cluster.server,
|
|
||||||
"insecure-skip-tls-verify": cluster.skipTLSVerify
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
contexts: kubeConfig.contexts.map(context => {
|
|
||||||
return {
|
|
||||||
name: context.name,
|
|
||||||
context: {
|
|
||||||
cluster: context.cluster,
|
|
||||||
user: context.user,
|
|
||||||
namespace: context.namespace
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
users: kubeConfig.users.map(user => {
|
|
||||||
return {
|
|
||||||
name: user.name,
|
|
||||||
user: {
|
|
||||||
"client-certificate-data": user.certData,
|
|
||||||
"client-certificate": user.certFile,
|
|
||||||
"client-key-data": user.keyData,
|
|
||||||
"client-key": user.keyFile,
|
|
||||||
"auth-provider": user.authProvider,
|
|
||||||
exec: user.exec,
|
|
||||||
token: user.token,
|
|
||||||
username: user.username,
|
|
||||||
password: user.password
|
|
||||||
}
|
|
||||||
};
|
|
||||||
})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
logger.debug("Dumping KubeConfig:", config);
|
logger.debug("Dumping KubeConfig:", config);
|
||||||
@ -243,70 +251,25 @@ export function dumpConfigYaml(kubeConfig: Partial<KubeConfig>): string {
|
|||||||
return yaml.safeDump(config, { skipInvalid: true });
|
return yaml.safeDump(config, { skipInvalid: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function podHasIssues(pod: V1Pod) {
|
|
||||||
// Logic adapted from dashboard
|
|
||||||
const notReady = !!pod.status.conditions.find(condition => {
|
|
||||||
return condition.type == "Ready" && condition.status !== "True";
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
notReady ||
|
|
||||||
pod.status.phase !== "Running" ||
|
|
||||||
pod.spec.priority > 500000 // We're interested in high priority pods events regardless of their running status
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getNodeWarningConditions(node: V1Node) {
|
|
||||||
return node.status?.conditions?.filter(c =>
|
|
||||||
c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades"
|
|
||||||
) ?? [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
* Checks if `config` has valid `Context`, `User`, `Cluster`, and `exec` fields (if present when required)
|
||||||
*
|
*
|
||||||
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
|
* Note: This function returns an error instead of throwing it, returning `undefined` if the validation passes
|
||||||
*/
|
*/
|
||||||
export function validateKubeConfig(config: KubeConfig, contextName: string, validationOpts: KubeConfigValidationOpts = {}): Error | undefined {
|
export function validateKubeConfig(config: KubeConfig, contextName: string): Error | undefined {
|
||||||
try {
|
const contextObject = config.getContextObject(contextName);
|
||||||
// we only receive a single context, cluster & user object here so lets validate them as this
|
|
||||||
// will be called when we add a new cluster to Lens
|
|
||||||
|
|
||||||
const { validateUser = true, validateCluster = true, validateExec = true } = validationOpts;
|
if (!contextObject) {
|
||||||
|
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
||||||
const contextObject = config.getContextObject(contextName);
|
|
||||||
|
|
||||||
// Validate the Context Object
|
|
||||||
if (!contextObject) {
|
|
||||||
return new Error(`No valid context object provided in kubeconfig for context '${contextName}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the Cluster Object
|
|
||||||
if (validateCluster && !config.getCluster(contextObject.cluster)) {
|
|
||||||
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = config.getUser(contextObject.user);
|
|
||||||
|
|
||||||
// Validate the User Object
|
|
||||||
if (validateUser && !user) {
|
|
||||||
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate exec command if present
|
|
||||||
if (validateExec && user?.exec) {
|
|
||||||
const execCommand = user.exec["command"];
|
|
||||||
// check if the command is absolute or not
|
|
||||||
const isAbsolute = path.isAbsolute(execCommand);
|
|
||||||
|
|
||||||
// validate the exec struct in the user object, start with the command field
|
|
||||||
if (!commandExists.sync(execCommand)) {
|
|
||||||
return new ExecValidationNotFoundError(execCommand, isAbsolute);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
} catch (error) {
|
|
||||||
return error;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!config.getCluster(contextObject.cluster)) {
|
||||||
|
return new Error(`No valid cluster object provided in kubeconfig for context '${contextName}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!config.getUser(contextObject.user)) {
|
||||||
|
return new Error(`No valid user object provided in kubeconfig for context '${contextName}'`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,9 +23,10 @@ import moment from "moment-timezone";
|
|||||||
import path from "path";
|
import path from "path";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import { ThemeStore } from "../../renderer/theme.store";
|
import { ThemeStore } from "../../renderer/theme.store";
|
||||||
import { ObservableToggleSet } from "../utils";
|
import { getAppVersion, ObservableToggleSet } from "../utils";
|
||||||
import type {monaco} from "react-monaco-editor";
|
import type {monaco} from "react-monaco-editor";
|
||||||
import merge from "lodash/merge";
|
import merge from "lodash/merge";
|
||||||
|
import { SemVer } from "semver";
|
||||||
|
|
||||||
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
export interface KubeconfigSyncEntry extends KubeconfigSyncValue {
|
||||||
filePath: string;
|
filePath: string;
|
||||||
@ -260,6 +261,32 @@ const editorConfiguration: PreferenceDescription<EditorConfiguration, EditorConf
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateChannels = new Map([
|
||||||
|
["latest", {
|
||||||
|
label: "Stable"
|
||||||
|
}],
|
||||||
|
["beta", {
|
||||||
|
label: "Beta"
|
||||||
|
}],
|
||||||
|
["alpha", {
|
||||||
|
label: "Alpha"
|
||||||
|
}],
|
||||||
|
]);
|
||||||
|
const defaultUpdateChannel = new SemVer(getAppVersion()).prerelease[0]?.toString() || "latest";
|
||||||
|
|
||||||
|
const updateChannel: PreferenceDescription<string> = {
|
||||||
|
fromStore(val) {
|
||||||
|
return updateChannels.has(val) ? val : defaultUpdateChannel;
|
||||||
|
},
|
||||||
|
toStore(val) {
|
||||||
|
if (!updateChannels.has(val) || val === defaultUpdateChannel) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return val;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
|
type PreferencesModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<infer T, any> ? T : never;
|
||||||
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
type UserStoreModelType<field extends keyof typeof DESCRIPTORS> = typeof DESCRIPTORS[field] extends PreferenceDescription<any, infer T> ? T : never;
|
||||||
|
|
||||||
@ -288,4 +315,9 @@ export const DESCRIPTORS = {
|
|||||||
syncKubeconfigEntries,
|
syncKubeconfigEntries,
|
||||||
editorConfiguration,
|
editorConfiguration,
|
||||||
terminalCopyOnSelect,
|
terminalCopyOnSelect,
|
||||||
|
updateChannel,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CONSTANTS = {
|
||||||
|
updateChannels,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { app, ipcMain } from "electron";
|
import { app, ipcMain } from "electron";
|
||||||
import semver from "semver";
|
import semver, { SemVer } from "semver";
|
||||||
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
import { action, computed, observable, reaction, makeObservable } from "mobx";
|
||||||
import { BaseStore } from "../base-store";
|
import { BaseStore } from "../base-store";
|
||||||
import migrations from "../../migrations/user-store";
|
import migrations from "../../migrations/user-store";
|
||||||
@ -75,6 +75,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
@observable downloadBinariesPath?: string;
|
@observable downloadBinariesPath?: string;
|
||||||
@observable kubectlBinariesPath?: string;
|
@observable kubectlBinariesPath?: string;
|
||||||
@observable terminalCopyOnSelect: boolean;
|
@observable terminalCopyOnSelect: boolean;
|
||||||
|
@observable updateChannel?: string;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download kubectl binaries matching cluster version
|
* Download kubectl binaries matching cluster version
|
||||||
@ -106,6 +107,8 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
return this.shell || process.env.SHELL || process.env.PTYSHELL;
|
return this.shell || process.env.SHELL || process.env.PTYSHELL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readonly isAllowedToDowngrade = new SemVer(getAppVersion()).prerelease[0] !== "latest";
|
||||||
|
|
||||||
startMainReactions() {
|
startMainReactions() {
|
||||||
// track telemetry availability
|
// track telemetry availability
|
||||||
reaction(() => this.allowTelemetry, allowed => {
|
reaction(() => this.allowTelemetry, allowed => {
|
||||||
@ -218,6 +221,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
this.syncKubeconfigEntries.replace(DESCRIPTORS.syncKubeconfigEntries.fromStore(preferences?.syncKubeconfigEntries));
|
||||||
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
this.editorConfiguration = DESCRIPTORS.editorConfiguration.fromStore(preferences?.editorConfiguration);
|
||||||
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
this.terminalCopyOnSelect = DESCRIPTORS.terminalCopyOnSelect.fromStore(preferences?.terminalCopyOnSelect);
|
||||||
|
this.updateChannel = DESCRIPTORS.updateChannel.fromStore(preferences?.updateChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
toJSON(): UserStoreModel {
|
toJSON(): UserStoreModel {
|
||||||
@ -240,6 +244,7 @@ export class UserStore extends BaseStore<UserStoreModel> /* implements UserStore
|
|||||||
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
syncKubeconfigEntries: DESCRIPTORS.syncKubeconfigEntries.toStore(this.syncKubeconfigEntries),
|
||||||
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
editorConfiguration: DESCRIPTORS.editorConfiguration.toStore(this.editorConfiguration),
|
||||||
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
terminalCopyOnSelect: DESCRIPTORS.terminalCopyOnSelect.toStore(this.terminalCopyOnSelect),
|
||||||
|
updateChannel: DESCRIPTORS.updateChannel.toStore(this.updateChannel),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -19,5 +19,22 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export * from "../../common/catalog-entities";
|
export {
|
||||||
|
KubernetesCluster,
|
||||||
|
kubernetesClusterCategory,
|
||||||
|
GeneralEntity,
|
||||||
|
WebLink,
|
||||||
|
} from "../../common/catalog-entities";
|
||||||
|
|
||||||
|
export type {
|
||||||
|
KubernetesClusterPrometheusMetrics,
|
||||||
|
KubernetesClusterSpec,
|
||||||
|
KubernetesClusterMetadata,
|
||||||
|
WebLinkSpec,
|
||||||
|
WebLinkStatus,
|
||||||
|
WebLinkStatusPhase,
|
||||||
|
KubernetesClusterStatusPhase,
|
||||||
|
KubernetesClusterStatus,
|
||||||
|
} from "../../common/catalog-entities";
|
||||||
|
|
||||||
export * from "../../common/catalog/catalog-entity";
|
export * from "../../common/catalog/catalog-entity";
|
||||||
|
|||||||
@ -27,8 +27,8 @@ import { areArgsUpdateAvailableToBackchannel, AutoUpdateLogPrefix, broadcastMess
|
|||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import { ipcMain } from "electron";
|
import { ipcMain } from "electron";
|
||||||
import { nextUpdateChannel } from "./utils/update-channel";
|
import { nextUpdateChannel } from "./utils/update-channel";
|
||||||
|
import { UserStore } from "../common/user-store";
|
||||||
|
|
||||||
const updateChannel = autoUpdater.channel;
|
|
||||||
let installVersion: null | string = null;
|
let installVersion: null | string = null;
|
||||||
|
|
||||||
export function isAutoUpdateEnabled() {
|
export function isAutoUpdateEnabled() {
|
||||||
@ -58,9 +58,13 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userStore = UserStore.getInstance();
|
||||||
|
|
||||||
autoUpdater.logger = logger;
|
autoUpdater.logger = logger;
|
||||||
autoUpdater.autoDownload = false;
|
autoUpdater.autoDownload = false;
|
||||||
autoUpdater.autoInstallOnAppQuit = false;
|
autoUpdater.autoInstallOnAppQuit = false;
|
||||||
|
autoUpdater.channel = userStore.updateChannel;
|
||||||
|
autoUpdater.allowDowngrade = userStore.isAllowedToDowngrade;
|
||||||
|
|
||||||
autoUpdater
|
autoUpdater
|
||||||
.on("update-available", (info: UpdateInfo) => {
|
.on("update-available", (info: UpdateInfo) => {
|
||||||
@ -104,7 +108,7 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.on("update-not-available", () => {
|
.on("update-not-available", () => {
|
||||||
const nextChannel = nextUpdateChannel(updateChannel, autoUpdater.channel);
|
const nextChannel = nextUpdateChannel(userStore.updateChannel, autoUpdater.channel);
|
||||||
|
|
||||||
logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`);
|
logger.info(`${AutoUpdateLogPrefix}: update not available from ${autoUpdater.channel}, will check ${nextChannel} channel next`);
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import logger from "./logger";
|
|||||||
import { apiKubePrefix } from "../common/vars";
|
import { apiKubePrefix } from "../common/vars";
|
||||||
import { getClusterIdFromHost, Singleton } from "../common/utils";
|
import { getClusterIdFromHost, Singleton } from "../common/utils";
|
||||||
import { catalogEntityRegistry } from "./catalog";
|
import { catalogEntityRegistry } from "./catalog";
|
||||||
import { KubernetesCluster, KubernetesClusterPrometheusMetrics, KubernetesClusterStatusPhase } from "../common/catalog-entities/kubernetes-cluster";
|
import { KubernetesCluster, KubernetesClusterPrometheusMetrics, LensKubernetesClusterStatus } from "../common/catalog-entities/kubernetes-cluster";
|
||||||
import { ipcMainOn } from "../common/ipc";
|
import { ipcMainOn } from "../common/ipc";
|
||||||
import { once } from "lodash";
|
import { once } from "lodash";
|
||||||
import { ClusterStore } from "../common/cluster-store";
|
import { ClusterStore } from "../common/cluster-store";
|
||||||
@ -35,6 +35,8 @@ import type { ClusterId } from "../common/cluster-types";
|
|||||||
|
|
||||||
const logPrefix = "[CLUSTER-MANAGER]:";
|
const logPrefix = "[CLUSTER-MANAGER]:";
|
||||||
|
|
||||||
|
const lensSpecificClusterStatuses: Set<string> = new Set(Object.values(LensKubernetesClusterStatus));
|
||||||
|
|
||||||
export class ClusterManager extends Singleton {
|
export class ClusterManager extends Singleton {
|
||||||
private store = ClusterStore.getInstance();
|
private store = ClusterStore.getInstance();
|
||||||
deleting = observable.set<ClusterId>();
|
deleting = observable.set<ClusterId>();
|
||||||
@ -90,6 +92,8 @@ export class ClusterManager extends Singleton {
|
|||||||
|
|
||||||
@action
|
@action
|
||||||
protected updateCatalog(clusters: Cluster[]) {
|
protected updateCatalog(clusters: Cluster[]) {
|
||||||
|
logger.debug("[CLUSTER-MANAGER]: updating catalog from cluster store");
|
||||||
|
|
||||||
for (const cluster of clusters) {
|
for (const cluster of clusters) {
|
||||||
this.updateEntityFromCluster(cluster);
|
this.updateEntityFromCluster(cluster);
|
||||||
}
|
}
|
||||||
@ -144,23 +148,28 @@ export class ClusterManager extends Singleton {
|
|||||||
@action
|
@action
|
||||||
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
|
protected updateEntityStatus(entity: KubernetesCluster, cluster?: Cluster) {
|
||||||
if (this.deleting.has(entity.getId())) {
|
if (this.deleting.has(entity.getId())) {
|
||||||
entity.status.phase = "deleting";
|
entity.status.phase = LensKubernetesClusterStatus.DELETING;
|
||||||
entity.status.enabled = false;
|
entity.status.enabled = false;
|
||||||
} else {
|
} else {
|
||||||
entity.status.phase = ((): KubernetesClusterStatusPhase => {
|
entity.status.phase = (() => {
|
||||||
if (!cluster) {
|
if (!cluster) {
|
||||||
return "disconnected";
|
return LensKubernetesClusterStatus.DISCONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cluster.accessible) {
|
if (cluster.accessible) {
|
||||||
return "connected";
|
return LensKubernetesClusterStatus.CONNECTED;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!cluster.disconnected) {
|
if (!cluster.disconnected) {
|
||||||
return "connecting";
|
return LensKubernetesClusterStatus.CONNECTING;
|
||||||
}
|
}
|
||||||
|
|
||||||
return "disconnected";
|
// Extensions are not allowed to use the Lens specific status phases
|
||||||
|
if (!lensSpecificClusterStatuses.has(entity?.status?.phase)) {
|
||||||
|
return entity.status.phase;
|
||||||
|
}
|
||||||
|
|
||||||
|
return LensKubernetesClusterStatus.DISCONNECTED;
|
||||||
})();
|
})();
|
||||||
|
|
||||||
entity.status.enabled = true;
|
entity.status.enabled = true;
|
||||||
@ -272,7 +281,9 @@ export function catalogEntityFromCluster(cluster: Cluster) {
|
|||||||
icon: {}
|
icon: {}
|
||||||
},
|
},
|
||||||
status: {
|
status: {
|
||||||
phase: cluster.disconnected ? "disconnected" : "connected",
|
phase: cluster.disconnected
|
||||||
|
? LensKubernetesClusterStatus.DISCONNECTED
|
||||||
|
: LensKubernetesClusterStatus.CONNECTED,
|
||||||
reason: "",
|
reason: "",
|
||||||
message: "",
|
message: "",
|
||||||
active: !cluster.disconnected
|
active: !cluster.disconnected
|
||||||
|
|||||||
@ -394,7 +394,6 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
@action disconnect() {
|
@action disconnect() {
|
||||||
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
|
||||||
this.unbindEvents();
|
this.unbindEvents();
|
||||||
this.contextHandler?.stopServer();
|
this.contextHandler?.stopServer();
|
||||||
this.disconnected = true;
|
this.disconnected = true;
|
||||||
@ -405,6 +404,7 @@ export class Cluster implements ClusterModel, ClusterState {
|
|||||||
this.allowedNamespaces = [];
|
this.allowedNamespaces = [];
|
||||||
this.resourceAccessStatuses.clear();
|
this.resourceAccessStatuses.clear();
|
||||||
this.pushState();
|
this.pushState();
|
||||||
|
logger.info(`[CLUSTER]: disconnect`, this.getMeta());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -43,8 +43,8 @@ export async function listReleases(pathToKubeconfig: string, namespace?: string)
|
|||||||
});
|
});
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -72,8 +72,8 @@ export async function installChart(chart: string, values: any, name: string | un
|
|||||||
namespace
|
namespace
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
} finally {
|
} finally {
|
||||||
await fse.unlink(fileName);
|
await fse.unlink(fileName);
|
||||||
}
|
}
|
||||||
@ -93,8 +93,8 @@ export async function upgradeRelease(name: string, chart: string, values: any, n
|
|||||||
log: stdout,
|
log: stdout,
|
||||||
release: getRelease(name, namespace, cluster)
|
release: getRelease(name, namespace, cluster)
|
||||||
};
|
};
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
} finally {
|
} finally {
|
||||||
await fse.unlink(fileName);
|
await fse.unlink(fileName);
|
||||||
}
|
}
|
||||||
@ -111,8 +111,8 @@ export async function getRelease(name: string, namespace: string, cluster: Clust
|
|||||||
release.resources = await getResources(name, namespace, cluster);
|
release.resources = await getResources(name, namespace, cluster);
|
||||||
|
|
||||||
return release;
|
return release;
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,8 +122,8 @@ export async function deleteRelease(name: string, namespace: string, pathToKubec
|
|||||||
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" delete ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -139,8 +139,8 @@ export async function getValues(name: string, { namespace, all = false, pathToKu
|
|||||||
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" get values ${name} ${all ? "--all" : ""} --output yaml --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -150,8 +150,8 @@ export async function getHistory(name: string, namespace: string, pathToKubeconf
|
|||||||
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" history ${name} --output json --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return JSON.parse(stdout);
|
return JSON.parse(stdout);
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,8 +161,8 @@ export async function rollback(name: string, namespace: string, revision: number
|
|||||||
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
const { stdout } = await promiseExec(`"${helm}" rollback ${name} ${revision} --namespace ${namespace} --kubeconfig ${pathToKubeconfig}`);
|
||||||
|
|
||||||
return stdout;
|
return stdout;
|
||||||
} catch ({ stderr }) {
|
} catch (error) {
|
||||||
throw stderr;
|
throw error?.stderr || error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -147,13 +147,18 @@ export function buildMenu(windowManager: WindowManager) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]),
|
]),
|
||||||
|
|
||||||
{ type: "separator" },
|
{ type: "separator" },
|
||||||
{
|
|
||||||
role: "close",
|
...(isMac ? [
|
||||||
label: "Close Window"
|
{
|
||||||
},
|
role: "close",
|
||||||
|
label: "Close Window",
|
||||||
|
accelerator: "Shift+Cmd+W"
|
||||||
|
}
|
||||||
|
] as MenuItemConstructorOptions[] : []),
|
||||||
|
|
||||||
...ignoreOnMac([
|
...ignoreOnMac([
|
||||||
{ type: "separator" },
|
|
||||||
{
|
{
|
||||||
label: "Exit",
|
label: "Exit",
|
||||||
accelerator: "Alt+F4",
|
accelerator: "Alt+F4",
|
||||||
|
|||||||
@ -41,7 +41,7 @@ export class HelmApiRoute {
|
|||||||
|
|
||||||
respondJson(response, chart);
|
respondJson(response, chart);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error getting chart", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -53,7 +53,7 @@ export class HelmApiRoute {
|
|||||||
|
|
||||||
respondJson(response, values);
|
respondJson(response, values);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error getting chart values", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,7 +66,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result, 201);
|
respondJson(response, result, 201);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error installing chart", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -79,7 +79,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result);
|
respondJson(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error updating chart", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +92,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result);
|
respondJson(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error rolling back chart", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,7 +105,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result);
|
respondJson(response, result);
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error listing release", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result);
|
respondJson(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error getting release", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -132,7 +132,7 @@ export class HelmApiRoute {
|
|||||||
respondText(response, result);
|
respondText(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error getting release values", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +145,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result);
|
respondJson(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error getting release history", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ export class HelmApiRoute {
|
|||||||
respondJson(response, result);
|
respondJson(response, result);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug(error);
|
logger.debug(error);
|
||||||
respondText(response, error, 422);
|
respondText(response, error?.toString() || "Error deleting release", 422);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,17 +33,17 @@ describe("nextUpdateChannel", () => {
|
|||||||
expect(nextUpdateChannel("latest", "alpha")).toEqual("beta");
|
expect(nextUpdateChannel("latest", "alpha")).toEqual("beta");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns rc if current channel is beta", () => {
|
it("returns latest if current channel is beta", () => {
|
||||||
expect(nextUpdateChannel("alpha", "beta")).toEqual("rc");
|
expect(nextUpdateChannel("alpha", "beta")).toEqual("latest");
|
||||||
expect(nextUpdateChannel("beta", "beta")).toEqual("rc");
|
expect(nextUpdateChannel("beta", "beta")).toEqual("latest");
|
||||||
expect(nextUpdateChannel("rc", "beta")).toEqual("rc");
|
expect(nextUpdateChannel("rc", "beta")).toEqual("latest");
|
||||||
expect(nextUpdateChannel("latest", "beta")).toEqual("rc");
|
expect(nextUpdateChannel("latest", "beta")).toEqual("latest");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns latest if current channel is rc", () => {
|
it("returns default if current channel is unknown", () => {
|
||||||
expect(nextUpdateChannel("alpha", "rc")).toEqual("latest");
|
expect(nextUpdateChannel("alpha", "rc")).toEqual("alpha");
|
||||||
expect(nextUpdateChannel("beta", "rc")).toEqual("latest");
|
expect(nextUpdateChannel("beta", "rc")).toEqual("beta");
|
||||||
expect(nextUpdateChannel("rc", "rc")).toEqual("latest");
|
expect(nextUpdateChannel("rc", "rc")).toEqual("rc");
|
||||||
expect(nextUpdateChannel("latest", "rc")).toEqual("latest");
|
expect(nextUpdateChannel("latest", "rc")).toEqual("latest");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -19,14 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the next update channel from the current updating channel
|
||||||
|
* @param defaultChannel The default (initial) channel to check
|
||||||
|
* @param channel The current channel that did not have a new version associated with it
|
||||||
|
* @returns The channel name of the next release version
|
||||||
|
*/
|
||||||
export function nextUpdateChannel(defaultChannel: string, channel: string): string {
|
export function nextUpdateChannel(defaultChannel: string, channel: string): string {
|
||||||
if (channel === "alpha") {
|
switch (channel) {
|
||||||
return "beta";
|
case "alpha":
|
||||||
} else if (channel === "beta") {
|
return "beta";
|
||||||
return "rc";
|
case "beta":
|
||||||
} else if (channel === "rc") {
|
return "latest"; // there is no RC currently
|
||||||
return "latest";
|
default:
|
||||||
} else {
|
return defaultChannel;
|
||||||
return defaultChannel;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,8 +23,8 @@ import "./helm-chart-details.scss";
|
|||||||
|
|
||||||
import React, { Component } from "react";
|
import React, { Component } from "react";
|
||||||
import { getChartDetails, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
import { getChartDetails, HelmChart } from "../../../common/k8s-api/endpoints/helm-charts.api";
|
||||||
import { observable, autorun, makeObservable } from "mobx";
|
import { observable, makeObservable, reaction } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { disposeOnUnmount, observer } from "mobx-react";
|
||||||
import { Drawer, DrawerItem } from "../drawer";
|
import { Drawer, DrawerItem } from "../drawer";
|
||||||
import { boundMethod, stopPropagation } from "../../utils";
|
import { boundMethod, stopPropagation } from "../../utils";
|
||||||
import { MarkdownViewer } from "../markdown-viewer";
|
import { MarkdownViewer } from "../markdown-viewer";
|
||||||
@ -64,20 +64,24 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
this.abortController?.abort();
|
this.abortController?.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
chartUpdater = autorun(() => {
|
componentDidMount() {
|
||||||
this.selectedChart = null;
|
disposeOnUnmount(this, [
|
||||||
const { chart: { name, repo, version } } = this.props;
|
reaction(() => this.props.chart, async ({ name, repo, version }) => {
|
||||||
|
try {
|
||||||
|
const { readme, versions } = await getChartDetails(repo, name, { version });
|
||||||
|
|
||||||
getChartDetails(repo, name, { version })
|
this.readme = readme;
|
||||||
.then(result => {
|
this.chartVersions = versions;
|
||||||
this.readme = result.readme;
|
this.selectedChart = versions[0];
|
||||||
this.chartVersions = result.versions;
|
} catch (error) {
|
||||||
this.selectedChart = result.versions[0];
|
this.error = error;
|
||||||
})
|
this.selectedChart = null;
|
||||||
.catch(error => {
|
}
|
||||||
this.error = error;
|
}, {
|
||||||
});
|
fireImmediately: true,
|
||||||
});
|
}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
async onVersionChange({ value: chart }: SelectOption<HelmChart>) {
|
async onVersionChange({ value: chart }: SelectOption<HelmChart>) {
|
||||||
@ -135,7 +139,7 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
value: chart,
|
value: chart,
|
||||||
}))}
|
}))}
|
||||||
isOptionDisabled={({ value: chart }) => chart.deprecated}
|
isOptionDisabled={({ value: chart }) => chart.deprecated}
|
||||||
value={selectedChart.getVersion()}
|
value={selectedChart}
|
||||||
onChange={onVersionChange}
|
onChange={onVersionChange}
|
||||||
/>
|
/>
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
@ -170,10 +174,6 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
if (!this.selectedChart) {
|
|
||||||
return <Spinner center />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.error) {
|
if (this.error) {
|
||||||
return (
|
return (
|
||||||
<div className="box grow">
|
<div className="box grow">
|
||||||
@ -182,6 +182,10 @@ export class HelmChartDetails extends Component<Props> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this.selectedChart) {
|
||||||
|
return <Spinner center />;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="box grow">
|
<div className="box grow">
|
||||||
{this.renderIntroduction()}
|
{this.renderIntroduction()}
|
||||||
|
|||||||
@ -26,6 +26,11 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-error {
|
||||||
|
color: red;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
@include release-status-bgs;
|
@include release-status-bgs;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -56,12 +56,13 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class ReleaseDetails extends Component<Props> {
|
export class ReleaseDetails extends Component<Props> {
|
||||||
@observable details: IReleaseDetails;
|
@observable details: IReleaseDetails | null = null;
|
||||||
@observable values = "";
|
@observable values = "";
|
||||||
@observable valuesLoading = false;
|
@observable valuesLoading = false;
|
||||||
@observable showOnlyUserSuppliedValues = true;
|
@observable showOnlyUserSuppliedValues = true;
|
||||||
@observable saving = false;
|
@observable saving = false;
|
||||||
@observable releaseSecret: Secret;
|
@observable releaseSecret: Secret;
|
||||||
|
@observable error?: string = undefined;
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
disposeOnUnmount(this, [
|
disposeOnUnmount(this, [
|
||||||
@ -96,9 +97,13 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
|
|
||||||
async loadDetails() {
|
async loadDetails() {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
|
|
||||||
this.details = null;
|
try {
|
||||||
this.details = await getRelease(release.getName(), release.getNs());
|
this.details = null;
|
||||||
|
this.details = await getRelease(release.getName(), release.getNs());
|
||||||
|
} catch (error) {
|
||||||
|
this.error = `Failed to get release details: ${error}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadValues() {
|
async loadValues() {
|
||||||
@ -237,11 +242,18 @@ export class ReleaseDetails extends Component<Props> {
|
|||||||
|
|
||||||
renderContent() {
|
renderContent() {
|
||||||
const { release } = this.props;
|
const { release } = this.props;
|
||||||
const { details } = this;
|
|
||||||
|
|
||||||
if (!release) return null;
|
if (!release) return null;
|
||||||
|
|
||||||
if (!details) {
|
if (this.error) {
|
||||||
|
return (
|
||||||
|
<div className="loading-error">
|
||||||
|
{this.error}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.details) {
|
||||||
return <Spinner center/>;
|
return <Spinner center/>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -79,6 +79,7 @@ export class HelmReleaseMenu extends React.Component<Props> {
|
|||||||
{...menuProps}
|
{...menuProps}
|
||||||
className={cssNames("HelmReleaseMenu", className)}
|
className={cssNames("HelmReleaseMenu", className)}
|
||||||
removeAction={this.remove}
|
removeAction={this.remove}
|
||||||
|
removeConfirmationMessage={() => <p>Remove Helm Release <b>{release.name}</b>?</p>}
|
||||||
>
|
>
|
||||||
{this.renderContent()}
|
{this.renderContent()}
|
||||||
</MenuActions>
|
</MenuActions>
|
||||||
|
|||||||
@ -30,7 +30,7 @@ import { Badge } from "../badge";
|
|||||||
import { ResourceMetrics } from "../resource-metrics";
|
import { ResourceMetrics } from "../resource-metrics";
|
||||||
import { podsStore } from "../+workloads-pods/pods.store";
|
import { podsStore } from "../+workloads-pods/pods.store";
|
||||||
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
import type { KubeObjectDetailsProps } from "../kube-object-details";
|
||||||
import { getMetricsByNodeNames, IClusterMetrics, Node } from "../../../common/k8s-api/endpoints";
|
import { formatNodeTaint, getMetricsByNodeNames, IClusterMetrics, Node } from "../../../common/k8s-api/endpoints";
|
||||||
import { NodeCharts } from "./node-charts";
|
import { NodeCharts } from "./node-charts";
|
||||||
import { makeObservable, observable, reaction } from "mobx";
|
import { makeObservable, observable, reaction } from "mobx";
|
||||||
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
import { PodDetailsList } from "../+workloads-pods/pod-details-list";
|
||||||
@ -132,11 +132,7 @@ export class NodeDetails extends React.Component<Props> {
|
|||||||
/>
|
/>
|
||||||
{taints.length > 0 && (
|
{taints.length > 0 && (
|
||||||
<DrawerItem name="Taints" labelsOnly>
|
<DrawerItem name="Taints" labelsOnly>
|
||||||
{
|
{taints.map(taint => <Badge key={taint.key} label={formatNodeTaint(taint)} />)}
|
||||||
taints.map(({ key, effect, value }) => (
|
|
||||||
<Badge key={key} label={`${key}=${value}:${effect}`} />
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</DrawerItem>
|
</DrawerItem>
|
||||||
)}
|
)}
|
||||||
{conditions &&
|
{conditions &&
|
||||||
|
|||||||
@ -27,7 +27,7 @@ import { cssNames, interval } from "../../utils";
|
|||||||
import { TabLayout } from "../layout/tab-layout";
|
import { TabLayout } from "../layout/tab-layout";
|
||||||
import { nodesStore } from "./nodes.store";
|
import { nodesStore } from "./nodes.store";
|
||||||
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
import { KubeObjectListLayout } from "../kube-object-list-layout";
|
||||||
import { getMetricsForAllNodes, INodeMetrics, Node } from "../../../common/k8s-api/endpoints/nodes.api";
|
import { formatNodeTaint, getMetricsForAllNodes, INodeMetrics, Node } from "../../../common/k8s-api/endpoints/nodes.api";
|
||||||
import { LineProgress } from "../line-progress";
|
import { LineProgress } from "../line-progress";
|
||||||
import { bytesToUnits } from "../../../common/utils/convertMemory";
|
import { bytesToUnits } from "../../../common/utils/convertMemory";
|
||||||
import { Tooltip, TooltipPosition } from "../tooltip";
|
import { Tooltip, TooltipPosition } from "../tooltip";
|
||||||
@ -227,6 +227,7 @@ export class Nodes extends React.Component<Props> {
|
|||||||
]}
|
]}
|
||||||
renderTableContents={node => {
|
renderTableContents={node => {
|
||||||
const tooltipId = `node-taints-${node.getId()}`;
|
const tooltipId = `node-taints-${node.getId()}`;
|
||||||
|
const taints = node.getTaints();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
<Badge flat key="name" label={node.getName()} tooltip={node.getName()} />,
|
<Badge flat key="name" label={node.getName()} tooltip={node.getName()} />,
|
||||||
@ -235,9 +236,9 @@ export class Nodes extends React.Component<Props> {
|
|||||||
this.renderMemoryUsage(node),
|
this.renderMemoryUsage(node),
|
||||||
this.renderDiskUsage(node),
|
this.renderDiskUsage(node),
|
||||||
<>
|
<>
|
||||||
<span id={tooltipId}>{node.getTaints().length}</span>
|
<span id={tooltipId}>{taints.length}</span>
|
||||||
<Tooltip targetId={tooltipId} tooltipOnParentHover={true} style={{ whiteSpace: "pre-line" }}>
|
<Tooltip targetId={tooltipId} tooltipOnParentHover={true} style={{ whiteSpace: "pre-line" }}>
|
||||||
{node.getTaints().map(({ key, value, effect }) => `${key}=${value}:${effect}`).join("\n")}
|
{taints.map(formatNodeTaint).join("\n")}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>,
|
</>,
|
||||||
node.getRoleLabels(),
|
node.getRoleLabels(),
|
||||||
|
|||||||
@ -29,11 +29,16 @@ import { Input } from "../input";
|
|||||||
import { isWindows } from "../../../common/vars";
|
import { isWindows } from "../../../common/vars";
|
||||||
import { FormSwitch, Switcher } from "../switch";
|
import { FormSwitch, Switcher } from "../switch";
|
||||||
import moment from "moment-timezone";
|
import moment from "moment-timezone";
|
||||||
|
import { CONSTANTS } from "../../../common/user-store/preferences-helpers";
|
||||||
|
|
||||||
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
const timezoneOptions: SelectOption<string>[] = moment.tz.names().map(zone => ({
|
||||||
label: zone,
|
label: zone,
|
||||||
value: zone,
|
value: zone,
|
||||||
}));
|
}));
|
||||||
|
const updateChannelOptions: SelectOption<string>[] = Array.from(
|
||||||
|
CONSTANTS.updateChannels.entries(),
|
||||||
|
([value, { label }]) => ({ value, label }),
|
||||||
|
);
|
||||||
|
|
||||||
export const Application = observer(() => {
|
export const Application = observer(() => {
|
||||||
const defaultShell = process.env.SHELL
|
const defaultShell = process.env.SHELL
|
||||||
@ -104,6 +109,18 @@ export const Application = observer(() => {
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
|
<section id="update-channel">
|
||||||
|
<SubTitle title="Update Channel"/>
|
||||||
|
<Select
|
||||||
|
options={updateChannelOptions}
|
||||||
|
value={UserStore.getInstance().updateChannel}
|
||||||
|
onChange={({ value }: SelectOption) => UserStore.getInstance().updateChannel = value}
|
||||||
|
themeName="lens"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
<section id="locale">
|
<section id="locale">
|
||||||
<SubTitle title="Locale Timezone" />
|
<SubTitle title="Locale Timezone" />
|
||||||
<Select
|
<Select
|
||||||
|
|||||||
@ -19,14 +19,23 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export = commandExists;
|
import { formatNodeTaint } from "../../../common/k8s-api/endpoints";
|
||||||
|
|
||||||
declare function commandExists(commandName: string): Promise<string>;
|
describe("formatNodeTaint tests", () => {
|
||||||
declare function commandExists(
|
it("should use value if defined", () => {
|
||||||
commandName: string,
|
expect(formatNodeTaint({
|
||||||
cb: (error: null, exists: boolean) => void
|
effect: "Foo",
|
||||||
): void;
|
key: "hello",
|
||||||
|
timeAdded: "pre",
|
||||||
|
value: "a"
|
||||||
|
})).toBe("hello=a:Foo");
|
||||||
|
});
|
||||||
|
|
||||||
declare namespace commandExists {
|
it("should not use value if not defined", () => {
|
||||||
function sync(commandName: string): boolean;
|
expect(formatNodeTaint({
|
||||||
}
|
effect: "Foo",
|
||||||
|
key: "hello",
|
||||||
|
timeAdded: "pre",
|
||||||
|
})).toBe("hello:Foo");
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -29,6 +29,7 @@ import { Tab, TabProps } from "../tabs";
|
|||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { Menu, MenuItem } from "../menu";
|
import { Menu, MenuItem } from "../menu";
|
||||||
import { observable, makeObservable } from "mobx";
|
import { observable, makeObservable } from "mobx";
|
||||||
|
import { isMac } from "../../../common/vars";
|
||||||
|
|
||||||
export interface DockTabProps extends TabProps<DockTabModel> {
|
export interface DockTabProps extends TabProps<DockTabModel> {
|
||||||
moreActions?: React.ReactNode;
|
moreActions?: React.ReactNode;
|
||||||
@ -94,7 +95,7 @@ export class DockTab extends React.Component<DockTabProps> {
|
|||||||
{!pinned && (
|
{!pinned && (
|
||||||
<Icon
|
<Icon
|
||||||
small material="close"
|
small material="close"
|
||||||
tooltip="Close (Ctrl+Shift+W)"
|
tooltip={`Close ${isMac ? "⌘+W" : "Ctrl+W"}`}
|
||||||
onClick={prevDefault(this.close)}
|
onClick={prevDefault(this.close)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -27,6 +27,16 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
&:not(:focus-within) .DockTab.active {
|
||||||
|
&::after {
|
||||||
|
color: var(--halfGray);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
color: var(--line-color-active);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.isOpen {
|
&.isOpen {
|
||||||
&.fullSize {
|
&.fullSize {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
@ -46,19 +46,31 @@ interface Props {
|
|||||||
|
|
||||||
@observer
|
@observer
|
||||||
export class Dock extends React.Component<Props> {
|
export class Dock extends React.Component<Props> {
|
||||||
onKeydown = (evt: React.KeyboardEvent<HTMLElement>) => {
|
private element = React.createRef<HTMLDivElement>();
|
||||||
const { close, closeTab, selectedTab } = dockStore;
|
|
||||||
|
|
||||||
if (!selectedTab) return;
|
componentDidMount() {
|
||||||
const { code, ctrlKey, shiftKey } = evt.nativeEvent;
|
document.addEventListener("keydown", this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
document.removeEventListener("keydown", this.onKeyDown);
|
||||||
|
}
|
||||||
|
|
||||||
|
onKeyDown = (evt: KeyboardEvent) => {
|
||||||
|
const { close, selectedTab, closeTab } = dockStore;
|
||||||
|
const { code, ctrlKey, metaKey, shiftKey } = evt;
|
||||||
|
// Determine if user working inside <Dock/> or using any other areas in app
|
||||||
|
const dockIsFocused = this.element?.current.contains(document.activeElement);
|
||||||
|
|
||||||
|
if (!selectedTab || !dockIsFocused) return;
|
||||||
|
|
||||||
if (shiftKey && code === "Escape") {
|
if (shiftKey && code === "Escape") {
|
||||||
close();
|
close();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctrlKey && code === "KeyW") {
|
if ((ctrlKey && code === "KeyW") || (metaKey && code === "KeyW")) {
|
||||||
if (selectedTab.pinned) close();
|
closeTab(selectedTab.id);
|
||||||
else closeTab(selectedTab.id);
|
this.element?.current.focus(); // Avoid loosing focus when closing tab
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -67,6 +79,7 @@ export class Dock extends React.Component<Props> {
|
|||||||
|
|
||||||
open();
|
open();
|
||||||
selectTab(tab.id);
|
selectTab(tab.id);
|
||||||
|
this.element?.current.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
renderTab(tab: DockTab) {
|
renderTab(tab: DockTab) {
|
||||||
@ -105,7 +118,7 @@ export class Dock extends React.Component<Props> {
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cssNames("Dock", className, { isOpen, fullSize })}
|
className={cssNames("Dock", className, { isOpen, fullSize })}
|
||||||
onKeyDown={this.onKeydown}
|
ref={this.element}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
>
|
>
|
||||||
<ResizingAnchor
|
<ResizingAnchor
|
||||||
|
|||||||
@ -222,7 +222,7 @@ export class Terminal {
|
|||||||
};
|
};
|
||||||
|
|
||||||
keyHandler = (evt: KeyboardEvent): boolean => {
|
keyHandler = (evt: KeyboardEvent): boolean => {
|
||||||
const { code, ctrlKey, type, metaKey } = evt;
|
const { code, ctrlKey, metaKey } = evt;
|
||||||
|
|
||||||
// Handle custom hotkey bindings
|
// Handle custom hotkey bindings
|
||||||
if (ctrlKey) {
|
if (ctrlKey) {
|
||||||
@ -248,11 +248,6 @@ export class Terminal {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pass the event above in DOM for <Dock/> to handle common actions
|
|
||||||
if (!evt.defaultPrevented) {
|
|
||||||
this.elem.dispatchEvent(new KeyboardEvent(type, evt));
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,6 +30,7 @@ import { navigate } from "../../navigation";
|
|||||||
import { cssNames, IClassName } from "../../utils";
|
import { cssNames, IClassName } from "../../utils";
|
||||||
import { Icon } from "../icon";
|
import { Icon } from "../icon";
|
||||||
import { HotbarIcon } from "./hotbar-icon";
|
import { HotbarIcon } from "./hotbar-icon";
|
||||||
|
import { LensKubernetesClusterStatus } from "../../../common/catalog-entities/kubernetes-cluster";
|
||||||
|
|
||||||
interface Props extends DOMAttributes<HTMLElement> {
|
interface Props extends DOMAttributes<HTMLElement> {
|
||||||
entity: CatalogEntity;
|
entity: CatalogEntity;
|
||||||
@ -77,7 +78,7 @@ export class HotbarEntityIcon extends React.Component<Props> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const className = cssNames("led", { online: this.props.entity.status.phase == "connected"}); // TODO: make it more generic
|
const className = cssNames("led", { online: this.props.entity.status.phase === LensKubernetesClusterStatus.CONNECTED}); // TODO: make it more generic
|
||||||
|
|
||||||
return <div className={className} />;
|
return <div className={className} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -193,7 +193,7 @@ export class Sidebar extends React.Component<Props> {
|
|||||||
source={metadata.source}
|
source={metadata.source}
|
||||||
src={spec.icon?.src}
|
src={spec.icon?.src}
|
||||||
className="mr-5"
|
className="mr-5"
|
||||||
onClick={() => navigate(routes.clusterURL())}
|
onClick={() => navigate("/")}
|
||||||
/>
|
/>
|
||||||
<div className={styles.clusterName}>
|
<div className={styles.clusterName}>
|
||||||
{metadata.name}
|
{metadata.name}
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
grid-template-rows: var(--main-layout-header);
|
grid-template-rows: var(--main-layout-header);
|
||||||
grid-template-areas: "title controls";
|
grid-template-areas: "title controls";
|
||||||
background-color: var(--layoutBackground);
|
background-color: var(--layoutBackground);
|
||||||
z-index: 1;
|
z-index: 2;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
grid-area: topbar;
|
grid-area: topbar;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { StorageHelper } from "./storageHelper";
|
|||||||
import { ClusterStore } from "../../common/cluster-store";
|
import { ClusterStore } from "../../common/cluster-store";
|
||||||
import logger from "../../main/logger";
|
import logger from "../../main/logger";
|
||||||
import { getHostedClusterId, getPath } from "../../common/utils";
|
import { getHostedClusterId, getPath } from "../../common/utils";
|
||||||
|
import { isTestEnv } from "../../common/vars";
|
||||||
|
|
||||||
const storage = observable({
|
const storage = observable({
|
||||||
initialized: false,
|
initialized: false,
|
||||||
@ -62,7 +63,9 @@ export function createAppStorage<T>(key: string, defaultValue: T, clusterId?: st
|
|||||||
.then(data => storage.data = data)
|
.then(data => storage.data = data)
|
||||||
.catch(() => null) // ignore empty / non-existing / invalid json files
|
.catch(() => null) // ignore empty / non-existing / invalid json files
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
logger.info(`${logPrefix} loading finished for ${filePath}`);
|
if (!isTestEnv) {
|
||||||
|
logger.info(`${logPrefix} loading finished for ${filePath}`);
|
||||||
|
}
|
||||||
storage.loaded = true;
|
storage.loaded = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user