From b2616d4cf2678f0ffd46acb8cf6945a086336825 Mon Sep 17 00:00:00 2001 From: Sebastian Malton Date: Tue, 13 Apr 2021 20:06:11 -0400 Subject: [PATCH] Turn on strict mode for TS Signed-off-by: Sebastian Malton --- src/common/catalog-entity.ts | 2 +- src/common/cluster-store.ts | 33 +-- src/common/kube-helpers.ts | 95 ++++---- src/common/protocol-handler/error.ts | 2 + src/common/protocol-handler/router.ts | 2 +- src/common/user-store.ts | 6 +- src/common/utils/autobind.ts | 11 +- src/common/utils/debouncePromise.ts | 10 - src/common/utils/index.ts | 2 +- src/common/utils/remove-falsy.ts | 20 ++ src/extensions/cluster-feature.ts | 4 +- src/extensions/extension-discovery.ts | 21 +- src/extensions/extension-installer.ts | 2 +- src/extensions/extension-loader.ts | 52 +++-- src/extensions/extension-store.ts | 7 +- src/extensions/lens-renderer-extension.ts | 8 +- src/extensions/registries/command-registry.ts | 2 +- .../registries/kube-object-detail-registry.ts | 21 +- .../registries/kube-object-status-registry.ts | 2 +- .../registries/page-menu-registry.ts | 32 +-- src/extensions/registries/page-registry.ts | 64 +++--- .../registries/protocol-handler-registry.ts | 2 +- src/main/app-updater.ts | 2 +- .../base-cluster-detector.ts | 31 +-- .../cluster-detectors/cluster-id-detector.ts | 3 +- .../cluster-detectors/detector-registry.ts | 19 +- .../distribution-detector.ts | 137 ++++++----- .../cluster-detectors/last-seen-detector.ts | 4 +- .../cluster-detectors/nodes-count-detector.ts | 5 +- .../cluster-detectors/version-detector.ts | 1 - src/main/cluster-manager.ts | 12 +- src/main/cluster.ts | 76 ++++--- src/main/context-handler.ts | 81 ++++--- src/main/extension-filesystem.ts | 3 +- src/main/helm/helm-chart-manager.ts | 10 +- src/main/helm/helm-release-manager.ts | 4 +- src/main/helm/helm-repo-manager.ts | 97 ++++---- src/main/helm/helm-service.ts | 72 +++++- src/main/kube-auth-proxy.ts | 28 +-- src/main/kubeconfig-manager.ts | 6 +- src/main/kubectl.ts | 30 +-- src/main/lens-binary.ts | 31 +-- src/main/lens-proxy.ts | 41 ++-- src/main/menu.ts | 23 +- src/main/prometheus/helm.ts | 16 +- src/main/prometheus/lens.ts | 14 +- src/main/prometheus/operator.ts | 39 ++-- src/main/prometheus/provider-registry.ts | 27 ++- src/main/prometheus/stacklight.ts | 14 +- src/main/resource-applier.ts | 6 +- src/main/router.ts | 28 ++- src/main/routes/helm-route.ts | 64 +++++- src/main/routes/kubeconfig-route.ts | 45 +++- src/main/routes/metrics-route.ts | 92 +++++--- src/main/routes/port-forward-route.ts | 120 ++++++---- src/main/routes/resource-applier-route.ts | 14 +- src/main/shell-session/node-shell-session.ts | 23 +- src/main/shell-session/shell-session.ts | 12 +- src/main/tray.ts | 6 +- src/main/window-manager.ts | 31 +-- src/migrations/cluster-store/3.6.0-beta.1.ts | 4 +- .../api/__tests__/kube-api-parse.test.ts | 2 +- src/renderer/api/api-manager.ts | 31 ++- src/renderer/api/endpoints/cluster.api.ts | 69 +++--- src/renderer/api/endpoints/configmap.api.ts | 12 +- src/renderer/api/endpoints/crd.api.ts | 131 +++++------ src/renderer/api/endpoints/cron-job.api.ts | 110 ++++----- src/renderer/api/endpoints/daemon-set.api.ts | 106 +++++---- src/renderer/api/endpoints/deployment.api.ts | 214 +++++++++--------- src/renderer/api/endpoints/endpoint.api.ts | 36 +-- src/renderer/api/endpoints/events.api.ts | 26 +-- src/renderer/api/endpoints/helm-charts.api.ts | 88 ++++--- .../api/endpoints/helm-releases.api.ts | 26 ++- src/renderer/api/endpoints/hpa.api.ts | 69 +++--- src/renderer/api/endpoints/ingress.api.ts | 80 ++++--- src/renderer/api/endpoints/job.api.ts | 130 +++++------ src/renderer/api/endpoints/limit-range.api.ts | 16 +- src/renderer/api/endpoints/metrics.api.ts | 11 +- src/renderer/api/endpoints/namespaces.api.ts | 12 +- .../api/endpoints/network-policy.api.ts | 34 ++- src/renderer/api/endpoints/nodes.api.ts | 151 ++++++------ .../endpoints/persistent-volume-claims.api.ts | 67 +++--- .../api/endpoints/persistent-volume.api.ts | 94 ++++---- src/renderer/api/endpoints/pod-metrics.api.ts | 8 +- .../api/endpoints/poddisruptionbudget.api.ts | 39 ++-- src/renderer/api/endpoints/pods.api.ts | 198 ++++++++-------- .../api/endpoints/podsecuritypolicy.api.ts | 150 ++++++------ src/renderer/api/endpoints/replica-set.api.ts | 76 +++---- .../api/endpoints/resource-applier.api.ts | 26 +-- .../api/endpoints/resource-quota.api.ts | 46 ++-- .../api/endpoints/role-binding.api.ts | 4 +- src/renderer/api/endpoints/role.api.ts | 6 +- src/renderer/api/endpoints/secret.api.ts | 14 +- .../endpoints/selfsubjectrulesreviews.api.ts | 37 ++- .../api/endpoints/service-accounts.api.ts | 4 +- src/renderer/api/endpoints/service.api.ts | 77 +++---- .../api/endpoints/stateful-set.api.ts | 136 ++++++----- .../api/endpoints/storage-class.api.ts | 10 +- src/renderer/api/index.ts | 2 +- src/renderer/api/json-api.ts | 2 +- src/renderer/api/kube-api-parse.ts | 20 +- src/renderer/api/kube-api.ts | 78 ++++--- src/renderer/api/kube-json-api.ts | 26 +-- src/renderer/api/kube-object.ts | 56 ++--- src/renderer/api/kube-watch-api.ts | 15 +- src/renderer/api/terminal-api.ts | 2 +- src/renderer/api/websocket-api.ts | 14 +- src/renderer/api/workload-kube-object.ts | 14 +- src/renderer/bootstrap.tsx | 4 + .../components/+add-cluster/add-cluster.tsx | 95 ++++---- .../+apps-helm-charts/helm-chart-details.tsx | 45 ++-- .../+apps-helm-charts/helm-chart.store.ts | 2 +- .../components/+namespaces/namespace.store.ts | 11 +- .../cluster-manager/cluster-view.route.ts | 2 +- src/renderer/components/context.ts | 8 +- src/renderer/components/input/input.tsx | 34 +-- src/renderer/item.store.ts | 8 +- src/renderer/kube-object.store.ts | 74 +++--- src/renderer/navigation/helpers.ts | 2 +- src/renderer/navigation/page-param.ts | 10 +- .../utils/__tests__/storageHelper.test.ts | 6 +- src/renderer/utils/cssNames.ts | 6 +- src/renderer/utils/storageHelper.ts | 6 +- tsconfig.json | 5 +- webpack.renderer.ts | 5 +- 125 files changed, 2451 insertions(+), 2118 deletions(-) delete mode 100755 src/common/utils/debouncePromise.ts create mode 100644 src/common/utils/remove-falsy.ts diff --git a/src/common/catalog-entity.ts b/src/common/catalog-entity.ts index ad3f0e591e..40e57a4fc3 100644 --- a/src/common/catalog-entity.ts +++ b/src/common/catalog-entity.ts @@ -27,7 +27,7 @@ export type CatalogEntityMetadata = { labels: { [key: string]: string; } - [key: string]: string | object; + [key: string]: string | object | undefined; }; export type CatalogEntityStatus = { diff --git a/src/common/cluster-store.ts b/src/common/cluster-store.ts index d1f11d8372..9b8f574e2b 100644 --- a/src/common/cluster-store.ts +++ b/src/common/cluster-store.ts @@ -12,6 +12,7 @@ import { saveToAppFiles } from "./utils/saveToAppFiles"; import { KubeConfig } from "@kubernetes/client-node"; import { handleRequest, requestMain, subscribeToBroadcast, unsubscribeAllFromBroadcast } from "./ipc"; import { ResourceType } from "../renderer/components/+cluster-settings/components/cluster-metrics-setting"; +import { assert } from "./utils"; export interface ClusterIconUpload { clusterId: string; @@ -51,7 +52,7 @@ export interface ClusterModel { workspace?: string; /** User context in kubeconfig */ - contextName?: string; + contextName: string; /** Preferences */ preferences?: ClusterPreferences; @@ -106,7 +107,7 @@ export class ClusterStore extends BaseStore { return filePath; } - @observable activeCluster: ClusterId; + @observable activeCluster: ClusterId | null = null; @observable removedClusters = observable.map(); @observable clusters = observable.map(); @@ -218,14 +219,14 @@ export class ClusterStore extends BaseStore { } @action - setActive(clusterId: ClusterId) { - const cluster = this.clusters.get(clusterId); + setActive(clusterId?: ClusterId | null) { + const cluster = this.getById(clusterId); - if (!cluster?.enabled) { - clusterId = null; + if (!clusterId || !cluster?.enabled) { + this.activeCluster = null; + } else { + this.activeCluster = clusterId; } - - this.activeCluster = clusterId; } deactivate(id: ClusterId) { @@ -238,8 +239,8 @@ export class ClusterStore extends BaseStore { return this.clusters.size > 0; } - getById(id: ClusterId): Cluster | null { - return this.clusters.get(id) ?? null; + getById(id?: ClusterId | null): Cluster | null { + return (id ? this.clusters.get(id) : null) ?? null; } @action @@ -304,7 +305,7 @@ export class ClusterStore extends BaseStore { let cluster = currentClusters.get(clusterModel.id); if (cluster) { - cluster.updateModel(clusterModel); + Object.assign(cluster, clusterModel); } else { cluster = new Cluster(clusterModel); @@ -322,14 +323,14 @@ export class ClusterStore extends BaseStore { } }); - this.activeCluster = newClusters.get(activeCluster)?.enabled ? activeCluster : null; + this.activeCluster = activeCluster && newClusters.get(activeCluster)?.enabled ? activeCluster : null; this.clusters.replace(newClusters); this.removedClusters.replace(removedClusters); } toJSON(): ClusterStoreModel { return toJS({ - activeCluster: this.activeCluster, + activeCluster: this.activeCluster ?? undefined, clusters: this.clustersList.map(cluster => cluster.toJSON()), }, { recurseEverything: true @@ -354,6 +355,10 @@ export function getHostedClusterId() { return getClusterIdFromHost(location.host); } -export function getHostedCluster(): Cluster { +export function getHostedCluster(): Cluster | null { return clusterStore.getById(getHostedClusterId()); } + +export function getHostedClusterStrict(): Cluster { + return assert(getHostedCluster(), "only can get the hosted cluster in a cluster frame"); +} diff --git a/src/common/kube-helpers.ts b/src/common/kube-helpers.ts index c2a2a8df93..4eb9e94ab4 100644 --- a/src/common/kube-helpers.ts +++ b/src/common/kube-helpers.ts @@ -6,6 +6,7 @@ import yaml from "js-yaml"; import logger from "../main/logger"; import commandExists from "command-exists"; import { ExecValidationNotFoundError } from "./custom-errors"; +import { NotFalsy } from "./utils"; export type KubeConfigValidationOpts = { validateCluster?: boolean; @@ -26,10 +27,12 @@ function resolveTilde(filePath: string) { export function loadConfig(pathOrContent?: string): KubeConfig { const kc = new KubeConfig(); - if (fse.pathExistsSync(pathOrContent)) { - kc.loadFromFile(path.resolve(resolveTilde(pathOrContent))); - } else { - kc.loadFromString(pathOrContent); + if (pathOrContent) { + if (fse.pathExistsSync(pathOrContent)) { + kc.loadFromFile(path.resolve(resolveTilde(pathOrContent))); + } else { + kc.loadFromString(pathOrContent); + } } return kc; @@ -75,9 +78,9 @@ export function splitConfig(kubeConfig: KubeConfig): KubeConfig[] { kubeConfig.contexts.forEach(ctx => { const kc = new KubeConfig(); - kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(n => n); - kc.users = [kubeConfig.getUser(ctx.user)].filter(n => n); - kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(n => n); + kc.clusters = [kubeConfig.getCluster(ctx.cluster)].filter(NotFalsy); + kc.users = [kubeConfig.getUser(ctx.user)].filter(NotFalsy); + kc.contexts = [kubeConfig.getContextObject(ctx.name)].filter(NotFalsy); kc.setCurrentContext(ctx.name); configs.push(kc); @@ -92,43 +95,37 @@ export function dumpConfigYaml(kubeConfig: Partial): string { kind: "Config", preferences: {}, "current-context": kubeConfig.currentContext, - clusters: kubeConfig.clusters.map(cluster => { - return { - name: cluster.name, - 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 - } - }; - }) + 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 + } + })), + contexts: kubeConfig.contexts?.map(context => ({ + name: context.name, + context: { + cluster: context.cluster, + user: context.user, + namespace: context.namespace + } + })), + 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 + } + })) }; logger.debug("Dumping KubeConfig:", config); @@ -139,21 +136,21 @@ export function dumpConfigYaml(kubeConfig: Partial): string { export function podHasIssues(pod: V1Pod) { // Logic adapted from dashboard - const notReady = !!pod.status.conditions.find(condition => { + 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 prio pods events regardless of their running status + pod.status?.phase !== "Running" || + (pod.spec?.priority && 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 => + return node.status?.conditions?.filter(c => c.status.toLowerCase() === "true" && c.type !== "Ready" && c.type !== "HostUpgrades" - ); + ) ?? []; } /** diff --git a/src/common/protocol-handler/error.ts b/src/common/protocol-handler/error.ts index ebe7adccd7..3a49a2398f 100644 --- a/src/common/protocol-handler/error.ts +++ b/src/common/protocol-handler/error.ts @@ -31,6 +31,8 @@ export class RoutingError extends Error { return "no extension ID"; case RoutingErrorType.MISSING_EXTENSION: return "extension not found"; + default: + throw new TypeError("this.type is not RoutingErrorType"); } } } diff --git a/src/common/protocol-handler/router.ts b/src/common/protocol-handler/router.ts index 7b7659992f..13938c522d 100644 --- a/src/common/protocol-handler/router.ts +++ b/src/common/protocol-handler/router.ts @@ -72,7 +72,7 @@ export abstract class LensProtocolRouter extends Singleton { /** * find the most specific matching handler and call it - * @param routes the array of (path schemas, handler) paris to match against + * @param routes the array of (path schemas, handler) pairs to match against * @param url the url (in its current state) */ protected _route(routes: [string, RouteHandler][], url: Url, extensionName?: string): void { diff --git a/src/common/user-store.ts b/src/common/user-store.ts index c7ad21f988..1866e83fa7 100644 --- a/src/common/user-store.ts +++ b/src/common/user-store.ts @@ -75,7 +75,7 @@ export class UserStore extends BaseStore { // open at system start-up reaction(() => this.preferences.openAtLogin, openAtLogin => { - app.setLoginItemSettings({ + app.setLoginItemSettings({ openAtLogin, openAsHidden: true, args: ["--hidden"] @@ -102,11 +102,11 @@ export class UserStore extends BaseStore { @action setHiddenTableColumns(tableId: string, names: Set | string[]) { - this.preferences.hiddenTableColumns[tableId] = Array.from(names); + (this.preferences.hiddenTableColumns ??= {})[tableId] = Array.from(names); } getHiddenTableColumns(tableId: string): Set { - return new Set(this.preferences.hiddenTableColumns[tableId]); + return new Set(this.preferences.hiddenTableColumns?.[tableId]); } @action diff --git a/src/common/utils/autobind.ts b/src/common/utils/autobind.ts index b5c706e362..b1cfbc1ec2 100644 --- a/src/common/utils/autobind.ts +++ b/src/common/utils/autobind.ts @@ -4,8 +4,13 @@ type Constructor = new (...args: any[]) => T; export function autobind() { return function (target: Constructor | object, prop?: string, descriptor?: PropertyDescriptor) { - if (target instanceof Function) return bindClass(target); - else return bindMethod(target, prop, descriptor); + if (target instanceof Function) { + return bindClass(target); + } + + if (prop) { + return bindMethod(target, prop, descriptor); + } }; } @@ -25,7 +30,7 @@ function bindClass(constructor: T) { }); } -function bindMethod(target: object, prop?: string, descriptor?: PropertyDescriptor) { +function bindMethod(target: object, prop: string, descriptor?: PropertyDescriptor) { if (!descriptor || typeof descriptor.value !== "function") { throw new Error(`@autobind() must be used on class or method only`); } diff --git a/src/common/utils/debouncePromise.ts b/src/common/utils/debouncePromise.ts deleted file mode 100755 index b5ad88d000..0000000000 --- a/src/common/utils/debouncePromise.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Debouncing promise evaluation - -export function debouncePromise(func: (...args: F) => T | Promise, timeout = 0): (...args: F) => Promise { - let timer: NodeJS.Timeout; - - return (...params: any[]) => new Promise(resolve => { - clearTimeout(timer); - timer = global.setTimeout(() => resolve(func.apply(this, params)), timeout); - }); -} diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts index 6f26bab2da..3e79b00c49 100644 --- a/src/common/utils/index.ts +++ b/src/common/utils/index.ts @@ -8,7 +8,6 @@ export * from "./base64"; export * from "./camelCase"; export * from "./cloneJson"; export * from "./delay"; -export * from "./debouncePromise"; export * from "./defineGlobal"; export * from "./getRandId"; export * from "./splitArray"; @@ -19,3 +18,4 @@ export * from "./downloadFile"; export * from "./escapeRegExp"; export * from "./tar"; export * from "./type-narrowing"; +export * from "./remove-falsy"; diff --git a/src/common/utils/remove-falsy.ts b/src/common/utils/remove-falsy.ts new file mode 100644 index 0000000000..02636ed1af --- /dev/null +++ b/src/common/utils/remove-falsy.ts @@ -0,0 +1,20 @@ +import { AssertionError } from "assert"; + +export function NotFalsy(input: T | undefined | null | false | "" | 0): input is T { + return Boolean(input); +} + +export function SecondNotFalsy(input: readonly [T, V | undefined | null | false | "" | 0]): input is [T, V] { + return Boolean(input[1]); +} + +export function assert(input: T | undefined | null | false | "" | 0, message?: string): T { + if (!NotFalsy(input)) { + throw new AssertionError({ + actual: input, + message: message ?? "input should pass checker", + }); + } + + return input; +} diff --git a/src/extensions/cluster-feature.ts b/src/extensions/cluster-feature.ts index ad3a258b0e..a0fbdcdd9c 100644 --- a/src/extensions/cluster-feature.ts +++ b/src/extensions/cluster-feature.ts @@ -12,9 +12,9 @@ import { clusterStore } from "../common/cluster-store"; export interface ClusterFeatureStatus { /** feature's current version, as set by the implementation */ - currentVersion: string; + currentVersion: string | null; /** feature's latest version, as set by the implementation */ - latestVersion: string; + latestVersion: string | null; /** whether the feature is installed or not, as set by the implementation */ installed: boolean; /** whether the feature can be upgraded or not, as set by the implementation */ diff --git a/src/extensions/extension-discovery.ts b/src/extensions/extension-discovery.ts index 0994f89995..0ef97397e8 100644 --- a/src/extensions/extension-discovery.ts +++ b/src/extensions/extension-discovery.ts @@ -6,6 +6,7 @@ import { observable, reaction, toJS, when } from "mobx"; import os from "os"; import path from "path"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; +import { assert, NotFalsy } from "../common/utils"; import { getBundledExtensions } from "../common/utils/app-version"; import logger from "../main/logger"; import { extensionInstaller, PackageJson } from "./extension-installer"; @@ -52,7 +53,7 @@ const isDirectoryLike = (lstat: fs.Stats) => lstat.isDirectory() || lstat.isSymb * - "remove": When extension is removed. The event is of type LensExtensionId */ export class ExtensionDiscovery { - protected bundledFolderPath: string; + protected bundledFolderPath?: string; private loadStarted = false; private extensions: Map = new Map(); @@ -136,7 +137,7 @@ export class ExtensionDiscovery { depth: 1, ignoreInitial: true, // Try to wait until the file has been completely copied. - // The OS might emit an event for added file even it's not completely written to the filesysten. + // The OS might emit an event for added file even it's not completely written to the filesystem. awaitWriteFinish: { // Wait 300ms until the file size doesn't change to consider the file written. // For a small file like package.json this should be plenty of time. @@ -236,7 +237,7 @@ export class ExtensionDiscovery { /** * Uninstalls extension. * The application will detect the folder unlink and remove the extension from the UI automatically. - * @param extension Extension to unistall. + * @param extension Extension to uninstall. */ async uninstallExtension({ absolutePath, manifest }: InstalledExtension) { logger.info(`${logModule} Uninstalling ${manifest.name}`); @@ -260,7 +261,6 @@ export class ExtensionDiscovery { // fs.remove won't throw if path is missing await fs.remove(path.join(extensionInstaller.extensionPackagesRoot, "package-lock.json")); - try { // Verify write access to static/extensions, which is needed for symlinking await fs.access(this.inTreeFolderPath, fs.constants.W_OK); @@ -314,16 +314,11 @@ export class ExtensionDiscovery { * Returns InstalledExtension from path to package.json file. * Also updates this.packagesJson. */ - protected async getByManifest(manifestPath: string, { isBundled = false }: { - isBundled?: boolean; - } = {}): Promise { - let manifestJson: LensExtensionManifest; + protected async getByManifest(manifestPath: string, { isBundled = false }: { isBundled?: boolean; } = {}): Promise { + let manifestJson: LensExtensionManifest | undefined = undefined; try { - // check manifest file for existence - fs.accessSync(manifestPath, fs.constants.F_OK); - - manifestJson = __non_webpack_require__(manifestPath); + manifestJson = __non_webpack_require__(manifestPath) as LensExtensionManifest; const installedManifestPath = this.getInstalledManifestPath(manifestJson.name); const isEnabled = isBundled || extensionsStore.isEnabled(installedManifestPath); @@ -380,8 +375,8 @@ export class ExtensionDiscovery { } async loadBundledExtensions() { + const folderPath = assert(this.bundledFolderPath, "load() must be called before loadBundledExtensions()"); const extensions: InstalledExtension[] = []; - const folderPath = this.bundledFolderPath; const bundledExtensions = getBundledExtensions(); const paths = await fs.readdir(folderPath); diff --git a/src/extensions/extension-installer.ts b/src/extensions/extension-installer.ts index 04b78bbe1a..b467e3bfaf 100644 --- a/src/extensions/extension-installer.ts +++ b/src/extensions/extension-installer.ts @@ -76,7 +76,7 @@ export class ExtensionInstaller { }); let stderr = ""; - child.stderr.on("data", data => { + child.stderr?.on("data", data => { stderr += String(data); }); diff --git a/src/extensions/extension-loader.ts b/src/extensions/extension-loader.ts index d36123b95a..165478b9c3 100644 --- a/src/extensions/extension-loader.ts +++ b/src/extensions/extension-loader.ts @@ -3,7 +3,7 @@ import { EventEmitter } from "events"; import { isEqual } from "lodash"; import { action, computed, observable, reaction, toJS, when } from "mobx"; import path from "path"; -import { getHostedCluster } from "../common/cluster-store"; +import { getHostedClusterStrict } from "../common/cluster-store"; import { broadcastMessage, handleRequest, requestMain, subscribeToBroadcast } from "../common/ipc"; import logger from "../main/logger"; import type { InstalledExtension } from "./extension-discovery"; @@ -188,14 +188,16 @@ export class ExtensionLoader { loadOnMain() { logger.debug(`${logModule}: load on main`); - this.autoInitExtensions(async (extension: LensMainExtension) => { + this.autoInitExtensions(async extension => { + const mainExt = extension as LensMainExtension; + // Each .add returns a function to remove the item const removeItems = [ - registries.menuRegistry.add(extension.appMenus) + registries.menuRegistry.add(mainExt.appMenus) ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { - if (removedExtension.id === extension.id) { + if (removedExtension.id === mainExt.id) { removeItems.forEach(remove => { remove(); }); @@ -208,17 +210,19 @@ export class ExtensionLoader { loadOnClusterManagerRenderer() { logger.debug(`${logModule}: load on main renderer (cluster manager)`); - this.autoInitExtensions(async (extension: LensRendererExtension) => { + this.autoInitExtensions(async extension => { + const rendererExt = extension as LensRendererExtension; + const removeItems = [ - registries.globalPageRegistry.add(extension.globalPages, extension), - registries.globalPageMenuRegistry.add(extension.globalPageMenus, extension), - registries.appPreferenceRegistry.add(extension.appPreferences), - registries.statusBarRegistry.add(extension.statusBarItems), - registries.commandRegistry.add(extension.commands), + registries.globalPageRegistry.add(rendererExt.globalPages, rendererExt), + registries.globalPageMenuRegistry.add(rendererExt.globalPageMenus, rendererExt), + registries.appPreferenceRegistry.add(rendererExt.appPreferences), + registries.statusBarRegistry.add(rendererExt.statusBarItems), + registries.commandRegistry.add(rendererExt.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { - if (removedExtension.id === extension.id) { + if (removedExtension.id === rendererExt.id) { removeItems.forEach(remove => { remove(); }); @@ -231,24 +235,26 @@ export class ExtensionLoader { loadOnClusterRenderer() { logger.debug(`${logModule}: load on cluster renderer (dashboard)`); - const cluster = getHostedCluster(); + const cluster = getHostedClusterStrict(); - this.autoInitExtensions(async (extension: LensRendererExtension) => { - if (await extension.isEnabledForCluster(cluster) === false) { + this.autoInitExtensions(async extension => { + const rendererExt = extension as LensRendererExtension; + + if (await rendererExt.isEnabledForCluster(cluster) === false) { return []; } const removeItems = [ - registries.clusterPageRegistry.add(extension.clusterPages, extension), - registries.clusterPageMenuRegistry.add(extension.clusterPageMenus, extension), - registries.kubeObjectMenuRegistry.add(extension.kubeObjectMenuItems), - registries.kubeObjectDetailRegistry.add(extension.kubeObjectDetailItems), - registries.kubeObjectStatusRegistry.add(extension.kubeObjectStatusTexts), - registries.commandRegistry.add(extension.commands), + registries.clusterPageRegistry.add(rendererExt.clusterPages, rendererExt), + registries.clusterPageMenuRegistry.add(rendererExt.clusterPageMenus, rendererExt), + registries.kubeObjectMenuRegistry.add(rendererExt.kubeObjectMenuItems), + registries.kubeObjectDetailRegistry.add(rendererExt.kubeObjectDetailItems), + registries.kubeObjectStatusRegistry.add(rendererExt.kubeObjectStatusTexts), + registries.commandRegistry.add(rendererExt.commands), ]; this.events.on("remove", (removedExtension: LensRendererExtension) => { - if (removedExtension.id === extension.id) { + if (removedExtension.id === rendererExt.id) { removeItems.forEach(remove => { remove(); }); @@ -289,7 +295,7 @@ export class ExtensionLoader { }); } - protected requireExtension(extension: InstalledExtension): LensExtensionConstructor { + protected requireExtension(extension: InstalledExtension): LensExtensionConstructor | undefined { let extEntrypoint = ""; try { @@ -314,7 +320,7 @@ export class ExtensionLoader { } } - getExtension(extId: LensExtensionId): InstalledExtension { + getExtension(extId: LensExtensionId): InstalledExtension | undefined { return this.extensions.get(extId); } diff --git a/src/extensions/extension-store.ts b/src/extensions/extension-store.ts index c1a1e62bd8..0c9d9ded2e 100644 --- a/src/extensions/extension-store.ts +++ b/src/extensions/extension-store.ts @@ -1,9 +1,10 @@ import { BaseStore } from "../common/base-store"; import * as path from "path"; import { LensExtension } from "./lens-extension"; +import { assert } from "../common/utils"; export abstract class ExtensionStore extends BaseStore { - protected extension: LensExtension; + protected extension?: LensExtension; async loadExtension(extension: LensExtension) { this.extension = extension; @@ -18,6 +19,8 @@ export abstract class ExtensionStore extends BaseStore { } protected cwd() { - return path.join(super.cwd(), "extension-store", this.extension.name); + const extension = assert(this.extension, "must call loadExtension() before calling cwd()"); + + return path.join(super.cwd(), "extension-store", extension.name); } } diff --git a/src/extensions/lens-renderer-extension.ts b/src/extensions/lens-renderer-extension.ts index bf1f8cbeb3..a1e97f9c0d 100644 --- a/src/extensions/lens-renderer-extension.ts +++ b/src/extensions/lens-renderer-extension.ts @@ -5,10 +5,10 @@ import { getExtensionPageUrl } from "./registries/page-registry"; import { CommandRegistration } from "./registries/command-registry"; export class LensRendererExtension extends LensExtension { - globalPages: PageRegistration[] = []; - clusterPages: PageRegistration[] = []; - globalPageMenus: PageMenuRegistration[] = []; - clusterPageMenus: ClusterPageMenuRegistration[] = []; + globalPages: PageRegistration[] = []; + clusterPages: PageRegistration[] = []; + globalPageMenus: PageMenuRegistration[] = []; + clusterPageMenus: ClusterPageMenuRegistration[] = []; kubeObjectStatusTexts: KubeObjectStatusRegistration[] = []; appPreferences: AppPreferenceRegistration[] = []; statusBarItems: StatusBarRegistration[] = []; diff --git a/src/extensions/registries/command-registry.ts b/src/extensions/registries/command-registry.ts index 45af9121a1..866f07b370 100644 --- a/src/extensions/registries/command-registry.ts +++ b/src/extensions/registries/command-registry.ts @@ -18,7 +18,7 @@ export interface CommandRegistration { } export class CommandRegistry extends BaseRegistry { - @observable activeEntity: CatalogEntity; + @observable activeEntity?: CatalogEntity; @action add(items: CommandRegistration | CommandRegistration[], extension?: LensExtension) { diff --git a/src/extensions/registries/kube-object-detail-registry.ts b/src/extensions/registries/kube-object-detail-registry.ts index 9c79b662ea..a196764051 100644 --- a/src/extensions/registries/kube-object-detail-registry.ts +++ b/src/extensions/registries/kube-object-detail-registry.ts @@ -12,17 +12,20 @@ export interface KubeObjectDetailRegistration { priority?: number; } +export interface RegisteredKubeObjectDetails extends KubeObjectDetailRegistration { + priority: number; +} + export class KubeObjectDetailRegistry extends BaseRegistry { getItemsForKind(kind: string, apiVersion: string) { - const items = this.getItems().filter((item) => { - return item.kind === kind && item.apiVersions.includes(apiVersion); - }).map((item) => { - if (item.priority === null) { - item.priority = 50; - } - - return item; - }); + const items = this.getItems() + .filter(item => ( + item.kind === kind + && item.apiVersions.includes(apiVersion) + )) + .map(item => ( + item.priority ??= 50, item as RegisteredKubeObjectDetails + )); return items.sort((a, b) => b.priority - a.priority); } diff --git a/src/extensions/registries/kube-object-status-registry.ts b/src/extensions/registries/kube-object-status-registry.ts index 5f7aab8d5d..49eded9b67 100644 --- a/src/extensions/registries/kube-object-status-registry.ts +++ b/src/extensions/registries/kube-object-status-registry.ts @@ -4,7 +4,7 @@ import { BaseRegistry } from "./base-registry"; export interface KubeObjectStatusRegistration { kind: string; apiVersions: string[]; - resolve: (object: KubeObject) => KubeObjectStatus; + resolve(object: KubeObject): KubeObjectStatus; } export class KubeObjectStatusRegistry extends BaseRegistry { diff --git a/src/extensions/registries/page-menu-registry.ts b/src/extensions/registries/page-menu-registry.ts index 8fe5b68b3b..445700b73e 100644 --- a/src/extensions/registries/page-menu-registry.ts +++ b/src/extensions/registries/page-menu-registry.ts @@ -6,13 +6,13 @@ import { action } from "mobx"; import { BaseRegistry } from "./base-registry"; import { LensExtension } from "../lens-extension"; -export interface PageMenuRegistration { - target?: PageTarget; +export interface PageMenuRegistration { + target?: PageTarget; title: React.ReactNode; components: PageMenuComponents; } -export interface ClusterPageMenuRegistration extends PageMenuRegistration { +export interface ClusterPageMenuRegistration extends PageMenuRegistration { id?: string; parentId?: string; } @@ -21,7 +21,7 @@ export interface PageMenuComponents { Icon: React.ComponentType; } -export class PageMenuRegistry extends BaseRegistry { +export class PageMenuRegistry> extends BaseRegistry { @action add(items: T[], ext: LensExtension) { const normalizedItems = items.map(menuItem => { @@ -37,23 +37,25 @@ export class PageMenuRegistry extends BaseRegist } } -export class ClusterPageMenuRegistry extends PageMenuRegistry { +export class ClusterPageMenuRegistry extends PageMenuRegistry> { getRootItems() { return this.getItems().filter((item) => !item.parentId); } - getSubItems(parent: ClusterPageMenuRegistration) { - return this.getItems().filter((item) => ( - item.parentId === parent.id && - item.target.extensionId === parent.target.extensionId - )); + getSubItems(parent: ClusterPageMenuRegistration) { + return this.getItems() + .filter(item => ( + item.parentId === parent.id + && item.target?.extensionId === parent.target?.extensionId + )); } - getByPage({ id: pageId, extensionId }: RegisteredPage) { - return this.getItems().find((item) => ( - item.target.pageId == pageId && - item.target.extensionId === extensionId - )); + getByPage({ id: pageId, extensionId }: RegisteredPage) { + return this.getItems() + .find((item) => ( + item.target?.pageId == pageId + && item.target.extensionId === extensionId + )); } } diff --git a/src/extensions/registries/page-registry.ts b/src/extensions/registries/page-registry.ts index 0ec6f27da0..09316f9a8c 100644 --- a/src/extensions/registries/page-registry.ts +++ b/src/extensions/registries/page-registry.ts @@ -6,54 +6,55 @@ import { BaseRegistry } from "./base-registry"; import { LensExtension, sanitizeExtensionName } from "../lens-extension"; import { PageParam, PageParamInit } from "../../renderer/navigation/page-param"; import { createPageParam } from "../../renderer/navigation/helpers"; +import { NotFalsy } from "../../common/utils"; -export interface PageRegistration { +export interface PageRegistration { /** * Page ID, part of extension's page url, must be unique within same extension * When not provided, first registered page without "id" would be used for page-menus without target.pageId for same extension */ id?: string; - params?: PageParams; + params?: PageParams>; components: PageComponents; } // exclude "name" field since provided as key in page.params -export type ExtensionPageParamInit = Omit; +export type ExtensionPageParamInit = Omit, "name" | "isSystem">; export interface PageComponents { Page: React.ComponentType; } -export interface PageTarget

{ +export interface PageTarget> { extensionId?: string; pageId?: string; params?: P; } -export interface PageParams { - [paramName: string]: V; -} +export type PageParams = Record; -export interface PageComponentProps

{ +export interface PageComponentProps = {}> { params?: { [N in keyof P]: PageParam; } } -export interface RegisteredPage { +export interface RegisteredPage { id: string; extensionId: string; url: string; // registered extension's page URL (without page params) - params: PageParams; // normalized params + params: PageParams>; // normalized params components: PageComponents; // normalized components } -export function getExtensionPageUrl(target: PageTarget): string { - const { extensionId, pageId = "", params: targetParams = {} } = target; +export function getExtensionPageUrl(target: PageTarget): string { + const { extensionId = "", pageId = "", params: targetParams = {} } = target; const pagePath = ["/extension", sanitizeExtensionName(extensionId), pageId] - .filter(Boolean) - .join("/").replace(/\/+/g, "/").replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id) + .filter(NotFalsy) + .join("/") + .replace(/\/+/g, "/") + .replace(/\/$/, ""); // normalize multi-slashes (e.g. coming from page.id) const pageUrl = new URL(pagePath, `http://localhost`); @@ -75,9 +76,9 @@ export function getExtensionPageUrl(target: PageTarget): string { return pageUrl.href.replace(pageUrl.origin, ""); } -export class PageRegistry extends BaseRegistry { - protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { - const { id: pageId } = page; +export class PageRegistry extends BaseRegistry, RegisteredPage> { + protected getRegisteredItem(page: PageRegistration, ext: LensExtension): RegisteredPage { + const { id: pageId = "" } = page; const extensionId = ext.name; const params = this.normalizeParams(page.params); const components = this.normalizeComponents(page.components, params); @@ -88,7 +89,7 @@ export class PageRegistry extends BaseRegistry }; } - protected normalizeComponents(components: PageComponents, params?: PageParams): PageComponents { + protected normalizeComponents(components: PageComponents, params?: PageParams>): PageComponents { if (params) { const { Page } = components; @@ -98,22 +99,21 @@ export class PageRegistry extends BaseRegistry return components; } - protected normalizeParams(params?: PageParams): PageParams { - if (!params) { - return; - } - Object.entries(params).forEach(([name, value]) => { - const paramInit: PageParamInit = typeof value === "object" - ? { name, ...value } - : { name, defaultValue: value }; - - params[paramInit.name] = createPageParam(paramInit); - }); - - return params as PageParams; + protected normalizeParams(params: PageParams> = {}): PageParams> { + return Object.fromEntries( + Object.entries(params) + .map(([name, value]) => [ + name, + createPageParam( + typeof value === "object" + ? { name, ...value } + : { name, defaultValue: value } + ) + ]) + ); } - getByPageTarget(target: PageTarget): RegisteredPage | null { + getByPageTarget(target: PageTarget): RegisteredPage | null { return this.getItems().find(page => page.extensionId === target.extensionId && page.id === target.pageId) || null; } } diff --git a/src/extensions/registries/protocol-handler-registry.ts b/src/extensions/registries/protocol-handler-registry.ts index dd637818a3..b35a8553a1 100644 --- a/src/extensions/registries/protocol-handler-registry.ts +++ b/src/extensions/registries/protocol-handler-registry.ts @@ -15,7 +15,7 @@ export interface RouteParams { /** * the parts of the URI query string */ - search: Record; + search: Record; /** * the matching parts of the path. The dynamic parts of the URI path. diff --git a/src/main/app-updater.ts b/src/main/app-updater.ts index 8f31df8b57..a0d707c97b 100644 --- a/src/main/app-updater.ts +++ b/src/main/app-updater.ts @@ -74,7 +74,7 @@ export const startUpdateChecking = once(function (interval = 1000 * 60 * 60 * 24 broadcastMessage(UpdateAvailableChannel, backchannel, info); } catch (error) { logger.error(`${AutoUpdateLogPrefix}: broadcasting failed`, { error }); - installVersion = undefined; + installVersion = null; } }); diff --git a/src/main/cluster-detectors/base-cluster-detector.ts b/src/main/cluster-detectors/base-cluster-detector.ts index 885f96c33e..daee66fdad 100644 --- a/src/main/cluster-detectors/base-cluster-detector.ts +++ b/src/main/cluster-detectors/base-cluster-detector.ts @@ -1,34 +1,19 @@ -import request, { RequestPromiseOptions } from "request-promise-native"; -import { Cluster } from "../cluster"; +import { RequestPromiseOptions } from "request-promise-native"; +import { Cluster, k8sRequest } from "../cluster"; export type ClusterDetectionResult = { - value: string | number | boolean + value?: string | number | boolean accuracy: number }; -export class BaseClusterDetector { - cluster: Cluster; - key: string; +export abstract class BaseClusterDetector { + abstract key: string; - constructor(cluster: Cluster) { - this.cluster = cluster; - } + constructor(public cluster: Cluster) {} - detect(): Promise { - return null; - } + abstract detect(): Promise; protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { - const apiUrl = this.cluster.kubeProxyUrl + path; - - return request(apiUrl, { - json: true, - timeout: 30000, - ...options, - headers: { - Host: `${this.cluster.id}.${new URL(this.cluster.kubeProxyUrl).host}`, // required in ClusterManager.getClusterForRequest() - ...(options.headers || {}), - }, - }); + return this.cluster[k8sRequest](path, options); } } diff --git a/src/main/cluster-detectors/cluster-id-detector.ts b/src/main/cluster-detectors/cluster-id-detector.ts index 810955afae..89e4f5a1a1 100644 --- a/src/main/cluster-detectors/cluster-id-detector.ts +++ b/src/main/cluster-detectors/cluster-id-detector.ts @@ -1,6 +1,7 @@ import { BaseClusterDetector } from "./base-cluster-detector"; import { createHash } from "crypto"; import { ClusterMetadataKey } from "../cluster"; +import { assert, NotFalsy } from "../../common/utils"; export class ClusterIdDetector extends BaseClusterDetector { key = ClusterMetadataKey.CLUSTER_ID; @@ -11,7 +12,7 @@ export class ClusterIdDetector extends BaseClusterDetector { try { id = await this.getDefaultNamespaceId(); } catch(_) { - id = this.cluster.apiUrl; + id = assert(this.cluster.apiUrl, "ClusterIdDetector can only detect for valid Cluster instances"); } const value = createHash("sha256").update(id).digest("hex"); diff --git a/src/main/cluster-detectors/detector-registry.ts b/src/main/cluster-detectors/detector-registry.ts index b1d1b73447..935a16a000 100644 --- a/src/main/cluster-detectors/detector-registry.ts +++ b/src/main/cluster-detectors/detector-registry.ts @@ -1,5 +1,6 @@ import { observable } from "mobx"; import { ClusterMetadata } from "../../common/cluster-store"; +import { NotFalsy } from "../../common/utils"; import { Cluster } from "../cluster"; import { BaseClusterDetector, ClusterDetectionResult } from "./base-cluster-detector"; import { ClusterIdDetector } from "./cluster-id-detector"; @@ -8,10 +9,12 @@ import { LastSeenDetector } from "./last-seen-detector"; import { NodesCountDetector } from "./nodes-count-detector"; import { VersionDetector } from "./version-detector"; -export class DetectorRegistry { - registry = observable.array([], { deep: false }); +type DerivedConstructor = new (cluster: Cluster) => BaseClusterDetector; - add(detectorClass: typeof BaseClusterDetector) { +export class DetectorRegistry { + registry = observable.array([], { deep: false }); + + add(detectorClass: DerivedConstructor) { this.registry.push(detectorClass); } @@ -33,13 +36,11 @@ export class DetectorRegistry { // detector raised error, do nothing } } - const metadata: ClusterMetadata = {}; - for (const [key, result] of Object.entries(results)) { - metadata[key] = result.value; - } - - return metadata; + return Object.fromEntries( + Object.entries(results) + .filter(([, result]) => NotFalsy(result)) + ); } } diff --git a/src/main/cluster-detectors/distribution-detector.ts b/src/main/cluster-detectors/distribution-detector.ts index 73d5541d17..996dfceda4 100644 --- a/src/main/cluster-detectors/distribution-detector.ts +++ b/src/main/cluster-detectors/distribution-detector.ts @@ -1,30 +1,77 @@ import { BaseClusterDetector } from "./base-cluster-detector"; import { ClusterMetadataKey } from "../cluster"; +function isGKE(version: string) { + return version.includes("gke"); +} + +function isEKS(version: string) { + return version.includes("eks"); +} + +function isIKS(version: string) { + return version.includes("IKS"); +} + +function isMirantis(version: string) { + return version.includes("-mirantis-") || version.includes("-docker-"); +} + +function isTke(version: string) { + return version.includes("-tke."); +} + +function isCustom(version: string) { + return version.includes("+"); +} + +function isVMWare(version: string) { + return version.includes("+vmware"); +} + +function isRke(version: string) { + return version.includes("-rancher"); +} + +function isK3s(version: string) { + return version.includes("+k3s"); +} + +function isK0s(version: string) { + return version.includes("-k0s"); +} + +function isAlibaba(version: string) { + return version.includes("-aliyun"); +} + +function isHuawei(version: string) { + return version.includes("-CCE"); +} + export class DistributionDetector extends BaseClusterDetector { key = ClusterMetadataKey.DISTRIBUTION; - version: string; public async detect() { - this.version = await this.getKubernetesVersion(); + const version = await this.getKubernetesVersion(); - if (this.isRke()) { + if (isRke(version)) { return { value: "rke", accuracy: 80}; } - if (this.isK3s()) { + if (isK3s(version)) { return { value: "k3s", accuracy: 80}; } - if (this.isGKE()) { + if (isGKE(version)) { return { value: "gke", accuracy: 80}; } - if (this.isEKS()) { + if (isEKS(version)) { return { value: "eks", accuracy: 80}; } - if (this.isIKS()) { + if (isIKS(version)) { return { value: "iks", accuracy: 80}; } @@ -36,27 +83,27 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "digitalocean", accuracy: 90}; } - if (this.isK0s()) { + if (isK0s(version)) { return { value: "k0s", accuracy: 80}; } - - if (this.isVMWare()) { + + if (isVMWare(version)) { return { value: "vmware", accuracy: 90}; } - if (this.isMirantis()) { + if (isMirantis(version)) { return { value: "mirantis", accuracy: 90}; } - if (this.isAlibaba()) { + if (isAlibaba(version)) { return { value: "alibaba", accuracy: 90}; } - if (this.isHuawei()) { + if (isHuawei(version)) { return { value: "huawei", accuracy: 90}; } - if (this.isTke()) { + if (isTke(version)) { return { value: "tencent", accuracy: 90}; } @@ -76,12 +123,12 @@ export class DistributionDetector extends BaseClusterDetector { return { value: "docker-desktop", accuracy: 80}; } - if (this.isCustom() && await this.isOpenshift()) { - return { value: "openshift", accuracy: 90}; - } + if (isCustom(version)) { + if (await this.isOpenshift()) { + return { value: "openshift", accuracy: 90 }; + } - if (this.isCustom()) { - return { value: "custom", accuracy: 10}; + return { value: "custom", accuracy: 10 }; } return { value: "unknown", accuracy: 10}; @@ -95,28 +142,12 @@ export class DistributionDetector extends BaseClusterDetector { return response.gitVersion; } - protected isGKE() { - return this.version.includes("gke"); - } - - protected isEKS() { - return this.version.includes("eks"); - } - - protected isIKS() { - return this.version.includes("IKS"); - } - protected isAKS() { - return this.cluster.apiUrl.includes("azmk8s.io"); - } - - protected isMirantis() { - return this.version.includes("-mirantis-") || this.version.includes("-docker-"); + return this.cluster.apiUrl?.includes("azmk8s.io"); } protected isDigitalOcean() { - return this.cluster.apiUrl.endsWith("k8s.ondigitalocean.com"); + return this.cluster.apiUrl?.endsWith("k8s.ondigitalocean.com"); } protected isMinikube() { @@ -135,38 +166,6 @@ export class DistributionDetector extends BaseClusterDetector { return this.cluster.contextName === "docker-desktop"; } - protected isTke() { - return this.version.includes("-tke."); - } - - protected isCustom() { - return this.version.includes("+"); - } - - protected isVMWare() { - return this.version.includes("+vmware"); - } - - protected isRke() { - return this.version.includes("-rancher"); - } - - protected isK3s() { - return this.version.includes("+k3s"); - } - - protected isK0s() { - return this.version.includes("-k0s"); - } - - protected isAlibaba() { - return this.version.includes("-aliyun"); - } - - protected isHuawei() { - return this.version.includes("-CCE"); - } - protected async isOpenshift() { try { const response = await this.k8sRequest(""); diff --git a/src/main/cluster-detectors/last-seen-detector.ts b/src/main/cluster-detectors/last-seen-detector.ts index 0a9bcf9f74..54811fa865 100644 --- a/src/main/cluster-detectors/last-seen-detector.ts +++ b/src/main/cluster-detectors/last-seen-detector.ts @@ -5,7 +5,9 @@ export class LastSeenDetector extends BaseClusterDetector { key = ClusterMetadataKey.LAST_SEEN; public async detect() { - if (!this.cluster.accessible) return null; + if (!this.cluster.accessible) { + return null; + } await this.k8sRequest("/version"); diff --git a/src/main/cluster-detectors/nodes-count-detector.ts b/src/main/cluster-detectors/nodes-count-detector.ts index 45584df5bd..c0f1c2cb38 100644 --- a/src/main/cluster-detectors/nodes-count-detector.ts +++ b/src/main/cluster-detectors/nodes-count-detector.ts @@ -5,7 +5,10 @@ export class NodesCountDetector extends BaseClusterDetector { key = ClusterMetadataKey.NODES_COUNT; public async detect() { - if (!this.cluster.accessible) return null; + if (!this.cluster.accessible) { + return null; + } + const nodeCount = await this.getNodeCount(); return { value: nodeCount, accuracy: 100}; diff --git a/src/main/cluster-detectors/version-detector.ts b/src/main/cluster-detectors/version-detector.ts index b19979db8a..89ac498e6a 100644 --- a/src/main/cluster-detectors/version-detector.ts +++ b/src/main/cluster-detectors/version-detector.ts @@ -3,7 +3,6 @@ import { ClusterMetadataKey } from "../cluster"; export class VersionDetector extends BaseClusterDetector { key = ClusterMetadataKey.VERSION; - value: string; public async detect() { const version = await this.getKubernetesVersion(); diff --git a/src/main/cluster-manager.ts b/src/main/cluster-manager.ts index 047fb0ca95..200510e772 100644 --- a/src/main/cluster-manager.ts +++ b/src/main/cluster-manager.ts @@ -169,23 +169,23 @@ export class ClusterManager extends Singleton { }); } - getClusterForRequest(req: http.IncomingMessage): Cluster { - let cluster: Cluster = null; + getClusterForRequest(req: http.IncomingMessage): Cluster | null { + let cluster: Cluster | null = null; // lens-server is connecting to 127.0.0.1:/ - if (req.headers.host.startsWith("127.0.0.1")) { - const clusterId = req.url.split("/")[1]; + if (req.headers.host?.startsWith("127.0.0.1")) { + const clusterId = req.url?.split("/")[1]; cluster = clusterStore.getById(clusterId); if (cluster) { // we need to swap path prefix so that request is proxied to kube api - req.url = req.url.replace(`/${clusterId}`, apiKubePrefix); + req.url = req.url?.replace(`/${clusterId}`, apiKubePrefix); } } else if (req.headers["x-cluster-id"]) { cluster = clusterStore.getById(req.headers["x-cluster-id"].toString()); } else { - const clusterId = getClusterIdFromHost(req.headers.host); + const clusterId = getClusterIdFromHost(req.headers.host ?? ""); cluster = clusterStore.getById(clusterId); } diff --git a/src/main/cluster.ts b/src/main/cluster.ts index 7a02c3e0c9..ec3d341466 100644 --- a/src/main/cluster.ts +++ b/src/main/cluster.ts @@ -15,6 +15,9 @@ import logger from "./logger"; import { VersionDetector } from "./cluster-detectors/version-detector"; import { detectorRegistry } from "./cluster-detectors/detector-registry"; import plimit from "p-limit"; +import { assert, NotFalsy } from "../common/utils"; + +export const k8sRequest = Symbol("k8sRequest"); export enum ClusterStatus { AccessGranted = 2, @@ -38,12 +41,12 @@ export type ClusterRefreshOptions = { export interface ClusterState { initialized: boolean; enabled: boolean; - apiUrl: string; + apiUrl?: string; online: boolean; disconnected: boolean; accessible: boolean; ready: boolean; - failureReason: string; + failureReason?: string; isAdmin: boolean; allowedNamespaces: string[] allowedResources: string[] @@ -63,20 +66,20 @@ export class Cluster implements ClusterModel, ClusterState { * * @internal */ - public kubeCtl: Kubectl; + public kubeCtl?: Kubectl; /** * Context handler * * @internal */ - public contextHandler: ContextHandler; + public contextHandler?: ContextHandler; /** * Owner reference * * If extension sets this it needs to also mark cluster as enabled on activate (or when added to a store) */ - public ownerRef: string; - protected kubeconfigManager: KubeconfigManager; + public ownerRef?: string; + protected kubeconfigManager?: KubeconfigManager; protected eventDisposers: Function[] = []; protected activated = false; private resourceAccessStatuses: Map = new Map(); @@ -85,7 +88,7 @@ export class Cluster implements ClusterModel, ClusterState { whenReady = when(() => this.ready); /** - * Is cluster object initializinng on-going + * Is cluster object initializing on-going * * @observable */ @@ -118,14 +121,14 @@ export class Cluster implements ClusterModel, ClusterState { * * @observable */ - @observable apiUrl: string; // cluster server url + @observable apiUrl?: string; // cluster server url /** * Internal authentication proxy URL * * @observable * @internal */ - @observable kubeProxyUrl: string; // lens-proxy to kube-api url + @observable kubeProxyUrl?: string; // lens-proxy to kube-api url /** * Is cluster instance enabled (disabled clusters are currently hidden) * @@ -167,7 +170,7 @@ export class Cluster implements ClusterModel, ClusterState { * * @observable */ - @observable failureReason: string; + @observable failureReason?: string; /** * Does user have admin like access * @@ -186,7 +189,7 @@ export class Cluster implements ClusterModel, ClusterState { * * @observable */ - @observable preferences: ClusterPreferences = {}; + @observable preferences: ClusterPreferences; /** * Metadata * @@ -211,7 +214,7 @@ export class Cluster implements ClusterModel, ClusterState { * * @observable */ - @observable accessibleNamespaces: string[] = []; + @observable accessibleNamespaces?: string[]; /** * Is cluster available @@ -253,13 +256,24 @@ export class Cluster implements ClusterModel, ClusterState { } constructor(model: ClusterModel) { - this.updateModel(model); + this.id = model.id; + this.kubeConfigPath = model.kubeConfigPath; + this.workspace = model.workspace || "Default"; + this.contextName = model.contextName; + this.preferences = model.preferences ?? {}; + this.metadata = model.metadata ?? {}; + this.ownerRef = model.ownerRef; + this.accessibleNamespaces = model.accessibleNamespaces; try { const kubeconfig = this.getKubeconfig(); validateKubeConfig(kubeconfig, this.contextName, { validateCluster: true, validateUser: false, validateExec: false}); - this.apiUrl = kubeconfig.getCluster(kubeconfig.getContextObject(this.contextName).cluster).server; + const cluster = kubeconfig.getContextObject(this.contextName)?.cluster; + + if (cluster) { + this.apiUrl = kubeconfig.getCluster(cluster)?.server; + } } catch(err) { logger.error(err); logger.error(`[CLUSTER] Failed to load kubeconfig for the cluster '${this.name || this.contextName}' (context: ${this.contextName}, kubeconfig: ${this.kubeConfigPath}).`); @@ -274,15 +288,6 @@ export class Cluster implements ClusterModel, ClusterState { return !!this.ownerRef; } - /** - * Update cluster data model - * - * @param model - */ - @action updateModel(model: ClusterModel) { - Object.assign(this, model); - } - /** * Initialize a cluster (can be done only in main process) * @@ -323,7 +328,7 @@ export class Cluster implements ClusterModel, ClusterState { if (ipcMain) { this.eventDisposers.push( reaction(() => this.getState(), () => this.pushState()), - reaction(() => this.prometheusPreferences, (prefs) => this.contextHandler.setupPrometheus(prefs), { equals: comparer.structural, }), + reaction(() => this.prometheusPreferences, (prefs) => this.contextHandler?.setupPrometheus(prefs), { equals: comparer.structural, }), () => { clearInterval(refreshTimer); clearInterval(refreshMetadataTimer); @@ -488,15 +493,17 @@ export class Cluster implements ClusterModel, ClusterState { /** * @internal */ - async getProxyKubeconfigPath(): Promise { - return this.kubeconfigManager.getPath(); + async getProxyKubeconfigPath(): Promise { + return this.kubeconfigManager?.getPath(); } - protected async k8sRequest(path: string, options: RequestPromiseOptions = {}): Promise { + async [k8sRequest](path: string, options: RequestPromiseOptions = {}): Promise { + const baseUrl = assert(this.kubeProxyUrl, "constructor failed, should not be accessing k8s"); + options.headers ??= {}; options.json ??= true; options.timeout ??= 30000; - options.headers.Host = `${this.id}.${new URL(this.kubeProxyUrl).host}`; // required in ClusterManager.getClusterForRequest() + options.headers.Host = `${this.id}.${new URL(baseUrl).host}`; // required in ClusterManager.getClusterForRequest() return request(this.kubeProxyUrl + path, options); } @@ -511,7 +518,7 @@ export class Cluster implements ClusterModel, ClusterState { const prometheusPrefix = this.preferences.prometheus?.prefix || ""; const metricsPath = `/api/v1/namespaces/${prometheusPath}/proxy${prometheusPrefix}/api/v1/query_range`; - return this.k8sRequest(metricsPath, { + return this[k8sRequest](metricsPath, { timeout: 0, resolveWithFullResponse: false, json: true, @@ -526,8 +533,7 @@ export class Cluster implements ClusterModel, ClusterState { const versionData = await versionDetector.detect(); this.metadata.version = versionData.value; - - this.failureReason = null; + this.failureReason = undefined; return ClusterStatus.AccessGranted; } catch (error) { @@ -574,7 +580,7 @@ export class Cluster implements ClusterModel, ClusterState { spec: { resourceAttributes } }); - return accessReview.body.status.allowed; + return accessReview.body.status?.allowed ?? false; } catch (error) { logger.error(`failed to request selfSubjectAccessReview: ${error}`); @@ -676,7 +682,7 @@ export class Cluster implements ClusterModel, ClusterState { } protected async getAllowedNamespaces() { - if (this.accessibleNamespaces.length) { + if (this.accessibleNamespaces?.length) { return this.accessibleNamespaces; } @@ -685,10 +691,10 @@ export class Cluster implements ClusterModel, ClusterState { try { const namespaceList = await api.listNamespace(); - return namespaceList.body.items.map(ns => ns.metadata.name); + return namespaceList.body.items.map(ns => ns.metadata?.name).filter(NotFalsy); } catch (error) { const ctx = (await this.getProxyKubeconfig()).getContextObject(this.contextName); - const namespaceList = [ctx.namespace].filter(Boolean); + const namespaceList = [ctx?.namespace].filter(NotFalsy); if (namespaceList.length === 0 && error instanceof HttpError && error.statusCode === 403) { logger.info("[CLUSTER]: listing namespaces is forbidden, broadcasting", { clusterId: this.id }); diff --git a/src/main/context-handler.ts b/src/main/context-handler.ts index e94520b9be..908ca2d884 100644 --- a/src/main/context-handler.ts +++ b/src/main/context-handler.ts @@ -1,4 +1,4 @@ -import type { PrometheusProvider, PrometheusService } from "./prometheus/provider-registry"; +import type { PrometheusService } from "./prometheus/provider-registry"; import type { ClusterPrometheusPreferences } from "../common/cluster-store"; import type { Cluster } from "./cluster"; import type httpProxy from "http-proxy"; @@ -8,35 +8,54 @@ import { prometheusProviders } from "../common/prometheus-providers"; import logger from "./logger"; import { getFreePort } from "./port"; import { KubeAuthProxy } from "./kube-auth-proxy"; +import { assert, NotFalsy } from "../common/utils"; +import { AssertionError } from "assert"; + +interface VerifiedUrl extends UrlWithStringQuery { + hostname: string; +} export class ContextHandler { - public proxyPort: number; - public clusterUrl: UrlWithStringQuery; - protected kubeAuthProxy: KubeAuthProxy; - protected apiTarget: httpProxy.ServerOptions; - protected prometheusProvider: string; - protected prometheusPath: string; + public proxyPort?: number; + public clusterUrl: VerifiedUrl; + protected kubeAuthProxy?: KubeAuthProxy; + protected apiTarget?: httpProxy.ServerOptions; + protected prometheusProvider?: string; + protected prometheusPath?: string; constructor(protected cluster: Cluster) { - this.clusterUrl = url.parse(cluster.apiUrl); + const apiUrl = assert(cluster.apiUrl, "ContextHandler may only be created for valid clusters"); + + const clusterUrl = url.parse(apiUrl); + + if (!clusterUrl.hostname) { + throw new AssertionError({ + actual: clusterUrl.hostname, + message: "clusterUrl must have a hostname" + }); + } + + this.clusterUrl = clusterUrl as VerifiedUrl; this.setupPrometheus(cluster.preferences); } public setupPrometheus(preferences: ClusterPrometheusPreferences = {}) { this.prometheusProvider = preferences.prometheusProvider?.type; - this.prometheusPath = null; if (preferences.prometheus) { const { namespace, service, port } = preferences.prometheus; this.prometheusPath = `${namespace}/services/${service}:${port}`; + } else { + this.prometheusPath = undefined; } } - protected async resolvePrometheusPath(): Promise { + protected async resolvePrometheusPath(): Promise { const prometheusService = await this.getPrometheusService(); - if (!prometheusService) return null; + if (!prometheusService) return; + const { service, namespace, port } = prometheusService; return `${namespace}/services/${service}:${port}`; @@ -56,24 +75,26 @@ export class ContextHandler { return prometheusProviders.find(p => p.id === this.prometheusProvider); } - async getPrometheusService(): Promise { + async getPrometheusService(): Promise { const providers = this.prometheusProvider ? prometheusProviders.filter(provider => provider.id == this.prometheusProvider) : prometheusProviders; - const prometheusPromises: Promise[] = providers.map(async (provider: PrometheusProvider): Promise => { - const apiClient = (await this.cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); - return await provider.getPrometheusService(apiClient); - }); - const resolvedPrometheusServices = await Promise.all(prometheusPromises); - - return resolvedPrometheusServices.filter(n => n)[0]; + return (await Promise.allSettled(providers + .map(provider => ( + this.cluster.getProxyKubeconfig() + .then(kc => kc.makeApiClient(CoreV1Api)) + .then(client => provider.getPrometheusService(client)) + )) + )) + .map(result => ( + result.status === "fulfilled" + ? result.value + : undefined + )) + .find(NotFalsy); } - async getPrometheusPath(): Promise { - if (!this.prometheusPath) { - this.prometheusPath = await this.resolvePrometheusPath(); - } - - return this.prometheusPath; + async getPrometheusPath(): Promise { + return this.prometheusPath ??= await this.resolvePrometheusPath(); } async resolveAuthProxyUrl() { @@ -111,11 +132,7 @@ export class ContextHandler { } async ensurePort(): Promise { - if (!this.proxyPort) { - this.proxyPort = await getFreePort(); - } - - return this.proxyPort; + return this.proxyPort ??= await getFreePort(); } async ensureServer() { @@ -126,7 +143,7 @@ export class ContextHandler { if (this.cluster.preferences.httpsProxy) { proxyEnv.HTTPS_PROXY = this.cluster.preferences.httpsProxy; } - this.kubeAuthProxy = new KubeAuthProxy(this.cluster, this.proxyPort, proxyEnv); + this.kubeAuthProxy = new KubeAuthProxy(this.cluster, await this.ensurePort(), proxyEnv); await this.kubeAuthProxy.run(); } } @@ -134,7 +151,7 @@ export class ContextHandler { stopServer() { if (this.kubeAuthProxy) { this.kubeAuthProxy.exit(); - this.kubeAuthProxy = null; + this.kubeAuthProxy = undefined; } } diff --git a/src/main/extension-filesystem.ts b/src/main/extension-filesystem.ts index eddb7b747f..1c67d14bb8 100644 --- a/src/main/extension-filesystem.ts +++ b/src/main/extension-filesystem.ts @@ -36,7 +36,8 @@ export class FilesystemProvisionerStore extends BaseStore { this.registeredExtensions.set(extensionName, dirPath); } - const dirPath = this.registeredExtensions.get(extensionName); + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const dirPath = this.registeredExtensions.get(extensionName)!; await fse.ensureDir(dirPath); diff --git a/src/main/helm/helm-chart-manager.ts b/src/main/helm/helm-chart-manager.ts index 69619a56d4..326b428706 100644 --- a/src/main/helm/helm-chart-manager.ts +++ b/src/main/helm/helm-chart-manager.ts @@ -19,10 +19,12 @@ export class HelmChartManager { this.repo = repo; } - public async chart(name: string) { + public async chart(name?: string) { const charts = await this.charts(); - return charts[name]; + if (name) { + return charts[name]; + } } public async charts(): Promise { @@ -37,7 +39,7 @@ export class HelmChartManager { } } - public async getReadme(name: string, version = "") { + public async getReadme(name: string, version: string) { const helm = await helmCli.binaryPath(); if(version && version != "") { @@ -51,7 +53,7 @@ export class HelmChartManager { } } - public async getValues(name: string, version = "") { + public async getValues(name: string, version: string) { const helm = await helmCli.binaryPath(); if(version && version != "") { diff --git a/src/main/helm/helm-release-manager.ts b/src/main/helm/helm-release-manager.ts index 58a4e99798..e23bf925df 100644 --- a/src/main/helm/helm-release-manager.ts +++ b/src/main/helm/helm-release-manager.ts @@ -5,6 +5,7 @@ import { promiseExec} from "../promise-exec"; import { helmCli } from "./helm-cli"; import { Cluster } from "../cluster"; import { toCamelCase } from "../../common/utils/camelCase"; +import { assert, NotFalsy } from "../../common/utils"; export class HelmReleaseManager { @@ -114,7 +115,8 @@ export class HelmReleaseManager { protected async getResources(name: string, namespace: string, cluster: Cluster) { const helm = await helmCli.binaryPath(); - const kubectl = await cluster.kubeCtl.getPath(); + const kubectl = assert(await cluster.kubeCtl?.getPath(), "Cluster Kubectl must be instantiated"); + const pathToKubeconfig = await cluster.getProxyKubeconfigPath(); const { stdout } = await promiseExec(`"${helm}" get manifest ${name} --namespace ${namespace} --kubeconfig ${pathToKubeconfig} | "${kubectl}" get -n ${namespace} --kubeconfig ${pathToKubeconfig} -f - -o=json`).catch(() => { return { stdout: JSON.stringify({items: []})}; diff --git a/src/main/helm/helm-repo-manager.ts b/src/main/helm/helm-repo-manager.ts index 74afb166b1..f7fffa382f 100644 --- a/src/main/helm/helm-repo-manager.ts +++ b/src/main/helm/helm-repo-manager.ts @@ -6,10 +6,11 @@ import { Singleton } from "../../common/utils/singleton"; import { customRequestPromise } from "../../common/request"; import orderBy from "lodash/orderBy"; import logger from "../logger"; +import { AssertionError } from "assert"; export type HelmEnv = Record & { - HELM_REPOSITORY_CACHE?: string; - HELM_REPOSITORY_CONFIG?: string; + HELM_REPOSITORY_CACHE: string; + HELM_REPOSITORY_CONFIG: string; }; export interface HelmRepoConfig { @@ -19,7 +20,7 @@ export interface HelmRepoConfig { export interface HelmRepo { name: string; url: string; - cacheFilePath?: string + cacheFilePath: string caFile?: string, certFile?: string, insecureSkipTlsVerify?: boolean, @@ -31,9 +32,8 @@ export interface HelmRepo { export class HelmRepoManager extends Singleton { static cache = {}; // todo: remove implicit updates in helm-chart-manager.ts - protected repos: HelmRepo[]; - protected helmEnv: HelmEnv; - protected initialized: boolean; + protected repos: HelmRepo[] = []; + protected helmEnv?: HelmEnv; async loadAvailableRepos(): Promise { const res = await customRequestPromise({ @@ -46,43 +46,46 @@ export class HelmRepoManager extends Singleton { return orderBy(res.body, repo => repo.name); } - async init() { + async init(): Promise { helmCli.setLogger(logger); await helmCli.ensureBinary(); - if (!this.initialized) { - this.helmEnv = await this.parseHelmEnv(); + try { + return this.helmEnv ?? await this.parseHelmEnv(); + } finally { await this.update(); - this.initialized = true; } } - protected async parseHelmEnv() { + protected async parseHelmEnv(): Promise { const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" env`).catch((error) => { - throw(error.stderr); - }); - const lines = stdout.split(/\r?\n/); // split by new line feed - const env: HelmEnv = {}; - lines.forEach((line: string) => { - const [key, value] = line.split("="); + try { + const { stdout } = await promiseExec(`"${helm}" env`); + const envEntries = stdout.split(/\r?\n/) // split by new line feed + .map(line => line.split("=")) + .filter(line => line.length === 2) + .map(([key, value]) => [key, value.replace(/"/g, "")]); // strip quotas + const env = Object.fromEntries(envEntries); - if (key && value) { - env[key] = value.replace(/"/g, ""); // strip quotas + if (!env.HELM_REPOSITORY_CACHE || !env.HELM_REPOSITORY_CONFIG) { + throw new AssertionError({ + actual: env, + message: "HELM_REPOSITORY_CACHE and HELM_REPOSITORY_CONFIG must be defined" + }); } - }); - return env; + return env as HelmEnv; + } catch (error) { + throw error.stderr; + } } public async repositories(): Promise { - if (!this.initialized) { - await this.init(); - } + const helmEnv = await this.init(); try { - const repoConfigFile = this.helmEnv.HELM_REPOSITORY_CONFIG; + const repoConfigFile = helmEnv.HELM_REPOSITORY_CONFIG; const { repositories }: HelmRepoConfig = await readFile(repoConfigFile, "utf8") .then((yamlContent: string) => yaml.safeLoad(yamlContent)) .catch(() => ({ @@ -97,7 +100,7 @@ export class HelmRepoManager extends Singleton { return repositories.map(repo => ({ ...repo, - cacheFilePath: `${this.helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml` + cacheFilePath: `${helmEnv.HELM_REPOSITORY_CACHE}/${repo.name}-index.yaml` })); } catch (error) { logger.error(`[HELM]: repositories listing error "${error}"`); @@ -106,7 +109,7 @@ export class HelmRepoManager extends Singleton { } } - public async repository(name: string) { + public async repository(name?: string) { const repositories = await this.repositories(); return repositories.find(repo => repo.name == name); @@ -114,21 +117,23 @@ export class HelmRepoManager extends Singleton { public async update() { const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" repo update`).catch((error) => { - return { stdout: error.stdout }; - }); - return stdout; + try { + return (await promiseExec(`"${helm}" repo update`)).stdout; + } catch (error) { + return error.stdout; + } } - public async addRepo({ name, url }: HelmRepo) { + public async addRepo({ name, url }: { name: string, url: string }) { logger.info(`[HELM]: adding repo "${name}" from ${url}`); const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" repo add ${name} ${url}`).catch((error) => { - throw(error.stderr); - }); - return stdout; + try { + return (await promiseExec(`"${helm}" repo add ${name} ${url}`)).stdout; + } catch (error) { + return error.stdout; + } } public async addСustomRepo(repoAttributes : HelmRepo) { @@ -143,21 +148,23 @@ export class HelmRepoManager extends Singleton { const certFile = repoAttributes.certFile ? ` --cert-file "${repoAttributes.certFile}"` : ""; const addRepoCommand = `"${helm}" repo add ${repoAttributes.name} ${repoAttributes.url}${insecureSkipTlsVerify}${username}${password}${caFile}${keyFile}${certFile}`; - const { stdout } = await promiseExec(addRepoCommand).catch((error) => { - throw(error.stderr); - }); - return stdout; + try { + return (await promiseExec(addRepoCommand)).stdout; + } catch (error) { + return error.stdout; + } } public async removeRepo({ name, url }: HelmRepo): Promise { logger.info(`[HELM]: removing repo "${name}" from ${url}`); const helm = await helmCli.binaryPath(); - const { stdout } = await promiseExec(`"${helm}" repo remove ${name}`).catch((error) => { - throw(error.stderr); - }); - return stdout; + try { + return (await promiseExec(`"${helm}" repo remove ${name} ${url}`)).stdout; + } catch (error) { + return error.stdout; + } } } diff --git a/src/main/helm/helm-service.ts b/src/main/helm/helm-service.ts index 9682e58a84..d2396ab0f0 100644 --- a/src/main/helm/helm-service.ts +++ b/src/main/helm/helm-service.ts @@ -10,6 +10,10 @@ class HelmService { public async installChart(cluster: Cluster, data: { chart: string; values: {}; name: string; namespace: string; version: string }) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + if (!proxyKubeconfig) { + return; + } + return await releaseManager.installChart(data.chart, data.values, data.name, data.namespace, data.version, proxyKubeconfig); } @@ -31,74 +35,116 @@ class HelmService { return charts; } - public async getChart(repoName: string, chartName: string, version = "") { + public async getChart(repoName?: string, chartName?: string, version?: string | null) { const result = { readme: "", versions: {} }; const repo = await repoManager.repository(repoName); + + if (!repo || !chartName) { + return void logger.warn("[HELM-SERVICE]: Missing required information on getChart request", { repo, chartName }); + } + const chartManager = new HelmChartManager(repo); const chart = await chartManager.chart(chartName); - result.readme = await chartManager.getReadme(chartName, version); + if (!chart) { + return void logger.warn("[HELM-SERVICE]: Missing required information on getChart request", { chart }); + } + + result.readme = await chartManager.getReadme(chartName, version || ""); result.versions = chart; return result; } - public async getChartValues(repoName: string, chartName: string, version = "") { + public async getChartValues(repoName?: string, chartName?: string, version?: string | null) { const repo = await repoManager.repository(repoName); + + if (!repo || !chartName) { + return void logger.warn("[HELM-SERVICE]: Missing required information on getChartValues request", { repo, chartName }); + } + const chartManager = new HelmChartManager(repo); - return chartManager.getValues(chartName, version); + return chartManager.getValues(chartName, version || ""); } - public async listReleases(cluster: Cluster, namespace: string = null) { + public async listReleases(cluster: Cluster, namespace?: string) { await repoManager.init(); const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + if (!proxyKubeconfig) { + return void logger.warn("[HELM-SERVICE]: Missing required information on listReleases request", { proxyKubeconfig }); + } + return await releaseManager.listReleases(proxyKubeconfig, namespace); } - public async getRelease(cluster: Cluster, releaseName: string, namespace: string) { + public async getRelease(cluster: Cluster, releaseName?: string, namespace?: string) { + if (!releaseName || !namespace) { + return void logger.warn("[HELM-SERVICE]: Missing required information on getRelease request", { releaseName, namespace }); + } + logger.debug("Fetch release"); return await releaseManager.getRelease(releaseName, namespace, cluster); } - public async getReleaseValues(cluster: Cluster, releaseName: string, namespace: string) { + public async getReleaseValues(cluster: Cluster, releaseName?: string, namespace?: string) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + if (!proxyKubeconfig || !releaseName || !namespace) { + return void logger.warn("[HELM-SERVICE]: Missing required information on getReleaseValues request", { proxyKubeconfig, releaseName, namespace }); + } + logger.debug("Fetch release values"); return await releaseManager.getValues(releaseName, namespace, proxyKubeconfig); } - public async getReleaseHistory(cluster: Cluster, releaseName: string, namespace: string) { + public async getReleaseHistory(cluster: Cluster, releaseName?: string, namespace?: string) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + if (!proxyKubeconfig || !releaseName || !namespace) { + return void logger.warn("[HELM-SERVICE]: Missing required information on getReleaseHistory request", { proxyKubeconfig, releaseName, namespace }); + } + logger.debug("Fetch release history"); return await releaseManager.getHistory(releaseName, namespace, proxyKubeconfig); } - public async deleteRelease(cluster: Cluster, releaseName: string, namespace: string) { + public async deleteRelease(cluster: Cluster, releaseName?: string, namespace?: string) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + if (!proxyKubeconfig || !releaseName || !namespace) { + return void logger.warn("[HELM-SERVICE]: Missing required information on deleteRelease request", { proxyKubeconfig, releaseName, namespace }); + } + logger.debug("Delete release"); return await releaseManager.deleteRelease(releaseName, namespace, proxyKubeconfig); } - public async updateRelease(cluster: Cluster, releaseName: string, namespace: string, data: { chart: string; values: {}; version: string }) { + public async updateRelease(cluster: Cluster, releaseName?: string, namespace?: string, data?: { chart: string; values: {}; version: string }) { + if (!releaseName || !namespace || !data) { + return void logger.warn("[HELM-SERVICE]: Missing required information on updateRelease request", { releaseName, namespace, data }); + } + logger.debug("Upgrade release"); return await releaseManager.upgradeRelease(releaseName, data.chart, data.values, namespace, data.version, cluster); } - public async rollback(cluster: Cluster, releaseName: string, namespace: string, revision: number) { + public async rollback(cluster: Cluster, releaseName?: string, namespace?: string, revision?: number) { const proxyKubeconfig = await cluster.getProxyKubeconfigPath(); + if (!proxyKubeconfig || !releaseName || !namespace || !revision) { + return void logger.warn("[HELM-SERVICE]: Missing required information on rollback request", { proxyKubeconfig, releaseName, namespace, revision }); + } + logger.debug("Rollback release"); const output = await releaseManager.rollback(releaseName, namespace, revision, proxyKubeconfig); @@ -123,6 +169,10 @@ class HelmService { const firstVersion = semver.coerce(first.version || 0); const secondVersion = semver.coerce(second.version || 0); + if (!firstVersion || !secondVersion) { + return 0; // consider this case as equal + } + return semver.compare(secondVersion, firstVersion); }); } diff --git a/src/main/kube-auth-proxy.ts b/src/main/kube-auth-proxy.ts index 589fe8fa16..a3b65a0f8a 100644 --- a/src/main/kube-auth-proxy.ts +++ b/src/main/kube-auth-proxy.ts @@ -5,6 +5,7 @@ import type { Cluster } from "./cluster"; import { Kubectl } from "./kubectl"; import logger from "./logger"; import * as url from "url"; +import { assert } from "../common/utils"; export interface KubeAuthProxyLog { data: string; @@ -12,23 +13,24 @@ export interface KubeAuthProxyLog { } export class KubeAuthProxy { - public lastError: string; + public lastError?: string; protected cluster: Cluster; - protected env: NodeJS.ProcessEnv = null; - protected proxyProcess: ChildProcess; + protected env: NodeJS.ProcessEnv; + protected proxyProcess?: ChildProcess; protected port: number; protected kubectl: Kubectl; + readonly acceptHosts: string; constructor(cluster: Cluster, port: number, env: NodeJS.ProcessEnv) { this.env = env; this.port = port; this.cluster = cluster; this.kubectl = Kubectl.bundled(); - } - - get acceptHosts() { - return url.parse(this.cluster.apiUrl).hostname; + this.acceptHosts = assert( + this.cluster.apiUrl && url.parse(this.cluster.apiUrl).hostname, + "Cluster must be properly initialized to have a proxy created for it", + ); } public async run(): Promise { @@ -57,11 +59,11 @@ export class KubeAuthProxy { }); this.proxyProcess.on("exit", (code) => { - this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: code > 0 }); + this.sendIpcLogMessage({ data: `proxy exited with code: ${code}`, error: Boolean(code && code > 0) }); this.exit(); }); - this.proxyProcess.stdout.on("data", (data) => { + this.proxyProcess.stdout?.on("data", (data) => { let logItem = data.toString(); if (logItem.startsWith("Starting to serve on")) { @@ -70,7 +72,7 @@ export class KubeAuthProxy { this.sendIpcLogMessage({ data: logItem }); }); - this.proxyProcess.stderr.on("data", (data) => { + this.proxyProcess.stderr?.on("data", (data) => { this.lastError = this.parseError(data.toString()); this.sendIpcLogMessage({ data: data.toString(), error: true }); }); @@ -108,8 +110,8 @@ export class KubeAuthProxy { logger.debug("[KUBE-AUTH]: stopping local proxy", this.cluster.getMeta()); this.proxyProcess.kill(); this.proxyProcess.removeAllListeners(); - this.proxyProcess.stderr.removeAllListeners(); - this.proxyProcess.stdout.removeAllListeners(); - this.proxyProcess = null; + this.proxyProcess.stderr?.removeAllListeners(); + this.proxyProcess.stdout?.removeAllListeners(); + this.proxyProcess = undefined; } } diff --git a/src/main/kubeconfig-manager.ts b/src/main/kubeconfig-manager.ts index f88ce87840..02043d45c0 100644 --- a/src/main/kubeconfig-manager.ts +++ b/src/main/kubeconfig-manager.ts @@ -9,7 +9,7 @@ import logger from "./logger"; export class KubeconfigManager { protected configDir = app.getPath("temp"); - protected tempFile: string; + protected tempFile?: string; private constructor(protected cluster: Cluster, protected contextHandler: ContextHandler, protected port: number) { } @@ -63,7 +63,7 @@ export class KubeconfigManager { { name: contextName, server: this.resolveProxyUrl(), - skipTLSVerify: undefined, + skipTLSVerify: false, } ], users: [ @@ -74,7 +74,7 @@ export class KubeconfigManager { user: "proxy", name: contextName, cluster: contextName, - namespace: kubeConfig.getContextObject(contextName).namespace, + namespace: kubeConfig.getContextObject(contextName)?.namespace, } ] }; diff --git a/src/main/kubectl.ts b/src/main/kubectl.ts index 7e0d6ed5c7..6e49ebe0f1 100644 --- a/src/main/kubectl.ts +++ b/src/main/kubectl.ts @@ -55,7 +55,7 @@ export function bundledKubectlPath(): string { export class Kubectl { public kubectlVersion: string; - protected directory: string; + protected directory?: string; protected url: string; protected path: string; protected dirname: string; @@ -77,12 +77,17 @@ export class Kubectl { constructor(clusterVersion: string) { const versionParts = /^v?(\d+\.\d+)(.*)/.exec(clusterVersion); - const minorVersion = versionParts[1]; + + if (!versionParts || versionParts.length === 0) { + throw new Error("ClusterVersion must start of the form v#.#.#"); + } + + const prev = kubectlMap.get(versionParts[1]); /* minorVersion is the first two digits of kube server version if the version map includes that, use that version, if not, fallback to the exact x.y.z of kube version */ - if (kubectlMap.has(minorVersion)) { - this.kubectlVersion = kubectlMap.get(minorVersion); + if (prev) { + this.kubectlVersion = prev; logger.debug(`Set kubectl version ${this.kubectlVersion} for cluster version ${clusterVersion} using version map`); } else { this.kubectlVersion = versionParts[1] + versionParts[2]; @@ -273,7 +278,7 @@ export class Kubectl { logger.info(`Downloading kubectl ${this.kubectlVersion} from ${this.url} to ${this.path}`); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const stream = customRequest({ url: this.url, gzip: true, @@ -360,13 +365,12 @@ export class Kubectl { await fsPromises.writeFile(zshScriptPath, zshScript.toString(), { mode: 0o644 }); } - protected getDownloadMirror() { - const mirror = packageMirrors.get(userStore.preferences?.downloadMirror); - - if (mirror) { - return mirror; - } - - return packageMirrors.get("default"); // MacOS packages are only available from default + protected getDownloadMirror(): string { + return ( + userStore.preferences?.downloadMirror + && packageMirrors.get(userStore.preferences?.downloadMirror) + ) + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + ?? packageMirrors.get("default")!; } } diff --git a/src/main/lens-binary.ts b/src/main/lens-binary.ts index 3cf5a5fce7..b1c24af54e 100644 --- a/src/main/lens-binary.ts +++ b/src/main/lens-binary.ts @@ -5,6 +5,7 @@ import { ensureDir, pathExists } from "fs-extra"; import * as tar from "tar"; import { isWindows } from "../common/vars"; import winston from "winston"; +import { noop } from "../common/utils"; export type LensBinaryOpts = { version: string; @@ -17,16 +18,16 @@ export type LensBinaryOpts = { export class LensBinary { public binaryVersion: string; - protected directory: string; - protected url: string; - protected path: string; - protected tarPath: string; + protected directory?: string; + protected url?: string; + protected path?: string; + protected tarPath?: string; protected dirname: string; protected binaryName: string; protected platformName: string; protected arch: string; protected originalBinaryName: string; - protected requestOpts: request.Options; + protected requestOpts?: request.Options; protected logger: Console | winston.Logger; constructor(opts: LensBinaryOpts) { @@ -177,19 +178,21 @@ export class LensBinary { stream.on("error", (error) => { this.logger.error(error); - fs.unlink(binaryPath, () => { - // do nothing - }); - throw(error); + fs.unlink(binaryPath, noop); + throw error; }); - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { file.on("close", () => { this.logger.debug(`${this.originalBinaryName} binary download closed`); - if (!this.tarPath) fs.chmod(binaryPath, 0o755, (err) => { - if (err) reject(err); - }); - resolve(); + + if (!this.tarPath) { + fs.promises.chmod(binaryPath, 0o755) + .then(resolve) + .catch(reject); + } else { + resolve(); + } }); stream.pipe(file); }); diff --git a/src/main/lens-proxy.ts b/src/main/lens-proxy.ts index 98bdd97f54..2775e6a223 100644 --- a/src/main/lens-proxy.ts +++ b/src/main/lens-proxy.ts @@ -10,10 +10,11 @@ import { ClusterManager } from "./cluster-manager"; import { ContextHandler } from "./context-handler"; import logger from "./logger"; import { NodeShellSession, LocalShellSession } from "./shell-session"; +import { assert } from "../common/utils"; export class LensProxy { protected origin: string; - protected proxyServer: http.Server; + protected proxyServer?: http.Server; protected router: Router; protected closed = false; protected retryCounters = new Map(); @@ -36,7 +37,7 @@ export class LensProxy { close() { logger.info("Closing proxy server"); - this.proxyServer.close(); + this.proxyServer?.close(); this.closed = true; } @@ -52,7 +53,7 @@ export class LensProxy { }); spdyProxy.on("upgrade", (req: http.IncomingMessage, socket: net.Socket, head: Buffer) => { - if (req.url.startsWith(`${apiPrefix}?`)) { + if (req.url?.startsWith(`${apiPrefix}?`)) { this.handleWsUpgrade(req, socket, head); } else { this.handleProxyUpgrade(proxy, req, socket, head); @@ -69,10 +70,19 @@ export class LensProxy { const cluster = this.clusterManager.getClusterForRequest(req); if (cluster) { - const proxyUrl = await cluster.contextHandler.resolveAuthProxyUrl() + req.url.replace(apiKubePrefix, ""); - const apiUrl = url.parse(cluster.apiUrl); - const pUrl = url.parse(proxyUrl); - const connectOpts = { port: parseInt(pUrl.port), host: pUrl.hostname }; + const authProxyUrl = assert( + await cluster.contextHandler?.resolveAuthProxyUrl(), + "Cluster must be fully initialized to be proxied to", + ); + const proxyUrl = authProxyUrl + (req.url?.replace(apiKubePrefix, "") ?? ""); + + const apiUrlRaw = assert(cluster.apiUrl, "ContextHandler may only be created for valid clusters"); + const apiUrl = url.parse(apiUrlRaw); + + const pUrl = assert(url.parse(proxyUrl), "proxyUrl must be a valid URL"); + const rawPort = assert(pUrl.port, "Port must be specified on proxyUrl"); + const host = assert(pUrl.hostname, "Hostname must be specified on proxyUrl"); + const connectOpts = { port: parseInt(rawPort), host }; const proxySocket = new net.Socket(); proxySocket.connect(connectOpts, () => { @@ -171,8 +181,11 @@ export class LensProxy { const ws = new WebSocket.Server({ noServer: true }); return ws.on("connection", ((socket: WebSocket, req: http.IncomingMessage) => { - const cluster = this.clusterManager.getClusterForRequest(req); - const nodeParam = url.parse(req.url, true).query["node"]?.toString(); + const cluster = assert( + this.clusterManager.getClusterForRequest(req), + "ClusterID must be a valid ID" + ); + const nodeParam = req.url && url.parse(req.url, true).query["node"]?.toString(); const shell = nodeParam ? new NodeShellSession(socket, cluster, nodeParam) : new LocalShellSession(socket, cluster); @@ -182,8 +195,8 @@ export class LensProxy { })); } - protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise { - if (req.url.startsWith(apiKubePrefix)) { + protected async getProxyTarget(req: http.IncomingMessage, contextHandler: ContextHandler): Promise { + if (req.url?.startsWith(apiKubePrefix)) { delete req.headers.authorization; req.url = req.url.replace(apiKubePrefix, ""); const isWatchRequest = req.url.includes("watch="); @@ -193,14 +206,15 @@ export class LensProxy { } protected getRequestId(req: http.IncomingMessage) { - return req.headers.host + req.url; + return (req.headers.host ?? "") + (req.url ?? ""); } protected async handleRequest(proxy: httpProxy, req: http.IncomingMessage, res: http.ServerResponse) { const cluster = this.clusterManager.getClusterForRequest(req); if (cluster) { - const proxyTarget = await this.getProxyTarget(req, cluster.contextHandler); + const contextHandler = assert(cluster.contextHandler, "Cluster must be initialized to handle requests"); + const proxyTarget = await this.getProxyTarget(req, contextHandler); if (proxyTarget) { // allow to fetch apis in "clusterId.localhost:port" from "localhost:port" @@ -210,6 +224,7 @@ export class LensProxy { return proxy.web(req, res, proxyTarget); } } + this.router.route(cluster, req, res); } diff --git a/src/main/menu.ts b/src/main/menu.ts index 1dcd320584..91532fad7c 100644 --- a/src/main/menu.ts +++ b/src/main/menu.ts @@ -14,7 +14,12 @@ import { exitApp } from "./exit-app"; import { broadcastMessage } from "../common/ipc"; import * as packageJson from "../../package.json"; -export type MenuTopId = "mac" | "file" | "edit" | "view" | "help"; +type UniversalTopMenuId = "file" | "edit" | "view" | "help"; +export type MenuTopId = "mac" | UniversalTopMenuId; + +interface AppMenus extends Record { + mac?: MenuItemConstructorOptions; +} export function initMenu(windowManager: WindowManager) { return autorun(() => buildMenu(windowManager), { @@ -67,8 +72,8 @@ export function buildMenu(windowManager: WindowManager) { submenu: [ { label: "About Lens", - click(menuItem: MenuItem, browserWindow: BrowserWindow) { - showAbout(browserWindow); + click(menuItem: MenuItem, browserWindow?: BrowserWindow) { + browserWindow && showAbout(browserWindow); } }, { type: "separator" }, @@ -245,15 +250,15 @@ export function buildMenu(windowManager: WindowManager) { ...ignoreOnMac([ { label: "About Lens", - click(menuItem: MenuItem, browserWindow: BrowserWindow) { - showAbout(browserWindow); + click(menuItem: MenuItem, browserWindow?: BrowserWindow) { + browserWindow && showAbout(browserWindow); } } ]) ] }; // Prepare menu items order - const appMenu: Record = { + const appMenu = { mac: macAppMenu, file: fileMenu, edit: editMenu, @@ -273,7 +278,7 @@ export function buildMenu(windowManager: WindowManager) { }); if (!isMac) { - delete appMenu.mac; + delete (appMenu as AppMenus).mac; } const menu = Menu.buildFromTemplate(Object.values(appMenu)); @@ -284,9 +289,9 @@ export function buildMenu(windowManager: WindowManager) { // this is a workaround for the test environment (spectron) not being able to directly access // the application menus (https://github.com/electron-userland/spectron/issues/21) ipcMain.on("test-menu-item-click", (event: IpcMainEvent, ...names: string[]) => { - let menu: Menu = Menu.getApplicationMenu(); + let menu: Menu | undefined | null = Menu.getApplicationMenu(); + let menuItem: MenuItem | undefined; const parentLabels: string[] = []; - let menuItem: MenuItem; for (const name of names) { parentLabels.push(name); diff --git a/src/main/prometheus/helm.ts b/src/main/prometheus/helm.ts index 438cc87a64..e1e4af9ef4 100644 --- a/src/main/prometheus/helm.ts +++ b/src/main/prometheus/helm.ts @@ -8,25 +8,15 @@ export class PrometheusHelm extends PrometheusLens { name = "Helm"; rateAccuracy = "5m"; - public async getPrometheusService(client: CoreV1Api): Promise { + public async getPrometheusService(client: CoreV1Api): Promise { const labelSelector = "app=prometheus,component=server,heritage=Helm"; try { - const serviceList = await client.listServiceForAllNamespaces(false, "", null, labelSelector); - const service = serviceList.body.items[0]; + const serviceList = await client.listServiceForAllNamespaces(false, "", undefined, labelSelector); - if (!service) return; - - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port - }; + return super.getPrometheusServiceRaw(serviceList.body.items[0]); } catch(error) { logger.warn(`PrometheusHelm: failed to list services: ${error.toString()}`); - - return; } } } diff --git a/src/main/prometheus/lens.ts b/src/main/prometheus/lens.ts index 2d85a5a0a2..8634fbee33 100644 --- a/src/main/prometheus/lens.ts +++ b/src/main/prometheus/lens.ts @@ -2,28 +2,22 @@ import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusSer import { CoreV1Api } from "@kubernetes/client-node"; import logger from "../logger"; -export class PrometheusLens implements PrometheusProvider { +export class PrometheusLens extends PrometheusProvider { id = "lens"; name = "Lens"; rateAccuracy = "1m"; - public async getPrometheusService(client: CoreV1Api): Promise { + public async getPrometheusService(client: CoreV1Api): Promise { try { const resp = await client.readNamespacedService("prometheus", "lens-metrics"); - const service = resp.body; - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port - }; + return super.getPrometheusServiceRaw(resp.body); } catch(error) { logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); } } - public getQueries(opts: PrometheusQueryOpts): PrometheusQuery { + public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined { switch(opts.category) { case "cluster": return { diff --git a/src/main/prometheus/operator.ts b/src/main/prometheus/operator.ts index 3e7bff071e..cb51922899 100644 --- a/src/main/prometheus/operator.ts +++ b/src/main/prometheus/operator.ts @@ -1,39 +1,40 @@ import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusService } from "./provider-registry"; -import { CoreV1Api, V1Service } from "@kubernetes/client-node"; +import { CoreV1Api } from "@kubernetes/client-node"; import logger from "../logger"; -export class PrometheusOperator implements PrometheusProvider { +export class PrometheusOperator extends PrometheusProvider { rateAccuracy = "1m"; id = "operator"; name = "Prometheus Operator"; - public async getPrometheusService(client: CoreV1Api): Promise { + public async getPrometheusService(client: CoreV1Api): Promise { try { - let service: V1Service; + let serviceItem; for (const labelSelector of ["operated-prometheus=true", "self-monitor=true"]) { - if (!service) { - const serviceList = await client.listServiceForAllNamespaces(null, null, null, labelSelector); - - service = serviceList.body.items[0]; - } + serviceItem ??= ( + await client.listServiceForAllNamespaces(undefined, undefined, undefined, labelSelector) + )?.body.items[0]; } - if (!service) return; - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port - }; + const { metadata, spec } = serviceItem ?? {}; + const { namespace, name: service } = metadata ?? {}; + const { ports: [{ port }] = [] } = spec ?? {}; + + if (port && namespace && service) { + return { + id: this.id, + namespace, + service, + port, + }; + } } catch(error) { logger.warn(`PrometheusOperator: failed to list services: ${error.toString()}`); - - return; } } - public getQueries(opts: PrometheusQueryOpts): PrometheusQuery { + public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined { switch(opts.category) { case "cluster": return { diff --git a/src/main/prometheus/provider-registry.ts b/src/main/prometheus/provider-registry.ts index c649560c85..4b7826d665 100644 --- a/src/main/prometheus/provider-registry.ts +++ b/src/main/prometheus/provider-registry.ts @@ -1,4 +1,4 @@ -import { CoreV1Api } from "@kubernetes/client-node"; +import { CoreV1Api, V1Service } from "@kubernetes/client-node"; export type PrometheusClusterQuery = { memoryUsage: string; @@ -59,11 +59,26 @@ export type PrometheusService = { port: number; }; -export interface PrometheusProvider { - id: string; - name: string; - getQueries(opts: PrometheusQueryOpts): PrometheusQuery; - getPrometheusService(client: CoreV1Api): Promise; +export abstract class PrometheusProvider { + abstract id: string; + abstract name: string; + abstract getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined; + abstract getPrometheusService(client: CoreV1Api): Promise; + + protected getPrometheusServiceRaw(raw?: V1Service): PrometheusService | undefined { + const { metadata, spec } = raw ?? {}; + const { namespace, name: service } = metadata ?? {}; + const { ports: [{ port }] = [] } = spec ?? {}; + + if (port && namespace && service) { + return { + id: this.id, + namespace, + service, + port, + }; + } + } } export type PrometheusProviderList = { diff --git a/src/main/prometheus/stacklight.ts b/src/main/prometheus/stacklight.ts index 07ac8f6668..3df15b1a91 100644 --- a/src/main/prometheus/stacklight.ts +++ b/src/main/prometheus/stacklight.ts @@ -2,28 +2,22 @@ import { PrometheusProvider, PrometheusQueryOpts, PrometheusQuery, PrometheusSer import { CoreV1Api } from "@kubernetes/client-node"; import logger from "../logger"; -export class PrometheusStacklight implements PrometheusProvider { +export class PrometheusStacklight extends PrometheusProvider { id = "stacklight"; name = "Stacklight"; rateAccuracy = "1m"; - public async getPrometheusService(client: CoreV1Api): Promise { + public async getPrometheusService(client: CoreV1Api): Promise { try { const resp = await client.readNamespacedService("prometheus-server", "stacklight"); - const service = resp.body; - return { - id: this.id, - namespace: service.metadata.namespace, - service: service.metadata.name, - port: service.spec.ports[0].port - }; + return super.getPrometheusServiceRaw(resp.body); } catch(error) { logger.warn(`PrometheusLens: failed to list services: ${error.response.body.message}`); } } - public getQueries(opts: PrometheusQueryOpts): PrometheusQuery { + public getQueries(opts: PrometheusQueryOpts): PrometheusQuery | undefined { switch(opts.category) { case "cluster": return { diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts index 6f1b0a8e0f..fd33739643 100644 --- a/src/main/resource-applier.ts +++ b/src/main/resource-applier.ts @@ -7,7 +7,7 @@ import path from "path"; import * as tempy from "tempy"; import logger from "./logger"; import { appEventBus } from "../common/event-bus"; -import { cloneJsonObject } from "../common/utils"; +import { assert, cloneJsonObject, NotFalsy } from "../common/utils"; export class ResourceApplier { constructor(protected cluster: Cluster) { @@ -21,7 +21,7 @@ export class ResourceApplier { } protected async kubectlApply(content: string): Promise { - const { kubeCtl } = this.cluster; + const kubeCtl = assert(this.cluster.kubeCtl, "Cluster must be initialized correctly before being applied against"); const kubectlPath = await kubeCtl.getPath(); const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); @@ -53,7 +53,7 @@ export class ResourceApplier { } public async kubectlApplyAll(resources: string[]): Promise { - const { kubeCtl } = this.cluster; + const kubeCtl = assert(this.cluster.kubeCtl, "Cluster must be initialized correctly before being applied against"); const kubectlPath = await kubeCtl.getPath(); const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath(); diff --git a/src/main/router.ts b/src/main/router.ts index 6fa14e1444..3c489afbb5 100644 --- a/src/main/router.ts +++ b/src/main/router.ts @@ -11,12 +11,12 @@ import logger from "./logger"; export interface RouterRequestOpts { req: http.IncomingMessage; res: http.ServerResponse; - cluster: Cluster; + cluster: Cluster | null; params: RouteParams; url: URL; } -export interface RouteParams extends Record { +export interface RouteParams extends Record { path?: string; // *-route namespace?: string; service?: string; @@ -30,7 +30,7 @@ export interface LensApiRequest

{ path: string; payload: P; params: RouteParams; - cluster: Cluster; + cluster: Cluster | null; response: http.ServerResponse; query: URLSearchParams; raw: { @@ -52,10 +52,10 @@ export class Router { return path.resolve(__static); } - public async route(cluster: Cluster, req: http.IncomingMessage, res: http.ServerResponse): Promise { - const url = new URL(req.url, "http://localhost"); + public async route(cluster: Cluster | null, req: http.IncomingMessage, res: http.ServerResponse): Promise { + const url = new URL(req.url ?? "", "http://localhost"); const path = url.pathname; - const method = req.method.toLowerCase(); + const method = req.method?.toLowerCase() ?? "get"; const matchingRoute = this.router.route(method, path); const routeFound = !matchingRoute.isBoom; @@ -118,6 +118,13 @@ export class Router { return; } + if (!req.url) { + logger.error("handleStaticFile: no URL in request"); + res.statusCode = 404; + + return res.end(); + } + try { const filename = path.basename(req.url); // redirect requests to [appName].js, [appName].html /sockjs-node/ to webpack-dev-server (for hot-reload support) @@ -132,6 +139,7 @@ export class Router { return; } + const data = await readFile(asset); res.setHeader("Content-Type", this.getMimeType(asset)); @@ -141,10 +149,10 @@ export class Router { if (retryCount > 5) { logger.error("handleStaticFile:", err.toString()); res.statusCode = 404; - res.end(); - return; + return res.end(); } + this.handleStaticFile(`${publicPath}/${appName}.html`, res, req, Math.max(retryCount, 0) + 1); } } @@ -154,7 +162,9 @@ export class Router { this.router.add( { method: "get", path: "/{path*}" }, ({ params, response, raw: { req } }: LensApiRequest) => { - this.handleStaticFile(params.path, response, req); + if (params.path) { + this.handleStaticFile(params.path, response, req); + } }); this.router.add({ method: "get", path: "/version"}, versionRoute.getVersion.bind(versionRoute)); diff --git a/src/main/routes/helm-route.ts b/src/main/routes/helm-route.ts index c3c0bdb353..a332b8e637 100644 --- a/src/main/routes/helm-route.ts +++ b/src/main/routes/helm-route.ts @@ -17,7 +17,7 @@ class HelmApiRoute extends LensApi { try { const chart = await helmService.getChart(params.repo, params.chart, query.get("version")); - this.respondJson(response, chart); + this.respondJson(response, chart ?? {}); } catch (error) { this.respondText(response, error, 422); } @@ -29,7 +29,7 @@ class HelmApiRoute extends LensApi { try { const values = await helmService.getChartValues(params.repo, params.chart, query.get("version")); - this.respondJson(response, values); + this.respondJson(response, values ?? {}); } catch (error) { this.respondText(response, error, 422); } @@ -38,10 +38,16 @@ class HelmApiRoute extends LensApi { public async installChart(request: LensApiRequest) { const { payload, cluster, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on installChart request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.installChart(cluster, payload); - this.respondJson(response, result, 201); + this.respondJson(response, result ?? {}, 201); } catch (error) { logger.debug(error); this.respondText(response, error, 422); @@ -51,10 +57,16 @@ class HelmApiRoute extends LensApi { public async updateRelease(request: LensApiRequest) { const { cluster, params, payload, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on updateRelease request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.updateRelease(cluster, params.release, params.namespace, payload ); - this.respondJson(response, result); + this.respondJson(response, result ?? {}); } catch (error) { logger.debug(error); this.respondText(response, error, 422); @@ -64,10 +76,16 @@ class HelmApiRoute extends LensApi { public async rollbackRelease(request: LensApiRequest) { const { cluster, params, payload, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on rollbackRelease request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.rollback(cluster, params.release, params.namespace, payload.revision); - this.respondJson(response, result); + this.respondJson(response, result ?? {}); } catch (error) { logger.debug(error); this.respondText(response, error, 422); @@ -77,10 +95,16 @@ class HelmApiRoute extends LensApi { public async listReleases(request: LensApiRequest) { const { cluster, params, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on listReleases request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.listReleases(cluster, params.namespace); - this.respondJson(response, result); + this.respondJson(response, result ?? {}); } catch(error) { logger.debug(error); this.respondText(response, error, 422); @@ -90,6 +114,12 @@ class HelmApiRoute extends LensApi { public async getRelease(request: LensApiRequest) { const { cluster, params, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on getRelease request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.getRelease(cluster, params.release, params.namespace); @@ -103,10 +133,16 @@ class HelmApiRoute extends LensApi { public async getReleaseValues(request: LensApiRequest) { const { cluster, params, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on getReleaseValues request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.getReleaseValues(cluster, params.release, params.namespace); - this.respondText(response, result); + this.respondText(response, result ?? ""); } catch (error) { logger.debug(error); this.respondText(response, error, 422); @@ -116,6 +152,12 @@ class HelmApiRoute extends LensApi { public async getReleaseHistory(request: LensApiRequest) { const { cluster, params, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on getReleaseHistory request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.getReleaseHistory(cluster, params.release, params.namespace); @@ -129,10 +171,16 @@ class HelmApiRoute extends LensApi { public async deleteRelease(request: LensApiRequest) { const { cluster, params, response } = request; + if (!cluster) { + logger.error("[HELM-ROUTE]: no cluster defined on deleteRelease request"); + + return this.respondText(response, "No Cluster defined on request", 404); + } + try { const result = await helmService.deleteRelease(cluster, params.release, params.namespace); - this.respondJson(response, result); + this.respondJson(response, result ?? ""); } catch (error) { logger.debug(error); this.respondText(response, error, 422); diff --git a/src/main/routes/kubeconfig-route.ts b/src/main/routes/kubeconfig-route.ts index bad5ecd57a..c153025586 100644 --- a/src/main/routes/kubeconfig-route.ts +++ b/src/main/routes/kubeconfig-route.ts @@ -2,8 +2,15 @@ import { LensApiRequest } from "../router"; import { LensApi } from "../lens-api"; import { Cluster } from "../cluster"; import { CoreV1Api, V1Secret } from "@kubernetes/client-node"; +import logger from "../logger"; +import { assert } from "../../common/utils"; +import { AssertionError } from "assert"; function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster) { + if (!secret.data) { + return; + } + const tokenData = Buffer.from(secret.data["token"], "base64"); return { @@ -32,7 +39,7 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster "context": { "user": username, "cluster": cluster.contextName, - "namespace": secret.metadata.namespace, + "namespace": secret.metadata?.namespace, } } ], @@ -43,17 +50,35 @@ function generateKubeConfig(username: string, secret: V1Secret, cluster: Cluster class KubeconfigRoute extends LensApi { public async routeServiceAccountRoute(request: LensApiRequest) { - const { params, response, cluster} = request; - const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); - const secretList = await client.listNamespacedSecret(params.namespace); - const secret = secretList.body.items.find(secret => { - const { annotations } = secret.metadata; + const { params, response, cluster: maybeCluster } = request; - return annotations && annotations["kubernetes.io/service-account.name"] == params.account; - }); - const data = generateKubeConfig(params.account, secret, cluster); + try { + const cluster = assert(maybeCluster, "No Cluster defined on request"); + const namespace = assert(params.namespace, "Namespace not provided"); + const account = assert(params.account, "AccountName not provided"); - this.respondJson(response, data); + const client = (await cluster.getProxyKubeconfig()).makeApiClient(CoreV1Api); + const secretList = await client.listNamespacedSecret(namespace); + const secret = assert( + secretList.body.items + .find(({ metadata }) => ( + metadata?.annotations?.["kubernetes.io/service-account.name"] == account + )), + "No secret found matching the account name", + ); + + const data = generateKubeConfig(account, secret, cluster); + + this.respondJson(response, data ?? {}); + } catch (error) { + logger.error(`[KUBECONFIG-ROUTE]: routeServiceAccount failed: ${error}`); + + if (error instanceof AssertionError) { + this.respondText(response, error.message, 404); + } else { + this.respondText(response, error.toString(), 404); + } + } } } diff --git a/src/main/routes/metrics-route.ts b/src/main/routes/metrics-route.ts index d132f7b0ae..3a73cc0ed8 100644 --- a/src/main/routes/metrics-route.ts +++ b/src/main/routes/metrics-route.ts @@ -4,6 +4,9 @@ import { LensApi } from "../lens-api"; import { Cluster, ClusterMetadataKey } from "../cluster"; import { ClusterPrometheusMetadata } from "../../common/cluster-store"; import logger from "../logger"; +import { assert, NotFalsy } from "../../common/utils"; +import { AssertionError } from "assert"; +import { PrometheusQueryOpts } from "../prometheus/provider-registry"; export type IMetricsQuery = string | string[] | { [metricName: string]: string; @@ -41,50 +44,69 @@ async function loadMetrics(promQueries: string[], cluster: Cluster, prometheusPa } class MetricsRoute extends LensApi { - async routeMetrics({ response, cluster, payload, query }: LensApiRequest) { - const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); - const prometheusMetadata: ClusterPrometheusMetadata = {}; - + async routeMetrics({ response, cluster: maybeCluster, payload, query }: LensApiRequest) { try { - const [prometheusPath, prometheusProvider] = await Promise.all([ - cluster.contextHandler.getPrometheusPath(), - cluster.contextHandler.getPrometheusProvider() - ]); + const cluster = assert(maybeCluster, "No Cluster defined on request"); + const contextHandler = assert(cluster.contextHandler, "Cluster must be initialized to be routed against"); - prometheusMetadata.provider = prometheusProvider?.id; - prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; + const queryParams: IMetricsQuery = Object.fromEntries(query.entries()); + const prometheusMetadata: ClusterPrometheusMetadata = {}; - if (!prometheusPath) { + try { + const [prometheusPath, prometheusProvider] = await Promise.all([ + contextHandler.getPrometheusPath(), + contextHandler.getPrometheusProvider() + ]); + + prometheusMetadata.provider = prometheusProvider?.id; + prometheusMetadata.autoDetected = !cluster.preferences.prometheusProvider?.type; + + if (!prometheusPath) { + prometheusMetadata.success = false; + this.respondJson(response, {}); + + return; + } + + // return data in same structure as query + if (typeof payload === "string") { + const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams); + + this.respondJson(response, data); + } else if (Array.isArray(payload)) { + const data = await loadMetrics(payload, cluster, prometheusPath, queryParams); + + this.respondJson(response, data); + } else if (payload && typeof payload === "object") { + const queries = Object.entries(payload) + .map(([queryName, queryOpts]) => { + const queries = prometheusProvider?.getQueries(queryOpts as PrometheusQueryOpts); + + if (queries) { + return (queries as Record)[queryName]; + } + }) + .filter(NotFalsy); + const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); + const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); + + this.respondJson(response, data); + } + prometheusMetadata.success = true; + } catch { prometheusMetadata.success = false; this.respondJson(response, {}); - - return; + } finally { + cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; } + } catch (error) { + logger.error(`[METRICS-ROUTE]: routeMetrics failed: ${error}`); - // return data in same structure as query - if (typeof payload === "string") { - const [data] = await loadMetrics([payload], cluster, prometheusPath, queryParams); - - this.respondJson(response, data); - } else if (Array.isArray(payload)) { - const data = await loadMetrics(payload, cluster, prometheusPath, queryParams); - - this.respondJson(response, data); + if (error instanceof AssertionError) { + this.respondText(response, error.message, 404); } else { - const queries = Object.entries(payload).map(([queryName, queryOpts]) => ( - (prometheusProvider.getQueries(queryOpts) as Record)[queryName] - )); - const result = await loadMetrics(queries, cluster, prometheusPath, queryParams); - const data = Object.fromEntries(Object.keys(payload).map((metricName, i) => [metricName, result[i]])); - - this.respondJson(response, data); + this.respondText(response, error.toString(), 404); } - prometheusMetadata.success = true; - } catch { - prometheusMetadata.success = false; - this.respondJson(response, {}); - } finally { - cluster.metadata[ClusterMetadataKey.PROMETHEUS] = prometheusMetadata; } } } diff --git a/src/main/routes/port-forward-route.ts b/src/main/routes/port-forward-route.ts index 0b5954948d..477266889b 100644 --- a/src/main/routes/port-forward-route.ts +++ b/src/main/routes/port-forward-route.ts @@ -6,33 +6,59 @@ import { getFreePort } from "../port"; import { shell } from "electron"; import * as tcpPortUsed from "tcp-port-used"; import logger from "../logger"; +import { AssertionError } from "assert"; +import { assert } from "../../common/utils"; + +interface PortForwardOpts { + clusterId: string; + process?: ChildProcessWithoutNullStreams; + kubeConfig: string; + kind: string; + namespace: string; + name: string; + port: string; + localPort?: number; +} + +interface GetPortForwardOptions { + clusterId: string; + kind: string; + name: string; + namespace: string; + port: string; +} class PortForward { public static portForwards: PortForward[] = []; - static getPortforward(forward: {clusterId: string; kind: string; name: string; namespace: string; port: string}) { - return PortForward.portForwards.find((pf) => { - return ( - pf.clusterId == forward.clusterId && - pf.kind == forward.kind && - pf.name == forward.name && - pf.namespace == forward.namespace && - pf.port == forward.port - ); - }); + static getPortforward(forward: GetPortForwardOptions) { + return PortForward.portForwards.find(pf => ( + pf.clusterId == forward.clusterId && + pf.kind == forward.kind && + pf.name == forward.name && + pf.namespace == forward.namespace && + pf.port == forward.port + )); } public clusterId: string; - public process: ChildProcessWithoutNullStreams; + public process?: ChildProcessWithoutNullStreams; public kubeConfig: string; public kind: string; public namespace: string; public name: string; public port: string; - public localPort: number; + public localPort?: number; - constructor(obj: any) { - Object.assign(this, obj); + constructor(obj: PortForwardOpts) { + this.clusterId = obj.clusterId; + this.process = obj.process; + this.kubeConfig = obj.kubeConfig; + this.kind = obj.kind; + this.namespace = obj.namespace; + this.name = obj.name; + this.port = obj.port; + this.localPort = obj.localPort; } public async start() { @@ -75,39 +101,51 @@ class PortForward { } class PortForwardRoute extends LensApi { - public async routePortForward(request: LensApiRequest) { - const { params, response, cluster} = request; - const { namespace, port, resourceType, resourceName } = params; - let portForward = PortForward.getPortforward({ - clusterId: cluster.id, kind: resourceType, name: resourceName, - namespace, port - }); + const { params, response, cluster: maybeCluster } = request; - if (!portForward) { - logger.info(`Creating a new port-forward ${namespace}/${resourceType}/${resourceName}:${port}`); - portForward = new PortForward({ - clusterId: cluster.id, - kind: resourceType, - namespace, - name: resourceName, - port, - kubeConfig: await cluster.getProxyKubeconfigPath() - }); - const started = await portForward.start(); + try { + const cluster = assert(maybeCluster, "No Cluster defined on request"); + const namespace = assert(params.namespace, "Namespace not provided"); + const port = assert(params.port, "Port not provided"); + const name = assert(params.resourceName, "ResourceName not provided"); + const kind = assert(params.resourceType, "ResourceName not provided"); - if (!started) { - this.respondJson(response, { - message: "Failed to open port-forward" - }, 400); + let portForward = PortForward.getPortforward({ clusterId: cluster.id, kind, name, namespace, port }); - return; + if (!portForward) { + const kubeConfig = assert(await cluster.getProxyKubeconfigPath(), "Cluster must be initialized before being port forwarded from"); + + logger.info(`Creating a new port-forward ${namespace}/${kind}/${name}:${port}`); + portForward = new PortForward({ + clusterId: cluster.id, + kind, + namespace, + name, + port, + kubeConfig, + }); + const started = await portForward.start(); + + if (!started) { + return void this.respondJson(response, { + message: "Failed to open port-forward" + }, 400); + } + } + + portForward.open(); + + this.respondJson(response, {}); + } catch (error) { + logger.error(`[PORT-FORWARD-ROUTE]: routeServiceAccount failed: ${error}`); + + if (error instanceof AssertionError) { + this.respondText(response, error.message, 404); + } else { + this.respondText(response, error.toString(), 404); } } - - portForward.open(); - - this.respondJson(response, {}); } } diff --git a/src/main/routes/resource-applier-route.ts b/src/main/routes/resource-applier-route.ts index 1e532dde46..2d54ff256e 100644 --- a/src/main/routes/resource-applier-route.ts +++ b/src/main/routes/resource-applier-route.ts @@ -1,17 +1,27 @@ import { LensApiRequest } from "../router"; import { LensApi } from "../lens-api"; import { ResourceApplier } from "../resource-applier"; +import { assert } from "../../common/utils"; +import { AssertionError } from "assert"; +import logger from "../logger"; class ResourceApplierApiRoute extends LensApi { public async applyResource(request: LensApiRequest) { - const { response, cluster, payload } = request; + const { response, cluster: maybeCluster, payload } = request; try { + const cluster = assert(maybeCluster, "No Cluster defined on request"); const resource = await new ResourceApplier(cluster).apply(payload); this.respondJson(response, [resource], 200); } catch (error) { - this.respondText(response, error, 422); + logger.error(`[RESOURCE-APPLIER-ROUTE]: routeServiceAccount failed: ${error}`); + + if (error instanceof AssertionError) { + this.respondText(response, error.message, 404); + } else { + this.respondText(response, error, 422); + } } } } diff --git a/src/main/shell-session/node-shell-session.ts b/src/main/shell-session/node-shell-session.ts index 16d467138d..0c87276042 100644 --- a/src/main/shell-session/node-shell-session.ts +++ b/src/main/shell-session/node-shell-session.ts @@ -9,21 +9,20 @@ export class NodeShellSession extends ShellSession { ShellType = "node-shell"; protected podId = `node-shell-${uuid()}`; - protected kc: KubeConfig; constructor(socket: WebSocket, cluster: Cluster, protected nodeName: string) { super(socket, cluster); } public async open() { - this.kc = await this.cluster.getProxyKubeconfig(); + const kc = await this.cluster.getProxyKubeconfig(); const shell = await this.kubectl.getPath(); try { - await this.createNodeShellPod(); - await this.waitForRunningPod(); + await this.createNodeShellPod(kc); + await this.waitForRunningPod(kc); } catch (error) { - this.deleteNodeShellPod(); + this.deleteNodeShellPod(kc); this.sendResponse("Error occurred. "); throw new ShellOpenError("failed to create node pod", error); @@ -35,9 +34,8 @@ export class NodeShellSession extends ShellSession { super.open(shell, args, env); } - protected createNodeShellPod() { - return this - .kc + protected createNodeShellPod(kc: KubeConfig) { + return kc .makeApiClient(k8s.CoreV1Api) .createNamespacedPod("kube-system", { metadata: { @@ -67,9 +65,9 @@ export class NodeShellSession extends ShellSession { }); } - protected waitForRunningPod(): Promise { + protected waitForRunningPod(kc: KubeConfig): Promise { return new Promise((resolve, reject) => { - const watch = new k8s.Watch(this.kc); + const watch = new k8s.Watch(kc); watch .watch(`/api/v1/namespaces/kube-system/pods`, @@ -99,9 +97,8 @@ export class NodeShellSession extends ShellSession { }); } - protected deleteNodeShellPod() { - this - .kc + protected deleteNodeShellPod(kc: KubeConfig) { + kc .makeApiClient(k8s.CoreV1Api) .deleteNamespacedPod(this.podId, "kube-system"); } diff --git a/src/main/shell-session/shell-session.ts b/src/main/shell-session/shell-session.ts index eebaed0605..4ffb189552 100644 --- a/src/main/shell-session/shell-session.ts +++ b/src/main/shell-session/shell-session.ts @@ -25,9 +25,9 @@ export abstract class ShellSession { protected kubectl: Kubectl; protected running = false; - protected shellProcess: pty.IPty; + protected shellProcess?: pty.IPty; protected kubectlBinDirP: Promise; - protected kubeconfigPathP: Promise; + protected kubeconfigPathP: Promise; protected get cwd(): string | undefined { return this.cluster.preferences?.terminalCWD; @@ -71,19 +71,21 @@ export abstract class ShellSession { switch (data[0]) { case "0": - this.shellProcess.write(message); + this.shellProcess?.write(message); break; case "4": const { Width, Height } = JSON.parse(message); - this.shellProcess.resize(Width, Height); + this.shellProcess?.resize(Width, Height); break; } }) .on("close", () => { if (this.running) { try { - process.kill(this.shellProcess.pid); + if (this.shellProcess?.pid) { + process.kill(this.shellProcess?.pid); + } } catch (e) { } } diff --git a/src/main/tray.ts b/src/main/tray.ts index a15fa845d3..4b3ea480ea 100644 --- a/src/main/tray.ts +++ b/src/main/tray.ts @@ -13,7 +13,7 @@ import { exitApp } from "./exit-app"; const TRAY_LOG_PREFIX = "[TRAY]"; // note: instance of Tray should be saved somewhere, otherwise it disappears -export let tray: Tray; +export let tray: Tray | undefined; export function getTrayIcon(): string { return path.resolve( @@ -43,7 +43,7 @@ export function initTray(windowManager: WindowManager) { try { const menu = createTrayMenu(windowManager); - tray.setContextMenu(menu); + tray?.setContextMenu(menu); } catch (error) { logger.error(`${TRAY_LOG_PREFIX}: building failed`, { error }); } @@ -53,7 +53,7 @@ export function initTray(windowManager: WindowManager) { return () => { disposers.forEach(disposer => disposer()); tray?.destroy(); - tray = null; + tray = undefined; }; } diff --git a/src/main/window-manager.ts b/src/main/window-manager.ts index afd6b03670..300e9ee75c 100644 --- a/src/main/window-manager.ts +++ b/src/main/window-manager.ts @@ -12,12 +12,12 @@ import { IpcRendererNavigationEvents } from "../renderer/navigation/events"; import logger from "./logger"; export class WindowManager extends Singleton { - protected mainWindow: BrowserWindow; - protected splashWindow: BrowserWindow; - protected windowState: windowStateKeeper.State; + protected mainWindow: BrowserWindow | null = null; + protected splashWindow: BrowserWindow | null = null; + protected windowState: windowStateKeeper.State | null = null; protected disposers: Record = {}; - @observable activeClusterId: ClusterId; + @observable activeClusterId?: ClusterId; constructor(protected proxyPort: number) { super(); @@ -30,7 +30,7 @@ export class WindowManager extends Singleton { return `http://localhost:${this.proxyPort}`; } - async initMainWindow(showSplash = true) { + async initMainWindow(showSplash = true): Promise { // Manage main window size and position with state persistence if (!this.windowState) { this.windowState = windowStateKeeper({ @@ -77,7 +77,7 @@ export class WindowManager extends Singleton { // clean up this.mainWindow.on("closed", () => { - this.windowState.unmanage(); + this.windowState?.unmanage(); this.mainWindow = null; this.splashWindow = null; app.dock?.hide(); // hide icon in dock (mac-os) @@ -104,6 +104,8 @@ export class WindowManager extends Singleton { } catch (err) { dialog.showErrorBox("ERROR!", err.toString()); } + + return this.mainWindow; } protected async initMenu() { @@ -122,17 +124,18 @@ export class WindowManager extends Singleton { } async ensureMainWindow(): Promise { - if (!this.mainWindow) await this.initMainWindow(); - this.mainWindow.show(); + const mainWindow = this.mainWindow ?? await this.initMainWindow(); - return this.mainWindow; + mainWindow.show(); + + return mainWindow; } sendToView({ channel, frameInfo, data = [] }: { channel: string, frameInfo?: ClusterFrameInfo, data?: any[] }) { if (frameInfo) { - this.mainWindow.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data); + this.mainWindow?.webContents.sendToFrame([frameInfo.processId, frameInfo.frameId], channel, ...data); } else { - this.mainWindow.webContents.send(channel, ...data); + this.mainWindow?.webContents.send(channel, ...data); } } @@ -152,7 +155,7 @@ export class WindowManager extends Singleton { } reload() { - const frameInfo = clusterFrameMap.get(this.activeClusterId); + const frameInfo = this.activeClusterId && clusterFrameMap.get(this.activeClusterId); if (frameInfo) { this.sendToView({ channel: IpcRendererNavigationEvents.RELOAD_PAGE, frameInfo }); @@ -186,8 +189,8 @@ export class WindowManager extends Singleton { } destroy() { - this.mainWindow.destroy(); - this.splashWindow.destroy(); + this.mainWindow?.destroy(); + this.splashWindow?.destroy(); this.mainWindow = null; this.splashWindow = null; Object.entries(this.disposers).forEach(([name, dispose]) => { diff --git a/src/migrations/cluster-store/3.6.0-beta.1.ts b/src/migrations/cluster-store/3.6.0-beta.1.ts index ca2d0ccbed..e44bad7bab 100644 --- a/src/migrations/cluster-store/3.6.0-beta.1.ts +++ b/src/migrations/cluster-store/3.6.0-beta.1.ts @@ -26,7 +26,7 @@ export default migration({ */ try { // take the embedded kubeconfig and dump it into a file - cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig); + cluster.kubeConfigPath = ClusterStore.embedCustomKubeConfig(cluster.id, cluster.kubeConfig ?? ""); cluster.contextName = loadConfig(cluster.kubeConfigPath).getCurrentContext(); delete cluster.kubeConfig; @@ -51,7 +51,7 @@ export default migration({ } } catch (error) { printLog(`Failed to migrate cluster icon for cluster "${cluster.id}"`, error); - delete cluster.preferences.icon; + delete cluster.preferences?.icon; } return cluster; diff --git a/src/renderer/api/__tests__/kube-api-parse.test.ts b/src/renderer/api/__tests__/kube-api-parse.test.ts index bc4528ad4e..3c76c18649 100644 --- a/src/renderer/api/__tests__/kube-api-parse.test.ts +++ b/src/renderer/api/__tests__/kube-api-parse.test.ts @@ -13,7 +13,7 @@ import { IKubeApiParsed, parseKubeApi } from "../kube-api-parse"; /** * [, ] */ -type KubeApiParseTestData = [string, Required]; +type KubeApiParseTestData = [string, IKubeApiParsed]; const tests: KubeApiParseTestData[] = [ ["/apis/apiextensions.k8s.io/v1beta1/customresourcedefinitions/prometheuses.monitoring.coreos.com", { diff --git a/src/renderer/api/api-manager.ts b/src/renderer/api/api-manager.ts index 90e63f692b..4c20aa3821 100644 --- a/src/renderer/api/api-manager.ts +++ b/src/renderer/api/api-manager.ts @@ -3,13 +3,14 @@ import type { KubeObjectStore } from "../kube-object.store"; import { action, observable } from "mobx"; import { autobind } from "../utils"; import { KubeApi, parseKubeApi } from "./kube-api"; +import { KubeObject } from "./kube-object"; @autobind() export class ApiManager { - private apis = observable.map(); - private stores = observable.map(); + private apis = observable.map>(); + private stores = observable.map>>(); - getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { + getApi(pathOrCallback: string | ((api: KubeApi) => boolean)) { if (typeof pathOrCallback === "string") { return this.apis.get(pathOrCallback) || this.apis.get(parseKubeApi(pathOrCallback).apiBase); } @@ -18,10 +19,14 @@ export class ApiManager { } getApiByKind(kind: string, apiVersion: string) { - return Array.from(this.apis.values()).find((api) => api.kind === kind && api.apiVersionWithGroup === apiVersion); + for (const api of this.apis.values()) { + if (api.kind === kind && api.apiVersionWithGroup === apiVersion) { + return api; + } + } } - registerApi(apiBase: string, api: KubeApi) { + registerApi(apiBase: string, api: KubeApi) { if (!this.apis.has(apiBase)) { this.stores.forEach((store) => { if(store.api === api) { @@ -33,13 +38,13 @@ export class ApiManager { } } - protected resolveApi(api: string | KubeApi): KubeApi { + protected resolveApi(api: string | KubeApi): KubeApi | undefined { if (typeof api === "string") return this.getApi(api); return api; } - unregisterApi(api: string | KubeApi) { + unregisterApi(api: string | KubeApi) { if (typeof api === "string") this.apis.delete(api); else { const apis = Array.from(this.apis.entries()); @@ -50,14 +55,20 @@ export class ApiManager { } @action - registerStore(store: KubeObjectStore, apis: KubeApi[] = [store.api]) { + registerStore(store: KubeObjectStore>, apis: KubeApi[] = [store.api]) { apis.forEach(api => { this.stores.set(api.apiBase, store); }); } - getStore(api: string | KubeApi): S { - return this.stores.get(this.resolveApi(api)?.apiBase) as S; + getStore>>(api: string | KubeApi): S | undefined { + const apiBase = this.resolveApi(api)?.apiBase; + + if (!apiBase) { + return; + } + + return this.stores.get(apiBase) as S; } } diff --git a/src/renderer/api/endpoints/cluster.api.ts b/src/renderer/api/endpoints/cluster.api.ts index e96ab7f082..c538cc0183 100644 --- a/src/renderer/api/endpoints/cluster.api.ts +++ b/src/renderer/api/endpoints/cluster.api.ts @@ -2,7 +2,7 @@ import { IMetrics, IMetricsReqParams, metricsApi } from "./metrics.api"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -export class ClusterApi extends KubeApi { +export class ClusterApi extends KubeApi { static kind = "Cluster"; static namespaced = true; @@ -50,42 +50,43 @@ export interface IClusterMetrics { fsUsage: T; } -export class Cluster extends KubeObject { +interface ClusterSpec { + clusterNetwork?: { + serviceDomain?: string; + pods?: { + cidrBlocks?: string[]; + }; + services?: { + cidrBlocks?: string[]; + }; + }; + providerSpec: { + value: { + profile: string; + }; + }; +} + +interface ClusterKubeStatus { + apiEndpoints: { + host: string; + port: string; + }[]; + providerStatus: { + adminUser?: string; + adminPassword?: string; + kubeconfig?: string; + processState?: string; + lensAddress?: string; + }; + errorMessage?: string; + errorReason?: string; +} + +export class Cluster extends KubeObject { static kind = "Cluster"; static apiBase = "/apis/cluster.k8s.io/v1alpha1/clusters"; - spec: { - clusterNetwork?: { - serviceDomain?: string; - pods?: { - cidrBlocks?: string[]; - }; - services?: { - cidrBlocks?: string[]; - }; - }; - providerSpec: { - value: { - profile: string; - }; - }; - }; - status?: { - apiEndpoints: { - host: string; - port: string; - }[]; - providerStatus: { - adminUser?: string; - adminPassword?: string; - kubeconfig?: string; - processState?: string; - lensAddress?: string; - }; - errorMessage?: string; - errorReason?: string; - }; - getStatus() { if (this.metadata.deletionTimestamp) return ClusterStatus.REMOVING; if (!this.status || !this.status) return ClusterStatus.CREATING; diff --git a/src/renderer/api/endpoints/configmap.api.ts b/src/renderer/api/endpoints/configmap.api.ts index 042fb59d86..1eebd4986a 100644 --- a/src/renderer/api/endpoints/configmap.api.ts +++ b/src/renderer/api/endpoints/configmap.api.ts @@ -1,22 +1,14 @@ import { KubeObject } from "../kube-object"; -import { KubeJsonApiData } from "../kube-json-api"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; @autobind() -export class ConfigMap extends KubeObject { +export class ConfigMap extends KubeObject { static kind = "ConfigMap"; static namespaced = true; static apiBase = "/api/v1/configmaps"; - constructor(data: KubeJsonApiData) { - super(data); - this.data = this.data || {}; - } - - data: { - [param: string]: string; - }; + data: Record = {}; getKeys(): string[] { return Object.keys(this.data); diff --git a/src/renderer/api/endpoints/crd.api.ts b/src/renderer/api/endpoints/crd.api.ts index ad7d9d67ca..ed63e99e3d 100644 --- a/src/renderer/api/endpoints/crd.api.ts +++ b/src/renderer/api/endpoints/crd.api.ts @@ -17,93 +17,96 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & { JSONPath: string; }; -export class CustomResourceDefinition extends KubeObject { +interface CustomResourceDefinitionSpec { + group: string; + version?: string; // deprecated in v1 api + names: { + plural: string; + singular: string; + kind: string; + listKind: string; + }; + scope: "Namespaced" | "Cluster" | string; + validation?: any; + versions: { + name: string; + served: boolean; + storage: boolean; + schema?: unknown; // required in v1 but not present in v1beta + additionalPrinterColumns?: AdditionalPrinterColumnsV1[]; + }[]; + conversion: { + strategy?: string; + webhook?: any; + }; + additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1 +} + +interface CustomResourceDefinitionStatus { + conditions: { + lastTransitionTime: string; + message: string; + reason: string; + status: string; + type?: string; + }[]; + acceptedNames: { + plural: string; + singular: string; + kind: string; + shortNames: string[]; + listKind: string; + }; + storedVersions: string[]; +} + +export class CustomResourceDefinition extends KubeObject { static kind = "CustomResourceDefinition"; static namespaced = false; static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions"; - spec: { - group: string; - version?: string; // deprecated in v1 api - names: { - plural: string; - singular: string; - kind: string; - listKind: string; - }; - scope: "Namespaced" | "Cluster" | string; - validation?: any; - versions: { - name: string; - served: boolean; - storage: boolean; - schema?: unknown; // required in v1 but not present in v1beta - additionalPrinterColumns?: AdditionalPrinterColumnsV1[] - }[]; - conversion: { - strategy?: string; - webhook?: any; - }; - additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1 - }; - status: { - conditions: { - lastTransitionTime: string; - message: string; - reason: string; - status: string; - type?: string; - }[]; - acceptedNames: { - plural: string; - singular: string; - kind: string; - shortNames: string[]; - listKind: string; - }; - storedVersions: string[]; - }; - getResourceUrl() { - return crdResourcesURL({ - params: { - group: this.getGroup(), - name: this.getPluralName(), - } - }); + const group = this.getGroup(); + const name = this.getPluralName(); + + if (group && name) { + return crdResourcesURL({ params: { group, name } }); + } } getResourceApiBase() { - const { group } = this.spec; + const { group } = this.spec ?? {}; return `/apis/${group}/${this.getVersion()}/${this.getPluralName()}`; } getPluralName() { - return this.getNames().plural; + return this.getNames()?.plural; } getResourceKind() { - return this.spec.names.kind; + return this.spec?.names.kind; } getResourceTitle() { const name = this.getPluralName(); - return name[0].toUpperCase() + name.substr(1); + if (name) { + return name[0].toUpperCase() + name.substr(1); + } } getGroup() { - return this.spec.group; + return this.spec?.group; } getScope() { - return this.spec.scope; + return this.spec?.scope; } getVersion() { // v1 has removed the spec.version property, if it is present it must match the first version - return this.spec.versions[0]?.name ?? this.spec.version; + return this.spec?.versions[0]?.name ?? this.spec?.version; } isNamespaced() { @@ -111,20 +114,20 @@ export class CustomResourceDefinition extends KubeObject { } getStoredVersions() { - return this.status.storedVersions.join(", "); + return this.status?.storedVersions.join(", "); } getNames() { - return this.spec.names; + return this.spec?.names; } getConversion() { - return JSON.stringify(this.spec.conversion); + return JSON.stringify(this.spec?.conversion); } getPrinterColumns(ignorePriority = true): AdditionalPrinterColumnsV1[] { - const columns = this.spec.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns - ?? this.spec.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape + const columns = this.spec?.versions.find(a => this.getVersion() == a.name)?.additionalPrinterColumns + ?? this.spec?.additionalPrinterColumns?.map(({ JSONPath, ...rest }) => ({ ...rest, jsonPath: JSONPath })) // map to V1 shape ?? []; return columns @@ -133,13 +136,13 @@ export class CustomResourceDefinition extends KubeObject { } getValidation() { - return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2); + return JSON.stringify(this.spec?.validation ?? this.spec?.versions?.[0]?.schema, null, 2); } getConditions() { if (!this.status?.conditions) return []; - return this.status.conditions.map(condition => { + return this.status?.conditions.map(condition => { const { message, reason, lastTransitionTime, status } = condition; return { @@ -151,7 +154,7 @@ export class CustomResourceDefinition extends KubeObject { } } -export const crdApi = new KubeApi({ +export const crdApi = new KubeApi({ objectConstructor: CustomResourceDefinition, checkPreferredVersion: true, }); diff --git a/src/renderer/api/endpoints/cron-job.api.ts b/src/renderer/api/endpoints/cron-job.api.ts index 5063fbd66c..fea2d31d1d 100644 --- a/src/renderer/api/endpoints/cron-job.api.ts +++ b/src/renderer/api/endpoints/cron-job.api.ts @@ -5,7 +5,7 @@ import { formatDuration } from "../../utils/formatDuration"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; -export class CronJobApi extends KubeApi { +export class CronJobApi extends KubeApi { suspend(params: { namespace: string; name: string }) { return this.request.patch(this.getUrl(params), { data: { @@ -37,82 +37,72 @@ export class CronJobApi extends KubeApi { } } +interface CronJobSpec { + schedule: string; + concurrencyPolicy: string; + suspend: boolean; + jobTemplate: { + metadata: { + creationTimestamp?: string; + labels?: { + [key: string]: string; + }; + annotations?: { + [key: string]: string; + }; + }; + spec: { + template: { + metadata: { + creationTimestamp?: string; + }; + spec: { + containers: IPodContainer[]; + restartPolicy: string; + terminationGracePeriodSeconds: number; + dnsPolicy: string; + hostPID: boolean; + schedulerName: string; + }; + }; + }; + }; + successfulJobsHistoryLimit: number; + failedJobsHistoryLimit: number; +} + +interface CronJobStatus { + lastScheduleTime?: string; +} + @autobind() -export class CronJob extends KubeObject { +export class CronJob extends KubeObject { static kind = "CronJob"; static namespaced = true; static apiBase = "/apis/batch/v1beta1/cronjobs"; - kind: string; - apiVersion: string; - metadata: { - name: string; - namespace: string; - selfLink: string; - uid: string; - resourceVersion: string; - creationTimestamp: string; - labels: { - [key: string]: string; - }; - annotations: { - [key: string]: string; - }; - }; - spec: { - schedule: string; - concurrencyPolicy: string; - suspend: boolean; - jobTemplate: { - metadata: { - creationTimestamp?: string; - labels?: { - [key: string]: string; - }; - annotations?: { - [key: string]: string; - }; - }; - spec: { - template: { - metadata: { - creationTimestamp?: string; - }; - spec: { - containers: IPodContainer[]; - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; - hostPID: boolean; - schedulerName: string; - }; - }; - }; - }; - successfulJobsHistoryLimit: number; - failedJobsHistoryLimit: number; - }; - status: { - lastScheduleTime?: string; - }; - getSuspendFlag() { - return this.spec.suspend.toString(); + return this.spec?.suspend.toString(); } getLastScheduleTime() { - if (!this.status.lastScheduleTime) return "-"; - const diff = moment().diff(this.status.lastScheduleTime); + if (!this.status?.lastScheduleTime) return "-"; + const diff = moment().diff(this.status?.lastScheduleTime); return formatDuration(diff, true); } getSchedule() { - return this.spec.schedule; + return this.spec?.schedule; } isNeverRun() { const schedule = this.getSchedule(); + + if (!schedule) { + return true; + } + const daysInMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; const stamps = schedule.split(" "); const day = Number(stamps[stamps.length - 3]); // 1-31 @@ -124,7 +114,7 @@ export class CronJob extends KubeObject { } isSuspend() { - return this.spec.suspend; + return this.spec?.suspend; } } diff --git a/src/renderer/api/endpoints/daemon-set.api.ts b/src/renderer/api/endpoints/daemon-set.api.ts index 8dab807517..cb7d469cad 100644 --- a/src/renderer/api/endpoints/daemon-set.api.ts +++ b/src/renderer/api/endpoints/daemon-set.api.ts @@ -1,68 +1,64 @@ import get from "lodash/get"; import { IPodContainer } from "./pods.api"; -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; +import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; +export interface DaemonSetSpec extends WorkloadSpec { + template: { + metadata: { + creationTimestamp?: string; + labels: { + name: string; + }; + }; + spec: { + containers: IPodContainer[]; + initContainers?: IPodContainer[]; + restartPolicy: string; + terminationGracePeriodSeconds: number; + dnsPolicy: string; + hostPID: boolean; + affinity?: IAffinity; + nodeSelector?: { + [selector: string]: string; + }; + securityContext: {}; + schedulerName: string; + tolerations: { + key: string; + operator: string; + effect: string; + tolerationSeconds: number; + }[]; + }; + }; + updateStrategy: { + type: string; + rollingUpdate: { + maxUnavailable: number; + }; + }; + revisionHistoryLimit: number; +} + +interface DaemonSetStatus { + currentNumberScheduled: number; + numberMisscheduled: number; + desiredNumberScheduled: number; + numberReady: number; + observedGeneration: number; + updatedNumberScheduled: number; + numberAvailable: number; + numberUnavailable: number; +} + @autobind() -export class DaemonSet extends WorkloadKubeObject { +export class DaemonSet extends WorkloadKubeObject { static kind = "DaemonSet"; static namespaced = true; static apiBase = "/apis/apps/v1/daemonsets"; - spec: { - selector: { - matchLabels: { - [name: string]: string; - }; - }; - template: { - metadata: { - creationTimestamp?: string; - labels: { - name: string; - }; - }; - spec: { - containers: IPodContainer[]; - initContainers?: IPodContainer[]; - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; - hostPID: boolean; - affinity?: IAffinity; - nodeSelector?: { - [selector: string]: string; - }; - securityContext: {}; - schedulerName: string; - tolerations: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; - }[]; - }; - }; - updateStrategy: { - type: string; - rollingUpdate: { - maxUnavailable: number; - }; - }; - revisionHistoryLimit: number; - }; - status: { - currentNumberScheduled: number; - numberMisscheduled: number; - desiredNumberScheduled: number; - numberReady: number; - observedGeneration: number; - updatedNumberScheduled: number; - numberAvailable: number; - numberUnavailable: number; - }; - getImages() { const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); const initContainers: IPodContainer[] = get(this, "spec.template.spec.initContainers", []); diff --git a/src/renderer/api/endpoints/deployment.api.ts b/src/renderer/api/endpoints/deployment.api.ts index 107e970d98..3acb407f08 100644 --- a/src/renderer/api/endpoints/deployment.api.ts +++ b/src/renderer/api/endpoints/deployment.api.ts @@ -1,18 +1,18 @@ import moment from "moment"; -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; +import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; -export class DeploymentApi extends KubeApi { +export class DeploymentApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { return `${this.getUrl(params)}/scale`; } - getReplicas(params: { namespace: string; name: string }): Promise { - return this.request - .get(this.getScaleApiUrl(params)) - .then(({ status }: any) => status?.replicas); + async getReplicas(params: { namespace: string; name: string }): Promise { + const { status } = await this.request.get(this.getScaleApiUrl(params)); + + return status?.replicas ?? 0; } scale(params: { namespace: string; name: string }, replicas: number) { @@ -66,110 +66,114 @@ interface IContainerProbe { failureThreshold?: number; } +interface DeploymentSpec extends WorkloadSpec { + replicas: number; + template: { + metadata: { + creationTimestamp?: string; + labels: { + [app: string]: string; + }; + annotations?: { + [app: string]: string; + }; + }; + spec: { + containers: { + name: string; + image: string; + args?: string[]; + ports?: { + name: string; + containerPort: number; + protocol: string; + }[]; + env?: { + name: string; + value: string; + }[]; + resources: { + limits?: { + cpu: string; + memory: string; + }; + requests: { + cpu: string; + memory: string; + }; + }; + volumeMounts?: { + name: string; + mountPath: string; + }[]; + livenessProbe?: IContainerProbe; + readinessProbe?: IContainerProbe; + startupProbe?: IContainerProbe; + terminationMessagePath: string; + terminationMessagePolicy: string; + imagePullPolicy: string; + }[]; + restartPolicy: string; + terminationGracePeriodSeconds: number; + dnsPolicy: string; + affinity?: IAffinity; + nodeSelector?: { + [selector: string]: string; + }; + serviceAccountName: string; + serviceAccount: string; + securityContext: {}; + schedulerName: string; + tolerations?: { + key: string; + operator: string; + effect: string; + tolerationSeconds: number; + }[]; + volumes?: { + name: string; + configMap: { + name: string; + defaultMode: number; + optional: boolean; + }; + }[]; + }; + }; + strategy: { + type: string; + rollingUpdate: { + maxUnavailable: number; + maxSurge: number; + }; + }; +} + +interface DeploymentStatus { + observedGeneration: number; + replicas: number; + updatedReplicas: number; + readyReplicas: number; + availableReplicas?: number; + unavailableReplicas?: number; + conditions: { + type: string; + status: string; + lastUpdateTime: string; + lastTransitionTime: string; + reason: string; + message: string; + }[]; +} + @autobind() -export class Deployment extends WorkloadKubeObject { +export class Deployment extends WorkloadKubeObject { static kind = "Deployment"; static namespaced = true; static apiBase = "/apis/apps/v1/deployments"; - spec: { - replicas: number; - selector: { matchLabels: { [app: string]: string } }; - template: { - metadata: { - creationTimestamp?: string; - labels: { [app: string]: string }; - annotations?: { [app: string]: string }; - }; - spec: { - containers: { - name: string; - image: string; - args?: string[]; - ports?: { - name: string; - containerPort: number; - protocol: string; - }[]; - env?: { - name: string; - value: string; - }[]; - resources: { - limits?: { - cpu: string; - memory: string; - }; - requests: { - cpu: string; - memory: string; - }; - }; - volumeMounts?: { - name: string; - mountPath: string; - }[]; - livenessProbe?: IContainerProbe; - readinessProbe?: IContainerProbe; - startupProbe?: IContainerProbe; - terminationMessagePath: string; - terminationMessagePolicy: string; - imagePullPolicy: string; - }[]; - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; - affinity?: IAffinity; - nodeSelector?: { - [selector: string]: string; - }; - serviceAccountName: string; - serviceAccount: string; - securityContext: {}; - schedulerName: string; - tolerations?: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; - }[]; - volumes?: { - name: string; - configMap: { - name: string; - defaultMode: number; - optional: boolean; - }; - }[]; - }; - }; - strategy: { - type: string; - rollingUpdate: { - maxUnavailable: number; - maxSurge: number; - }; - }; - }; - status: { - observedGeneration: number; - replicas: number; - updatedReplicas: number; - readyReplicas: number; - availableReplicas?: number; - unavailableReplicas?: number; - conditions: { - type: string; - status: string; - lastUpdateTime: string; - lastTransitionTime: string; - reason: string; - message: string; - }[]; - }; - getConditions(activeOnly = false) { - const { conditions } = this.status; + const { conditions } = this.status ?? {}; if (!conditions) return []; @@ -185,7 +189,7 @@ export class Deployment extends WorkloadKubeObject { } getReplicas() { - return this.spec.replicas || 0; + return this.spec?.replicas ?? 0; } } diff --git a/src/renderer/api/endpoints/endpoint.api.ts b/src/renderer/api/endpoints/endpoint.api.ts index d19c2f127e..63db014a2b 100644 --- a/src/renderer/api/endpoints/endpoint.api.ts +++ b/src/renderer/api/endpoints/endpoint.api.ts @@ -36,13 +36,16 @@ export class EndpointAddress implements IEndpointAddress { targetRef?: { kind: string; namespace: string; + apiVersion: string; name: string; uid: string; resourceVersion: string; }; constructor(data: IEndpointAddress) { - Object.assign(this, data); + this.hostname = data.hostname; + this.ip = data.ip; + this.nodeName = data.nodeName; } getId() { @@ -53,12 +56,14 @@ export class EndpointAddress implements IEndpointAddress { return this.hostname; } - getTargetRef(): ITargetRef { + getTargetRef(): ITargetRef | null { if (this.targetRef) { - return Object.assign(this.targetRef, {apiVersion: "v1"}); - } else { - return null; + this.targetRef.apiVersion = "v1"; + + return this.targetRef; } + + return null; } } @@ -68,7 +73,9 @@ export class EndpointSubset implements IEndpointSubset { ports: IEndpointPort[]; constructor(data: IEndpointSubset) { - Object.assign(this, data); + this.addresses = data.addresses; + this.notReadyAddresses = data.notReadyAddresses; + this.ports = data.ports; } getAddresses(): EndpointAddress[] { @@ -101,27 +108,24 @@ export class EndpointSubset implements IEndpointSubset { } @autobind() -export class Endpoint extends KubeObject { +export class Endpoint extends KubeObject { static kind = "Endpoints"; static namespaced = true; static apiBase = "/api/v1/endpoints"; - subsets: IEndpointSubset[]; + subsets?: IEndpointSubset[]; getEndpointSubsets(): EndpointSubset[] { - const subsets = this.subsets || []; - - return subsets.map(s => new EndpointSubset(s)); + return (this.subsets ?? []).map(s => new EndpointSubset(s)); } toString(): string { - if(this.subsets) { - return this.getEndpointSubsets().map(es => es.toString()).join(", "); - } else { - return ""; + if (this.subsets) { + return this.getEndpointSubsets().map(String).join(", "); } - } + return ""; + } } export const endpointApi = new KubeApi({ diff --git a/src/renderer/api/endpoints/events.api.ts b/src/renderer/api/endpoints/events.api.ts index 4fb21aaaa5..947700de2d 100644 --- a/src/renderer/api/endpoints/events.api.ts +++ b/src/renderer/api/endpoints/events.api.ts @@ -5,12 +5,12 @@ import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; @autobind() -export class KubeEvent extends KubeObject { +export class KubeEvent extends KubeObject { static kind = "Event"; static namespaced = true; static apiBase = "/api/v1/events"; - involvedObject: { + involvedObject?: { kind: string; namespace: string; name: string; @@ -19,26 +19,26 @@ export class KubeEvent extends KubeObject { resourceVersion: string; fieldPath: string; }; - reason: string; - message: string; - source: { + reason?: string; + message?: string; + source?: { component: string; host: string; }; - firstTimestamp: string; - lastTimestamp: string; - count: number; - type: "Normal" | "Warning" | string; - eventTime: null; - reportingComponent: string; - reportingInstance: string; + firstTimestamp?: string; + lastTimestamp?: string; + count?: number; + type?: "Normal" | "Warning" | string; + eventTime?: null; + reportingComponent?: string; + reportingInstance?: string; isWarning() { return this.type === "Warning"; } getSource() { - const { component, host } = this.source; + const { component, host } = this.source ?? {}; return `${component} ${host || ""}`; } diff --git a/src/renderer/api/endpoints/helm-charts.api.ts b/src/renderer/api/endpoints/helm-charts.api.ts index 093adf9aef..ff8b5b35b5 100644 --- a/src/renderer/api/endpoints/helm-charts.api.ts +++ b/src/renderer/api/endpoints/helm-charts.api.ts @@ -1,9 +1,33 @@ import { compile } from "path-to-regexp"; import { apiBase } from "../index"; import { stringify } from "querystring"; -import { autobind } from "../../utils"; +import { autobind, NotFalsy } from "../../utils"; -export type RepoHelmChartList = Record; +export interface HelmChartData { + apiVersion: string; + name: string; + version: string; + repo: string; + kubeVersion?: string; + created: string; + description?: string; + digest: string; + keywords?: string[]; + home?: string; + sources?: string[]; + maintainers?: { + name: string; + email: string; + url: string; + }[]; + engine?: string; + icon?: string; + appVersion?: string; + deprecated?: boolean; + tillerVersion?: string; +} + +export type RepoHelmChartList = Record; export type HelmChartList = Record; export interface IHelmChartDetails { @@ -17,31 +41,27 @@ const endpoint = compile(`/v2/charts/:repo?/:name?`) as (params?: { }) => string; export const helmChartsApi = { - list() { - return apiBase - .get(endpoint()) - .then(data => { - return Object - .values(data) - .reduce((allCharts, repoCharts) => allCharts.concat(Object.values(repoCharts)), []) - .map(([chart]) => HelmChart.create(chart)); - }); + async list() { + const data = await apiBase.get(endpoint()); + + return Object.values(data) + .flatMap(chartList => Object.values(chartList)[0]) + .filter(NotFalsy) + .map(HelmChart.create); }, - get(repo: string, name: string, readmeVersion?: string) { + async get(repo: string, name: string, readmeVersion?: string) { const path = endpoint({ repo, name }); - return apiBase - .get(`${path}?${stringify({ version: readmeVersion })}`) - .then(data => { - const versions = data.versions.map(HelmChart.create); - const readme = data.readme; + const data = await apiBase + .get(`${path}?${stringify({ version: readmeVersion })}`); + const versions = data.versions.map(HelmChart.create); + const readme = data.readme; - return { - readme, - versions, - }; - }); + return { + readme, + versions, + }; }, getValues(repo: string, name: string, version: string) { @@ -51,12 +71,28 @@ export const helmChartsApi = { }; @autobind() -export class HelmChart { - constructor(data: any) { - Object.assign(this, data); +export class HelmChart implements HelmChartData { + constructor(data: HelmChartData) { + this.apiVersion = data.apiVersion; + this.name = data.name; + this.version = data.version; + this.repo = data.repo; + this.kubeVersion = data.kubeVersion; + this.created = data.created; + this.description = data.description; + this.digest = data.digest; + this.keywords = data.keywords; + this.home = data.home; + this.sources = data.sources; + this.maintainers = data.maintainers; + this.engine = data.engine; + this.icon = data.icon; + this.appVersion = data.appVersion; + this.deprecated = data.deprecated; + this.tillerVersion = data.tillerVersion; } - static create(data: any) { + static create(data: HelmChartData) { return new HelmChart(data); } diff --git a/src/renderer/api/endpoints/helm-releases.api.ts b/src/renderer/api/endpoints/helm-releases.api.ts index 9a5a6dadd0..ff4692e68f 100644 --- a/src/renderer/api/endpoints/helm-releases.api.ts +++ b/src/renderer/api/endpoints/helm-releases.api.ts @@ -28,7 +28,7 @@ interface IReleaseRawDetails extends IReleasePayload { } export interface IReleaseDetails extends IReleasePayload { - resources: KubeObject[]; + resources: KubeObject[]; } export interface IReleaseCreatePayload { @@ -79,7 +79,7 @@ export const helmReleasesApi = { const path = endpoint({ name, namespace }); return apiBase.get(path).then(details => { - const items: KubeObject[] = JSON.parse(details.resources).items; + const items: KubeObject[] = JSON.parse(details.resources).items; const resources = items.map(item => KubeObject.create(item)); return { @@ -136,13 +136,29 @@ export const helmReleasesApi = { } }; +export interface HelmReleaseData { + appVersion: string; + name: string; + namespace: string; + chart: string; + status: string; + updated: string; + revision: string; +} + @autobind() export class HelmRelease implements ItemObject { - constructor(data: any) { - Object.assign(this, data); + constructor(data: HelmReleaseData) { + this.appVersion = data.appVersion; + this.name = data.name; + this.namespace = data.namespace; + this.chart = data.chart; + this.status = data.status; + this.updated = data.updated; + this.revision = data.revision; } - static create(data: any) { + static create(data: HelmReleaseData) { return new HelmRelease(data); } diff --git a/src/renderer/api/endpoints/hpa.api.ts b/src/renderer/api/endpoints/hpa.api.ts index 4876ee43eb..830f123528 100644 --- a/src/renderer/api/endpoints/hpa.api.ts +++ b/src/renderer/api/endpoints/hpa.api.ts @@ -38,50 +38,51 @@ export interface IHpaMetric { }>; } -export class HorizontalPodAutoscaler extends KubeObject { +interface HorizontalPodAutoscalerSpec { + scaleTargetRef: { + kind: string; + name: string; + apiVersion: string; + }; + minReplicas: number; + maxReplicas: number; + metrics: IHpaMetric[]; +} + +interface HorizontalPodAutoscalerStatus { + currentReplicas: number; + desiredReplicas: number; + currentMetrics: IHpaMetric[]; + conditions: { + lastTransitionTime: string; + message: string; + reason: string; + status: string; + type: string; + }[]; +} + +export class HorizontalPodAutoscaler extends KubeObject { static kind = "HorizontalPodAutoscaler"; static namespaced = true; static apiBase = "/apis/autoscaling/v2beta1/horizontalpodautoscalers"; - spec: { - scaleTargetRef: { - kind: string; - name: string; - apiVersion: string; - }; - minReplicas: number; - maxReplicas: number; - metrics: IHpaMetric[]; - }; - status: { - currentReplicas: number; - desiredReplicas: number; - currentMetrics: IHpaMetric[]; - conditions: { - lastTransitionTime: string; - message: string; - reason: string; - status: string; - type: string; - }[]; - }; - getMaxPods() { - return this.spec.maxReplicas || 0; + return this.spec?.maxReplicas || 0; } getMinPods() { - return this.spec.minReplicas || 0; + return this.spec?.minReplicas || 0; } getReplicas() { - return this.status.currentReplicas; + return this.status?.currentReplicas; } getConditions() { - if (!this.status.conditions) return []; + if (!this.status?.conditions) return []; - return this.status.conditions.map(condition => { + return this.status?.conditions.map(condition => { const { message, reason, lastTransitionTime, status } = condition; return { @@ -93,23 +94,23 @@ export class HorizontalPodAutoscaler extends KubeObject { } getMetrics() { - return this.spec.metrics || []; + return this.spec?.metrics || []; } getCurrentMetrics() { - return this.status.currentMetrics || []; + return this.status?.currentMetrics || []; } - protected getMetricName(metric: IHpaMetric): string { + protected getMetricName(metric: IHpaMetric): string | undefined { const { type, resource, pods, object, external } = metric; switch (type) { case HpaMetricType.Resource: - return resource.name; + return resource?.name; case HpaMetricType.Pods: return pods.metricName; case HpaMetricType.Object: - return object.metricName; + return object?.metricName; case HpaMetricType.External: return external.metricName; } diff --git a/src/renderer/api/endpoints/ingress.api.ts b/src/renderer/api/endpoints/ingress.api.ts index 7d035ad591..24bb11f655 100644 --- a/src/renderer/api/endpoints/ingress.api.ts +++ b/src/renderer/api/endpoints/ingress.api.ts @@ -3,7 +3,7 @@ import { autobind } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; -export class IngressApi extends KubeApi { +export class IngressApi extends KubeApi { getMetrics(ingress: string, namespace: string): Promise { const opts = { category: "ingress", ingress }; @@ -61,44 +61,45 @@ export const getBackendServiceNamePort = (backend: IIngressBackend) => { return { serviceName, servicePort }; }; +interface IngressSpec { + tls: { + secretName: string; + }[]; + rules?: { + host?: string; + http: { + paths: { + path?: string; + backend: IIngressBackend; + }[]; + }; + }[]; + // extensions/v1beta1 + backend?: IExtensionsBackend; + // networking.k8s.io/v1 + defaultBackend?: INetworkingBackend & { + resource: { + apiGroup: string; + kind: string; + name: string; + }; + }; +} + +interface IngressStatus { + loadBalancer: { + ingress?: ILoadBalancerIngress[]; + }; +} + @autobind() -export class Ingress extends KubeObject { +export class Ingress extends KubeObject { static kind = "Ingress"; static namespaced = true; static apiBase = "/apis/networking.k8s.io/v1/ingresses"; - spec: { - tls: { - secretName: string; - }[]; - rules?: { - host?: string; - http: { - paths: { - path?: string; - backend: IIngressBackend; - }[]; - }; - }[]; - // extensions/v1beta1 - backend?: IExtensionsBackend; - // networking.k8s.io/v1 - defaultBackend?: INetworkingBackend & { - resource: { - apiGroup: string; - kind: string; - name: string; - } - } - }; - status: { - loadBalancer: { - ingress: ILoadBalancerIngress[]; - }; - }; - getRoutes() { - const { spec: { tls, rules } } = this; + const { spec: { tls, rules } = {} } = this; if (!rules) return []; @@ -135,7 +136,7 @@ export class Ingress extends KubeObject { } getHosts() { - const { spec: { rules } } = this; + const { spec: { rules } = {} } = this; if (!rules) return []; @@ -144,7 +145,7 @@ export class Ingress extends KubeObject { getPorts() { const ports: number[] = []; - const { spec: { tls, rules, backend, defaultBackend } } = this; + const { spec: { tls, rules, backend, defaultBackend } = {} } = this; const httpPort = 80; const tlsPort = 443; // Note: not using the port name (string) @@ -166,11 +167,9 @@ export class Ingress extends KubeObject { } getLoadBalancers() { - const { status: { loadBalancer = { ingress: [] } } } = this; - - return (loadBalancer.ingress ?? []).map(address => ( + return this.status?.loadBalancer?.ingress?.map(address => ( address.hostname || address.ip - )); + )) ?? []; } } @@ -179,5 +178,4 @@ export const ingressApi = new IngressApi({ // Add fallback for Kubernetes <1.19 checkPreferredVersion: true, fallbackApiBases: ["/apis/extensions/v1beta1/ingresses"], - logStuff: true -} as any); +}); diff --git a/src/renderer/api/endpoints/job.api.ts b/src/renderer/api/endpoints/job.api.ts index 65b9bcfdc3..4aff2a0d00 100644 --- a/src/renderer/api/endpoints/job.api.ts +++ b/src/renderer/api/endpoints/job.api.ts @@ -1,95 +1,87 @@ import get from "lodash/get"; import { autobind } from "../../utils"; -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; +import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; import { IPodContainer } from "./pods.api"; import { KubeApi } from "../kube-api"; import { JsonApiParams } from "../json-api"; +interface JobSpec extends WorkloadSpec { + parallelism?: number; + completions?: number; + backoffLimit?: number; + template: { + metadata: { + creationTimestamp?: string; + labels?: { + [name: string]: string; + }; + annotations?: { + [name: string]: string; + }; + }; + spec: { + containers: IPodContainer[]; + restartPolicy: string; + terminationGracePeriodSeconds: number; + dnsPolicy: string; + hostPID: boolean; + affinity?: IAffinity; + nodeSelector?: { + [selector: string]: string; + }; + tolerations?: { + key: string; + operator: string; + effect: string; + tolerationSeconds: number; + }[]; + schedulerName: string; + }; + }; + containers?: IPodContainer[]; + restartPolicy?: string; + terminationGracePeriodSeconds?: number; + dnsPolicy?: string; + serviceAccountName?: string; + serviceAccount?: string; + schedulerName?: string; +} + +interface JobStatus { + conditions: { + type: string; + status: string; + lastProbeTime: string; + lastTransitionTime: string; + message?: string; + }[]; + startTime: string; + completionTime: string; + succeeded: number; +} + @autobind() -export class Job extends WorkloadKubeObject { +export class Job extends WorkloadKubeObject { static kind = "Job"; static namespaced = true; static apiBase = "/apis/batch/v1/jobs"; - spec: { - parallelism?: number; - completions?: number; - backoffLimit?: number; - selector?: { - matchLabels: { - [name: string]: string; - }; - }; - template: { - metadata: { - creationTimestamp?: string; - labels?: { - [name: string]: string; - }; - annotations?: { - [name: string]: string; - }; - }; - spec: { - containers: IPodContainer[]; - restartPolicy: string; - terminationGracePeriodSeconds: number; - dnsPolicy: string; - hostPID: boolean; - affinity?: IAffinity; - nodeSelector?: { - [selector: string]: string; - }; - tolerations?: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; - }[]; - schedulerName: string; - }; - }; - containers?: IPodContainer[]; - restartPolicy?: string; - terminationGracePeriodSeconds?: number; - dnsPolicy?: string; - serviceAccountName?: string; - serviceAccount?: string; - schedulerName?: string; - }; - status: { - conditions: { - type: string; - status: string; - lastProbeTime: string; - lastTransitionTime: string; - message?: string; - }[]; - startTime: string; - completionTime: string; - succeeded: number; - }; - getDesiredCompletions() { - return this.spec.completions || 0; + return this.spec?.completions || 0; } getCompletions() { - return this.status.succeeded || 0; + return this.status?.succeeded || 0; } getParallelism() { - return this.spec.parallelism; + return this.spec?.parallelism; } getCondition() { // Type of Job condition could be only Complete or Failed // https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.14/#jobcondition-v1-batch - const { conditions } = this.status; - - if (!conditions) return; - - return conditions.find(({ status }) => status === "True"); + return this.status?.conditions.find(({ status }) => status === "True"); } getImages() { diff --git a/src/renderer/api/endpoints/limit-range.api.ts b/src/renderer/api/endpoints/limit-range.api.ts index bbb3941c87..4fdd168684 100644 --- a/src/renderer/api/endpoints/limit-range.api.ts +++ b/src/renderer/api/endpoints/limit-range.api.ts @@ -29,26 +29,26 @@ export interface LimitRangeItem extends LimitRangeParts { type: string } +interface LimitRangeSpec { + limits: LimitRangeItem[]; +} + @autobind() -export class LimitRange extends KubeObject { +export class LimitRange extends KubeObject { static kind = "LimitRange"; static namespaced = true; static apiBase = "/api/v1/limitranges"; - spec: { - limits: LimitRangeItem[]; - }; - getContainerLimits() { - return this.spec.limits.filter(limit => limit.type === LimitType.CONTAINER); + return this.spec?.limits.filter(limit => limit.type === LimitType.CONTAINER); } getPodLimits() { - return this.spec.limits.filter(limit => limit.type === LimitType.POD); + return this.spec?.limits.filter(limit => limit.type === LimitType.POD); } getPVCLimits() { - return this.spec.limits.filter(limit => limit.type === LimitType.PVC); + return this.spec?.limits.filter(limit => limit.type === LimitType.PVC); } } diff --git a/src/renderer/api/endpoints/metrics.api.ts b/src/renderer/api/endpoints/metrics.api.ts index 9c3ee74adc..2937758b78 100644 --- a/src/renderer/api/endpoints/metrics.api.ts +++ b/src/renderer/api/endpoints/metrics.api.ts @@ -14,8 +14,8 @@ export interface IMetrics { export interface IMetricsResult { metric: { - [name: string]: string; - instance: string; + [name: string]: string | undefined; + instance?: string; node?: string; pod?: string; kubernetes?: string; @@ -111,12 +111,11 @@ export function normalizeMetrics(metrics: IMetrics, frames = 60): IMetrics { return metrics; } -export function isMetricsEmpty(metrics: { [key: string]: IMetrics }) { +export function isMetricsEmpty(metrics: Record) { return Object.values(metrics).every(metric => !metric?.data?.result?.length); } -export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: string): { [key: string]: IMetrics } { - if (!metrics) return; +export function getItemMetrics(metrics: Record = {}, itemName: string): Record { const itemMetrics = { ...metrics }; for (const metric in metrics) { @@ -132,7 +131,7 @@ export function getItemMetrics(metrics: { [key: string]: IMetrics }, itemName: s return itemMetrics; } -export function getMetricLastPoints(metrics: { [key: string]: IMetrics }) { +export function getMetricLastPoints(metrics: Record) { const result: Partial<{ [metric: string]: number }> = {}; Object.keys(metrics).forEach(metricName => { diff --git a/src/renderer/api/endpoints/namespaces.api.ts b/src/renderer/api/endpoints/namespaces.api.ts index 430565bf57..467f29ee6f 100644 --- a/src/renderer/api/endpoints/namespaces.api.ts +++ b/src/renderer/api/endpoints/namespaces.api.ts @@ -7,18 +7,18 @@ export enum NamespaceStatus { TERMINATING = "Terminating", } +export interface NamespaceKubeStatus { + phase: string; +} + @autobind() -export class Namespace extends KubeObject { +export class Namespace extends KubeObject { static kind = "Namespace"; static namespaced = false; static apiBase = "/api/v1/namespaces"; - status?: { - phase: string; - }; - getStatus() { - return this.status ? this.status.phase : "-"; + return this.status?.phase ?? "-"; } } diff --git a/src/renderer/api/endpoints/network-policy.api.ts b/src/renderer/api/endpoints/network-policy.api.ts index eb531990c2..05d609936c 100644 --- a/src/renderer/api/endpoints/network-policy.api.ts +++ b/src/renderer/api/endpoints/network-policy.api.ts @@ -35,36 +35,32 @@ export interface IPolicyEgress { }[]; } +interface NetworkPolicySpec { + podSelector: { + matchLabels: { + [label: string]: string; + role: string; + }; + }; + policyTypes: string[]; + ingress: IPolicyIngress[]; + egress: IPolicyEgress[]; +} + @autobind() -export class NetworkPolicy extends KubeObject { +export class NetworkPolicy extends KubeObject { static kind = "NetworkPolicy"; static namespaced = true; static apiBase = "/apis/networking.k8s.io/v1/networkpolicies"; - spec: { - podSelector: { - matchLabels: { - [label: string]: string; - role: string; - }; - }; - policyTypes: string[]; - ingress: IPolicyIngress[]; - egress: IPolicyEgress[]; - }; - getMatchLabels(): string[] { - if (!this.spec.podSelector || !this.spec.podSelector.matchLabels) return []; - return Object - .entries(this.spec.podSelector.matchLabels) + .entries(this.spec?.podSelector?.matchLabels ?? {}) .map(data => data.join(":")); } getTypes(): string[] { - if (!this.spec.policyTypes) return []; - - return this.spec.policyTypes; + return this.spec?.policyTypes ?? []; } } diff --git a/src/renderer/api/endpoints/nodes.api.ts b/src/renderer/api/endpoints/nodes.api.ts index d1794f0fb7..e6f7da442e 100644 --- a/src/renderer/api/endpoints/nodes.api.ts +++ b/src/renderer/api/endpoints/nodes.api.ts @@ -1,9 +1,9 @@ import { KubeObject } from "../kube-object"; -import { autobind, cpuUnitsToNumber, unitsToBytes } from "../../utils"; +import { autobind, cpuUnitsToNumber, NotFalsy, unitsToBytes } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; -export class NodesApi extends KubeApi { +export class NodesApi extends KubeApi { getMetrics(): Promise { const opts = { category: "nodes"}; @@ -28,85 +28,86 @@ export interface INodeMetrics { fsSize: T; } +interface NodeSpec { + podCIDR: string; + externalID: string; + taints?: { + key: string; + value: string; + effect: string; + }[]; + unschedulable?: boolean; +} + +interface NodeStatus { + capacity: { + cpu: string; + memory: string; + pods: string; + }; + allocatable: { + cpu: string; + memory: string; + pods: string; + }; + conditions: { + type: string; + status?: string; + lastHeartbeatTime?: string; + lastTransitionTime?: string; + reason?: string; + message?: string; + }[]; + addresses: { + type: string; + address: string; + }[]; + nodeInfo: { + machineID: string; + systemUUID: string; + bootID: string; + kernelVersion: string; + osImage: string; + containerRuntimeVersion: string; + kubeletVersion: string; + kubeProxyVersion: string; + operatingSystem: string; + architecture: string; + }; + images: { + names: string[]; + sizeBytes: number; + }[]; +} + @autobind() -export class Node extends KubeObject { +export class Node extends KubeObject { static kind = "Node"; static namespaced = false; static apiBase = "/api/v1/nodes"; - spec: { - podCIDR: string; - externalID: string; - taints?: { - key: string; - value: string; - effect: string; - }[]; - unschedulable?: boolean; - }; - status: { - capacity: { - cpu: string; - memory: string; - pods: string; - }; - allocatable: { - cpu: string; - memory: string; - pods: string; - }; - conditions: { - type: string; - status?: string; - lastHeartbeatTime?: string; - lastTransitionTime?: string; - reason?: string; - message?: string; - }[]; - addresses: { - type: string; - address: string; - }[]; - nodeInfo: { - machineID: string; - systemUUID: string; - bootID: string; - kernelVersion: string; - osImage: string; - containerRuntimeVersion: string; - kubeletVersion: string; - kubeProxyVersion: string; - operatingSystem: string; - architecture: string; - }; - images: { - names: string[]; - sizeBytes: number; - }[]; - }; - getNodeConditionText() { - const { conditions } = this.status; + if (this.status?.conditions) { + return this.status.conditions + .filter(condition => condition.status === "True") + .map(condition => condition.type) + .join(" "); + } - if (!conditions) return ""; - - return conditions.reduce((types, current) => { - if (current.status !== "True") return ""; - - return types += ` ${current.type}`; - }, ""); + return ""; } getTaints() { - return this.spec.taints || []; + return this.spec?.taints ?? []; } getRoleLabels() { - const roleLabels = Object.keys(this.metadata.labels).filter(key => - key.includes("node-role.kubernetes.io") - ).map(key => key.match(/([^/]+$)/)[0]); // all after last slash + const roleLabels = Object.keys(this.metadata.labels ?? {}) + .filter(key => key.includes("node-role.kubernetes.io")) + .map(key => key.match(/([^/]+$)/)?.[0]) + .filter(NotFalsy); // all after last slash - if (this.metadata.labels["kubernetes.io/role"] != undefined) { + if (this.metadata.labels?.["kubernetes.io/role"]) { roleLabels.push(this.metadata.labels["kubernetes.io/role"]); } @@ -114,19 +115,19 @@ export class Node extends KubeObject { } getCpuCapacity() { - if (!this.status.capacity || !this.status.capacity.cpu) return 0; + if (!this.status?.capacity.cpu) return 0; return cpuUnitsToNumber(this.status.capacity.cpu); } getMemoryCapacity() { - if (!this.status.capacity || !this.status.capacity.memory) return 0; + if (!this.status?.capacity.memory) return 0; return unitsToBytes(this.status.capacity.memory); } getConditions() { - const conditions = this.status.conditions || []; + const conditions = this.status?.conditions ?? []; if (this.isUnschedulable()) { return [{ type: "SchedulingDisabled", status: "True" }, ...conditions]; @@ -148,21 +149,17 @@ export class Node extends KubeObject { } getKubeletVersion() { - return this.status.nodeInfo.kubeletVersion; + return this.status?.nodeInfo.kubeletVersion; } getOperatingSystem(): string { const label = this.getLabels().find(label => label.startsWith("kubernetes.io/os=")); - if (label) { - return label.split("=", 2)[1]; - } - - return "linux"; + return label?.split("=", 2)[1] ?? "linux"; } isUnschedulable() { - return this.spec.unschedulable; + return this.spec?.unschedulable ?? false; } } diff --git a/src/renderer/api/endpoints/persistent-volume-claims.api.ts b/src/renderer/api/endpoints/persistent-volume-claims.api.ts index 1d9e1f1dce..652aa81277 100644 --- a/src/renderer/api/endpoints/persistent-volume-claims.api.ts +++ b/src/renderer/api/endpoints/persistent-volume-claims.api.ts @@ -4,7 +4,7 @@ import { IMetrics, metricsApi } from "./metrics.api"; import { Pod } from "./pods.api"; import { KubeApi } from "../kube-api"; -export class PersistentVolumeClaimsApi extends KubeApi { +export class PersistentVolumeClaimsApi extends KubeApi { getMetrics(pvcName: string, namespace: string): Promise { return metricsApi.getMetrics({ diskUsage: { category: "pvc", pvc: pvcName }, @@ -21,35 +21,36 @@ export interface IPvcMetrics { diskCapacity: T; } +interface PersistentVolumeClaimSpec { + accessModes: string[]; + storageClassName: string; + selector: { + matchLabels: { + release: string; + }; + matchExpressions: { + key: string; // environment, + operator: string; // In, + values: string[]; // [dev] + }[]; + }; + resources: { + requests: { + storage: string; // 8Gi + }; + }; +} + +interface PersistentVolumeClaimStatus { + phase: string; // Pending +} + @autobind() -export class PersistentVolumeClaim extends KubeObject { +export class PersistentVolumeClaim extends KubeObject { static kind = "PersistentVolumeClaim"; static namespaced = true; static apiBase = "/api/v1/persistentvolumeclaims"; - spec: { - accessModes: string[]; - storageClassName: string; - selector: { - matchLabels: { - release: string; - }; - matchExpressions: { - key: string; // environment, - operator: string; // In, - values: string[]; // [dev] - }[]; - }; - resources: { - requests: { - storage: string; // 8Gi - }; - }; - }; - status: { - phase: string; // Pending - }; - getPods(allPods: Pod[]): Pod[] { const pods = allPods.filter(pod => pod.getNs() === this.getNs()); @@ -62,28 +63,20 @@ export class PersistentVolumeClaim extends KubeObject { } getStorage(): string { - if (!this.spec.resources || !this.spec.resources.requests) return "-"; - - return this.spec.resources.requests.storage; + return this.spec?.resources.requests.storage ?? "-"; } getMatchLabels(): string[] { - if (!this.spec.selector || !this.spec.selector.matchLabels) return []; - - return Object.entries(this.spec.selector.matchLabels) + return Object.entries(this.spec?.selector.matchLabels ?? {}) .map(([name, val]) => `${name}:${val}`); } getMatchExpressions() { - if (!this.spec.selector || !this.spec.selector.matchExpressions) return []; - - return this.spec.selector.matchExpressions; + return this.spec?.selector.matchExpressions ?? []; } getStatus(): string { - if (this.status) return this.status.phase; - - return "-"; + return this.status?.phase ?? "-"; } } diff --git a/src/renderer/api/endpoints/persistent-volume.api.ts b/src/renderer/api/endpoints/persistent-volume.api.ts index db286db062..ee8b1ed053 100644 --- a/src/renderer/api/endpoints/persistent-volume.api.ts +++ b/src/renderer/api/endpoints/persistent-volume.api.ts @@ -3,76 +3,72 @@ import { unitsToBytes } from "../../utils/convertMemory"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; +interface PersistentVolumeSpec { + capacity: { + storage: string; // 8Gi + }; + flexVolume: { + driver: string; + options: { + clusterNamespace: string; + image: string; + pool: string; + storageClass: string; + }; + }; + mountOptions?: string[]; + accessModes: string[]; + claimRef: { + kind: string; // PersistentVolumeClaim, + namespace: string; // storage, + name: string; // nfs-provisioner, + uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb, + apiVersion: string; // v1, + resourceVersion: string; // 292180 + }; + persistentVolumeReclaimPolicy: string; + storageClassName: string; + nfs?: { + path: string; + server: string; + }; +} + +interface PersistentVolumeStatus { + phase: string; + reason?: string; +} + @autobind() -export class PersistentVolume extends KubeObject { +export class PersistentVolume extends KubeObject { static kind = "PersistentVolume"; static namespaced = false; static apiBase = "/api/v1/persistentvolumes"; - spec: { - capacity: { - storage: string; // 8Gi - }; - flexVolume: { - driver: string; // ceph.rook.io/rook-ceph-system, - options: { - clusterNamespace: string; // rook-ceph, - image: string; // pvc-c5d7c485-9f1b-11e8-b0ea-9600000e54fb, - pool: string; // replicapool, - storageClass: string; // rook-ceph-block - }; - }; - mountOptions?: string[]; - accessModes: string[]; // [ReadWriteOnce] - claimRef: { - kind: string; // PersistentVolumeClaim, - namespace: string; // storage, - name: string; // nfs-provisioner, - uid: string; // c5d7c485-9f1b-11e8-b0ea-9600000e54fb, - apiVersion: string; // v1, - resourceVersion: string; // 292180 - }; - persistentVolumeReclaimPolicy: string; // Delete, - storageClassName: string; // rook-ceph-block - nfs?: { - path: string; - server: string; - }; - }; - - status: { - phase: string; - reason?: string; - }; - getCapacity(inBytes = false) { - const capacity = this.spec.capacity; + const storage = this.spec?.capacity?.storage ?? "0"; - if (capacity) { - if (inBytes) return unitsToBytes(capacity.storage); - - return capacity.storage; + if (inBytes) { + return unitsToBytes(storage); } - return 0; + return storage; } getStatus() { - if (!this.status) return; - - return this.status.phase || "-"; + return this.status?.phase ?? "-"; } getStorageClass(): string { - return this.spec.storageClassName; + return this.spec?.storageClassName ?? ""; } getClaimRefName(): string { - return this.spec.claimRef?.name ?? ""; + return this.spec?.claimRef?.name ?? ""; } getStorageClassName() { - return this.spec.storageClassName || ""; + return this.spec?.storageClassName ?? ""; } } diff --git a/src/renderer/api/endpoints/pod-metrics.api.ts b/src/renderer/api/endpoints/pod-metrics.api.ts index acf6e7b20f..2eac85a090 100644 --- a/src/renderer/api/endpoints/pod-metrics.api.ts +++ b/src/renderer/api/endpoints/pod-metrics.api.ts @@ -1,14 +1,14 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -export class PodMetrics extends KubeObject { +export class PodMetrics extends KubeObject { static kind = "Pod"; static namespaced = true; static apiBase = "/apis/metrics.k8s.io/v1beta1/pods"; - timestamp: string; - window: string; - containers: { + timestamp?: string; + window?: string; + containers?: { name: string; usage: { cpu: string; diff --git a/src/renderer/api/endpoints/poddisruptionbudget.api.ts b/src/renderer/api/endpoints/poddisruptionbudget.api.ts index 50ab2d5b3d..59af7636f6 100644 --- a/src/renderer/api/endpoints/poddisruptionbudget.api.ts +++ b/src/renderer/api/endpoints/poddisruptionbudget.api.ts @@ -1,45 +1,44 @@ import { autobind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +import { WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; + +interface PodDisruptionBudgetSpec extends WorkloadSpec { + minAvailable: string; + maxUnavailable: string; +} + +interface PodDisruptionBudgetStatus { + currentHealthy: number; + desiredHealthy: number; + disruptionsAllowed: number; + expectedPods: number; +} @autobind() -export class PodDisruptionBudget extends KubeObject { +export class PodDisruptionBudget extends WorkloadKubeObject { static kind = "PodDisruptionBudget"; static namespaced = true; static apiBase = "/apis/policy/v1beta1/poddisruptionbudgets"; - spec: { - minAvailable: string; - maxUnavailable: string; - selector: { matchLabels: { [app: string]: string } }; - }; - status: { - currentHealthy: number - desiredHealthy: number - disruptionsAllowed: number - expectedPods: number - }; - getSelectors() { - const selector = this.spec.selector; - - return KubeObject.stringifyLabels(selector ? selector.matchLabels : null); + return KubeObject.stringifyLabels(this.spec?.selector?.matchLabels); } getMinAvailable() { - return this.spec.minAvailable || "N/A"; + return this.spec?.minAvailable || "N/A"; } getMaxUnavailable() { - return this.spec.maxUnavailable || "N/A"; + return this.spec?.maxUnavailable || "N/A"; } getCurrentHealthy() { - return this.status.currentHealthy; + return this.status?.currentHealthy ?? 0; } getDesiredHealthy() { - return this.status.desiredHealthy; + return this.status?.desiredHealthy ?? 0; } } diff --git a/src/renderer/api/endpoints/pods.api.ts b/src/renderer/api/endpoints/pods.api.ts index 5bf319fbe8..f7047ff15e 100644 --- a/src/renderer/api/endpoints/pods.api.ts +++ b/src/renderer/api/endpoints/pods.api.ts @@ -1,9 +1,10 @@ -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; +import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; import { autobind } from "../../utils"; import { IMetrics, metricsApi } from "./metrics.api"; import { KubeApi } from "../kube-api"; +import { Primitive } from "type-fest"; -export class PodsApi extends KubeApi { +export class PodsApi extends KubeApi { async getLogs(params: { namespace: string; name: string }, query?: IPodLogsQuery): Promise { const path = `${this.getUrl(params)}/log`; @@ -142,7 +143,7 @@ interface IContainerProbe { export interface IPodContainerStatus { name: string; state?: { - [index: string]: object; + [index: string]: Record | undefined; running?: { startedAt: string; }; @@ -158,7 +159,7 @@ export interface IPodContainerStatus { }; }; lastState?: { - [index: string]: object; + [index: string]: Record | undefined; running?: { startedAt: string; }; @@ -181,92 +182,93 @@ export interface IPodContainerStatus { started?: boolean; } +interface PodSpec extends WorkloadSpec { + volumes?: { + name: string; + persistentVolumeClaim: { + claimName: string; + }; + emptyDir: { + medium?: string; + sizeLimit?: string; + }; + configMap: { + name: string; + }; + secret: { + secretName: string; + defaultMode: number; + }; + }[]; + initContainers: IPodContainer[]; + containers: IPodContainer[]; + restartPolicy?: string; + terminationGracePeriodSeconds?: number; + activeDeadlineSeconds?: number; + dnsPolicy?: string; + serviceAccountName: string; + serviceAccount: string; + automountServiceAccountToken?: boolean; + priority?: number; + priorityClassName?: string; + nodeName?: string; + nodeSelector?: { + [selector: string]: string; + }; + securityContext?: {}; + imagePullSecrets?: { + name: string; + }[]; + hostNetwork?: boolean; + hostPID?: boolean; + hostIPC?: boolean; + shareProcessNamespace?: boolean; + hostname?: string; + subdomain?: string; + schedulerName?: string; + tolerations?: { + key?: string; + operator?: string; + effect?: string; + tolerationSeconds?: number; + value?: string; + }[]; + hostAliases?: { + ip: string; + hostnames: string[]; + }; + affinity?: IAffinity; +} + +interface PodKubeStatus { + phase: string; + conditions: { + type: string; + status: string; + lastProbeTime: number; + lastTransitionTime: string; + }[]; + hostIP: string; + podIP: string; + startTime: string; + initContainerStatuses?: IPodContainerStatus[]; + containerStatuses?: IPodContainerStatus[]; + qosClass?: string; + reason?: string; +} + @autobind() -export class Pod extends WorkloadKubeObject { +export class Pod extends WorkloadKubeObject { static kind = "Pod"; static namespaced = true; static apiBase = "/api/v1/pods"; - spec: { - volumes?: { - name: string; - persistentVolumeClaim: { - claimName: string; - }; - emptyDir: { - medium?: string; - sizeLimit?: string; - }; - configMap: { - name: string; - }; - secret: { - secretName: string; - defaultMode: number; - }; - }[]; - initContainers: IPodContainer[]; - containers: IPodContainer[]; - restartPolicy?: string; - terminationGracePeriodSeconds?: number; - activeDeadlineSeconds?: number; - dnsPolicy?: string; - serviceAccountName: string; - serviceAccount: string; - automountServiceAccountToken?: boolean; - priority?: number; - priorityClassName?: string; - nodeName?: string; - nodeSelector?: { - [selector: string]: string; - }; - securityContext?: {}; - imagePullSecrets?: { - name: string; - }[]; - hostNetwork?: boolean; - hostPID?: boolean; - hostIPC?: boolean; - shareProcessNamespace?: boolean; - hostname?: string; - subdomain?: string; - schedulerName?: string; - tolerations?: { - key?: string; - operator?: string; - effect?: string; - tolerationSeconds?: number; - value?: string; - }[]; - hostAliases?: { - ip: string; - hostnames: string[]; - }; - affinity?: IAffinity; - }; - status?: { - phase: string; - conditions: { - type: string; - status: string; - lastProbeTime: number; - lastTransitionTime: string; - }[]; - hostIP: string; - podIP: string; - startTime: string; - initContainerStatuses?: IPodContainerStatus[]; - containerStatuses?: IPodContainerStatus[]; - qosClass?: string; - reason?: string; - }; - getInitContainers() { - return this.spec.initContainers || []; + return this.spec?.initContainers || []; } getContainers() { - return this.spec.containers || []; + return this.spec?.containers || []; } getAllContainers() { @@ -276,7 +278,7 @@ export class Pod extends WorkloadKubeObject { getRunningContainers() { const runningContainerNames = new Set( this.getContainerStatuses() - .filter(({ state }) => state.running) + .filter(({ state }) => state?.running) .map(({ name }) => name) ); @@ -309,7 +311,7 @@ export class Pod extends WorkloadKubeObject { } getPriorityClassName() { - return this.spec.priorityClassName || ""; + return this.spec?.priorityClassName || ""; } getStatus(): PodStatus { @@ -347,12 +349,12 @@ export class Pod extends WorkloadKubeObject { const statuses = this.getContainerStatuses(false); // not including initContainers for (const { state } of statuses.reverse()) { - if (state.waiting) { - return state.waiting.reason || "Waiting"; + if (state?.waiting) { + return state?.waiting.reason || "Waiting"; } - if (state.terminated) { - return state.terminated.reason || "Terminated"; + if (state?.terminated) { + return state?.terminated.reason || "Terminated"; } } @@ -368,7 +370,7 @@ export class Pod extends WorkloadKubeObject { } getVolumes() { - return this.spec.volumes || []; + return this.spec?.volumes || []; } getSecrets(): string[] { @@ -378,17 +380,16 @@ export class Pod extends WorkloadKubeObject { } getNodeSelectors(): string[] { - const { nodeSelector = {} } = this.spec; - - return Object.entries(nodeSelector).map(values => values.join(": ")); + return Object.entries(this.spec?.nodeSelector ?? {}) + .map(values => values.join(": ")); } getTolerations() { - return this.spec.tolerations || []; + return this.spec?.tolerations || []; } getAffinity(): IAffinity { - return this.spec.affinity; + return this.spec?.affinity ?? {}; } hasIssues() { @@ -419,8 +420,11 @@ export class Pod extends WorkloadKubeObject { return this.getProbe(container.startupProbe); } - getProbe(probeData: IContainerProbe) { - if (!probeData) return []; + getProbe(probeData?: IContainerProbe) { + if (!probeData) { + return []; + } + const { httpGet, exec, tcpSocket, initialDelaySeconds, timeoutSeconds, periodSeconds, successThreshold, failureThreshold @@ -458,11 +462,11 @@ export class Pod extends WorkloadKubeObject { } getNodeName() { - return this.spec.nodeName; + return this.spec?.nodeName; } getSelectedNodeOs(): string | undefined { - return this.spec.nodeSelector?.["kubernetes.io/os"] || this.spec.nodeSelector?.["beta.kubernetes.io/os"]; + return this.spec?.nodeSelector?.["kubernetes.io/os"] || this.spec?.nodeSelector?.["beta.kubernetes.io/os"]; } } diff --git a/src/renderer/api/endpoints/podsecuritypolicy.api.ts b/src/renderer/api/endpoints/podsecuritypolicy.api.ts index dc5113625d..5ace1ce050 100644 --- a/src/renderer/api/endpoints/podsecuritypolicy.api.ts +++ b/src/renderer/api/endpoints/podsecuritypolicy.api.ts @@ -2,89 +2,101 @@ import { autobind } from "../../utils"; import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; +interface PodSecurityPolicySpec { + allowPrivilegeEscalation?: boolean; + allowedCSIDrivers?: { + name: string; + }[]; + allowedCapabilities: string[]; + allowedFlexVolumes?: { + driver: string; + }[]; + allowedHostPaths?: { + pathPrefix: string; + readOnly: boolean; + }[]; + allowedProcMountTypes?: string[]; + allowedUnsafeSysctls?: string[]; + defaultAddCapabilities?: string[]; + defaultAllowPrivilegeEscalation?: boolean; + forbiddenSysctls?: string[]; + fsGroup?: { + rule: string; + ranges: { + max: number; + min: number; + }[]; + }; + hostIPC?: boolean; + hostNetwork?: boolean; + hostPID?: boolean; + hostPorts?: { + max: number; + min: number; + }[]; + privileged?: boolean; + readOnlyRootFilesystem?: boolean; + requiredDropCapabilities?: string[]; + runAsGroup?: { + ranges: { + max: number; + min: number; + }[]; + rule: string; + }; + runAsUser?: { + rule: string; + ranges: { + max: number; + min: number; + }[]; + }; + runtimeClass?: { + allowedRuntimeClassNames: string[]; + defaultRuntimeClassName: string; + }; + seLinux?: { + rule: string; + seLinuxOptions: { + level: string; + role: string; + type: string; + user: string; + }; + }; + supplementalGroups?: { + rule: string; + ranges: { + max: number; + min: number; + }[]; + }; + volumes?: string[]; +} + @autobind() -export class PodSecurityPolicy extends KubeObject { +export class PodSecurityPolicy extends KubeObject { static kind = "PodSecurityPolicy"; static namespaced = false; static apiBase = "/apis/policy/v1beta1/podsecuritypolicies"; - spec: { - allowPrivilegeEscalation?: boolean; - allowedCSIDrivers?: { - name: string; - }[]; - allowedCapabilities: string[]; - allowedFlexVolumes?: { - driver: string; - }[]; - allowedHostPaths?: { - pathPrefix: string; - readOnly: boolean; - }[]; - allowedProcMountTypes?: string[]; - allowedUnsafeSysctls?: string[]; - defaultAddCapabilities?: string[]; - defaultAllowPrivilegeEscalation?: boolean; - forbiddenSysctls?: string[]; - fsGroup?: { - rule: string; - ranges: { max: number; min: number }[]; - }; - hostIPC?: boolean; - hostNetwork?: boolean; - hostPID?: boolean; - hostPorts?: { - max: number; - min: number; - }[]; - privileged?: boolean; - readOnlyRootFilesystem?: boolean; - requiredDropCapabilities?: string[]; - runAsGroup?: { - ranges: { max: number; min: number }[]; - rule: string; - }; - runAsUser?: { - rule: string; - ranges: { max: number; min: number }[]; - }; - runtimeClass?: { - allowedRuntimeClassNames: string[]; - defaultRuntimeClassName: string; - }; - seLinux?: { - rule: string; - seLinuxOptions: { - level: string; - role: string; - type: string; - user: string; - }; - }; - supplementalGroups?: { - rule: string; - ranges: { max: number; min: number }[]; - }; - volumes?: string[]; - }; - isPrivileged() { - return !!this.spec.privileged; + return this.spec?.privileged ?? false; } getVolumes() { - return this.spec.volumes || []; + return this.spec?.volumes ?? []; } getRules() { - const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec; + const { fsGroup, runAsGroup, runAsUser, supplementalGroups, seLinux } = this.spec ?? {}; return { - fsGroup: fsGroup ? fsGroup.rule : "", - runAsGroup: runAsGroup ? runAsGroup.rule : "", - runAsUser: runAsUser ? runAsUser.rule : "", - supplementalGroups: supplementalGroups ? supplementalGroups.rule : "", - seLinux: seLinux ? seLinux.rule : "", + fsGroup: fsGroup?.rule ?? "", + runAsGroup: runAsGroup?.rule ?? "", + runAsUser: runAsUser?.rule ?? "", + supplementalGroups: supplementalGroups?.rule ?? "", + seLinux: seLinux?.rule ?? "", }; } } diff --git a/src/renderer/api/endpoints/replica-set.api.ts b/src/renderer/api/endpoints/replica-set.api.ts index eb1131f645..56e46ca255 100644 --- a/src/renderer/api/endpoints/replica-set.api.ts +++ b/src/renderer/api/endpoints/replica-set.api.ts @@ -1,10 +1,9 @@ -import get from "lodash/get"; import { autobind } from "../../utils"; -import { WorkloadKubeObject } from "../workload-kube-object"; -import { IPodContainer, Pod } from "./pods.api"; +import { WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; +import { Pod } from "./pods.api"; import { KubeApi } from "../kube-api"; -export class ReplicaSetApi extends KubeApi { +export class ReplicaSetApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { return `${this.getUrl(params)}/scale`; } @@ -27,56 +26,55 @@ export class ReplicaSetApi extends KubeApi { } } +interface ReplicaSetSpec extends WorkloadSpec { + replicas?: number; + template?: { + metadata: { + labels: { + app: string; + }; + }; + spec?: Pod["spec"]; + }; + minReadySeconds?: number; +} + +interface ReplicaSetStatus { + replicas: number; + fullyLabeledReplicas?: number; + readyReplicas?: number; + availableReplicas?: number; + observedGeneration?: number; + conditions?: { + type: string; + status: string; + lastUpdateTime: string; + lastTransitionTime: string; + reason: string; + message: string; + }[]; +} + @autobind() -export class ReplicaSet extends WorkloadKubeObject { +export class ReplicaSet extends WorkloadKubeObject { static kind = "ReplicaSet"; static namespaced = true; static apiBase = "/apis/apps/v1/replicasets"; - spec: { - replicas?: number; - selector: { matchLabels: { [app: string]: string } }; - template?: { - metadata: { - labels: { - app: string; - }; - }; - spec?: Pod["spec"]; - }; - minReadySeconds?: number; - }; - status: { - replicas: number; - fullyLabeledReplicas?: number; - readyReplicas?: number; - availableReplicas?: number; - observedGeneration?: number; - conditions?: { - type: string; - status: string; - lastUpdateTime: string; - lastTransitionTime: string; - reason: string; - message: string; - }[]; - }; getDesired() { - return this.spec.replicas || 0; + return this.spec?.replicas ?? 0; } getCurrent() { - return this.status.availableReplicas || 0; + return this.status?.availableReplicas ?? 0; } getReady() { - return this.status.readyReplicas || 0; + return this.status?.readyReplicas ?? 0; } getImages() { - const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); - - return [...containers].map(container => container.image); + return this.spec?.template?.spec?.containers?.map(container => container.image) ?? []; } } diff --git a/src/renderer/api/endpoints/resource-applier.api.ts b/src/renderer/api/endpoints/resource-applier.api.ts index 9150ae77c5..07c3cfebfe 100644 --- a/src/renderer/api/endpoints/resource-applier.api.ts +++ b/src/renderer/api/endpoints/resource-applier.api.ts @@ -1,6 +1,5 @@ import jsYaml from "js-yaml"; import { KubeObject } from "../kube-object"; -import { KubeJsonApiData } from "../kube-json-api"; import { apiBase } from "../index"; import { apiManager } from "../api-manager"; @@ -9,25 +8,22 @@ export const resourceApplierApi = { "kubectl.kubernetes.io/last-applied-configuration" ], - async update(resource: object | string): Promise { + async update>(resource: object | string): Promise { if (typeof resource === "string") { resource = jsYaml.safeLoad(resource); } - return apiBase - .post("/stack", { data: resource }) - .then(data => { - const items = data.map(obj => { - const api = apiManager.getApiByKind(obj.kind, obj.apiVersion); + const data = await apiBase.post("/stack", { data: resource }); + const items = data.map(obj => { + const api = apiManager.getApiByKind(obj.kind, obj.apiVersion); - if (api) { - return new api.objectConstructor(obj); - } else { - return new KubeObject(obj); - } - }); + if (api) { + return new api.objectConstructor(obj) as D; + } - return items.length === 1 ? items[0] : items; - }); + return new KubeObject(obj) as D; + }); + + return items.length === 1 ? items[0] : items; } }; diff --git a/src/renderer/api/endpoints/resource-quota.api.ts b/src/renderer/api/endpoints/resource-quota.api.ts index e2d37a9081..c27785c29a 100644 --- a/src/renderer/api/endpoints/resource-quota.api.ts +++ b/src/renderer/api/endpoints/resource-quota.api.ts @@ -1,9 +1,8 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -import { KubeJsonApiData } from "../kube-json-api"; export interface IResourceQuotaValues { - [quota: string]: string; + [quota: string]: string | undefined; // Compute Resource Quota "limits.cpu"?: string; @@ -30,36 +29,29 @@ export interface IResourceQuotaValues { "count/deployments.extensions"?: string; } -export class ResourceQuota extends KubeObject { +interface ResourceQuotaSpec { + hard: IResourceQuotaValues; + scopeSelector?: { + matchExpressions: { + operator: string; + scopeName: string; + values: string[]; + }[]; + }; +} + +interface ResourceQuotaStatus { + hard: IResourceQuotaValues; + used: IResourceQuotaValues; +} + +export class ResourceQuota extends KubeObject { static kind = "ResourceQuota"; static namespaced = true; static apiBase = "/api/v1/resourcequotas"; - constructor(data: KubeJsonApiData) { - super(data); - this.spec = this.spec || {} as any; - } - - spec: { - hard: IResourceQuotaValues; - scopeSelector?: { - matchExpressions: { - operator: string; - scopeName: string; - values: string[]; - }[]; - }; - }; - - status: { - hard: IResourceQuotaValues; - used: IResourceQuotaValues; - }; - getScopeSelector() { - const { matchExpressions = [] } = this.spec.scopeSelector || {}; - - return matchExpressions; + return this.spec?.scopeSelector?.matchExpressions ?? []; } } diff --git a/src/renderer/api/endpoints/role-binding.api.ts b/src/renderer/api/endpoints/role-binding.api.ts index 866656ee56..b0984fcf11 100644 --- a/src/renderer/api/endpoints/role-binding.api.ts +++ b/src/renderer/api/endpoints/role-binding.api.ts @@ -10,13 +10,13 @@ export interface IRoleBindingSubject { } @autobind() -export class RoleBinding extends KubeObject { +export class RoleBinding extends KubeObject { static kind = "RoleBinding"; static namespaced = true; static apiBase = "/apis/rbac.authorization.k8s.io/v1/rolebindings"; subjects?: IRoleBindingSubject[]; - roleRef: { + roleRef?: { kind: string; name: string; apiGroup?: string; diff --git a/src/renderer/api/endpoints/role.api.ts b/src/renderer/api/endpoints/role.api.ts index c89834ed05..378f3e20ca 100644 --- a/src/renderer/api/endpoints/role.api.ts +++ b/src/renderer/api/endpoints/role.api.ts @@ -1,12 +1,12 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -export class Role extends KubeObject { +export class Role extends KubeObject { static kind = "Role"; static namespaced = true; static apiBase = "/apis/rbac.authorization.k8s.io/v1/roles"; - rules: { + rules?: { verbs: string[]; apiGroups: string[]; resources: string[]; @@ -14,7 +14,7 @@ export class Role extends KubeObject { }[]; getRules() { - return this.rules || []; + return this.rules ?? []; } } diff --git a/src/renderer/api/endpoints/secret.api.ts b/src/renderer/api/endpoints/secret.api.ts index 16262570df..124dfd1907 100644 --- a/src/renderer/api/endpoints/secret.api.ts +++ b/src/renderer/api/endpoints/secret.api.ts @@ -1,5 +1,4 @@ import { KubeObject } from "../kube-object"; -import { KubeJsonApiData } from "../kube-json-api"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; @@ -20,21 +19,16 @@ export interface ISecretRef { } @autobind() -export class Secret extends KubeObject { +export class Secret extends KubeObject { static kind = "Secret"; static namespaced = true; static apiBase = "/api/v1/secrets"; - type: SecretType; + type?: SecretType; data: { - [prop: string]: string; + [prop: string]: string | undefined; token?: string; - }; - - constructor(data: KubeJsonApiData) { - super(data); - this.data = this.data || {}; - } + } = {}; getKeys(): string[] { return Object.keys(this.data); diff --git a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts index f47fc29a4e..5cac6b4371 100644 --- a/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts +++ b/src/renderer/api/endpoints/selfsubjectrulesreviews.api.ts @@ -1,14 +1,13 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; -export class SelfSubjectRulesReviewApi extends KubeApi { +export class SelfSubjectRulesReviewApi extends KubeApi { create({ namespace = "default" }): Promise { return super.create({}, { spec: { namespace }, - } - ); + }); } } @@ -20,32 +19,28 @@ export interface ISelfSubjectReviewRule { nonResourceURLs?: string[]; } -export class SelfSubjectRulesReview extends KubeObject { +interface SelfSubjectRulesReviewSpec { + // todo: add more types from api docs + namespace?: string; +} + +interface SelfSubjectRulesReviewStatus { + resourceRules: ISelfSubjectReviewRule[]; + nonResourceRules: ISelfSubjectReviewRule[]; + incomplete: boolean; +} + +export class SelfSubjectRulesReview extends KubeObject { static kind = "SelfSubjectRulesReview"; static namespaced = false; static apiBase = "/apis/authorization.k8s.io/v1/selfsubjectrulesreviews"; - spec: { - // todo: add more types from api docs - namespace?: string; - }; - - status: { - resourceRules: ISelfSubjectReviewRule[]; - nonResourceRules: ISelfSubjectReviewRule[]; - incomplete: boolean; - }; - getResourceRules() { - const rules = this.status && this.status.resourceRules || []; - - return rules.map(rule => this.normalize(rule)); + return this.status?.resourceRules.map(rule => this.normalize(rule)) ?? []; } getNonResourceRules() { - const rules = this.status && this.status.nonResourceRules || []; - - return rules.map(rule => this.normalize(rule)); + return this.status?.nonResourceRules.map(rule => this.normalize(rule)) ?? []; } protected normalize(rule: ISelfSubjectReviewRule): ISelfSubjectReviewRule { diff --git a/src/renderer/api/endpoints/service-accounts.api.ts b/src/renderer/api/endpoints/service-accounts.api.ts index 9f449cdec4..d5611301c5 100644 --- a/src/renderer/api/endpoints/service-accounts.api.ts +++ b/src/renderer/api/endpoints/service-accounts.api.ts @@ -3,7 +3,7 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; @autobind() -export class ServiceAccount extends KubeObject { +export class ServiceAccount extends KubeObject { static kind = "ServiceAccount"; static namespaced = true; static apiBase = "/api/v1/serviceaccounts"; @@ -24,6 +24,6 @@ export class ServiceAccount extends KubeObject { } } -export const serviceAccountsApi = new KubeApi({ +export const serviceAccountsApi = new KubeApi({ objectConstructor: ServiceAccount, }); diff --git a/src/renderer/api/endpoints/service.api.ts b/src/renderer/api/endpoints/service.api.ts index 6c02873139..9ff26c2655 100644 --- a/src/renderer/api/endpoints/service.api.ts +++ b/src/renderer/api/endpoints/service.api.ts @@ -7,6 +7,7 @@ export interface IServicePort { protocol: string; port: number; targetPort: number; + nodePort?: number; } export class ServicePort implements IServicePort { @@ -17,7 +18,11 @@ export class ServicePort implements IServicePort { nodePort?: number; constructor(data: IServicePort) { - Object.assign(this, data); + this.name = data.name; + this.protocol = data.protocol; + this.port = data.port; + this.targetPort = data.targetPort; + this.nodePort = data.nodePort; } toString() { @@ -29,64 +34,60 @@ export class ServicePort implements IServicePort { } } +interface ServiceSpec { + type: string; + clusterIP: string; + externalTrafficPolicy?: string; + loadBalancerIP?: string; + sessionAffinity: string; + selector: { + [key: string]: string; + }; + ports: ServicePort[]; + externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips +} + +interface ServiceStatus { + loadBalancer?: { + ingress?: { + ip?: string; + hostname?: string; + }[]; + }; +} + @autobind() -export class Service extends KubeObject { +export class Service extends KubeObject { static kind = "Service"; static namespaced = true; static apiBase = "/api/v1/services"; - spec: { - type: string; - clusterIP: string; - externalTrafficPolicy?: string; - loadBalancerIP?: string; - sessionAffinity: string; - selector: { [key: string]: string }; - ports: ServicePort[]; - externalIPs?: string[]; // https://kubernetes.io/docs/concepts/services-networking/service/#external-ips - }; - - status: { - loadBalancer?: { - ingress?: { - ip?: string; - hostname?: string; - }[]; - }; - }; - getClusterIp() { - return this.spec.clusterIP; + return this.spec?.clusterIP; } getExternalIps() { - const lb = this.getLoadBalancer(); - - if (lb && lb.ingress) { - return lb.ingress.map(val => val.ip || val.hostname); - } - - return this.spec.externalIPs || []; + return this.getLoadBalancer() + ?.ingress + ?.map(val => val.ip || val.hostname) + ?? this.spec?.externalIPs + ?? []; } getType() { - return this.spec.type || "-"; + return this.spec?.type || "-"; } getSelector(): string[] { - if (!this.spec.selector) return []; - - return Object.entries(this.spec.selector).map(val => val.join("=")); + return Object.entries(this.spec?.selector ?? {}).map(val => val.join("=")); } getPorts(): ServicePort[] { - const ports = this.spec.ports || []; - - return ports.map(p => new ServicePort(p)); + return this.spec?.ports.map(p => new ServicePort(p)) ?? []; } getLoadBalancer() { - return this.status.loadBalancer; + return this.status?.loadBalancer ?? {}; } isActive() { diff --git a/src/renderer/api/endpoints/stateful-set.api.ts b/src/renderer/api/endpoints/stateful-set.api.ts index add2a554ba..362e0d4e35 100644 --- a/src/renderer/api/endpoints/stateful-set.api.ts +++ b/src/renderer/api/endpoints/stateful-set.api.ts @@ -1,10 +1,8 @@ -import get from "lodash/get"; -import { IPodContainer } from "./pods.api"; -import { IAffinity, WorkloadKubeObject } from "../workload-kube-object"; +import { IAffinity, WorkloadKubeObject, WorkloadSpec } from "../workload-kube-object"; import { autobind } from "../../utils"; import { KubeApi } from "../kube-api"; -export class StatefulSetApi extends KubeApi { +export class StatefulSetApi extends KubeApi { protected getScaleApiUrl(params: { namespace: string; name: string }) { return `${this.getUrl(params)}/scale`; } @@ -27,83 +25,77 @@ export class StatefulSetApi extends KubeApi { } } +interface StatefulSetSpec extends WorkloadSpec { + serviceName: string; + replicas: number; + template: { + metadata: { + labels: { + app: string; + }; + }; + spec: { + containers: { + name: string; + image: string; + ports: { + containerPort: number; + name: string; + }[]; + volumeMounts: { + name: string; + mountPath: string; + }[]; + }[]; + affinity?: IAffinity; + nodeSelector?: { + [selector: string]: string; + }; + tolerations: { + key: string; + operator: string; + effect: string; + tolerationSeconds: number; + }[]; + }; + }; + volumeClaimTemplates: { + metadata: { + name: string; + }; + spec: { + accessModes: string[]; + resources: { + requests: { + storage: string; + }; + }; + }; + }[]; +} + +interface StatefulSetStatus { + observedGeneration: number; + replicas: number; + currentReplicas: number; + readyReplicas: number; + currentRevision: string; + updateRevision: string; + collisionCount: number; +} + @autobind() -export class StatefulSet extends WorkloadKubeObject { +export class StatefulSet extends WorkloadKubeObject { static kind = "StatefulSet"; static namespaced = true; static apiBase = "/apis/apps/v1/statefulsets"; - spec: { - serviceName: string; - replicas: number; - selector: { - matchLabels: { - [key: string]: string; - }; - }; - template: { - metadata: { - labels: { - app: string; - }; - }; - spec: { - containers: { - name: string; - image: string; - ports: { - containerPort: number; - name: string; - }[]; - volumeMounts: { - name: string; - mountPath: string; - }[]; - }[]; - affinity?: IAffinity; - nodeSelector?: { - [selector: string]: string; - }; - tolerations: { - key: string; - operator: string; - effect: string; - tolerationSeconds: number; - }[]; - }; - }; - volumeClaimTemplates: { - metadata: { - name: string; - }; - spec: { - accessModes: string[]; - resources: { - requests: { - storage: string; - }; - }; - }; - }[]; - }; - status: { - observedGeneration: number; - replicas: number; - currentReplicas: number; - readyReplicas: number; - currentRevision: string; - updateRevision: string; - collisionCount: number; - }; - getReplicas() { - return this.spec.replicas || 0; + return this.spec?.replicas ?? 0; } getImages() { - const containers: IPodContainer[] = get(this, "spec.template.spec.containers", []); - - return [...containers].map(container => container.image); + return this.spec?.template.spec.containers.map(container => container.image) ?? []; } } diff --git a/src/renderer/api/endpoints/storage-class.api.ts b/src/renderer/api/endpoints/storage-class.api.ts index 085701742c..01d37e5493 100644 --- a/src/renderer/api/endpoints/storage-class.api.ts +++ b/src/renderer/api/endpoints/storage-class.api.ts @@ -3,16 +3,16 @@ import { KubeObject } from "../kube-object"; import { KubeApi } from "../kube-api"; @autobind() -export class StorageClass extends KubeObject { +export class StorageClass extends KubeObject { static kind = "StorageClass"; static namespaced = false; static apiBase = "/apis/storage.k8s.io/v1/storageclasses"; - provisioner: string; // e.g. "storage.k8s.io/v1" + provisioner?: string; // e.g. "storage.k8s.io/v1" mountOptions?: string[]; - volumeBindingMode: string; - reclaimPolicy: string; - parameters: { + volumeBindingMode?: string; + reclaimPolicy?: string; + parameters?: { [param: string]: string; // every provisioner has own set of these parameters }; diff --git a/src/renderer/api/index.ts b/src/renderer/api/index.ts index 7c2b55d0e1..ad7513674a 100644 --- a/src/renderer/api/index.ts +++ b/src/renderer/api/index.ts @@ -7,7 +7,7 @@ export const apiBase = new JsonApi({ apiBase: apiPrefix, debug: isDevelopment || isDebugging, }); -export const apiKube = new KubeJsonApi({ +export const apiKube = new KubeJsonApi({ apiBase: apiKubePrefix, debug: isDevelopment, }); diff --git a/src/renderer/api/json-api.ts b/src/renderer/api/json-api.ts index df12b08ab7..009374da48 100644 --- a/src/renderer/api/json-api.ts +++ b/src/renderer/api/json-api.ts @@ -114,7 +114,7 @@ export class JsonApi { reqUrl += (reqUrl.includes("?") ? "&" : "?") + queryString; } const infoLog: JsonApiLog = { - method: reqInit.method.toUpperCase(), + method: reqInit.method?.toUpperCase() ?? "", reqUrl, reqInit, }; diff --git a/src/renderer/api/kube-api-parse.ts b/src/renderer/api/kube-api-parse.ts index 285610bece..5996385dac 100644 --- a/src/renderer/api/kube-api-parse.ts +++ b/src/renderer/api/kube-api-parse.ts @@ -1,7 +1,7 @@ // Parse kube-api path and get api-version, group, etc. import type { KubeObject } from "./kube-object"; -import { splitArray } from "../../common/utils"; +import { NotFalsy, splitArray } from "../../common/utils"; import { apiManager } from "./api-manager"; export interface IKubeObjectRef { @@ -12,10 +12,10 @@ export interface IKubeObjectRef { } export interface IKubeApiLinkRef { - apiPrefix?: string; + apiPrefix: string; apiVersion: string; resource: string; - name: string; + name?: string; namespace?: string; } @@ -70,7 +70,7 @@ export function parseKubeApi(path: string): IKubeApiParsed { * There is no well defined selection from an array of items that were * separated by '/' * - * Solution is to create a huristic. Namely: + * Solution is to create a heuristic. Namely: * 1. if '.' in left[0] then apiGroup <- left[0] * 2. if left[1] matches /^v[0-9]/ then apiGroup, apiVersion <- left[0], left[1] * 3. otherwise assume apiVersion <- left[0] @@ -88,10 +88,10 @@ export function parseKubeApi(path: string): IKubeApiParsed { } } - const apiVersionWithGroup = [apiGroup, apiVersion].filter(v => v).join("/"); - const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); + const apiVersionWithGroup = [apiGroup, apiVersion].filter(NotFalsy).join("/"); + const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(NotFalsy).join("/"); - if (!apiBase) { + if (!apiBase || !apiVersion || !resource) { throw new Error(`invalid apiPath: ${path}`); } @@ -103,7 +103,7 @@ export function parseKubeApi(path: string): IKubeApiParsed { }; } -export function createKubeApiURL(ref: IKubeApiLinkRef): string { +export function createKubeApiURL(ref: Partial): string { const { apiPrefix = "/apis", resource, apiVersion, name } = ref; let { namespace } = ref; @@ -112,11 +112,11 @@ export function createKubeApiURL(ref: IKubeApiLinkRef): string { } return [apiPrefix, apiVersion, namespace, resource, name] - .filter(v => v) + .filter(NotFalsy) .join("/"); } -export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string { +export function lookupApiLink(ref: IKubeObjectRef, parentObject: KubeObject): string { const { kind, apiVersion, name, namespace = parentObject.getNs() diff --git a/src/renderer/api/kube-api.ts b/src/renderer/api/kube-api.ts index 98833e3d4d..2221a8088e 100644 --- a/src/renderer/api/kube-api.ts +++ b/src/renderer/api/kube-api.ts @@ -13,7 +13,7 @@ import byline from "byline"; import { IKubeWatchEvent } from "./kube-watch-api"; import { ReadableWebToNodeStream } from "../utils/readableStream"; -export interface IKubeApiOptions { +export interface IKubeApiOptions> { /** * base api-path for listing all resources, e.g. "/api/v1/pods" */ @@ -27,8 +27,8 @@ export interface IKubeApiOptions { */ fallbackApiBases?: string[]; - objectConstructor?: IKubeObjectConstructor; - request?: KubeJsonApi; + objectConstructor: IKubeObjectConstructor; + request?: KubeJsonApi; isNamespaced?: boolean; kind?: string; checkPreferredVersion?: boolean; @@ -67,8 +67,8 @@ export interface IKubeApiCluster { } } -export function forCluster(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor): KubeApi { - const request = new KubeJsonApi({ +export function forCluster>(cluster: IKubeApiCluster, kubeClass: IKubeObjectConstructor): KubeApi { + const request = new KubeJsonApi({ apiBase: apiKubePrefix, debug: isDevelopment, }, { @@ -83,7 +83,7 @@ export function forCluster(cluster: IKubeApiCluster, kubeC }); } -export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) { +export function ensureObjectSelfLink(api: KubeApi>, object: KubeJsonApiData) { if (!object.metadata.selfLink) { object.metadata.selfLink = createKubeApiURL({ apiPrefix: api.apiPrefix, @@ -95,7 +95,7 @@ export function ensureObjectSelfLink(api: KubeApi, object: KubeJsonApiData) { } } -export type KubeApiWatchCallback = (data: IKubeWatchEvent, error: any) => void; +export type KubeApiWatchCallback = (data?: IKubeWatchEvent, error?: any) => void; export type KubeApiWatchOptions = { namespace: string; @@ -103,7 +103,7 @@ export type KubeApiWatchOptions = { abortController?: AbortController }; -export class KubeApi { +export class KubeApi = KubeObject> { readonly kind: string; readonly apiBase: string; readonly apiPrefix: string; @@ -113,23 +113,29 @@ export class KubeApi { readonly apiResource: string; readonly isNamespaced: boolean; - public objectConstructor: IKubeObjectConstructor; - protected request: KubeJsonApi; + public objectConstructor: IKubeObjectConstructor; + protected request: KubeJsonApi; protected resourceVersions = new Map(); - protected watchDisposer: () => void; + protected watchDisposer?: () => void; + protected fallbackApiBases: string[]; + protected fullApiBase: string; + protected forceCheckPreferredVersion: boolean; - constructor(protected options: IKubeApiOptions) { + constructor(options: IKubeApiOptions) { + const { objectConstructor, request = apiKube } = options; const { - objectConstructor = KubeObject as IKubeObjectConstructor, - request = apiKube, - kind = options.objectConstructor?.kind, - isNamespaced = options.objectConstructor?.namespaced - } = options || {}; + kind = objectConstructor.kind, + isNamespaced = objectConstructor.namespaced ?? false, + apiBase: fullApiBase = objectConstructor.apiBase, + fallbackApiBases = [], + checkPreferredVersion = false, + } = options; - if (!options.apiBase) { - options.apiBase = objectConstructor.apiBase; - } - const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(options.apiBase); + this.fallbackApiBases = fallbackApiBases; + this.fullApiBase = fullApiBase; + this.forceCheckPreferredVersion = checkPreferredVersion; + + const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(fullApiBase); this.kind = kind; this.isNamespaced = isNamespaced; @@ -157,8 +163,7 @@ export class KubeApi { * First tries options.apiBase, then urls in order from options.fallbackApiBases. */ private async getLatestApiPrefixGroup() { - // Note that this.options.apiBase is the "full" url, whereas this.apiBase is parsed - const apiBases = [this.options.apiBase, ...this.options.fallbackApiBases]; + const apiBases = [this.fullApiBase, ...this.fallbackApiBases]; for (const apiUrl of apiBases) { // Split e.g. "/apis/extensions/v1beta1/ingresses" to parts @@ -193,7 +198,7 @@ export class KubeApi { * Get the apiPrefix and apiGroup to be used for fetching the preferred version. */ private async getPreferredVersionPrefixGroup() { - if (this.options.fallbackApiBases) { + if (this.fallbackApiBases.length > 0) { try { return await this.getLatestApiPrefixGroup(); } catch (error) { @@ -209,11 +214,11 @@ export class KubeApi { } protected async checkPreferredVersion() { - if (this.options.fallbackApiBases && !this.options.checkPreferredVersion) { + if (this.fallbackApiBases && !this.forceCheckPreferredVersion) { throw new Error("checkPreferredVersion must be enabled if fallbackApiBases is set in KubeApi"); } - if (this.options.checkPreferredVersion && this.apiVersionPreferred === undefined) { + if (this.forceCheckPreferredVersion && this.apiVersionPreferred === undefined) { const { apiPrefix, apiGroup } = await this.getPreferredVersionPrefixGroup(); // The apiPrefix and apiGroup might change due to fallbackApiBases, so we must override them @@ -273,7 +278,7 @@ export class KubeApi { return query; } - protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any { + protected parseResponse(data: KubeJsonApiData | KubeJsonApiData[] | KubeJsonApiDataList, namespace?: string): any { if (!data) return; const KubeObjectConstructor = this.objectConstructor; @@ -294,9 +299,9 @@ export class KubeApi { return items.map((item) => { const object = new KubeObjectConstructor({ + ...item, kind: this.kind, apiVersion, - ...item, }); ensureObjectSelfLink(this, object); @@ -384,16 +389,19 @@ export class KubeApi { }); const watchUrl = this.getWatchUrl(namespace); - const responsePromise = this.request.getResponse(watchUrl, null, { + const responsePromise = this.request.getResponse(watchUrl, undefined, { signal: abortController.signal }); responsePromise.then((response) => { if (!response.ok && !abortController.signal.aborted) { - callback?.(null, response); - - return; + return callback?.(undefined, response); } + + if (!response.body) { + return callback?.(undefined, new Error("Response.body is not defined")); + } + const nodeStream = new ReadableWebToNodeStream(response.body); ["end", "close", "error"].forEach((eventName) => { @@ -417,7 +425,7 @@ export class KubeApi { if (event.type === "ERROR" && event.object.kind === "Status") { errorReceived = true; - callback(null, new KubeStatus(event.object as any)); + callback?.(undefined, new KubeStatus(event.object as any)); return; } @@ -434,9 +442,9 @@ export class KubeApi { }, (error) => { if (error instanceof DOMException) return; // AbortController rejects, we can ignore it - callback?.(null, error); + callback?.(undefined, error); }).catch((error) => { - callback?.(null, error); + callback?.(undefined, error); }); const disposer = () => { diff --git a/src/renderer/api/kube-json-api.ts b/src/renderer/api/kube-json-api.ts index 362ee5438e..df3428b6fe 100644 --- a/src/renderer/api/kube-json-api.ts +++ b/src/renderer/api/kube-json-api.ts @@ -1,6 +1,7 @@ import { JsonApi, JsonApiData, JsonApiError } from "./json-api"; +import { IKubeObjectMetadata } from "./kube-object"; -export interface KubeJsonApiDataList { +export interface KubeJsonApiDataList> { kind: string; apiVersion: string; items: T[]; @@ -10,25 +11,12 @@ export interface KubeJsonApiDataList { }; } -export interface KubeJsonApiData extends JsonApiData { +export interface KubeJsonApiData extends JsonApiData { kind: string; apiVersion: string; - metadata: { - uid: string; - name: string; - namespace?: string; - creationTimestamp?: string; - resourceVersion: string; - continue?: string; - finalizers?: string[]; - selfLink?: string; - labels?: { - [label: string]: string; - }; - annotations?: { - [annotation: string]: string; - }; - }; + metadata: IKubeObjectMetadata; + spec?: Spec; + status?: Status; } export interface KubeJsonApiError extends JsonApiError { @@ -42,7 +30,7 @@ export interface KubeJsonApiError extends JsonApiError { }; } -export class KubeJsonApi extends JsonApi { +export class KubeJsonApi extends JsonApi> { protected parseError(error: KubeJsonApiError | any, res: Response): string[] { const { status, reason, message } = error; diff --git a/src/renderer/api/kube-object.ts b/src/renderer/api/kube-object.ts index 7d0c34de33..2d986a5613 100644 --- a/src/renderer/api/kube-object.ts +++ b/src/renderer/api/kube-object.ts @@ -8,10 +8,10 @@ import { apiKube } from "./index"; import { JsonApiParams } from "./json-api"; import { resourceApplierApi } from "./endpoints/resource-applier.api"; -export type IKubeObjectConstructor = (new (data: KubeJsonApiData | any) => T) & { - kind?: string; +export type IKubeObjectConstructor> = (new (data: KubeJsonApiData) => T) & { + kind: string; namespaced?: boolean; - apiBase?: string; + apiBase: string; }; export interface IKubeObjectMetadata { @@ -66,19 +66,20 @@ export class KubeStatus { export type IKubeMetaField = keyof IKubeObjectMetadata; @autobind() -export class KubeObject implements ItemObject { +export class KubeObject implements ItemObject { static readonly kind: string; static readonly namespaced: boolean; + static readonly apiBase: string; static create(data: any) { return new KubeObject(data); } - static isNonSystem(item: KubeJsonApiData | KubeObject) { - return !item.metadata.name.startsWith("system:"); + static isNonSystem(item: KubeJsonApiData | KubeObject) { + return !item.metadata?.name.startsWith("system:"); } - static isJsonApiData(object: any): object is KubeJsonApiData { + static isJsonApiData(object: any): object is KubeJsonApiData { return !object.items && object.metadata; } @@ -86,23 +87,26 @@ export class KubeObject implements ItemObject { return object.items && object.metadata; } - static stringifyLabels(labels: { [name: string]: string }): string[] { - if (!labels) return []; - - return Object.entries(labels).map(([name, value]) => `${name}=${value}`); + static stringifyLabels(labels?: Record | null): string[] { + return Object.entries(labels ?? {}).map(([name, value]) => `${name}=${value}`); } - constructor(data: KubeJsonApiData) { - Object.assign(this, data); + constructor(data: KubeJsonApiData) { + this.apiVersion = data.apiVersion; + this.kind = data.kind; + this.metadata = data.metadata; + this.spec = data.spec; + this.status = data.status; } apiVersion: string; kind: string; metadata: IKubeObjectMetadata; - status?: any; // todo: type-safety support + spec?: Spec; + status?: Status; get selfLink() { - return this.metadata.selfLink; + return this.metadata?.selfLink; } getId() { @@ -110,25 +114,25 @@ export class KubeObject implements ItemObject { } getResourceVersion() { - return this.metadata.resourceVersion; + return this.metadata?.resourceVersion; } getName() { - return this.metadata.name; + return this.metadata?.name; } getNs() { // avoid "null" serialization via JSON.stringify when post data - return this.metadata.namespace || undefined; + return this.metadata?.namespace || undefined; } getTimeDiffFromNow(): number { - return Date.now() - new Date(this.metadata.creationTimestamp).getTime(); + return Date.now() - new Date(this.metadata?.creationTimestamp).getTime(); } getAge(humanize = true, compact = true, fromNow = false): string | number { if (fromNow) { - return moment(this.metadata.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used + return moment(this.metadata?.creationTimestamp).fromNow(); // "string", getTimeDiffFromNow() cannot be used } const diff = this.getTimeDiffFromNow(); @@ -140,15 +144,15 @@ export class KubeObject implements ItemObject { } getFinalizers(): string[] { - return this.metadata.finalizers || []; + return this.metadata?.finalizers || []; } getLabels(): string[] { - return KubeObject.stringifyLabels(this.metadata.labels); + return KubeObject.stringifyLabels(this.metadata?.labels ?? {}); } getAnnotations(filter = false): string[] { - const labels = KubeObject.stringifyLabels(this.metadata.annotations); + const labels = KubeObject.stringifyLabels(this.metadata?.annotations ?? {}); return filter ? labels.filter(label => { const skip = resourceApplierApi.annotations.some(key => label.startsWith(key)); @@ -158,7 +162,7 @@ export class KubeObject implements ItemObject { } getOwnerRefs() { - const refs = this.metadata.ownerReferences || []; + const refs = this.metadata?.ownerReferences || []; return refs.map(ownerRef => ({ ...ownerRef, @@ -183,8 +187,8 @@ export class KubeObject implements ItemObject { } // use unified resource-applier api for updating all k8s objects - async update(data: Partial) { - return resourceApplierApi.update({ + async update>(data: Partial): Promise { + return resourceApplierApi.update({ ...this.toPlainObject(), ...data, }); diff --git a/src/renderer/api/kube-watch-api.ts b/src/renderer/api/kube-watch-api.ts index f2f50193db..a21f031673 100644 --- a/src/renderer/api/kube-watch-api.ts +++ b/src/renderer/api/kube-watch-api.ts @@ -10,8 +10,9 @@ import { autobind, noop } from "../utils"; import { KubeApi } from "./kube-api"; import { KubeJsonApiData } from "./kube-json-api"; import { isDebugging, isProduction } from "../../common/vars"; +import { KubeObject } from "./kube-object"; -export interface IKubeWatchEvent { +export interface IKubeWatchEvent = KubeJsonApiData> { type: "ADDED" | "MODIFIED" | "DELETED" | "ERROR"; object?: T; } @@ -31,15 +32,15 @@ export interface IKubeWatchLog { @autobind() export class KubeWatchApi { - @observable context: ClusterContext = null; + @observable context: ClusterContext | null = null; contextReady = when(() => Boolean(this.context)); - isAllowedApi(api: KubeApi): boolean { - return Boolean(this.context?.cluster.isAllowedResource(api.kind)); + isAllowedApi(api: KubeApi): boolean { + return Boolean(this.context?.cluster?.isAllowedResource(api.kind)); } - preloadStores(stores: KubeObjectStore[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) { + preloadStores(stores: KubeObjectStore>[], opts: { namespaces?: string[], loadOnce?: boolean } = {}) { const limitRequests = plimit(1); // load stores one by one to allow quick skipping when fast clicking btw pages const preloading: Promise[] = []; @@ -57,14 +58,14 @@ export class KubeWatchApi { }; } - subscribeStores(stores: KubeObjectStore[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void { + subscribeStores(stores: KubeObjectStore>[], opts: IKubeWatchSubscribeStoreOptions = {}): () => void { const { preload = true, waitUntilLoaded = true, loadOnce = false, } = opts; const subscribingNamespaces = opts.namespaces ?? this.context?.allNamespaces ?? []; const unsubscribeList: Function[] = []; let isUnsubscribed = false; const load = (namespaces = subscribingNamespaces) => this.preloadStores(stores, { namespaces, loadOnce }); - let preloading = preload && load(); + let preloading = preload ? load() : undefined; let cancelReloading: IReactionDisposer = noop; const subscribe = () => { diff --git a/src/renderer/api/terminal-api.ts b/src/renderer/api/terminal-api.ts index 1a94052586..fe40a80e10 100644 --- a/src/renderer/api/terminal-api.ts +++ b/src/renderer/api/terminal-api.ts @@ -31,7 +31,7 @@ export type TerminalApiQuery = Record & { }; export class TerminalApi extends WebSocketApi { - protected size: { Width: number; Height: number }; + protected size?: { Width: number; Height: number }; public onReady = new EventEmitter<[]>(); public isReady = false; diff --git a/src/renderer/api/websocket-api.ts b/src/renderer/api/websocket-api.ts index 79a92cf99e..6bec17046c 100644 --- a/src/renderer/api/websocket-api.ts +++ b/src/renderer/api/websocket-api.ts @@ -1,5 +1,6 @@ import { observable } from "mobx"; import { EventEmitter } from "../../common/event-emitter"; +import logger from "../../main/logger"; interface IParams { url?: string; // connection url, starts with ws:// or wss:// @@ -24,7 +25,7 @@ export enum WebSocketApiState { } export class WebSocketApi { - protected socket: WebSocket; + protected socket?: WebSocket; protected pendingCommands: IMessage[] = []; protected reconnectTimer: any; protected pingTimer: any; @@ -72,9 +73,12 @@ export class WebSocketApi { } connect(url = this.params.url) { - if (this.socket) { - this.socket.close(); // close previous connection first + if (!url) { + return void logger.warn("[WEBSOCKET-API]: cannot connect, url is undefined"); } + + this.socket?.close(); // close previous connection first + this.socket = new WebSocket(url); this.socket.onopen = this._onOpen.bind(this); this.socket.onmessage = this._onMessage.bind(this); @@ -100,7 +104,7 @@ export class WebSocketApi { destroy() { if (!this.socket) return; this.socket.close(); - this.socket = null; + this.socket = undefined; this.pendingCommands = []; this.removeAllListeners(); clearTimeout(this.reconnectTimer); @@ -121,7 +125,7 @@ export class WebSocketApi { }; if (this.isConnected) { - this.socket.send(msg.data); + this.socket?.send(msg.data); } else { this.pendingCommands.push(msg); diff --git a/src/renderer/api/workload-kube-object.ts b/src/renderer/api/workload-kube-object.ts index 185d3d502c..690a6d27d5 100644 --- a/src/renderer/api/workload-kube-object.ts +++ b/src/renderer/api/workload-kube-object.ts @@ -46,13 +46,17 @@ export interface IAffinity { }; } -export class WorkloadKubeObject extends KubeObject { - spec: any; // todo: add proper types +export interface WorkloadSpec { + selector: { + matchLabels: { + [name: string]: string; + }; + } +} +export class WorkloadKubeObject extends KubeObject { getSelectors(): string[] { - const selector = this.spec.selector; - - return KubeObject.stringifyLabels(selector ? selector.matchLabels : null); + return KubeObject.stringifyLabels(this.spec?.selector?.matchLabels ?? {}); } getNodeSelectors(): string[] { diff --git a/src/renderer/bootstrap.tsx b/src/renderer/bootstrap.tsx index c9ef607871..362c99382e 100644 --- a/src/renderer/bootstrap.tsx +++ b/src/renderer/bootstrap.tsx @@ -47,6 +47,10 @@ export { export async function bootstrap(App: AppComponent) { const rootElem = document.getElementById("app"); + if (!rootElem) { + throw new Error("Unexpected HTML doesn't not include #app"); + } + await attachChromeDebugger(); rootElem.classList.toggle("is-mac", isMac); diff --git a/src/renderer/components/+add-cluster/add-cluster.tsx b/src/renderer/components/+add-cluster/add-cluster.tsx index b9392e2b95..0e7fa239a8 100644 --- a/src/renderer/components/+add-cluster/add-cluster.tsx +++ b/src/renderer/components/+add-cluster/add-cluster.tsx @@ -11,11 +11,11 @@ import { AceEditor } from "../ace-editor"; import { Button } from "../button"; import { Icon } from "../icon"; import { kubeConfigDefaultPath, loadConfig, splitConfig, validateConfig, validateKubeConfig } from "../../../common/kube-helpers"; -import { ClusterModel, ClusterStore, clusterStore } from "../../../common/cluster-store"; +import { ClusterStore, clusterStore } from "../../../common/cluster-store"; import { v4 as uuid } from "uuid"; import { navigate } from "../../navigation"; import { userStore } from "../../../common/user-store"; -import { cssNames } from "../../utils"; +import { cssNames, SecondNotFalsy } from "../../utils"; import { Notifications } from "../notifications"; import { Tab, Tabs } from "../tabs"; import { ExecValidationNotFoundError } from "../../../common/custom-errors"; @@ -23,6 +23,7 @@ import { appEventBus } from "../../../common/event-bus"; import { PageLayout } from "../layout/page-layout"; import { docsUrl } from "../../../common/vars"; import { catalogURL } from "../+catalog"; +import logger from "../../../main/logger"; enum KubeConfigSourceTab { FILE = "file", @@ -31,7 +32,7 @@ enum KubeConfigSourceTab { @observer export class AddCluster extends React.Component { - @observable.ref kubeConfigLocal: KubeConfig; + @observable.ref kubeConfigLocal?: KubeConfig; @observable.ref error: React.ReactNode; @observable kubeContexts = observable.map(); // available contexts from kubeconfig-file or user-input @@ -79,9 +80,11 @@ export class AddCluster extends React.Component { switch (this.sourceTab) { case KubeConfigSourceTab.FILE: - const contexts = this.getContexts(this.kubeConfigLocal); + if (this.kubeConfigLocal) { + const contexts = this.getContexts(this.kubeConfigLocal); - this.kubeContexts.replace(contexts); + this.kubeContexts.replace(contexts); + } break; case KubeConfigSourceTab.TEXT: try { @@ -112,7 +115,13 @@ export class AddCluster extends React.Component { selectKubeConfigDialog = async () => { const { dialog, BrowserWindow } = remote; - const { canceled, filePaths } = await dialog.showOpenDialog(BrowserWindow.getFocusedWindow(), { + const window = BrowserWindow.getFocusedWindow(); + + if (!window) { + return void logger.warn("[ADD-CLUSTER]: No focused windows"); + } + + const { canceled, filePaths } = await dialog.showOpenDialog(window, { defaultPath: this.kubeConfigPath, properties: ["openFile", "showHiddenFiles"], message: `Select custom kubeconfig file`, @@ -131,52 +140,54 @@ export class AddCluster extends React.Component { @action addClusters = () => { - let newClusters: ClusterModel[] = []; + if (!this.selectedContexts.length) { + this.error = "Please select at least one cluster context"; + + return; + } + + this.error = ""; + this.isWaiting = true; try { - if (!this.selectedContexts.length) { - this.error = "Please select at least one cluster context"; - - return; - } - this.error = ""; - this.isWaiting = true; appEventBus.emit({ name: "cluster-add", action: "click" }); - newClusters = this.selectedContexts.filter(context => { - try { - const kubeConfig = this.kubeContexts.get(context); - validateKubeConfig(kubeConfig, context); + const newClusters = this.selectedContexts + .map(context => [context, this.kubeContexts.get(context)] as const) + .filter(SecondNotFalsy) + .filter(([context, kubeConfig]) => { + try { + validateKubeConfig(kubeConfig, context); - return true; - } catch (err) { - this.error = String(err.message); + return true; + } catch (err) { + this.error = String(err.message); - if (err instanceof ExecValidationNotFoundError) { - Notifications.error(<>Error while adding cluster(s): {this.error}); + if (err instanceof ExecValidationNotFoundError) { + Notifications.error(<>Error while adding cluster(s): {this.error}); + + return false; + } - return false; - } else { throw new Error(err); } - } - }).map(context => { - const clusterId = uuid(); - const kubeConfig = this.kubeContexts.get(context); - const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE - ? this.kubeConfigPath // save link to original kubeconfig in file-system - : ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder + }) + .map(([, kubeConfig]) => { + const clusterId = uuid(); + const kubeConfigPath = this.sourceTab === KubeConfigSourceTab.FILE + ? this.kubeConfigPath // save link to original kubeconfig in file-system + : ClusterStore.embedCustomKubeConfig(clusterId, kubeConfig); // save in app-files folder - return { - id: clusterId, - kubeConfigPath, - contextName: kubeConfig.currentContext, - preferences: { - clusterName: kubeConfig.currentContext, - httpsProxy: this.proxyServer || undefined, - }, - }; - }); + return { + id: clusterId, + kubeConfigPath, + contextName: kubeConfig.currentContext, + preferences: { + clusterName: kubeConfig.currentContext, + httpsProxy: this.proxyServer || undefined, + }, + }; + }); runInAction(() => { clusterStore.addClusters(...newClusters); diff --git a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx index d31efc5438..a5e13044d5 100644 --- a/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx +++ b/src/renderer/components/+apps-helm-charts/helm-chart-details.tsx @@ -8,7 +8,6 @@ import { Drawer, DrawerItem } from "../drawer"; import { autobind, stopPropagation } from "../../utils"; import { MarkdownViewer } from "../markdown-viewer"; import { Spinner } from "../spinner"; -import { CancelablePromise } from "../../utils/cancelableFetch"; import { Button } from "../button"; import { Select, SelectOption } from "../select"; import { createInstallChartTab } from "../dock/install-chart.store"; @@ -21,19 +20,19 @@ interface Props { @observer export class HelmChartDetails extends Component { - @observable chartVersions: HelmChart[]; - @observable selectedChart: HelmChart; - @observable readme: string = null; - @observable error: string = null; + @observable chartVersions?: HelmChart[]; + @observable selectedChart?: HelmChart; + @observable readme?: string; + @observable error?: string; - private chartPromise: CancelablePromise<{ readme: string; versions: HelmChart[] }>; + private chartPromise?: Promise<{ readme: string; versions: HelmChart[] }>; componentWillUnmount() { - this.chartPromise?.cancel(); + // this.chartPromise?.cancel(); } chartUpdater = autorun(() => { - this.selectedChart = null; + this.selectedChart = undefined; const { chart: { name, repo, version } } = this.props; helmChartsApi.get(repo, name, version).then(result => { @@ -48,11 +47,11 @@ export class HelmChartDetails extends Component { @autobind() async onVersionChange({ value: version }: SelectOption) { - this.selectedChart = this.chartVersions.find(chart => chart.version === version); - this.readme = null; + this.selectedChart = this.chartVersions?.find(chart => chart.version === version); + this.readme = undefined; try { - this.chartPromise?.cancel(); + // this.chartPromise?.cancel(); const { chart: { name, repo } } = this.props; const { readme } = await (this.chartPromise = helmChartsApi.get(repo, name, version)); @@ -64,8 +63,10 @@ export class HelmChartDetails extends Component { @autobind() install() { - createInstallChartTab(this.selectedChart); - this.props.hideDetails(); + if (this.selectedChart) { + createInstallChartTab(this.selectedChart); + this.props.hideDetails(); + } } renderIntroduction() { @@ -76,34 +77,34 @@ export class HelmChartDetails extends Component {

event.currentTarget.src = placeholder} />
- {selectedChart.getDescription()} + {selectedChart?.getDescription()}