diff --git a/.github/workflows/cron-test.yaml b/.github/workflows/cron-test.yaml index 2358a34e9a..5be4bfac2d 100644 --- a/.github/workflows/cron-test.yaml +++ b/.github/workflows/cron-test.yaml @@ -51,7 +51,7 @@ jobs: command: npm ci - name: Build library parts - run: run npm build -- --ignore open-lens + run: npm run build -- --ignore open-lens - run: npm run test:unit name: Run tests diff --git a/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts b/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts index 547f78adef..6d05385a28 100644 --- a/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/kube-api-parse.test.ts @@ -114,7 +114,7 @@ const tests: KubeApiParseTestData[] = [ }], ]; -const throwtests = [ +const invalidTests = [ undefined, "", "ajklsmh", @@ -125,7 +125,7 @@ describe("parseApi unit tests", () => { expect(parseKubeApi(url)).toStrictEqual(expected); }); - it.each(throwtests)("testing %j should throw", (url) => { - expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath"); + it.each(invalidTests)("testing %j should throw", (url) => { + expect(parseKubeApi(url as never)).toBe(undefined); }); }); diff --git a/packages/core/src/common/k8s-api/api-manager/api-manager.ts b/packages/core/src/common/k8s-api/api-manager/api-manager.ts index 23574822ed..8e0eff8255 100644 --- a/packages/core/src/common/k8s-api/api-manager/api-manager.ts +++ b/packages/core/src/common/k8s-api/api-manager/api-manager.ts @@ -74,9 +74,13 @@ export class ApiManager { return iter.find(this.apis.values(), pathOrCallback); } - const { apiBase } = parseKubeApi(pathOrCallback); + const parsedApi = parseKubeApi(pathOrCallback); - return this.apis.get(apiBase); + if (!parsedApi) { + return undefined; + } + + return this.apis.get(parsedApi.apiBase); } getApiByKind(kind: string, apiVersion: string) { @@ -141,9 +145,10 @@ export class ApiManager { } const { apiBase } = typeof apiOrBase === "string" - ? parseKubeApi(apiOrBase) + ? parseKubeApi(apiOrBase) ?? {} : apiOrBase; - const api = this.getApi(apiBase); + + const api = apiBase && this.getApi(apiBase); if (!api) { return undefined; diff --git a/packages/core/src/common/k8s-api/json-api.ts b/packages/core/src/common/k8s-api/json-api.ts index 00325f3853..ad29cfae4c 100644 --- a/packages/core/src/common/k8s-api/json-api.ts +++ b/packages/core/src/common/k8s-api/json-api.ts @@ -26,6 +26,30 @@ export interface JsonApiError { errors?: { id: string; title: string; status?: number }[]; } +export interface KubeJsonApiErrorCause { + reason: string; + message: string; + field: string; +} + +export interface KubeJsonApiErrorDetails { + name: string; + group: string; + kind: string; + causes: KubeJsonApiErrorCause[]; +} + +export interface KubeJsonApiError { + kind: "Status"; + apiVersion: "v1"; + metadata: object; + status: string; + message: string; + reason: string; + details: KubeJsonApiErrorDetails; + code: number; +} + export interface JsonApiParams { data?: PartialDeep; // request body } @@ -246,7 +270,7 @@ export class JsonApi = Js export class JsonApiErrorParsed { isUsedForNotification = false; - constructor(private error: JsonApiError | DOMException, private messages: string[]) { + constructor(private error: JsonApiError | DOMException | KubeJsonApiError, private messages: string[]) { } get isAborted() { diff --git a/packages/core/src/common/k8s-api/kube-api-parse.ts b/packages/core/src/common/k8s-api/kube-api-parse.ts index 522e2812b8..e69d3ffa69 100644 --- a/packages/core/src/common/k8s-api/kube-api-parse.ts +++ b/packages/core/src/common/k8s-api/kube-api-parse.ts @@ -22,16 +22,16 @@ export interface IKubeApiParsed extends IKubeApiLinkRef { apiVersionWithGroup: string; } -export function parseKubeApi(path: string): IKubeApiParsed { +export function parseKubeApi(path: string): IKubeApiParsed | undefined { const apiPath = new URL(path, "https://localhost").pathname; const [, prefix, ...parts] = apiPath.split("/"); const apiPrefix = `/${prefix}`; const [left, right, namespaced] = array.split(parts, "namespaces"); - let apiGroup!: string; - let apiVersion!: string; - let namespace!: string; - let resource!: string; - let name!: string; + let apiGroup: string; + let apiVersion: string | undefined; + let namespace: string | undefined; + let resource: string; + let name: string | undefined; if (namespaced) { switch (right.length) { @@ -46,26 +46,21 @@ export function parseKubeApi(path: string): IKubeApiParsed { break; } - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - apiVersion = left.pop()!; - apiGroup = left.join("/"); + let rest: string[]; + + [apiVersion, ...rest] = left; + apiGroup = rest.join("/"); } else { - switch (left.length) { - case 0: - throw new Error(`invalid apiPath: ${apiPath}`); - case 4: - [apiGroup, apiVersion, resource, name] = left; - break; - case 2: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - resource = left.pop()!; - // fallthrough - case 1: - // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - apiVersion = left.pop()!; - apiGroup = ""; - break; - default: + if (left.length === 0) { + return undefined; + } + + if (left.length === 1 || left.length === 2) { + [apiVersion, resource] = left; + apiGroup = ""; + } else if (left.length === 4) { + [apiGroup, apiVersion, resource, name] = left; + } else { /** * Given that * - `apiVersion` is `GROUP/VERSION` and @@ -82,15 +77,14 @@ export function parseKubeApi(path: string): IKubeApiParsed { * 3. otherwise assume apiVersion <- left[0] * 4. always resource, name <- left[(0 or 1)+1..] */ - if (left[0].includes(".") || left[1].match(/^v[0-9]/)) { - [apiGroup, apiVersion] = left; - resource = left.slice(2).join("/"); - } else { - apiGroup = ""; - apiVersion = left[0]; - [resource, name] = left.slice(1); - } - break; + if (left[0].includes(".") || left[1].match(/^v[0-9]/)) { + [apiGroup, apiVersion] = left; + resource = left.slice(2).join("/"); + } else { + apiGroup = ""; + apiVersion = left[0]; + [resource, name] = left.slice(1); + } } } @@ -98,7 +92,7 @@ export function parseKubeApi(path: string): IKubeApiParsed { const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); if (!apiBase) { - throw new Error(`invalid apiPath: ${apiPath}`); + return undefined; } return { @@ -110,7 +104,7 @@ export function parseKubeApi(path: string): IKubeApiParsed { } function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is IKubeApiParsed { - return "apiGroup" in refOrParsed; + return "apiGroup" in refOrParsed && !!refOrParsed.apiGroup; } export function createKubeApiURL(linkRef: IKubeApiLinkRef): string; diff --git a/packages/core/src/common/k8s-api/kube-api.ts b/packages/core/src/common/k8s-api/kube-api.ts index cf9d8f15a7..4bdbd02e45 100644 --- a/packages/core/src/common/k8s-api/kube-api.ts +++ b/packages/core/src/common/k8s-api/kube-api.ts @@ -264,12 +264,16 @@ export class KubeApi< allowedUsableVersions, } = opts; - assert(fullApiPathname, "apiBase MUST be provied either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase"); + assert(fullApiPathname, "apiBase MUST be provided either via KubeApiOptions.apiBase or KubeApiOptions.objectConstructor.apiBase"); assert(request, "request MUST be provided if not in a cluster page frame context"); - const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parseKubeApi(fullApiPathname); + const parsedApi = parseKubeApi(fullApiPathname); - assert(kind, "kind MUST be provied either via KubeApiOptions.kind or KubeApiOptions.objectConstructor.kind"); + assert(parsedApi, "apiBase MUST be a valid kube api pathname"); + + const { apiBase, apiPrefix, apiGroup, apiVersion, resource } = parsedApi; + + assert(kind, "kind MUST be provided either via KubeApiOptions.kind or KubeApiOptions.objectConstructor.kind"); assert(apiPrefix, "apiBase MUST be parsable as a kubeApi selfLink style string"); this.doCheckPreferredVersion = doCheckPreferredVersion; @@ -308,8 +312,14 @@ export class KubeApi< const apiBases = new Set(rawApiBases); for (const apiUrl of apiBases) { + const parsedApi = parseKubeApi(apiUrl); + + if (!parsedApi) { + continue; + } + try { - const { apiPrefix, apiGroup, resource } = parseKubeApi(apiUrl); + const { apiPrefix, apiGroup, resource } = parsedApi; const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList; const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]); @@ -324,8 +334,8 @@ export class KubeApi< }; } } - } catch (error) { - // Exception is ignored as we can try the next url + } catch { + // ignore exception to try next url } } diff --git a/packages/core/src/common/k8s-api/kube-api/get-kube-api-from-path.injectable.ts b/packages/core/src/common/k8s-api/kube-api/get-kube-api-from-path.injectable.ts index 669185684f..61ac7b1e96 100644 --- a/packages/core/src/common/k8s-api/kube-api/get-kube-api-from-path.injectable.ts +++ b/packages/core/src/common/k8s-api/kube-api/get-kube-api-from-path.injectable.ts @@ -7,18 +7,18 @@ import { parseKubeApi } from "../kube-api-parse"; import { kubeApiInjectionToken } from "./kube-api-injection-token"; import type { KubeApi } from "../kube-api"; +export type GetKubeApiFromPath = (apiPath: string) => KubeApi | undefined; + const getKubeApiFromPathInjectable = getInjectable({ id: "get-kube-api-from-path", - instantiate: (di) => { + instantiate: (di): GetKubeApiFromPath => { const kubeApis = di.injectMany(kubeApiInjectionToken); return (apiPath: string) => { const parsed = parseKubeApi(apiPath); - const kubeApi = kubeApis.find((api) => api.apiBase === parsed.apiBase); - - return (kubeApi as KubeApi) || undefined; + return kubeApis.find((api) => api.apiBase === parsed?.apiBase); }; }, }); diff --git a/packages/core/src/common/k8s-api/kube-object.store.ts b/packages/core/src/common/k8s-api/kube-object.store.ts index 9effdc8f57..9efaca4dee 100644 --- a/packages/core/src/common/k8s-api/kube-object.store.ts +++ b/packages/core/src/common/k8s-api/kube-object.store.ts @@ -324,7 +324,11 @@ export class KubeObjectStore< @action async loadFromPath(resourcePath: string) { - const { namespace, name } = parseKubeApi(resourcePath); + const parsedApi = parseKubeApi(resourcePath); + + assert(parsedApi, "resourcePath must be a valid kube api"); + + const { namespace, name } = parsedApi; assert(name, "name must be part of resourcePath"); diff --git a/packages/core/src/common/k8s-api/kube-object.ts b/packages/core/src/common/k8s-api/kube-object.ts index e43633aed6..9549cd5917 100644 --- a/packages/core/src/common/k8s-api/kube-object.ts +++ b/packages/core/src/common/k8s-api/kube-object.ts @@ -26,15 +26,11 @@ import type { ItemObject } from "@k8slens/list-layout"; import type { Patch } from "rfc6902"; import assert from "assert"; import type { JsonObject } from "type-fest"; -import requestKubeObjectPatchInjectable - from "./endpoints/resource-applier.api/request-patch.injectable"; +import requestKubeObjectPatchInjectable from "./endpoints/resource-applier.api/request-patch.injectable"; import { apiKubeInjectionToken } from "./api-kube"; -import requestKubeObjectCreationInjectable - from "./endpoints/resource-applier.api/request-update.injectable"; +import requestKubeObjectCreationInjectable from "./endpoints/resource-applier.api/request-update.injectable"; import { dump } from "js-yaml"; -import { - getLegacyGlobalDiForExtensionApi, -} from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import autoBind from "auto-bind"; export type KubeJsonApiDataFor = K extends KubeObject @@ -552,14 +548,30 @@ export class KubeObject< } constructor(data: KubeJsonApiData) { - if (typeof data !== "object") { + if (!isObject(data)) { throw new TypeError(`Cannot create a KubeObject from ${typeof data}`); } - if (!data.metadata || typeof data.metadata !== "object") { + if (!isObject(data.metadata)) { throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata`, data); } + if (!isString(data.metadata.name)) { + throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.name being a string`, data); + } + + if (!isString(data.metadata.uid)) { + throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.uid being a string`, data); + } + + if (!isString(data.metadata.resourceVersion)) { + throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.resourceVersion being a string`, data); + } + + if (!isString(data.metadata.selfLink)) { + throw new KubeCreationError(`Cannot create a KubeObject from an object without metadata.selfLink being a string`, data); + } + Object.assign(this, data); autoBind(this); } diff --git a/packages/core/src/common/utils/get-error-message.ts b/packages/core/src/common/utils/get-error-message.ts index b5d7dd4244..84e6c8083a 100644 --- a/packages/core/src/common/utils/get-error-message.ts +++ b/packages/core/src/common/utils/get-error-message.ts @@ -2,6 +2,9 @@ * Copyright (c) OpenLens Authors. All rights reserved. * Licensed under MIT License. See LICENSE in root directory for more information. */ + +import { JsonApiErrorParsed } from "../k8s-api/json-api"; + export const getErrorMessage = (error: unknown): string => { if (typeof error === "string") { return error; @@ -11,5 +14,9 @@ export const getErrorMessage = (error: unknown): string => { return error.message; } + if (error instanceof JsonApiErrorParsed) { + return error.toString(); + } + return JSON.stringify(error); }; diff --git a/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap b/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap index 266fef88c4..6eafa73d66 100644 --- a/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap +++ b/packages/core/src/features/cluster/namespaces/__snapshots__/edit-namespace-from-new-tab.test.tsx.snap @@ -2099,9 +2099,9 @@ metadata: uid: some-uid name: some-name resourceVersion: some-resource-version - selfLink: /apis/some-api-version/namespaces/some-uid somePropertyToBeChanged: some-changed-value someAddedProperty: some-new-value + selfLink: /apis/some-api-version/namespaces/some-uid @@ -4435,9 +4435,9 @@ metadata: uid: some-uid name: some-name resourceVersion: some-resource-version - selfLink: /apis/some-api-version/namespaces/some-uid somePropertyToBeRemoved: some-value somePropertyToBeChanged: some-old-value + selfLink: /apis/some-api-version/namespaces/some-uid @@ -5962,9 +5962,9 @@ metadata: uid: some-uid name: some-name resourceVersion: some-resource-version - selfLink: /apis/some-api-version/namespaces/some-uid somePropertyToBeRemoved: some-value somePropertyToBeChanged: some-old-value + selfLink: /apis/some-api-version/namespaces/some-uid @@ -5976,7 +5976,7 @@ metadata: `; -exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given no changes in the configuration, when selecting to save when saving resolves with failure renders 1`] = ` +exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace given no changes in the configuration, when selecting to save when saving fails renders 1`] = `
@@ -7466,9 +7466,9 @@ metadata: uid: some-uid name: some-name resourceVersion: some-resource-version - selfLink: /apis/some-api-version/namespaces/some-uid somePropertyToBeRemoved: some-value somePropertyToBeChanged: some-old-value + selfLink: /apis/some-api-version/namespaces/some-uid labels: k8slens-edit-resource-version: some-api-version @@ -8220,9 +8220,9 @@ metadata: uid: some-uid name: some-name resourceVersion: some-resource-version - selfLink: /apis/some-api-version/namespaces/some-uid somePropertyToBeRemoved: some-value somePropertyToBeChanged: some-old-value + selfLink: /apis/some-api-version/namespaces/some-uid
@@ -9568,9 +9568,9 @@ metadata: uid: some-uid name: some-name resourceVersion: some-resource-version - selfLink: /apis/some-api-version/namespaces/some-uid somePropertyToBeRemoved: some-value somePropertyToBeChanged: some-old-value + selfLink: /apis/some-api-version/namespaces/some-uid @@ -9582,7 +9582,7 @@ metadata: `; -exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close when saving resolves with failure renders 1`] = ` +exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close when saving failings renders 1`] = `
@@ -10334,594 +10334,7 @@ metadata: `; -exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close when saving resolves with success renders 1`] = ` - -
-
-
-
-
- -
- - - close - - -
- Close -
-
-
-
-
-
-
-
- +
+ +`; + +exports[`cluster/namespaces - edit namespace from new tab when navigating to namespaces when namespaces resolve when clicking the context menu for a namespace when clicking to edit namespace when call for namespace resolves with namespace when selecting to save and close when saving resolves with success renders 1`] = ` + +
+
+
+
+
+ +
+ + + close + + +
+ Close +
+
+
+
+
+
+
+
+