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)}
+ />
- );
- })
+
+ ))
}