From b27a2654e45cfd0cebd626fbad027822c4f15575 Mon Sep 17 00:00:00 2001
From: Jim Ehrismann
Date: Wed, 3 Nov 2021 11:53:54 -0400
Subject: [PATCH 1/5] Use JSON patch for editing components (#3674)
Signed-off-by: Jim Ehrismann
---
package.json | 1 +
.../k8s-api/endpoints/resource-applier.api.ts | 34 ++++---
src/common/k8s-api/kube-object.store.ts | 21 +++--
src/common/k8s-api/kube-object.ts | 59 ++++++++----
src/main/resource-applier.ts | 89 ++++++++++++++-----
src/main/router.ts | 1 +
src/main/routes/resource-applier-route.ts | 14 ++-
src/main/utils/http-responses.ts | 27 +++++-
.../+config-maps/config-map-details.tsx | 2 +
.../deployment-scale-dialog.test.tsx | 1 +
.../replicaset-scale-dialog.test.tsx | 1 +
.../statefulset-scale-dialog.test.tsx | 1 +
.../components/dock/create-resource.tsx | 48 +++++-----
.../components/dock/edit-resource.store.ts | 5 ++
.../components/dock/edit-resource.tsx | 26 +++---
src/renderer/components/dock/editor-panel.tsx | 5 +-
.../kube-object-menu/kube-object-menu.tsx | 8 +-
yarn.lock | 5 ++
18 files changed, 242 insertions(+), 106 deletions(-)
diff --git a/package.json b/package.json
index af46679ade..3321d17a34 100644
--- a/package.json
+++ b/package.json
@@ -237,6 +237,7 @@
"readable-stream": "^3.6.0",
"request": "^2.88.2",
"request-promise-native": "^1.0.9",
+ "rfc6902": "^4.0.2",
"semver": "^7.3.2",
"serializr": "^2.0.5",
"shell-env": "^3.0.1",
diff --git a/src/common/k8s-api/endpoints/resource-applier.api.ts b/src/common/k8s-api/endpoints/resource-applier.api.ts
index b6a35d3a60..14b5d5ad46 100644
--- a/src/common/k8s-api/endpoints/resource-applier.api.ts
+++ b/src/common/k8s-api/endpoints/resource-applier.api.ts
@@ -22,19 +22,27 @@
import jsYaml from "js-yaml";
import type { KubeJsonApiData } from "../kube-json-api";
import { apiBase } from "../index";
+import type { Patch } from "rfc6902";
-export const resourceApplierApi = {
- annotations: [
- "kubectl.kubernetes.io/last-applied-configuration"
- ],
+export const annotations = [
+ "kubectl.kubernetes.io/last-applied-configuration"
+];
- async update(resource: object | string): Promise {
- if (typeof resource === "string") {
- resource = jsYaml.safeLoad(resource);
- }
-
- const [data = null] = await apiBase.post("/stack", { data: resource });
-
- return data;
+export async function update(resource: object | string): Promise {
+ if (typeof resource === "string") {
+ resource = jsYaml.safeLoad(resource);
}
-};
+
+ return apiBase.post("/stack", { data: resource });
+}
+
+export async function patch(name: string, kind: string, ns: string, patch: Patch): Promise {
+ return apiBase.patch("/stack", {
+ data: {
+ name,
+ kind,
+ ns,
+ patch,
+ },
+ });
+}
diff --git a/src/common/k8s-api/kube-object.store.ts b/src/common/k8s-api/kube-object.store.ts
index f90eaab8e5..7c8118da1f 100644
--- a/src/common/k8s-api/kube-object.store.ts
+++ b/src/common/k8s-api/kube-object.store.ts
@@ -32,6 +32,7 @@ import { parseKubeApi } from "./kube-api-parse";
import type { KubeJsonApiData } from "./kube-json-api";
import type { RequestInit } from "node-fetch";
import AbortController from "abort-controller";
+import type { Patch } from "rfc6902";
export interface KubeObjectStoreLoadingParams {
namespaces: string[];
@@ -279,19 +280,29 @@ export abstract class KubeObjectStore extends ItemStore
return newItem;
}
- async update(item: T, data: Partial): Promise {
- const rawItem = await item.update(data);
+ private postUpdate(rawItem: KubeJsonApiData): T {
const newItem = new this.api.objectConstructor(rawItem);
+ const index = this.items.findIndex(item => item.getId() === newItem.getId());
ensureObjectSelfLink(this.api, newItem);
- const index = this.items.findIndex(item => item.getId() === newItem.getId());
-
- this.items.splice(index, 1, newItem);
+ if (index < 0) {
+ this.items.push(newItem);
+ } else {
+ this.items[index] = newItem;
+ }
return newItem;
}
+ async patch(item: T, patch: Patch): Promise {
+ return this.postUpdate(await item.patch(patch));
+ }
+
+ async update(item: T, data: Partial): Promise {
+ return this.postUpdate(await item.update(data));
+ }
+
async remove(item: T) {
await item.delete();
this.items.remove(item);
diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts
index c1e40eb2e5..bcbe6bf05f 100644
--- a/src/common/k8s-api/kube-object.ts
+++ b/src/common/k8s-api/kube-object.ts
@@ -27,9 +27,9 @@ import { autoBind, formatDuration } from "../utils";
import type { ItemObject } from "../item.store";
import { apiKube } from "./index";
import type { JsonApiParams } from "./json-api";
-import { resourceApplierApi } from "./endpoints/resource-applier.api";
+import * as resourceApplierApi from "./endpoints/resource-applier.api";
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
-import _ from "lodash";
+import type { Patch } from "rfc6902";
export type KubeObjectConstructor = (new (data: KubeJsonApiData | any) => K) & {
kind?: string;
@@ -191,18 +191,28 @@ export class KubeObject `${name}=${value}`);
}
- protected static readonly nonEditableFields = [
- "apiVersion",
- "kind",
- "metadata.name",
- "metadata.selfLink",
- "metadata.resourceVersion",
- "metadata.uid",
- "managedFields",
- "status",
+ /**
+ * These must be RFC6902 compliant paths
+ */
+ private static readonly nonEditiablePathPrefixes = [
+ "/metadata/managedFields",
+ "/status",
];
+ private static readonly nonEditablePaths = new Set([
+ "/apiVersion",
+ "/kind",
+ "/metadata/name",
+ "/metadata/selfLink",
+ "/metadata/resourceVersion",
+ "/metadata/uid",
+ ...KubeObject.nonEditiablePathPrefixes,
+ ]);
constructor(data: KubeJsonApiData) {
+ if (typeof data !== "object") {
+ throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
+ }
+
Object.assign(this, data);
autoBind(this);
}
@@ -286,14 +296,31 @@ export class KubeObject): Promise {
- for (const field of KubeObject.nonEditableFields) {
- if (!_.isEqual(_.get(this, field), _.get(data, field))) {
- throw new Error(`Failed to update Kube Object: ${field} has been modified`);
+ async patch(patch: Patch): Promise {
+ for (const op of patch) {
+ if (KubeObject.nonEditablePaths.has(op.path)) {
+ throw new Error(`Failed to update ${this.kind}: JSON pointer ${op.path} has been modified`);
+ }
+
+ for (const pathPrefix of KubeObject.nonEditiablePathPrefixes) {
+ if (op.path.startsWith(`${pathPrefix}/`)) {
+ throw new Error(`Failed to update ${this.kind}: Child JSON pointer of ${op.path} has been modified`);
+ }
}
}
+ return resourceApplierApi.patch(this.getName(), this.kind, this.getNs(), patch);
+ }
+
+ /**
+ * Perform a full update (or more specifically a replace)
+ *
+ * Note: this is brittle if `data` is not actually partial (but instead whole).
+ * As fields such as `resourceVersion` will probably out of date. This is a
+ * common race condition.
+ */
+ async update(data: Partial): Promise {
+ // use unified resource-applier api for updating all k8s objects
return resourceApplierApi.update({
...this.toPlainObject(),
...data,
diff --git a/src/main/resource-applier.ts b/src/main/resource-applier.ts
index f4772c5275..a305ff1091 100644
--- a/src/main/resource-applier.ts
+++ b/src/main/resource-applier.ts
@@ -22,55 +22,96 @@
import type { Cluster } from "./cluster";
import type { KubernetesObject } from "@kubernetes/client-node";
import { exec } from "child_process";
-import fs from "fs";
+import fs from "fs-extra";
import * as yaml from "js-yaml";
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 type { Patch } from "rfc6902";
+import { promiseExecFile } from "./promise-exec";
export class ResourceApplier {
- constructor(protected cluster: Cluster) {
+ constructor(protected cluster: Cluster) {}
+
+ /**
+ * Patch a kube resource's manifest, throwing any error that occurs.
+ * @param name The name of the kube resource
+ * @param kind The kind of the kube resource
+ * @param patch The list of JSON operations
+ * @param ns The optional namespace of the kube resource
+ */
+ async patch(name: string, kind: string, patch: Patch, ns?: string): Promise {
+ appEventBus.emit({ name: "resource", action: "patch" });
+
+ const kubectl = await this.cluster.ensureKubectl();
+ const kubectlPath = await kubectl.getPath();
+ const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
+ const args = [
+ "--kubeconfig", proxyKubeconfigPath,
+ "patch",
+ kind,
+ name,
+ ];
+
+ if (ns) {
+ args.push("--namespace", ns);
+ }
+
+ args.push(
+ "--type", "json",
+ "--patch", JSON.stringify(patch),
+ "-o", "json"
+ );
+
+ try {
+ const { stdout } = await promiseExecFile(kubectlPath, args);
+
+ return stdout;
+ } catch (error) {
+ throw error.stderr ?? error;
+ }
}
async apply(resource: KubernetesObject | any): Promise {
resource = this.sanitizeObject(resource);
appEventBus.emit({ name: "resource", action: "apply" });
- return await this.kubectlApply(yaml.safeDump(resource));
+ return this.kubectlApply(yaml.safeDump(resource));
}
protected async kubectlApply(content: string): Promise {
const kubectl = await this.cluster.ensureKubectl();
const kubectlPath = await kubectl.getPath();
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
+ const fileName = tempy.file({ name: "resource.yaml" });
+ const args = [
+ "apply",
+ "--kubeconfig", proxyKubeconfigPath,
+ "-o", "json",
+ "-f", fileName,
+ ];
- return new Promise((resolve, reject) => {
- const fileName = tempy.file({ name: "resource.yaml" });
+ logger.debug(`shooting manifests with ${kubectlPath}`, { args });
- fs.writeFileSync(fileName, content);
- const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${fileName}"`;
+ const execEnv = { ...process.env };
+ const httpsProxy = this.cluster.preferences?.httpsProxy;
- logger.debug(`shooting manifests with: ${cmd}`);
- const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
- const httpsProxy = this.cluster.preferences?.httpsProxy;
+ if (httpsProxy) {
+ execEnv.HTTPS_PROXY = httpsProxy;
+ }
- if (httpsProxy) {
- execEnv["HTTPS_PROXY"] = httpsProxy;
- }
- exec(cmd, { env: execEnv },
- (error, stdout, stderr) => {
- if (stderr != "") {
- fs.unlinkSync(fileName);
- reject(stderr);
+ try {
+ await fs.writeFile(fileName, content);
+ const { stdout } = await promiseExecFile(kubectlPath, args);
- return;
- }
- fs.unlinkSync(fileName);
- resolve(JSON.parse(stdout));
- });
- });
+ return stdout;
+ } catch (error) {
+ throw error?.stderr ?? error;
+ } finally {
+ await fs.unlink(fileName);
+ }
}
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise {
diff --git a/src/main/router.ts b/src/main/router.ts
index 50175aa9de..d758cc2314 100644
--- a/src/main/router.ts
+++ b/src/main/router.ts
@@ -198,5 +198,6 @@ export class Router {
// Resource Applier API
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
+ this.router.add({ method: "patch", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.patchResource);
}
}
diff --git a/src/main/routes/resource-applier-route.ts b/src/main/routes/resource-applier-route.ts
index cb716cf1f5..0adf916188 100644
--- a/src/main/routes/resource-applier-route.ts
+++ b/src/main/routes/resource-applier-route.ts
@@ -30,7 +30,19 @@ export class ResourceApplierApiRoute {
try {
const resource = await new ResourceApplier(cluster).apply(payload);
- respondJson(response, [resource], 200);
+ respondJson(response, resource, 200);
+ } catch (error) {
+ respondText(response, error, 422);
+ }
+ }
+
+ static async patchResource(request: LensApiRequest) {
+ const { response, cluster, payload } = request;
+
+ try {
+ const resource = await new ResourceApplier(cluster).patch(payload.name, payload.kind, payload.patch, payload.ns);
+
+ respondJson(response, resource, 200);
} catch (error) {
respondText(response, error, 422);
}
diff --git a/src/main/utils/http-responses.ts b/src/main/utils/http-responses.ts
index 36287806f1..5d57b6dd76 100644
--- a/src/main/utils/http-responses.ts
+++ b/src/main/utils/http-responses.ts
@@ -21,14 +21,37 @@
import type http from "http";
-export function respondJson(res: http.ServerResponse, content: any, status = 200) {
- respond(res, JSON.stringify(content), "application/json", status);
+/**
+ * Respond to a HTTP request with a body of JSON data
+ * @param res The HTTP response to write data to
+ * @param content The data or its JSON stringified version of it
+ * @param status [200] The status code to respond with
+ */
+export function respondJson(res: http.ServerResponse, content: Object | string, status = 200) {
+ const normalizedContent = typeof content === "object"
+ ? JSON.stringify(content)
+ : content;
+
+ respond(res, normalizedContent, "application/json", status);
}
+/**
+ * Respond to a HTTP request with a body of plain text data
+ * @param res The HTTP response to write data to
+ * @param content The string data to respond with
+ * @param status [200] The status code to respond with
+ */
export function respondText(res: http.ServerResponse, content: string, status = 200) {
respond(res, content, "text/plain", status);
}
+/**
+ * Respond to a HTTP request with a body of plain text data
+ * @param res The HTTP response to write data to
+ * @param content The string data to respond with
+ * @param contentType The HTTP Content-Type header value
+ * @param status [200] The status code to respond with
+ */
export function respond(res: http.ServerResponse, content: string, contentType: string, status = 200) {
res.setHeader("Content-Type", contentType);
res.statusCode = status;
diff --git a/src/renderer/components/+config-maps/config-map-details.tsx b/src/renderer/components/+config-maps/config-map-details.tsx
index abbd31c97a..6150aa1ad9 100644
--- a/src/renderer/components/+config-maps/config-map-details.tsx
+++ b/src/renderer/components/+config-maps/config-map-details.tsx
@@ -72,6 +72,8 @@ export class ConfigMapDetails extends React.Component {
<>ConfigMap {configMap.getName()} successfully updated.>
);
+ } catch (error) {
+ Notifications.error(`Failed to save config map: ${error}`);
} finally {
this.isSaving = false;
}
diff --git a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx
index 22c90dda2c..3bc22b8263 100644
--- a/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx
+++ b/src/renderer/components/+workloads-deployments/deployment-scale-dialog.test.tsx
@@ -112,6 +112,7 @@ const dummyDeployment: Deployment = {
toPlainObject: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
+ patch: jest.fn(),
};
describe("", () => {
diff --git a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx
index 7f8c31e99c..e948d3a339 100755
--- a/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx
+++ b/src/renderer/components/+workloads-replicasets/replicaset-scale-dialog.test.tsx
@@ -107,6 +107,7 @@ const dummyReplicaSet: ReplicaSet = {
toPlainObject: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
+ patch: jest.fn(),
};
describe("", () => {
diff --git a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx
index b27d8b0892..016d431eb2 100755
--- a/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx
+++ b/src/renderer/components/+workloads-statefulsets/statefulset-scale-dialog.test.tsx
@@ -117,6 +117,7 @@ const dummyStatefulSet: StatefulSet = {
toPlainObject: jest.fn(),
update: jest.fn(),
delete: jest.fn(),
+ patch: jest.fn(),
};
describe("", () => {
diff --git a/src/renderer/components/dock/create-resource.tsx b/src/renderer/components/dock/create-resource.tsx
index a357d0dc62..c7de411e3c 100644
--- a/src/renderer/components/dock/create-resource.tsx
+++ b/src/renderer/components/dock/create-resource.tsx
@@ -33,10 +33,10 @@ import { createResourceStore } from "./create-resource.store";
import type { DockTab } from "./dock.store";
import { EditorPanel } from "./editor-panel";
import { InfoPanel } from "./info-panel";
-import { resourceApplierApi } from "../../../common/k8s-api/endpoints/resource-applier.api";
-import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
+import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api";
import { Notifications } from "../notifications";
import { monacoModelsManager } from "./monaco-model-manager";
+import logger from "../../../common/logger";
interface Props {
className?: string;
@@ -95,7 +95,7 @@ export class CreateResource extends React.Component {
});
};
- create = async () => {
+ create = async (): Promise => {
if (this.error || !this.data.trim()) {
// do not save when field is empty or there is an error
return null;
@@ -103,31 +103,31 @@ export class CreateResource extends React.Component {
// skip empty documents if "---" pasted at the beginning or end
const resources = jsYaml.safeLoadAll(this.data).filter(Boolean);
- const createdResources: string[] = [];
- const errors: string[] = [];
- await Promise.all(
- resources.map(data => {
- return resourceApplierApi.update(data)
- .then(item => createdResources.push(item.metadata.name))
- .catch((err: JsonApiErrorParsed) => errors.push(err.toString()));
- })
- );
-
- if (errors.length) {
- errors.forEach(error => Notifications.error(error));
- if (!createdResources.length) throw errors[0];
+ if (resources.length === 0) {
+ return void logger.info("Nothing to create");
}
- const successMessage = (
-
- {createdResources.length === 1 ? "Resource" : "Resources"}{" "}
- {createdResources.join(", ")} successfully created
-
- );
- Notifications.ok(successMessage);
+ const createdResources: string[] = [];
- return successMessage;
+ for (const result of await Promise.allSettled(resources.map(resourceApplierApi.update))) {
+ if (result.status === "fulfilled") {
+ createdResources.push(result.value.metadata.name);
+ } else {
+ Notifications.error(result.reason.toString());
+ }
+ }
+
+ if (createdResources.length > 0) {
+ Notifications.ok((
+
+ {createdResources.length === 1 ? "Resource" : "Resources"}{" "}
+ {createdResources.join(", ")} successfully created
+
+ ));
+ }
+
+ return undefined;
};
renderControls(){
diff --git a/src/renderer/components/dock/edit-resource.store.ts b/src/renderer/components/dock/edit-resource.store.ts
index b5be580313..d8b6d06d6b 100644
--- a/src/renderer/components/dock/edit-resource.store.ts
+++ b/src/renderer/components/dock/edit-resource.store.ts
@@ -31,6 +31,7 @@ import {monacoModelsManager} from "./monaco-model-manager";
export interface EditingResource {
resource: string; // resource path, e.g. /api/v1/namespaces/default
draft?: string; // edited draft in yaml
+ firstDraft?: string;
}
export class EditResourceStore extends DockTabStore {
@@ -106,6 +107,10 @@ export class EditResourceStore extends DockTabStore {
return dockStore.getTabById(tabId);
}
+ clearInitialDraft(tabId: TabId): void {
+ delete this.getData(tabId)?.firstDraft;
+ }
+
reset() {
super.reset();
Array.from(this.watchers).forEach(([tabId, dispose]) => {
diff --git a/src/renderer/components/dock/edit-resource.tsx b/src/renderer/components/dock/edit-resource.tsx
index 19a238ab30..068855eebb 100644
--- a/src/renderer/components/dock/edit-resource.tsx
+++ b/src/renderer/components/dock/edit-resource.tsx
@@ -24,7 +24,7 @@ import "./edit-resource.scss";
import React from "react";
import { action, computed, makeObservable, observable } from "mobx";
import { observer } from "mobx-react";
-import jsYaml from "js-yaml";
+import yaml from "js-yaml";
import type { DockTab } from "./dock.store";
import { cssNames } from "../../utils";
import { editResourceStore } from "./edit-resource.store";
@@ -33,6 +33,7 @@ import { Badge } from "../badge";
import { EditorPanel } from "./editor-panel";
import { Spinner } from "../spinner";
import type { KubeObject } from "../../../common/k8s-api/kube-object";
+import { createPatch } from "rfc6902";
interface Props {
className?: string;
@@ -71,16 +72,17 @@ export class EditResource extends React.Component {
return draft;
}
- return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time
+ return yaml.safeDump(this.resource.toPlainObject()); // dump resource first time
}
@action
saveDraft(draft: string | object) {
if (typeof draft === "object") {
- draft = draft ? jsYaml.safeDump(draft) : undefined;
+ draft = draft ? yaml.safeDump(draft) : undefined;
}
editResourceStore.setData(this.tabId, {
+ firstDraft: draft, // this must be before the next line
...editResourceStore.getData(this.tabId),
draft,
});
@@ -95,16 +97,18 @@ export class EditResource extends React.Component {
if (this.error) {
return null;
}
- const store = editResourceStore.getStore(this.tabId);
- const updatedResource: KubeObject = await store.update(this.resource, jsYaml.safeLoad(this.draft));
- this.saveDraft(updatedResource.toPlainObject()); // update with new resourceVersion to avoid further errors on save
- const resourceType = updatedResource.kind;
- const resourceName = updatedResource.getName();
+ const store = editResourceStore.getStore(this.tabId);
+ const currentVersion = yaml.safeLoad(this.draft);
+ const firstVersion = yaml.safeLoad(editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
+ const patches = createPatch(firstVersion, currentVersion);
+ const updatedResource = await store.patch(this.resource, patches);
+
+ editResourceStore.clearInitialDraft(this.tabId);
return (
- {resourceType} {resourceName} updated.
+ {updatedResource.kind} {updatedResource.getName()} updated.
);
};
@@ -126,9 +130,9 @@ export class EditResource extends React.Component {
submittingMessage="Applying.."
controls={(
- Kind:
+ Kind:
Name:
- Namespace:
+ Namespace:
)}
/>
diff --git a/src/renderer/components/dock/editor-panel.tsx b/src/renderer/components/dock/editor-panel.tsx
index 31ccc5b2e2..bbf0b96e53 100644
--- a/src/renderer/components/dock/editor-panel.tsx
+++ b/src/renderer/components/dock/editor-panel.tsx
@@ -91,10 +91,7 @@ export class EditorPanel extends React.Component {
onChange = (value: string) => {
this.validate(value);
-
- if (this.props.onChange) {
- this.props.onChange(value, this.yamlError);
- }
+ this.props.onChange?.(value, this.yamlError);
};
render() {
diff --git a/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/src/renderer/components/kube-object-menu/kube-object-menu.tsx
index 1848fa2906..ac2a99cc88 100644
--- a/src/renderer/components/kube-object-menu/kube-object-menu.tsx
+++ b/src/renderer/components/kube-object-menu/kube-object-menu.tsx
@@ -44,15 +44,11 @@ export class KubeObjectMenu extends React.Component
Date: Thu, 7 Oct 2021 10:09:21 -0400
Subject: [PATCH 2/5] Catch metadata being undefined at KubeObject creation
(#3960)
Signed-off-by: Jim Ehrismann
---
src/common/k8s-api/kube-object.ts | 14 +++-
.../dock/__test__/log-tab.store.test.ts | 13 ++--
src/renderer/components/dock/dock.store.ts | 14 ++--
src/renderer/components/dock/log-tab.store.ts | 66 ++++++++++---------
4 files changed, 59 insertions(+), 48 deletions(-)
diff --git a/src/common/k8s-api/kube-object.ts b/src/common/k8s-api/kube-object.ts
index bcbe6bf05f..0cb4ac15f6 100644
--- a/src/common/k8s-api/kube-object.ts
+++ b/src/common/k8s-api/kube-object.ts
@@ -98,6 +98,12 @@ export interface KubeObjectStatus {
export type KubeMetaField = keyof KubeObjectMetadata;
+export class KubeCreationError extends Error {
+ constructor(message: string, public data: any) {
+ super(message);
+ }
+}
+
export class KubeObject implements ItemObject {
static readonly kind: string;
static readonly namespaced: boolean;
@@ -209,10 +215,14 @@ export class KubeObject ({ ...ownerRef, namespace }));
diff --git a/src/renderer/components/dock/__test__/log-tab.store.test.ts b/src/renderer/components/dock/__test__/log-tab.store.test.ts
index 8eea8ce970..5221c1a8c0 100644
--- a/src/renderer/components/dock/__test__/log-tab.store.test.ts
+++ b/src/renderer/components/dock/__test__/log-tab.store.test.ts
@@ -25,9 +25,11 @@ import { Pod } from "../../../../common/k8s-api/endpoints";
import { ThemeStore } from "../../../theme.store";
import { dockStore } from "../dock.store";
import { logTabStore } from "../log-tab.store";
-import { TerminalStore } from "../terminal.store";
import { deploymentPod1, deploymentPod2, deploymentPod3, dockerPod } from "./pod.mock";
import fse from "fs-extra";
+import { mockWindow } from "../../../../../__mocks__/windowMock";
+
+mockWindow();
jest.mock("react-monaco-editor", () => null);
@@ -45,7 +47,6 @@ describe("log tab store", () => {
beforeEach(() => {
UserStore.createInstance();
ThemeStore.createInstance();
- TerminalStore.createInstance();
});
afterEach(() => {
@@ -53,7 +54,6 @@ describe("log tab store", () => {
dockStore.reset();
UserStore.resetInstance();
ThemeStore.resetInstance();
- TerminalStore.resetInstance();
fse.remove("tmp");
});
@@ -135,11 +135,11 @@ describe("log tab store", () => {
});
// FIXME: this is failed when it's not .only == depends on something above
- it.only("closes tab if no pods left in store", () => {
+ it.only("closes tab if no pods left in store", async () => {
const selectedPod = new Pod(deploymentPod1);
const selectedContainer = selectedPod.getInitContainers()[0];
- logTabStore.createPodTab({
+ const id = logTabStore.createPodTab({
selectedPod,
selectedContainer
});
@@ -147,6 +147,7 @@ describe("log tab store", () => {
podsStore.items.clear();
expect(logTabStore.getData(dockStore.selectedTabId)).toBeUndefined();
- expect(dockStore.getTabById(dockStore.selectedTabId)).toBeUndefined();
+ expect(logTabStore.getData(id)).toBeUndefined();
+ expect(dockStore.getTabById(id)).toBeUndefined();
});
});
diff --git a/src/renderer/components/dock/dock.store.ts b/src/renderer/components/dock/dock.store.ts
index 7551de1a2f..f2fd74a064 100644
--- a/src/renderer/components/dock/dock.store.ts
+++ b/src/renderer/components/dock/dock.store.ts
@@ -160,7 +160,7 @@ export class DockStore implements DockStorageState {
window.addEventListener("resize", throttle(this.adjustHeight, 250));
// create monaco models
this.whenReady.then(() => {this.tabs.forEach(tab => {
- if (this.usesMonacoEditor(tab)) {
+ if (this.usesMonacoEditor(tab)) {
monacoModelsManager.addModel(tab.id);
}
});});
@@ -274,7 +274,7 @@ export class DockStore implements DockStorageState {
title
};
- // add monaco model
+ // add monaco model
if (this.usesMonacoEditor(tab)) {
monacoModelsManager.addModel(id);
}
@@ -287,14 +287,14 @@ export class DockStore implements DockStorageState {
}
@action
- async closeTab(tabId: TabId) {
+ closeTab(tabId: TabId) {
const tab = this.getTabById(tabId);
if (!tab || tab.pinned) {
return;
}
- // remove monaco model
+ // remove monaco model
if (this.usesMonacoEditor(tab)) {
monacoModelsManager.removeModel(tabId);
}
@@ -305,12 +305,6 @@ export class DockStore implements DockStorageState {
if (this.tabs.length) {
const newTab = this.tabs.slice(-1)[0]; // last
- if (newTab?.kind === TabKind.TERMINAL) {
- // close the dock when selected sibling inactive terminal tab
- const { TerminalStore } = await import("./terminal.store");
-
- if (!TerminalStore.getInstance(false)?.isConnected(newTab.id)) this.close();
- }
this.selectTab(newTab.id);
} else {
this.selectedTabId = null;
diff --git a/src/renderer/components/dock/log-tab.store.ts b/src/renderer/components/dock/log-tab.store.ts
index 5b6b73904f..d28410a8ad 100644
--- a/src/renderer/components/dock/log-tab.store.ts
+++ b/src/renderer/components/dock/log-tab.store.ts
@@ -25,6 +25,7 @@ import { podsStore } from "../+workloads-pods/pods.store";
import { IPodContainer, Pod } from "../../../common/k8s-api/endpoints";
import type { WorkloadKubeObject } from "../../../common/k8s-api/workload-kube-object";
+import logger from "../../../common/logger";
import { DockTabStore } from "./dock-tab.store";
import { dockStore, DockTabCreateSpecific, TabKind } from "./dock.store";
@@ -51,17 +52,15 @@ export class LogTabStore extends DockTabStore {
storageKey: "pod_logs"
});
- reaction(() => podsStore.items.length, () => {
- this.updateTabsData();
- });
+ reaction(() => podsStore.items.length, () => this.updateTabsData());
}
- createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): void {
+ createPodTab({ selectedPod, selectedContainer }: PodLogsTabData): string {
const podOwner = selectedPod.getOwnerRefs()[0];
const pods = podsStore.getPodsByOwnerId(podOwner?.uid);
const title = `Pod ${selectedPod.getName()}`;
- this.createLogsTab(title, {
+ return this.createLogsTab(title, {
pods: pods.length ? pods : [selectedPod],
selectedPod,
selectedContainer
@@ -95,9 +94,9 @@ export class LogTabStore extends DockTabStore {
...tabParams,
kind: TabKind.POD_LOGS,
}, false);
- }
+ }
- private createLogsTab(title: string, data: LogTabData) {
+ private createLogsTab(title: string, data: LogTabData): string {
const id = uniqueId("log-tab-");
this.createDockTab({ id, title });
@@ -106,38 +105,45 @@ export class LogTabStore extends DockTabStore {
showTimestamps: false,
previous: false
});
+
+ return id;
}
- private async updateTabsData() {
- const promises: Promise[] = [];
-
+ private updateTabsData() {
for (const [tabId, tabData] of this.data) {
- const pod = new Pod(tabData.selectedPod);
- const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
- const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
- const selectedPod = isSelectedPodInList ? pod : pods[0];
- const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0];
+ try {
+ if (!tabData.selectedPod) {
+ tabData.selectedPod = tabData.pods[0];
+ }
- if (pods.length) {
- this.setData(tabId, {
- ...tabData,
- selectedPod,
- selectedContainer,
- pods
- });
-
- this.renameTab(tabId);
- } else {
- promises.push(this.closeTab(tabId));
+ const pod = new Pod(tabData.selectedPod);
+ const pods = podsStore.getPodsByOwnerId(pod.getOwnerRefs()[0]?.uid);
+ const isSelectedPodInList = pods.find(item => item.getId() == pod.getId());
+ const selectedPod = isSelectedPodInList ? pod : pods[0];
+ const selectedContainer = isSelectedPodInList ? tabData.selectedContainer : pod.getAllContainers()[0];
+
+ if (pods.length > 0) {
+ this.setData(tabId, {
+ ...tabData,
+ selectedPod,
+ selectedContainer,
+ pods
+ });
+
+ this.renameTab(tabId);
+ } else {
+ this.closeTab(tabId);
+ }
+ } catch (error) {
+ logger.error(`[LOG-TAB-STORE]: failed to set data for tabId=${tabId} deleting`, error,);
+ this.data.delete(tabId);
}
}
-
- await Promise.all(promises);
}
- private async closeTab(tabId: string) {
+ private closeTab(tabId: string) {
this.clearData(tabId);
- await dockStore.closeTab(tabId);
+ dockStore.closeTab(tabId);
}
}
From 9782c37d11f4f6e980e85cdc2e44309e5505a253 Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Tue, 12 Oct 2021 07:31:11 -0400
Subject: [PATCH 3/5] Use the served and storage version of a CRD instead of
the first (#3999)
Signed-off-by: Jim Ehrismann
---
src/common/k8s-api/__tests__/crd.test.ts | 144 +++++++++++++----------
src/common/k8s-api/endpoints/crd.api.ts | 86 +++++++++++---
src/common/k8s-api/json-api.ts | 3 +-
src/renderer/utils/createStorage.ts | 5 +-
4 files changed, 152 insertions(+), 86 deletions(-)
diff --git a/src/common/k8s-api/__tests__/crd.test.ts b/src/common/k8s-api/__tests__/crd.test.ts
index 459a7f67d9..a981cdda12 100644
--- a/src/common/k8s-api/__tests__/crd.test.ts
+++ b/src/common/k8s-api/__tests__/crd.test.ts
@@ -20,92 +20,108 @@
*/
import { CustomResourceDefinition } from "../endpoints";
-import type { KubeObjectMetadata } from "../kube-object";
describe("Crds", () => {
describe("getVersion", () => {
- it("should get the first version name from the list of versions", () => {
+ it("should throw if none of the versions are served", () => {
const crd = new CustomResourceDefinition({
- apiVersion: "foo",
+ apiVersion: "apiextensions.k8s.io/v1",
kind: "CustomResourceDefinition",
- metadata: {} as KubeObjectMetadata,
+ metadata: {
+ name: "foo",
+ resourceVersion: "12345",
+ uid: "12345",
+ },
+ spec: {
+ versions: [
+ {
+ name: "123",
+ served: false,
+ storage: false,
+ },
+ {
+ name: "1234",
+ served: false,
+ storage: false,
+ },
+ ],
+ },
});
- crd.spec = {
- versions: [
- {
- name: "123",
- served: false,
- storage: false,
- }
- ]
- } as any;
+ expect(() => crd.getVersion()).toThrowError("Failed to find a version for CustomResourceDefinition foo");
+ });
+
+ it("should should get the version that is both served and stored", () => {
+ const crd = new CustomResourceDefinition({
+ apiVersion: "apiextensions.k8s.io/v1",
+ kind: "CustomResourceDefinition",
+ metadata: {
+ name: "foo",
+ resourceVersion: "12345",
+ uid: "12345",
+ },
+ spec: {
+ versions: [
+ {
+ name: "123",
+ served: true,
+ storage: true,
+ },
+ {
+ name: "1234",
+ served: false,
+ storage: false,
+ },
+ ],
+ },
+ });
expect(crd.getVersion()).toBe("123");
});
- it("should get the first version name from the list of versions (length 2)", () => {
+ it("should should get the version that is both served and stored even with version field", () => {
const crd = new CustomResourceDefinition({
- apiVersion: "foo",
+ apiVersion: "apiextensions.k8s.io/v1",
kind: "CustomResourceDefinition",
- metadata: {} as KubeObjectMetadata,
+ metadata: {
+ name: "foo",
+ resourceVersion: "12345",
+ uid: "12345",
+ },
+ spec: {
+ version: "abc",
+ versions: [
+ {
+ name: "123",
+ served: true,
+ storage: true,
+ },
+ {
+ name: "1234",
+ served: false,
+ storage: false,
+ },
+ ],
+ },
});
- crd.spec = {
- versions: [
- {
- name: "123",
- served: false,
- storage: false,
- },
- {
- name: "1234",
- served: false,
- storage: false,
- }
- ]
- } as any;
-
expect(crd.getVersion()).toBe("123");
});
- it("should get the first version name from the list of versions (length 2) even with version field", () => {
+ it("should get the version name from the version field", () => {
const crd = new CustomResourceDefinition({
- apiVersion: "foo",
+ apiVersion: "apiextensions.k8s.io/v1beta1",
kind: "CustomResourceDefinition",
- metadata: {} as KubeObjectMetadata,
+ metadata: {
+ name: "foo",
+ resourceVersion: "12345",
+ uid: "12345",
+ },
+ spec: {
+ version: "abc",
+ }
});
- crd.spec = {
- version: "abc",
- versions: [
- {
- name: "123",
- served: false,
- storage: false,
- },
- {
- name: "1234",
- served: false,
- storage: false,
- }
- ]
- } as any;
-
- expect(crd.getVersion()).toBe("123");
- });
-
- it("should get the first version name from the version field", () => {
- const crd = new CustomResourceDefinition({
- apiVersion: "foo",
- kind: "CustomResourceDefinition",
- metadata: {} as KubeObjectMetadata,
- });
-
- crd.spec = {
- version: "abc"
- } as any;
-
expect(crd.getVersion()).toBe("abc");
});
});
diff --git a/src/common/k8s-api/endpoints/crd.api.ts b/src/common/k8s-api/endpoints/crd.api.ts
index b1ab179cc4..98c8d38df0 100644
--- a/src/common/k8s-api/endpoints/crd.api.ts
+++ b/src/common/k8s-api/endpoints/crd.api.ts
@@ -19,10 +19,11 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
-import { KubeObject } from "../kube-object";
+import { KubeCreationError, KubeObject } from "../kube-object";
import { KubeApi } from "../kube-api";
import { crdResourcesURL } from "../../routes";
import { isClusterPageContext } from "../../utils/cluster-id-url-parsing";
+import type { KubeJsonApiData } from "../kube-json-api";
type AdditionalPrinterColumnsCommon = {
name: string;
@@ -39,10 +40,21 @@ type AdditionalPrinterColumnsV1Beta = AdditionalPrinterColumnsCommon & {
JSONPath: string;
};
+export interface CRDVersion {
+ name: string;
+ served: boolean;
+ storage: boolean;
+ schema?: object; // required in v1 but not present in v1beta
+ additionalPrinterColumns?: AdditionalPrinterColumnsV1[];
+}
+
export interface CustomResourceDefinition {
spec: {
group: string;
- version?: string; // deprecated in v1 api
+ /**
+ * @deprecated for apiextensions.k8s.io/v1 but used previously
+ */
+ version?: string;
names: {
plural: string;
singular: string;
@@ -50,19 +62,19 @@ export interface CustomResourceDefinition {
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[]
- }[];
+ /**
+ * @deprecated for apiextensions.k8s.io/v1 but used previously
+ */
+ validation?: object;
+ versions?: CRDVersion[];
conversion: {
strategy?: string;
webhook?: any;
};
- additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[]; // removed in v1
+ /**
+ * @deprecated for apiextensions.k8s.io/v1 but used previously
+ */
+ additionalPrinterColumns?: AdditionalPrinterColumnsV1Beta[];
};
status: {
conditions: {
@@ -83,11 +95,23 @@ export interface CustomResourceDefinition {
};
}
+export interface CRDApiData extends KubeJsonApiData {
+ spec: object; // TODO: make better
+}
+
export class CustomResourceDefinition extends KubeObject {
static kind = "CustomResourceDefinition";
static namespaced = false;
static apiBase = "/apis/apiextensions.k8s.io/v1/customresourcedefinitions";
+ constructor(data: CRDApiData) {
+ super(data);
+
+ if (!data.spec || typeof data.spec !== "object") {
+ throw new KubeCreationError("Cannot create a CustomResourceDefinition from an object without spec", data);
+ }
+ }
+
getResourceUrl() {
return crdResourcesURL({
params: {
@@ -125,9 +149,36 @@ export class CustomResourceDefinition extends KubeObject {
return this.spec.scope;
}
+ getPreferedVersion(): CRDVersion {
+ // Prefer the modern `versions` over the legacy `version`
+ if (this.spec.versions) {
+ for (const version of this.spec.versions) {
+ /**
+ * If the version is not served then 404 errors will occur
+ * We should also prefer the storage version
+ */
+ if (version.served && version.storage) {
+ return version;
+ }
+ }
+ } else if (this.spec.version) {
+ const { additionalPrinterColumns: apc } = this.spec;
+ const additionalPrinterColumns = apc?.map(({ JSONPath, ...apc}) => ({ ...apc, jsonPath: JSONPath }));
+
+ return {
+ name: this.spec.version,
+ served: true,
+ storage: true,
+ schema: this.spec.validation,
+ additionalPrinterColumns,
+ };
+ }
+
+ throw new Error(`Failed to find a version for CustomResourceDefinition ${this.metadata.name}`);
+ }
+
getVersion() {
- // 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.getPreferedVersion().name;
}
isNamespaced() {
@@ -147,17 +198,14 @@ export class CustomResourceDefinition extends KubeObject {
}
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.getPreferedVersion().additionalPrinterColumns ?? [];
return columns
- .filter(column => column.name != "Age")
- .filter(column => ignorePriority ? true : !column.priority);
+ .filter(column => column.name != "Age" && (ignorePriority || !column.priority));
}
getValidation() {
- return JSON.stringify(this.spec.validation ?? this.spec.versions?.[0]?.schema, null, 2);
+ return JSON.stringify(this.getPreferedVersion().schema, null, 2);
}
getConditions() {
diff --git a/src/common/k8s-api/json-api.ts b/src/common/k8s-api/json-api.ts
index 8690521099..dde8f5f5a2 100644
--- a/src/common/k8s-api/json-api.ts
+++ b/src/common/k8s-api/json-api.ts
@@ -27,8 +27,7 @@ import { stringify } from "querystring";
import { EventEmitter } from "../../common/event-emitter";
import logger from "../../common/logger";
-export interface JsonApiData {
-}
+export interface JsonApiData {}
export interface JsonApiError {
code?: number;
diff --git a/src/renderer/utils/createStorage.ts b/src/renderer/utils/createStorage.ts
index 854a7aef2f..2e79e35209 100755
--- a/src/renderer/utils/createStorage.ts
+++ b/src/renderer/utils/createStorage.ts
@@ -28,6 +28,7 @@ import { StorageHelper } from "./storageHelper";
import { ClusterStore } from "../../common/cluster-store";
import logger from "../../main/logger";
import { getHostedClusterId, getPath } from "../../common/utils";
+import { isTestEnv } from "../../common/vars";
const storage = observable({
initialized: false,
@@ -62,7 +63,9 @@ export function createAppStorage(key: string, defaultValue: T, clusterId?: st
.then(data => storage.data = data)
.catch(() => null) // ignore empty / non-existing / invalid json files
.finally(() => {
- logger.info(`${logPrefix} loading finished for ${filePath}`);
+ if (!isTestEnv) {
+ logger.info(`${logPrefix} loading finished for ${filePath}`);
+ }
storage.loaded = true;
});
From f2b2c34fe00304206b0b13347864bf0719b93eb2 Mon Sep 17 00:00:00 2001
From: Sebastian Malton
Date: Fri, 15 Oct 2021 12:11:46 -0400
Subject: [PATCH 4/5] Check object instanceof on all detail panels (#4054)
* Check object instanceof on all detail panels
Signed-off-by: Sebastian Malton
* Fix unit tests
- Remove prettier as snapShot testing is too tight of a bound
Signed-off-by: Sebastian Malton
Signed-off-by: Jim Ehrismann
---
.../+config-autoscalers/hpa-details.tsx | 26 +++++++-----
.../limit-range-details.tsx | 20 ++++++---
.../+config-maps/config-map-details.tsx | 42 +++++++++++--------
.../pod-disruption-budgets-details.tsx | 14 ++++++-
.../resource-quota-details.tsx | 13 +++++-
.../+config-secrets/secret-details.tsx | 13 +++++-
.../+custom-resources/crd-details.tsx | 14 ++++++-
.../crd-resource-details.tsx | 17 +++++++-
.../components/+events/event-details.tsx | 14 ++++++-
.../components/+events/kube-event-details.tsx | 14 ++++++-
.../+namespaces/namespace-details.tsx | 12 +++++-
.../+network-endpoints/endpoint-details.tsx | 13 +++++-
.../+network-ingresses/ingress-details.tsx | 9 +++-
.../network-policy-details.tsx | 10 ++++-
.../+network-services/service-details.tsx | 14 ++++++-
.../components/+nodes/node-details.tsx | 12 +++++-
.../pod-security-policy-details.tsx | 35 ++++++++++------
.../storage-class-details.tsx | 16 +++++--
.../volume-claim-details.tsx | 8 ++++
.../+storage-volumes/volume-details.tsx | 8 ++++
.../+workloads-cronjobs/cronjob-details.tsx | 24 +++++++----
.../daemonset-details.tsx | 12 +++++-
.../deployment-details.tsx | 12 +++++-
.../+workloads-jobs/job-details.tsx | 12 +++++-
.../+workloads-pods/pod-details.tsx | 12 +++++-
.../replicaset-details.tsx | 12 +++++-
.../statefulset-details.tsx | 12 +++++-
.../kube-object-meta/kube-object-meta.tsx | 14 ++++++-
28 files changed, 351 insertions(+), 83 deletions(-)
diff --git a/src/renderer/components/+config-autoscalers/hpa-details.tsx b/src/renderer/components/+config-autoscalers/hpa-details.tsx
index 4dd544e5b1..3e6c74b9bb 100644
--- a/src/renderer/components/+config-autoscalers/hpa-details.tsx
+++ b/src/renderer/components/+config-autoscalers/hpa-details.tsx
@@ -33,6 +33,7 @@ import { Table, TableCell, TableHead, TableRow } from "../table";
import { apiManager } from "../../../common/k8s-api/api-manager";
import { KubeObjectMeta } from "../kube-object-meta";
import { getDetailsUrl } from "../kube-detail-params";
+import logger from "../../../common/logger";
export interface HpaDetailsProps extends KubeObjectDetailsProps {
}
@@ -80,17 +81,13 @@ export class HpaDetails extends React.Component {
Current / Target
{
- hpa.getMetrics().map((metric, index) => {
- const name = renderName(metric);
- const values = hpa.getMetricValues(metric);
-
- return (
+ hpa.getMetrics()
+ .map((metric, index) => (
- {name}
- {values}
+ {renderName(metric)}
+ {hpa.getMetricValues(metric)}
- );
- })
+ ))
}
);
@@ -99,7 +96,16 @@ export class HpaDetails extends React.Component {
render() {
const { object: hpa } = this.props;
- if (!hpa) return null;
+ if (!hpa) {
+ return null;
+ }
+
+ if (!(hpa instanceof HorizontalPodAutoscaler)) {
+ logger.error("[HpaDetails]: passed object that is not an instanceof HorizontalPodAutoscaler", hpa);
+
+ return null;
+ }
+
const { scaleTargetRef } = hpa.spec;
return (
diff --git a/src/renderer/components/+config-limit-ranges/limit-range-details.tsx b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx
index e9acbd0a22..14ecbec7ed 100644
--- a/src/renderer/components/+config-limit-ranges/limit-range-details.tsx
+++ b/src/renderer/components/+config-limit-ranges/limit-range-details.tsx
@@ -28,6 +28,7 @@ import { LimitPart, LimitRange, LimitRangeItem, Resource } from "../../../common
import { KubeObjectMeta } from "../kube-object-meta";
import { DrawerItem } from "../drawer/drawer-item";
import { Badge } from "../badge";
+import logger from "../../../common/logger";
interface Props extends KubeObjectDetailsProps {
}
@@ -57,15 +58,13 @@ function renderResourceLimits(limit: LimitRangeItem, resource: Resource) {
function renderLimitDetails(limits: LimitRangeItem[], resources: Resource[]) {
- return resources.map(resource =>
+ return resources.map(resource => (
{
- limits.map(limit =>
- renderResourceLimits(limit, resource)
- )
+ limits.map(limit => renderResourceLimits(limit, resource))
}
- );
+ ));
}
@observer
@@ -73,7 +72,16 @@ export class LimitRangeDetails extends React.Component {
render() {
const { object: limitRange } = this.props;
- if (!limitRange) return null;
+ if (!limitRange) {
+ return null;
+ }
+
+ if (!(limitRange instanceof LimitRange)) {
+ logger.error("[LimitRangeDetails]: passed object that is not an instanceof LimitRange", limitRange);
+
+ return null;
+ }
+
const containerLimits = limitRange.getContainerLimits();
const podLimits = limitRange.getPodLimits();
const pvcLimits = limitRange.getPVCLimits();
diff --git a/src/renderer/components/+config-maps/config-map-details.tsx b/src/renderer/components/+config-maps/config-map-details.tsx
index 6150aa1ad9..082a340a88 100644
--- a/src/renderer/components/+config-maps/config-map-details.tsx
+++ b/src/renderer/components/+config-maps/config-map-details.tsx
@@ -30,8 +30,9 @@ import { Input } from "../input";
import { Button } from "../button";
import { configMapsStore } from "./config-maps.store";
import type { KubeObjectDetailsProps } from "../kube-object-details";
-import type { ConfigMap } from "../../../common/k8s-api/endpoints";
+import { ConfigMap } from "../../../common/k8s-api/endpoints";
import { KubeObjectMeta } from "../kube-object-meta";
+import logger from "../../../common/logger";
interface Props extends KubeObjectDetailsProps {
}
@@ -82,7 +83,16 @@ export class ConfigMapDetails extends React.Component {
render() {
const { object: configMap } = this.props;
- if (!configMap) return null;
+ if (!configMap) {
+ return null;
+ }
+
+ if (!(configMap instanceof ConfigMap)) {
+ logger.error("[ConfigMapDetails]: passed object that is not an instanceof ConfigMap", configMap);
+
+ return null;
+ }
+
const data = Array.from(this.data.entries());
return (
@@ -93,22 +103,20 @@ export class ConfigMapDetails extends React.Component {
<>
{
- data.map(([name, value]) => {
- return (
-
-
{name}
-
- this.data.set(name, v)}
- />
-
+ data.map(([name, value]) => (
+
+
{name}
+
+ this.data.set(name, v)}
+ />
- );
- })
+
+ ))
}