diff --git a/package.json b/package.json index af46679ade..4f294b73a2 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "productName": "OpenLens", "description": "OpenLens - Open Source IDE for Kubernetes", "homepage": "https://github.com/lensapp/lens", - "version": "5.2.6-beta.1", + "version": "5.2.6", "main": "static/build/main.js", "copyright": "© 2021 OpenLens Authors", "license": "MIT", @@ -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/__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/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/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/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..0cb4ac15f6 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; @@ -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; @@ -191,18 +197,32 @@ 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}`); + } + + if (!data.metadata || typeof data.metadata !== "object") { + throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata`, data); + } + Object.assign(this, data); autoBind(this); } @@ -264,7 +284,7 @@ export class KubeObject ({ ...ownerRef, namespace })); @@ -286,14 +306,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-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 abbd31c97a..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 { } @@ -72,6 +73,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; } @@ -80,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 ( @@ -91,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)} + />
- ); - }) +
+ )) }