diff --git a/package.json b/package.json index f421f92f5a..d56f14e3e3 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": "6.1.16", + "version": "6.1.17", "main": "static/build/main.js", "copyright": "© 2022 OpenLens Authors", "license": "MIT", diff --git a/src/extensions/extension-installer/extension-installer.ts b/src/extensions/extension-installer/extension-installer.ts index 1764435a54..f217f91bb9 100644 --- a/src/extensions/extension-installer/extension-installer.ts +++ b/src/extensions/extension-installer/extension-installer.ts @@ -16,6 +16,16 @@ interface Dependencies { extensionPackageRootDirectory: string; } +const baseNpmInstallArgs = [ + "install", + "--audit=false", + "--fund=false", + "--omit=dev", + "--omit=optional", + "--omit=peer", + "--prefer-offline", +]; + /** * Installs dependencies for extensions */ @@ -42,7 +52,7 @@ export class ExtensionInstaller { }); logger.info(`${logModule} installing dependencies at ${this.dependencies.extensionPackageRootDirectory}`); - await this.npm(["install", "--audit=false", "--fund=false", "--only=prod", "--prefer-offline"]); + await this.npm(...baseNpmInstallArgs); logger.info(`${logModule} dependencies installed at ${this.dependencies.extensionPackageRootDirectory}`); } finally { this.installLock.release(); @@ -58,14 +68,14 @@ export class ExtensionInstaller { try { logger.info(`${logModule} installing package from ${name} to ${this.dependencies.extensionPackageRootDirectory}`); - await this.npm(["install", "--audit=false", "--fund=false", "--only=prod", "--prefer-offline", name]); + await this.npm(...baseNpmInstallArgs, name); logger.info(`${logModule} package ${name} installed to ${this.dependencies.extensionPackageRootDirectory}`); } finally { this.installLock.release(); } }; - private npm(args: string[]): Promise { + private npm(...args: string[]): Promise { return new Promise((resolve, reject) => { const child = child_process.fork(this.npmPath, args, { cwd: this.dependencies.extensionPackageRootDirectory, diff --git a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts index e540c54687..652556bcb5 100644 --- a/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts +++ b/src/main/utils/shell-env/compute-unix-shell-environment.injectable.ts @@ -110,25 +110,37 @@ const computeUnixShellEnvironmentInjectable = getInjectable({ const stdout: Buffer[] = []; const stderr: Buffer[] = []; + const getErrorContext = (other: object = {}) => { + const context = { + ...other, + stdout: Buffer.concat(stdout).toString("utf-8"), + stderr: Buffer.concat(stderr).toString("utf-8"), + }; + + return JSON.stringify(context, null, 4); + }; + shellProcess.stdout.on("data", b => stdout.push(b)); shellProcess.stderr.on("data", b => stderr.push(b)); - shellProcess.on("error", (err) => resolve({ - callWasSuccessful: false, - error: `Failed to spawn ${shellPath}: ${err}`, - })); + shellProcess.on("error", (error) => { + if (opts.signal.aborted) { + resolve({ + callWasSuccessful: false, + error: `timeout: ${getErrorContext()}`, + }); + } else { + resolve({ + callWasSuccessful: false, + error: `Failed to spawn ${shellPath}: ${getErrorContext({ error: String(error) })}`, + }); + } + }); shellProcess.on("close", (code, signal) => { if (code || signal) { - const context = { - code, - signal, - stdout: Buffer.concat(stdout).toString("utf-8"), - stderr: Buffer.concat(stderr).toString("utf-8"), - }; - return resolve({ callWasSuccessful: false, - error: `Shell did not exit sucessfully: ${JSON.stringify(context, null, 4)}`, + error: `Shell did not exit sucessfully: ${getErrorContext({ code, signal })}`, }); } diff --git a/src/main/utils/shell-env/compute-unix-shell-environment.test.ts b/src/main/utils/shell-env/compute-unix-shell-environment.test.ts index 2de7204ae8..59a899da37 100644 --- a/src/main/utils/shell-env/compute-unix-shell-environment.test.ts +++ b/src/main/utils/shell-env/compute-unix-shell-environment.test.ts @@ -127,7 +127,11 @@ describe("computeUnixShellEnvironment technical tests", () => { it("should resolve with a failed call", async () => { await expect(unixShellEnv).resolves.toEqual({ callWasSuccessful: false, - error: `Failed to spawn ${shellPath}: Error: some-error`, + error: `Failed to spawn ${shellPath}: ${JSON.stringify({ + error: "Error: some-error", + stdout: "", + stderr: "", + }, null, 4)}`, }); }); }); @@ -229,7 +233,11 @@ describe("computeUnixShellEnvironment technical tests", () => { it("should resolve with a failed call", async () => { await expect(unixShellEnv).resolves.toEqual({ callWasSuccessful: false, - error: `Failed to spawn ${shellPath}: Error: some-error`, + error: `Failed to spawn ${shellPath}: ${JSON.stringify({ + error: "Error: some-error", + stdout: "", + stderr: "", + }, null, 4)}`, }); }); }); @@ -330,7 +338,11 @@ describe("computeUnixShellEnvironment technical tests", () => { it("should resolve with a failed call", async () => { await expect(unixShellEnv).resolves.toEqual({ callWasSuccessful: false, - error: `Failed to spawn ${shellPath}: Error: some-error`, + error: `Failed to spawn ${shellPath}: ${JSON.stringify({ + error: "Error: some-error", + stdout: "", + stderr: "", + }, null, 4)}`, }); }); }); @@ -430,7 +442,11 @@ describe("computeUnixShellEnvironment technical tests", () => { it("should resolve with a failed call", async () => { await expect(unixShellEnv).resolves.toEqual({ callWasSuccessful: false, - error: `Failed to spawn ${shellPath}: Error: some-error`, + error: `Failed to spawn ${shellPath}: ${JSON.stringify({ + error: "Error: some-error", + stdout: "", + stderr: "", + }, null, 4)}`, }); }); }); diff --git a/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap index d3e0cd1d5a..f15bde7996 100644 --- a/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap +++ b/src/renderer/components/+custom-resources/__tests__/__snapshots__/custom-resource-details.test.tsx.snap @@ -45,11 +45,7 @@ exports[` with a CRD with a boolean field should displa -
    -
  • - false -
  • -
+ false
@@ -101,11 +97,7 @@ exports[` with a CRD with a boolean field should displa -
    -
  • - true -
  • -
+ true
@@ -157,11 +149,7 @@ exports[` with a CRD with a number field should display -
    -
  • - 0 -
  • -
+ 0
@@ -213,11 +201,7 @@ exports[` with a CRD with a number field should display -
    -
  • - 1234 -
  • -
+ 1234
diff --git a/src/renderer/components/+custom-resources/crd-resource-details.tsx b/src/renderer/components/+custom-resources/crd-resource-details.tsx index a52d60c7ee..3bd1e821fa 100644 --- a/src/renderer/components/+custom-resources/crd-resource-details.tsx +++ b/src/renderer/components/+custom-resources/crd-resource-details.tsx @@ -15,11 +15,10 @@ import { KubeObjectMeta } from "../kube-object-meta"; import { Input } from "../input"; import type { AdditionalPrinterColumnsV1 } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; import { CustomResourceDefinition } from "../../../common/k8s-api/endpoints/custom-resource-definition.api"; -import { convertKubectlJsonPathToNodeJsonPath } from "../../utils/jsonPath"; +import { safeJSONPathValue } from "../../utils/jsonPath"; import type { KubeObjectMetadata, KubeObjectStatus } from "../../../common/k8s-api/kube-object"; import { KubeObject } from "../../../common/k8s-api/kube-object"; import logger from "../../../common/logger"; -import { JSONPath } from "@astronautlabs/jsonpath"; export interface CustomResourceDetailsProps extends KubeObjectDetailsProps { crd: CustomResourceDefinition; @@ -66,7 +65,7 @@ export class CustomResourceDetails extends React.Component ( - {convertSpecValue(JSONPath.query(resource, convertKubectlJsonPathToNodeJsonPath(jsonPath)))} + {convertSpecValue(safeJSONPathValue(resource, jsonPath))} )); } diff --git a/src/renderer/components/+custom-resources/crd-resources.tsx b/src/renderer/components/+custom-resources/crd-resources.tsx index 7b3d87529f..ca8884bcd3 100644 --- a/src/renderer/components/+custom-resources/crd-resources.tsx +++ b/src/renderer/components/+custom-resources/crd-resources.tsx @@ -11,7 +11,7 @@ import { KubeObjectListLayout } from "../kube-object-list-layout"; import type { IComputedValue } from "mobx"; import { computed, makeObservable } from "mobx"; import type { ApiManager } from "../../../common/k8s-api/api-manager"; -import { safeJSONPathValue } from "../../utils/jsonPath"; +import { formatJSONValue, safeJSONPathValue } from "../../utils/jsonPath"; import { TabLayout } from "../layout/tab-layout-2"; import { withInjectables } from "@ogre-tools/injectable-react"; import customResourcesRouteParametersInjectable from "./custom-resources-route-parameters.injectable"; @@ -73,7 +73,7 @@ class NonInjectedCustomResources extends React.Component { [columnId.age]: customResource => -customResource.getCreationTimestamp(), ...Object.fromEntries(extraColumns.map(({ name, jsonPath }) => [ name, - customResource => safeJSONPathValue(customResource, jsonPath), + customResource => formatJSONValue(safeJSONPathValue(customResource, jsonPath)), ])), }} searchFilters={[ diff --git a/src/renderer/utils/__tests__/jsonPath.test.tsx b/src/renderer/utils/__tests__/jsonPath.test.ts similarity index 89% rename from src/renderer/utils/__tests__/jsonPath.test.tsx rename to src/renderer/utils/__tests__/jsonPath.test.ts index 49e75585f9..312522a63f 100644 --- a/src/renderer/utils/__tests__/jsonPath.test.tsx +++ b/src/renderer/utils/__tests__/jsonPath.test.ts @@ -5,7 +5,7 @@ import { convertKubectlJsonPathToNodeJsonPath, safeJSONPathValue } from "../jsonPath"; -describe("parseJsonPath", () => { +describe("convertKubectlJsonPathToNodeJsonPath", () => { it("should convert \\. to use indexed notation", () => { const res = convertKubectlJsonPathToNodeJsonPath(".metadata.labels.kubesphere\\.io/alias-name"); @@ -82,13 +82,13 @@ describe("safeJSONPathValue", () => { it("should convert boolean values to strings", () => { const res = safeJSONPathValue({ bar: false }, ".bar"); - expect(res).toBe("false"); + expect(res).toBe(false); }); it("should convert number values to strings", () => { const res = safeJSONPathValue({ bar: 0 }, ".bar"); - expect(res).toBe("0"); + expect(res).toBe(0); }); it("should join sliced entries with commas only", () => { @@ -103,7 +103,7 @@ describe("safeJSONPathValue", () => { ], }, ".bar[].foo"); - expect(res).toBe("1"); + expect(res).toBe(1); }); it("should join an array of values using JSON.stringify", () => { @@ -114,7 +114,7 @@ describe("safeJSONPathValue", () => { ], }, ".bar"); - expect(res).toBe(`["world","hello"]`); + expect(res).toEqual(["world", "hello"]); }); it("should stringify an object value", () => { @@ -122,7 +122,7 @@ describe("safeJSONPathValue", () => { foo: { bar: "bat" }, }, ".foo"); - expect(res).toBe(`{"bar":"bat"}`); + expect(res).toEqual({ "bar":"bat" }); }); it("should use convertKubectlJsonPathToNodeJsonPath", () => { @@ -155,7 +155,7 @@ describe("safeJSONPathValue", () => { const res = safeJSONPathValue(obj, ".spec.metrics[*].external.highWatermark.."); - expect(res).toBe("100, 100"); + expect(res).toEqual(["100", "100"]); }); it("should not throw if path is invalid jsonpath", () => { @@ -163,6 +163,16 @@ describe("safeJSONPathValue", () => { foo: { "hello.world": "bat" }, }, "asd["); - expect(res).toBe(""); + expect(res).toBe(undefined); + }); + + it("should retrive value with '/' in jsonpath", () => { + const res = safeJSONPathValue({ + foo: { + "hello/world": "bat", + }, + }, ".foo.hello/world"); + + expect(res).toBe("bat"); }); }); diff --git a/src/renderer/utils/jsonPath.ts b/src/renderer/utils/jsonPath.ts index 3ba970a044..57fffbaea9 100644 --- a/src/renderer/utils/jsonPath.ts +++ b/src/renderer/utils/jsonPath.ts @@ -6,7 +6,7 @@ import { JSONPath } from "@astronautlabs/jsonpath"; import { TypedRegEx } from "typed-regex"; -const slashDashSearch = /[\\-]/g; +const slashDashSearch = /[/\\-]/g; const pathByBareDots = /(?<=\w)\./; const textBeforeFirstSquare = /^.*(?=\[)/g; const backSlash = /\\/g; @@ -22,6 +22,7 @@ const trailingDotDot = /\.\.$/; * * Known shorthands: * - Leading `$` is optional (but implied) + * - The string `/` can be used without a leading `\` escapement * - The string `\.` is used to denote the "value of '.'" and not "next key" * - The string `-` can be used while not in quotes * - `[]` as shorthand for `[0]` @@ -75,7 +76,15 @@ function convertToIndexNotation(key: string, firstItem = false) { } } -function formatJSONValue(value: unknown) { +export function formatJSONValue(value: unknown): string { + if (value == null) { + return ""; + } + + if (Array.isArray(value)) { + return value.map(formatJSONValue).join(", "); + } + if (typeof value === "object") { return JSON.stringify(value); } @@ -88,21 +97,21 @@ function formatJSONValue(value: unknown) { * * This function will also stringify the value retreived from the object */ -export function safeJSONPathValue(obj: object, path: string): string { +export function safeJSONPathValue(obj: object, path: string): unknown { try { const parsedPath = JSONPath.parse(convertKubectlJsonPathToNodeJsonPath(path)); - const isSlice = parsedPath.some((exp: any) => exp.expression.type === "slice" || "wildcard"); + const isSlice = parsedPath.some((exp: any) => exp.expression.type === "slice" || exp.expression.type === "wildcard"); const value = JSONPath.query(obj, JSONPath.stringify(parsedPath), isSlice ? Infinity : 1); if (isSlice) { - return value.map(formatJSONValue).join(", "); + return value; } - return formatJSONValue(value[0]); + return value[0]; } catch (error) { // something failed console.warn("[JSON-PATH]: failed to parse jsonpath", error); - return ""; + return undefined; } }