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:
@@ -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:
@@ -10334,594 +10334,7 @@ metadata:
-
-
-
-
-
-
-
-
-
- close
-
-
-
- Close
-
-
-
-
-
-
-
-
-
-
- Resource not found
+
+
+
+ Kind:
+
+
+ Namespace
+
+
+ Name:
+
+
+ some-name
+
+
+ Namespace:
+
+
+ default
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ close
+
+
+
+ Close
+
+
+
+
+
+
+
+