1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Merge branch 'master' into ui-components/resizing-anchor-error-boundary

This commit is contained in:
Gabriel Accettola 2023-04-24 17:01:48 +02:00 committed by GitHub
commit 804f45422d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
27 changed files with 1190 additions and 964 deletions

View File

@ -51,7 +51,7 @@ jobs:
command: npm ci command: npm ci
- name: Build library parts - name: Build library parts
run: run npm build -- --ignore open-lens run: npm run build -- --ignore open-lens
- run: npm run test:unit - run: npm run test:unit
name: Run tests name: Run tests

View File

@ -114,7 +114,7 @@ const tests: KubeApiParseTestData[] = [
}], }],
]; ];
const throwtests = [ const invalidTests = [
undefined, undefined,
"", "",
"ajklsmh", "ajklsmh",
@ -125,7 +125,7 @@ describe("parseApi unit tests", () => {
expect(parseKubeApi(url)).toStrictEqual(expected); expect(parseKubeApi(url)).toStrictEqual(expected);
}); });
it.each(throwtests)("testing %j should throw", (url) => { it.each(invalidTests)("testing %j should throw", (url) => {
expect(() => parseKubeApi(url as never)).toThrowError("invalid apiPath"); expect(parseKubeApi(url as never)).toBe(undefined);
}); });
}); });

View File

@ -74,9 +74,13 @@ export class ApiManager {
return iter.find(this.apis.values(), pathOrCallback); 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) { getApiByKind(kind: string, apiVersion: string) {
@ -141,9 +145,10 @@ export class ApiManager {
} }
const { apiBase } = typeof apiOrBase === "string" const { apiBase } = typeof apiOrBase === "string"
? parseKubeApi(apiOrBase) ? parseKubeApi(apiOrBase) ?? {}
: apiOrBase; : apiOrBase;
const api = this.getApi(apiBase);
const api = apiBase && this.getApi(apiBase);
if (!api) { if (!api) {
return undefined; return undefined;

View File

@ -26,6 +26,30 @@ export interface JsonApiError {
errors?: { id: string; title: string; status?: number }[]; 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<D> { export interface JsonApiParams<D> {
data?: PartialDeep<D>; // request body data?: PartialDeep<D>; // request body
} }
@ -246,7 +270,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = Js
export class JsonApiErrorParsed { export class JsonApiErrorParsed {
isUsedForNotification = false; isUsedForNotification = false;
constructor(private error: JsonApiError | DOMException, private messages: string[]) { constructor(private error: JsonApiError | DOMException | KubeJsonApiError, private messages: string[]) {
} }
get isAborted() { get isAborted() {

View File

@ -22,16 +22,16 @@ export interface IKubeApiParsed extends IKubeApiLinkRef {
apiVersionWithGroup: string; apiVersionWithGroup: string;
} }
export function parseKubeApi(path: string): IKubeApiParsed { export function parseKubeApi(path: string): IKubeApiParsed | undefined {
const apiPath = new URL(path, "https://localhost").pathname; const apiPath = new URL(path, "https://localhost").pathname;
const [, prefix, ...parts] = apiPath.split("/"); const [, prefix, ...parts] = apiPath.split("/");
const apiPrefix = `/${prefix}`; const apiPrefix = `/${prefix}`;
const [left, right, namespaced] = array.split(parts, "namespaces"); const [left, right, namespaced] = array.split(parts, "namespaces");
let apiGroup!: string; let apiGroup: string;
let apiVersion!: string; let apiVersion: string | undefined;
let namespace!: string; let namespace: string | undefined;
let resource!: string; let resource: string;
let name!: string; let name: string | undefined;
if (namespaced) { if (namespaced) {
switch (right.length) { switch (right.length) {
@ -46,26 +46,21 @@ export function parseKubeApi(path: string): IKubeApiParsed {
break; break;
} }
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion let rest: string[];
apiVersion = left.pop()!;
apiGroup = left.join("/"); [apiVersion, ...rest] = left;
apiGroup = rest.join("/");
} else { } else {
switch (left.length) { if (left.length === 0) {
case 0: return undefined;
throw new Error(`invalid apiPath: ${apiPath}`); }
case 4:
[apiGroup, apiVersion, resource, name] = left; if (left.length === 1 || left.length === 2) {
break; [apiVersion, resource] = left;
case 2: apiGroup = "";
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion } else if (left.length === 4) {
resource = left.pop()!; [apiGroup, apiVersion, resource, name] = left;
// fallthrough } else {
case 1:
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
apiVersion = left.pop()!;
apiGroup = "";
break;
default:
/** /**
* Given that * Given that
* - `apiVersion` is `GROUP/VERSION` and * - `apiVersion` is `GROUP/VERSION` and
@ -82,15 +77,14 @@ export function parseKubeApi(path: string): IKubeApiParsed {
* 3. otherwise assume apiVersion <- left[0] * 3. otherwise assume apiVersion <- left[0]
* 4. always resource, name <- left[(0 or 1)+1..] * 4. always resource, name <- left[(0 or 1)+1..]
*/ */
if (left[0].includes(".") || left[1].match(/^v[0-9]/)) { if (left[0].includes(".") || left[1].match(/^v[0-9]/)) {
[apiGroup, apiVersion] = left; [apiGroup, apiVersion] = left;
resource = left.slice(2).join("/"); resource = left.slice(2).join("/");
} else { } else {
apiGroup = ""; apiGroup = "";
apiVersion = left[0]; apiVersion = left[0];
[resource, name] = left.slice(1); [resource, name] = left.slice(1);
} }
break;
} }
} }
@ -98,7 +92,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/"); const apiBase = [apiPrefix, apiGroup, apiVersion, resource].filter(v => v).join("/");
if (!apiBase) { if (!apiBase) {
throw new Error(`invalid apiPath: ${apiPath}`); return undefined;
} }
return { return {
@ -110,7 +104,7 @@ export function parseKubeApi(path: string): IKubeApiParsed {
} }
function isIKubeApiParsed(refOrParsed: IKubeApiLinkRef | IKubeApiParsed): refOrParsed is 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; export function createKubeApiURL(linkRef: IKubeApiLinkRef): string;

View File

@ -264,12 +264,16 @@ export class KubeApi<
allowedUsableVersions, allowedUsableVersions,
} = opts; } = 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"); 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"); assert(apiPrefix, "apiBase MUST be parsable as a kubeApi selfLink style string");
this.doCheckPreferredVersion = doCheckPreferredVersion; this.doCheckPreferredVersion = doCheckPreferredVersion;
@ -308,8 +312,14 @@ export class KubeApi<
const apiBases = new Set(rawApiBases); const apiBases = new Set(rawApiBases);
for (const apiUrl of apiBases) { for (const apiUrl of apiBases) {
const parsedApi = parseKubeApi(apiUrl);
if (!parsedApi) {
continue;
}
try { try {
const { apiPrefix, apiGroup, resource } = parseKubeApi(apiUrl); const { apiPrefix, apiGroup, resource } = parsedApi;
const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList; const list = await this.request.get(`${apiPrefix}/${apiGroup}`) as KubeApiResourceVersionList;
const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]); const resourceVersions = getOrderedVersions(list, this.allowedUsableVersions?.[apiGroup]);
@ -324,8 +334,8 @@ export class KubeApi<
}; };
} }
} }
} catch (error) { } catch {
// Exception is ignored as we can try the next url // ignore exception to try next url
} }
} }

View File

@ -7,18 +7,18 @@ import { parseKubeApi } from "../kube-api-parse";
import { kubeApiInjectionToken } from "./kube-api-injection-token"; import { kubeApiInjectionToken } from "./kube-api-injection-token";
import type { KubeApi } from "../kube-api"; import type { KubeApi } from "../kube-api";
export type GetKubeApiFromPath = (apiPath: string) => KubeApi | undefined;
const getKubeApiFromPathInjectable = getInjectable({ const getKubeApiFromPathInjectable = getInjectable({
id: "get-kube-api-from-path", id: "get-kube-api-from-path",
instantiate: (di) => { instantiate: (di): GetKubeApiFromPath => {
const kubeApis = di.injectMany(kubeApiInjectionToken); const kubeApis = di.injectMany(kubeApiInjectionToken);
return (apiPath: string) => { return (apiPath: string) => {
const parsed = parseKubeApi(apiPath); const parsed = parseKubeApi(apiPath);
const kubeApi = kubeApis.find((api) => api.apiBase === parsed.apiBase); return kubeApis.find((api) => api.apiBase === parsed?.apiBase);
return (kubeApi as KubeApi) || undefined;
}; };
}, },
}); });

View File

@ -324,7 +324,11 @@ export class KubeObjectStore<
@action @action
async loadFromPath(resourcePath: string) { 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"); assert(name, "name must be part of resourcePath");

View File

@ -26,15 +26,11 @@ import type { ItemObject } from "@k8slens/list-layout";
import type { Patch } from "rfc6902"; import type { Patch } from "rfc6902";
import assert from "assert"; import assert from "assert";
import type { JsonObject } from "type-fest"; import type { JsonObject } from "type-fest";
import requestKubeObjectPatchInjectable import requestKubeObjectPatchInjectable from "./endpoints/resource-applier.api/request-patch.injectable";
from "./endpoints/resource-applier.api/request-patch.injectable";
import { apiKubeInjectionToken } from "./api-kube"; import { apiKubeInjectionToken } from "./api-kube";
import requestKubeObjectCreationInjectable import requestKubeObjectCreationInjectable from "./endpoints/resource-applier.api/request-update.injectable";
from "./endpoints/resource-applier.api/request-update.injectable";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import { import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
getLegacyGlobalDiForExtensionApi,
} from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api";
import autoBind from "auto-bind"; import autoBind from "auto-bind";
export type KubeJsonApiDataFor<K> = K extends KubeObject<infer Metadata, infer Status, infer Spec> export type KubeJsonApiDataFor<K> = K extends KubeObject<infer Metadata, infer Status, infer Spec>
@ -552,14 +548,30 @@ export class KubeObject<
} }
constructor(data: KubeJsonApiData<Metadata, Status, Spec>) { constructor(data: KubeJsonApiData<Metadata, Status, Spec>) {
if (typeof data !== "object") { if (!isObject(data)) {
throw new TypeError(`Cannot create a KubeObject from ${typeof 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); 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); Object.assign(this, data);
autoBind(this); autoBind(this);
} }

View File

@ -2,6 +2,9 @@
* Copyright (c) OpenLens Authors. All rights reserved. * Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information. * 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 => { export const getErrorMessage = (error: unknown): string => {
if (typeof error === "string") { if (typeof error === "string") {
return error; return error;
@ -11,5 +14,9 @@ export const getErrorMessage = (error: unknown): string => {
return error.message; return error.message;
} }
if (error instanceof JsonApiErrorParsed) {
return error.toString();
}
return JSON.stringify(error); return JSON.stringify(error);
}; };

View File

@ -12,8 +12,6 @@ import createEditResourceTabInjectable from "../../../renderer/components/dock/e
import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable"; import getRandomIdForEditResourceTabInjectable from "../../../renderer/components/dock/edit-resource/get-random-id-for-edit-resource-tab.injectable";
import type { AsyncFnMock } from "@async-fn/jest"; import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
import type { CallForPatchResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
import callForPatchResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-patch-resource/call-for-patch-resource.injectable";
import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable"; import dockStoreInjectable from "../../../renderer/components/dock/dock/store.injectable";
import { Namespace } from "../../../common/k8s-api/endpoints"; import { Namespace } from "../../../common/k8s-api/endpoints";
import showSuccessNotificationInjectable from "../../../renderer/components/notifications/show-success-notification.injectable"; import showSuccessNotificationInjectable from "../../../renderer/components/notifications/show-success-notification.injectable";
@ -21,15 +19,22 @@ import showErrorNotificationInjectable from "../../../renderer/components/notifi
import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable"; import readJsonFileInjectable from "../../../common/fs/read-json-file.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable"; import hostedClusterIdInjectable from "../../../renderer/cluster-frame-context/hosted-cluster-id.injectable";
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable"; import type { ApiKubePatch } from "../../../renderer/k8s/api-kube-patch.injectable";
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable"; import type { ApiKubeGet } from "../../../renderer/k8s/api-kube-get.injectable";
import apiKubePatchInjectable from "../../../renderer/k8s/api-kube-patch.injectable";
import apiKubeGetInjectable from "../../../renderer/k8s/api-kube-get.injectable";
import type { KubeJsonApiData } from "../../../common/k8s-api/kube-json-api";
import type { BaseKubeJsonApiObjectMetadata, KubeObjectScope } from "../../../common/k8s-api/kube-object";
import { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
import type { ShowNotification } from "../../../renderer/components/notifications";
import React from "react";
describe("cluster/namespaces - edit namespace from new tab", () => { describe("cluster/namespaces - edit namespace from new tab", () => {
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
let callForResourceMock: AsyncFnMock<CallForResource>; let apiKubePatchMock: AsyncFnMock<ApiKubePatch>;
let callForPatchResourceMock: AsyncFnMock<CallForPatchResource>; let apiKubeGetMock: AsyncFnMock<ApiKubeGet>;
let showSuccessNotificationMock: jest.Mock; let showSuccessNotificationMock: jest.MockedFunction<ShowNotification>;
let showErrorNotificationMock: jest.Mock; let showErrorNotificationMock: jest.MockedFunction<ShowNotification>;
beforeEach(() => { beforeEach(() => {
builder = getApplicationBuilder(); builder = getApplicationBuilder();
@ -57,11 +62,11 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
.mockReturnValueOnce("some-second-tab-id"), .mockReturnValueOnce("some-second-tab-id"),
); );
callForResourceMock = asyncFn(); apiKubePatchMock = asyncFn();
windowDi.override(callForResourceInjectable, () => callForResourceMock); windowDi.override(apiKubePatchInjectable, () => apiKubePatchMock);
callForPatchResourceMock = asyncFn(); apiKubeGetMock = asyncFn();
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock); windowDi.override(apiKubeGetInjectable, () => apiKubeGetMock);
}); });
builder.afterWindowStart(() => { builder.afterWindowStart(() => {
@ -156,16 +161,16 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
}); });
it("calls for namespace", () => { it("calls for namespace", () => {
expect(callForResourceMock).toHaveBeenCalledWith( expect(apiKubeGetMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid", "/apis/some-api-version/namespaces/some-uid",
); );
}); });
describe("when call for namespace resolves with namespace", () => { describe("when call for namespace resolves with namespace", () => {
let someNamespace: Namespace; let someNamespaceData: KubeJsonApiData<BaseKubeJsonApiObjectMetadata<KubeObjectScope.Cluster>, unknown, unknown>;
beforeEach(async () => { beforeEach(async () => {
someNamespace = new Namespace({ someNamespaceData = ({
apiVersion: "some-api-version", apiVersion: "some-api-version",
kind: "Namespace", kind: "Namespace",
@ -173,16 +178,12 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
uid: "some-uid", uid: "some-uid",
name: "some-name", name: "some-name",
resourceVersion: "some-resource-version", resourceVersion: "some-resource-version",
selfLink: "/apis/some-api-version/namespaces/some-uid",
somePropertyToBeRemoved: "some-value", somePropertyToBeRemoved: "some-value",
somePropertyToBeChanged: "some-old-value", somePropertyToBeChanged: "some-old-value",
}, },
}); });
await callForResourceMock.resolve({ await apiKubeGetMock.resolve(someNamespaceData);
callWasSuccessful: true,
response: someNamespace,
});
}); });
it("renders", () => { it("renders", () => {
@ -206,9 +207,9 @@ metadata:
uid: some-uid uid: some-uid
name: some-name name: some-name
resourceVersion: some-resource-version resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value somePropertyToBeChanged: some-old-value
selfLink: /apis/some-api-version/namespaces/some-uid
`); `);
}); });
@ -226,15 +227,22 @@ metadata:
}); });
it("calls for save with just the adding version label", () => { it("calls for save with just the adding version label", () => {
expect(callForPatchResourceMock).toHaveBeenCalledWith( expect(apiKubePatchMock).toHaveBeenCalledWith(
someNamespace, "/apis/some-api-version/namespaces/some-uid",
[{ {
op: "add", data: [{
path: "/metadata/labels", op: "add",
value: { path: "/metadata/labels",
"k8slens-edit-resource-version": "some-api-version", value: {
"k8slens-edit-resource-version": "some-api-version",
},
}],
},
{
headers: {
"content-type": "application/json-patch+json",
}, },
}], },
); );
}); });
@ -262,9 +270,11 @@ metadata:
describe("when saving resolves with success", () => { describe("when saving resolves with success", () => {
beforeEach(async () => { beforeEach(async () => {
await callForPatchResourceMock.resolve({ await apiKubePatchMock.resolve({
callWasSuccessful: true, kind: "Namespace",
response: { name: "some-name", kind: "Namespace" }, metadata: {
name: "some-name",
},
}); });
}); });
@ -309,12 +319,9 @@ metadata:
}); });
}); });
describe("when saving resolves with failure", () => { describe("when saving fails", () => {
beforeEach(async () => { beforeEach(async () => {
await callForPatchResourceMock.resolve({ await apiKubePatchMock.reject(new Error("some-error"));
callWasSuccessful: false,
error: "some-error",
});
}); });
it("renders", () => { it("renders", () => {
@ -380,9 +387,11 @@ metadata:
describe("when saving resolves with success", () => { describe("when saving resolves with success", () => {
beforeEach(async () => { beforeEach(async () => {
await callForPatchResourceMock.resolve({ await apiKubePatchMock.resolve({
callWasSuccessful: true, kind: "Namespace",
response: { name: "some-name", kind: "Namespace" }, metadata: {
name: "some-name",
},
}); });
}); });
@ -397,12 +406,9 @@ metadata:
}); });
}); });
describe("when saving resolves with failure", () => { describe("when saving failings", () => {
beforeEach(async () => { beforeEach(async () => {
await callForPatchResourceMock.resolve({ await apiKubePatchMock.reject(new Error("some-error"));
callWasSuccessful: false,
error: "Some error",
});
}); });
it("renders", () => { it("renders", () => {
@ -415,6 +421,57 @@ metadata:
).toBeInTheDocument(); ).toBeInTheDocument();
}); });
}); });
describe("when saving failings with a JsonApiError", () => {
beforeEach(async () => {
await apiKubePatchMock.reject(new JsonApiErrorParsed(
{
kind: "Status",
apiVersion: "v1",
metadata: {},
status: "Failure",
message: "PodDisruptionBudget.policy \"frontend-pdb\" is invalid: spec.minAvailable: Invalid value: -10: must be greater than or equal to 0",
reason: "Invalid",
details: {
name: "frontend-pdb",
group: "policy",
kind: "PodDisruptionBudget",
causes: [
{
reason: "FieldValueInvalid",
message: "Invalid value: -10: must be greater than or equal to 0",
field: "spec.minAvailable",
},
],
},
code: 422,
},
[
"PodDisruptionBudget.policy \"frontend-pdb\" is invalid: spec.minAvailable: Invalid value: -10: must be greater than or equal to 0",
],
));
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("does not close the dock tab", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows an error notification with a condensed message", () => {
expect(showErrorNotificationMock).toBeCalledWith(
<p>
{"Failed to save resource:"}
{" "}
{'PodDisruptionBudget.policy "frontend-pdb" is invalid: spec.minAvailable: Invalid value: -10: must be greater than or equal to 0'}
</p>,
);
});
});
}); });
describe("when selecting to cancel", () => { describe("when selecting to cancel", () => {
@ -451,9 +508,9 @@ metadata:
uid: some-uid uid: some-uid
name: some-name name: some-name
resourceVersion: some-resource-version resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value someAddedProperty: some-new-value
selfLink: /apis/some-api-version/namespaces/some-uid
`, `,
}, },
}); });
@ -474,9 +531,9 @@ metadata:
uid: some-uid uid: some-uid
name: some-name name: some-name
resourceVersion: some-resource-version resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value someAddedProperty: some-new-value
selfLink: /apis/some-api-version/namespaces/some-uid
`); `);
}); });
@ -499,9 +556,9 @@ metadata:
uid: some-uid uid: some-uid
name: some-name name: some-name
resourceVersion: some-resource-version resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value somePropertyToBeChanged: some-old-value
selfLink: /apis/some-api-version/namespaces/some-uid
`, `,
draft: `apiVersion: some-api-version draft: `apiVersion: some-api-version
kind: Namespace kind: Namespace
@ -509,9 +566,9 @@ metadata:
uid: some-uid uid: some-uid
name: some-name name: some-name
resourceVersion: some-resource-version resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeChanged: some-changed-value somePropertyToBeChanged: some-changed-value
someAddedProperty: some-new-value someAddedProperty: some-new-value
selfLink: /apis/some-api-version/namespaces/some-uid
`, `,
}); });
}); });
@ -526,41 +583,46 @@ metadata:
}); });
it("calls for save with changed configuration", () => { it("calls for save with changed configuration", () => {
expect(callForPatchResourceMock).toHaveBeenCalledWith( expect(apiKubePatchMock).toHaveBeenCalledWith(
someNamespace, "/apis/some-api-version/namespaces/some-uid",
[ {
{ data: [
op: "remove", {
path: "/metadata/somePropertyToBeRemoved", op: "remove",
}, path: "/metadata/somePropertyToBeRemoved",
{
op: "add",
path: "/metadata/someAddedProperty",
value: "some-new-value",
},
{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
}, },
{
op: "add",
path: "/metadata/someAddedProperty",
value: "some-new-value",
},
{
op: "add",
path: "/metadata/labels",
value: {
"k8slens-edit-resource-version": "some-api-version",
},
},
{
op: "replace",
path: "/metadata/somePropertyToBeChanged",
value: "some-changed-value",
},
],
},
{
headers: {
"content-type": "application/json-patch+json",
}, },
{ },
op: "replace",
path: "/metadata/somePropertyToBeChanged",
value: "some-changed-value",
},
],
); );
}); });
it("given save resolves and another change in configuration, when saving, calls for save with changed configuration", async () => { it("given save resolves and another change in configuration, when saving, calls for save with changed configuration", async () => {
await callForPatchResourceMock.resolve({ await apiKubePatchMock.resolve({
callWasSuccessful: true, kind: "Namespace",
metadata: {
response: {
name: "some-name", name: "some-name",
kind: "Namespace",
}, },
}); });
@ -585,7 +647,7 @@ metadata:
}); });
callForPatchResourceMock.mockClear(); apiKubePatchMock.mockClear();
const saveButton = rendered.getByTestId( const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id", "save-edit-resource-from-tab-for-some-first-tab-id",
@ -593,15 +655,22 @@ metadata:
fireEvent.click(saveButton); fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith( expect(apiKubePatchMock).toHaveBeenCalledWith(
someNamespace, "/apis/some-api-version/namespaces/some-uid",
[ {
{ data: [
op: "add", {
path: "/metadata/someOtherAddedProperty", op: "add",
value: "some-other-new-value", path: "/metadata/someOtherAddedProperty",
value: "some-other-new-value",
},
],
},
{
headers: {
"content-type": "application/json-patch+json",
}, },
], },
); );
}); });
}); });
@ -692,7 +761,7 @@ metadata:
describe("given clicking the context menu for second namespace, when clicking to edit namespace", () => { describe("given clicking the context menu for second namespace, when clicking to edit namespace", () => {
beforeEach(() => { beforeEach(() => {
callForResourceMock.mockClear(); apiKubeGetMock.mockClear();
// TODO: Make implementation match the description // TODO: Make implementation match the description
const namespaceStub = new Namespace(someOtherNamespaceDataStub); const namespaceStub = new Namespace(someOtherNamespaceDataStub);
@ -725,7 +794,7 @@ metadata:
}); });
it("calls for second namespace", () => { it("calls for second namespace", () => {
expect(callForResourceMock).toHaveBeenCalledWith( expect(apiKubeGetMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-other-uid", "/apis/some-api-version/namespaces/some-other-uid",
); );
}); });
@ -747,10 +816,7 @@ metadata:
}, },
}); });
await callForResourceMock.resolve({ await apiKubeGetMock.resolve(someOtherNamespace);
callWasSuccessful: true,
response: someOtherNamespace,
});
}); });
it("renders", () => { it("renders", () => {
@ -773,7 +839,7 @@ metadata:
}); });
it("when selecting to save, calls for save of second namespace with just the add edit version label", () => { it("when selecting to save, calls for save of second namespace with just the add edit version label", () => {
callForPatchResourceMock.mockClear(); apiKubePatchMock.mockClear();
const saveButton = rendered.getByTestId( const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-second-tab-id", "save-edit-resource-from-tab-for-some-second-tab-id",
@ -781,21 +847,28 @@ metadata:
fireEvent.click(saveButton); fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith( expect(apiKubePatchMock).toHaveBeenCalledWith(
someOtherNamespace, "/apis/some-api-version/namespaces/some-other-uid",
[{ {
op: "add", data: [{
path: "/metadata/labels", op: "add",
value: { path: "/metadata/labels",
"k8slens-edit-resource-version": "some-api-version", value: {
"k8slens-edit-resource-version": "some-api-version",
},
}],
},
{
headers: {
"content-type": "application/json-patch+json",
}, },
}], },
); );
}); });
describe("when clicking dock tab for the first namespace", () => { describe("when clicking dock tab for the first namespace", () => {
beforeEach(() => { beforeEach(() => {
callForResourceMock.mockClear(); apiKubeGetMock.mockClear();
const tab = rendered.getByTestId("dock-tab-for-some-first-tab-id"); const tab = rendered.getByTestId("dock-tab-for-some-first-tab-id");
@ -825,7 +898,7 @@ metadata:
}); });
it("does not call for namespace", () => { it("does not call for namespace", () => {
expect(callForResourceMock).not.toHaveBeenCalledWith("/apis/some-api-version/namespaces/some-uid"); expect(apiKubeGetMock).not.toHaveBeenCalledWith("/apis/some-api-version/namespaces/some-uid");
}); });
it("has configuration in the editor", () => { it("has configuration in the editor", () => {
@ -839,14 +912,14 @@ metadata:
uid: some-uid uid: some-uid
name: some-name name: some-name
resourceVersion: some-resource-version resourceVersion: some-resource-version
selfLink: /apis/some-api-version/namespaces/some-uid
somePropertyToBeRemoved: some-value somePropertyToBeRemoved: some-value
somePropertyToBeChanged: some-old-value somePropertyToBeChanged: some-old-value
selfLink: /apis/some-api-version/namespaces/some-uid
`); `);
}); });
it("when selecting to save, calls for save of first namespace with just the new edit version label", () => { it("when selecting to save, calls for save of first namespace with just the new edit version label", () => {
callForPatchResourceMock.mockClear(); apiKubePatchMock.mockClear();
const saveButton = rendered.getByTestId( const saveButton = rendered.getByTestId(
"save-edit-resource-from-tab-for-some-first-tab-id", "save-edit-resource-from-tab-for-some-first-tab-id",
@ -854,15 +927,22 @@ metadata:
fireEvent.click(saveButton); fireEvent.click(saveButton);
expect(callForPatchResourceMock).toHaveBeenCalledWith( expect(apiKubePatchMock).toHaveBeenCalledWith(
someNamespace, "/apis/some-api-version/namespaces/some-uid",
[ { {
op: "add", data: [{
path: "/metadata/labels", op: "add",
value: { path: "/metadata/labels",
"k8slens-edit-resource-version": "some-api-version", value: {
"k8slens-edit-resource-version": "some-api-version",
},
}],
},
{
headers: {
"content-type": "application/json-patch+json",
}, },
}], },
); );
}); });
}); });
@ -870,41 +950,9 @@ metadata:
}); });
}); });
describe("when call for namespace resolves without namespace", () => {
beforeEach(async () => {
await callForResourceMock.resolve({
callWasSuccessful: true,
response: undefined,
});
});
it("renders", () => {
expect(rendered.baseElement).toMatchSnapshot();
});
it("still shows the dock tab for editing namespace", () => {
expect(
rendered.getByTestId("dock-tab-for-some-first-tab-id"),
).toBeInTheDocument();
});
it("shows error message", () => {
expect(
rendered.getByTestId("dock-tab-content-for-some-first-tab-id"),
).toHaveTextContent("Resource not found");
});
it("does not show error notification", () => {
expect(showErrorNotificationMock).not.toHaveBeenCalled();
});
});
describe("when call for namespace resolves with failure", () => { describe("when call for namespace resolves with failure", () => {
beforeEach(async () => { beforeEach(async () => {
await callForResourceMock.resolve({ await apiKubeGetMock.reject(new Error("some-error-missing-namespace"));
callWasSuccessful: false,
error: "some-error",
});
}); });
it("renders", () => { it("renders", () => {

View File

@ -8,8 +8,8 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder"; import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
import type { AsyncFnMock } from "@async-fn/jest"; import type { AsyncFnMock } from "@async-fn/jest";
import asyncFn from "@async-fn/jest"; import asyncFn from "@async-fn/jest";
import type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable"; import type { RequestKubeResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/request-kube-resource.injectable";
import callForResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable"; import requestKubeResourceInjectable from "../../../renderer/components/dock/edit-resource/edit-resource-model/request-kube-resource.injectable";
import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable"; import directoryForLensLocalStorageInjectable from "../../../common/directory-for-lens-local-storage/directory-for-lens-local-storage.injectable";
import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable"; import writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
import { TabKind } from "../../../renderer/components/dock/dock/store"; import { TabKind } from "../../../renderer/components/dock/dock/store";
@ -17,14 +17,14 @@ import { Namespace } from "../../../common/k8s-api/endpoints";
describe("cluster/namespaces - edit namespaces from previously opened tab", () => { describe("cluster/namespaces - edit namespaces from previously opened tab", () => {
let builder: ApplicationBuilder; let builder: ApplicationBuilder;
let callForNamespaceMock: AsyncFnMock<CallForResource>; let requestKubeResourceMock: AsyncFnMock<RequestKubeResource>;
beforeEach(() => { beforeEach(() => {
builder = getApplicationBuilder(); builder = getApplicationBuilder();
builder.setEnvironmentToClusterFrame(); builder.setEnvironmentToClusterFrame();
callForNamespaceMock = asyncFn(); requestKubeResourceMock = asyncFn();
builder.beforeWindowStart(({ windowDi }) => { builder.beforeWindowStart(({ windowDi }) => {
windowDi.override( windowDi.override(
@ -32,7 +32,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
() => "/some-directory-for-lens-local-storage", () => "/some-directory-for-lens-local-storage",
); );
windowDi.override(callForResourceInjectable, () => callForNamespaceMock); windowDi.override(requestKubeResourceInjectable, () => requestKubeResourceMock);
}); });
builder.afterWindowStart(() => { builder.afterWindowStart(() => {
@ -97,7 +97,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
}); });
it("calls for namespace", () => { it("calls for namespace", () => {
expect(callForNamespaceMock).toHaveBeenCalledWith( expect(requestKubeResourceMock).toHaveBeenCalledWith(
"/apis/some-api-version/namespaces/some-uid", "/apis/some-api-version/namespaces/some-uid",
); );
}); });
@ -122,7 +122,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
// TODO: Figure out why act is needed here. In CI it works without it. // TODO: Figure out why act is needed here. In CI it works without it.
await act(async () => { await act(async () => {
await callForNamespaceMock.resolve({ await requestKubeResourceMock.resolve({
callWasSuccessful: true, callWasSuccessful: true,
response: someNamespace, response: someNamespace,
}); });

View File

@ -4,6 +4,7 @@
*/ */
import { getInjectable, createInstantiationTargetDecorator, instantiationDecoratorToken } from "@ogre-tools/injectable"; import { getInjectable, createInstantiationTargetDecorator, instantiationDecoratorToken } from "@ogre-tools/injectable";
import { pick } from "lodash"; import { pick } from "lodash";
import { inspect } from "util";
import { parseKubeApi } from "../../../common/k8s-api/kube-api-parse"; import { parseKubeApi } from "../../../common/k8s-api/kube-api-parse";
import showDetailsInjectable from "../../../renderer/components/kube-detail-params/show-details.injectable"; import showDetailsInjectable from "../../../renderer/components/kube-detail-params/show-details.injectable";
import emitTelemetryInjectable from "./emit-telemetry.injectable"; import emitTelemetryInjectable from "./emit-telemetry.injectable";
@ -21,13 +22,15 @@ const telemetryDecoratorForShowDetailsInjectable = getInjectable({
? { ? {
action: "open", action: "open",
...(() => { ...(() => {
try { const parsedApi = parseKubeApi(args[0]);
return {
resource: pick(parseKubeApi(args[0]), "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"), if (!parsedApi) {
}; return { error: `invalid apiPath: ${inspect(args[0])}` };
} catch (error) {
return { error: `${error}` };
} }
return {
resource: pick(parsedApi, "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"),
};
})(), })(),
} }
: { : {

View File

@ -21,7 +21,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
showDetails = di.inject(showDetailsInjectable); showDetails = di.inject(showDetailsInjectable);
}); });
it("when showDetails is called with no selflink (ie closing) should emit telemetry with param indicating closing the drawer", () => { it("when showDetails is called with no selfLink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
showDetails(undefined); showDetails(undefined);
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -34,7 +34,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
}); });
}); });
it("when showDetails is called with empty selflink (ie closing) should emit telemetry with param indicating closing the drawer", () => { it("when showDetails is called with empty selfLink (ie closing) should emit telemetry with param indicating closing the drawer", () => {
showDetails(""); showDetails("");
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -47,7 +47,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
}); });
}); });
it("when showDetails is called with valid selflink should emit telemetry with param indicating opening the drawer with that resource", () => { it("when showDetails is called with valid selfLink should emit telemetry with param indicating opening the drawer with that resource", () => {
showDetails("/api/v1/namespaces/default/pods/some-name"); showDetails("/api/v1/namespaces/default/pods/some-name");
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -68,7 +68,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
}); });
}); });
it("when showDetails is called with invalid selflink should emit telemetry with param indicating opening the drawer but also show error", () => { it("when showDetails is called with invalid selfLink should emit telemetry with param indicating opening the drawer but also show error", () => {
showDetails("some-non-self-link-value"); showDetails("some-non-self-link-value");
expect(emitAppEventMock).toBeCalledWith({ expect(emitAppEventMock).toBeCalledWith({
@ -77,7 +77,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
name: "show-details", name: "show-details",
params: { params: {
action: "open", action: "open",
error: "Error: invalid apiPath: /some-non-self-link-value", error: "invalid apiPath: 'some-non-self-link-value'",
}, },
}); });
}); });

View File

@ -22,8 +22,16 @@ describe("NetworkPolicyDetails", () => {
it("should render w/o errors", () => { it("should render w/o errors", () => {
const policy = new NetworkPolicy({ const policy = new NetworkPolicy({
metadata: {} as never, metadata: {
spec: {} as never, name: "some-network-policy-name",
namespace: "some-namespace",
resourceVersion: "1",
selfLink: "/apis/networking.k8s.io/v1/namespace/some-namespace/some-network-policy-name",
uid: "1",
},
spec: {
podSelector: {},
},
apiVersion: "networking.k8s.io/v1", apiVersion: "networking.k8s.io/v1",
kind: "NetworkPolicy", kind: "NetworkPolicy",
}); });
@ -34,7 +42,13 @@ describe("NetworkPolicyDetails", () => {
it("should render egress nodeSelector", async () => { it("should render egress nodeSelector", async () => {
const policy = new NetworkPolicy({ const policy = new NetworkPolicy({
metadata: {} as never, metadata: {
name: "some-network-policy-name",
namespace: "some-namespace",
resourceVersion: "1",
selfLink: "/apis/networking.k8s.io/v1/namespace/some-namespace/some-network-policy-name",
uid: "1",
},
spec: { spec: {
egress: [{ egress: [{
to: [{ to: [{
@ -58,7 +72,13 @@ describe("NetworkPolicyDetails", () => {
it("should not crash if egress nodeSelector doesn't have matchLabels", async () => { it("should not crash if egress nodeSelector doesn't have matchLabels", async () => {
const policy = new NetworkPolicy({ const policy = new NetworkPolicy({
metadata: {} as never, metadata: {
name: "some-network-policy-name",
namespace: "some-namespace",
resourceVersion: "1",
selfLink: "/apis/networking.k8s.io/v1/namespace/some-namespace/some-network-policy-name",
uid: "1",
},
spec: { spec: {
egress: [{ egress: [{
to: [{ to: [{

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import callForPatchResourceInjectable from "./call-for-patch-resource.injectable";
export default getGlobalOverride(callForPatchResourceInjectable, () => () => {
throw new Error(
"Tried to call patching of kube resource without explicit override.",
);
});

View File

@ -1,49 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AsyncResult } from "@k8slens/utilities";
import apiManagerInjectable from "../../../../../../common/k8s-api/api-manager/manager.injectable";
import type { JsonPatch } from "../../../../../../common/k8s-api/kube-object.store";
import type { KubeObject } from "../../../../../../common/k8s-api/kube-object";
import assert from "assert";
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
export type CallForPatchResource = (
item: KubeObject,
patch: JsonPatch
) => AsyncResult<{ name: string; kind: string }>;
const callForPatchResourceInjectable = getInjectable({
id: "call-for-patch-resource",
instantiate: (di): CallForPatchResource => {
const apiManager = di.inject(apiManagerInjectable);
return async (item, patch) => {
const store = apiManager.getStore(item.selfLink);
assert(store);
let kubeObject: KubeObject;
try {
kubeObject = await store.patch(item, patch);
} catch (e: any) {
return {
callWasSuccessful: false,
error: getErrorMessage(e),
};
}
return {
callWasSuccessful: true,
response: { name: kubeObject.getName(), kind: kubeObject.kind },
};
};
},
causesSideEffects: true,
});
export default callForPatchResourceInjectable;

View File

@ -1,12 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getGlobalOverride } from "@k8slens/test-utils";
import callForResourceInjectable from "./call-for-resource.injectable";
export default getGlobalOverride(callForResourceInjectable, () => () => {
throw new Error(
"Tried to call for kube resource without explicit override.",
);
});

View File

@ -1,41 +0,0 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import { KubeObject } from "../../../../../../common/k8s-api/kube-object";
import { parseKubeApi } from "../../../../../../common/k8s-api/kube-api-parse";
import type { AsyncResult } from "@k8slens/utilities";
import { getErrorMessage } from "../../../../../../common/utils/get-error-message";
import apiKubeInjectable from "../../../../../k8s/api-kube.injectable";
export type CallForResource = (selfLink: string) => AsyncResult<KubeObject | undefined>;
const callForResourceInjectable = getInjectable({
id: "call-for-resource",
instantiate: (di): CallForResource => {
const apiKube = di.inject(apiKubeInjectable);
return async (apiPath: string) => {
const parsed = parseKubeApi(apiPath);
if (!parsed.name) {
return { callWasSuccessful: false, error: "Invalid API path" };
}
try {
return {
callWasSuccessful: true,
response: new KubeObject(await apiKube.get(apiPath)),
};
} catch (e) {
return { callWasSuccessful: false, error: getErrorMessage(e) };
}
};
},
causesSideEffects: true,
});
export default callForResourceInjectable;

View File

@ -3,8 +3,8 @@
* Licensed under MIT License. See LICENSE in root directory for more information. * Licensed under MIT License. See LICENSE in root directory for more information.
*/ */
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable"; import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
import type { CallForResource } from "./call-for-resource/call-for-resource.injectable"; import type { RequestKubeResource } from "./request-kube-resource.injectable";
import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable"; import requestKubeResourceInjectable from "./request-kube-resource.injectable";
import { waitUntilDefined } from "@k8slens/utilities"; import { waitUntilDefined } from "@k8slens/utilities";
import editResourceTabStoreInjectable from "../store.injectable"; import editResourceTabStoreInjectable from "../store.injectable";
import type { EditingResource, EditResourceTabStore } from "../store"; import type { EditingResource, EditResourceTabStore } from "../store";
@ -12,8 +12,8 @@ import { action, computed, observable, runInAction } from "mobx";
import type { KubeObject, RawKubeObject } from "../../../../../common/k8s-api/kube-object"; import type { KubeObject, RawKubeObject } from "../../../../../common/k8s-api/kube-object";
import yaml from "js-yaml"; import yaml from "js-yaml";
import assert from "assert"; import assert from "assert";
import type { CallForPatchResource } from "./call-for-patch-resource/call-for-patch-resource.injectable"; import type { RequestPatchKubeResource } from "./request-patch-kube-resource.injectable";
import callForPatchResourceInjectable from "./call-for-patch-resource/call-for-patch-resource.injectable"; import requestPatchKubeResourceInjectable from "./request-patch-kube-resource.injectable";
import { createPatch } from "rfc6902"; import { createPatch } from "rfc6902";
import type { ShowNotification } from "../../../notifications"; import type { ShowNotification } from "../../../notifications";
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable"; import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
@ -28,8 +28,8 @@ const editResourceModelInjectable = getInjectable({
const store = di.inject(editResourceTabStoreInjectable); const store = di.inject(editResourceTabStoreInjectable);
const model = new EditResourceModel({ const model = new EditResourceModel({
callForResource: di.inject(callForResourceInjectable), requestKubeResource: di.inject(requestKubeResourceInjectable),
callForPatchResource: di.inject(callForPatchResourceInjectable), requestPatchKubeResource: di.inject(requestPatchKubeResourceInjectable),
showSuccessNotification: di.inject(showSuccessNotificationInjectable), showSuccessNotification: di.inject(showSuccessNotificationInjectable),
showErrorNotification: di.inject(showErrorNotificationInjectable), showErrorNotification: di.inject(showErrorNotificationInjectable),
store, store,
@ -50,8 +50,8 @@ const editResourceModelInjectable = getInjectable({
export default editResourceModelInjectable; export default editResourceModelInjectable;
interface Dependencies { interface Dependencies {
callForResource: CallForResource; requestKubeResource: RequestKubeResource;
callForPatchResource: CallForPatchResource; requestPatchKubeResource: RequestPatchKubeResource;
waitForEditingResource: () => Promise<EditingResource>; waitForEditingResource: () => Promise<EditingResource>;
showSuccessNotification: ShowNotification; showSuccessNotification: ShowNotification;
showErrorNotification: ShowNotification; showErrorNotification: ShowNotification;
@ -59,17 +59,21 @@ interface Dependencies {
readonly tabId: string; readonly tabId: string;
} }
function getEditSelfLinkFor(object: RawKubeObject): string { function getEditSelfLinkFor(object: RawKubeObject): string | undefined {
const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName]; const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName];
if (lensVersionLabel) { if (lensVersionLabel) {
const { apiVersionWithGroup, ...parsedApi } = parseKubeApi(object.metadata.selfLink); const parsedKubeApi = parseKubeApi(object.metadata.selfLink);
parsedApi.apiVersion = lensVersionLabel; if (!parsedKubeApi) {
return undefined;
}
const { apiVersionWithGroup, ...parsedApi } = parsedKubeApi;
return createKubeApiURL({ return createKubeApiURL({
...parsedApi, ...parsedApi,
apiVersion: `${parsedApi.apiGroup}/${parsedApi.apiVersion}`, apiVersion: lensVersionLabel,
}); });
} }
@ -130,7 +134,7 @@ export class EditResourceModel {
load = async (): Promise<void> => { load = async (): Promise<void> => {
await this.dependencies.waitForEditingResource(); await this.dependencies.waitForEditingResource();
let result = await this.dependencies.callForResource(this.selfLink); let result = await this.dependencies.requestKubeResource(this.selfLink);
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`); return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
@ -139,9 +143,13 @@ export class EditResourceModel {
if (result?.response?.metadata.labels?.[EditResourceLabelName]) { if (result?.response?.metadata.labels?.[EditResourceLabelName]) {
const parsed = parseKubeApi(this.selfLink); const parsed = parseKubeApi(this.selfLink);
if (!parsed) {
return void this.dependencies.showErrorNotification(`Object's selfLink is invalid: "${this.selfLink}"`);
}
parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName]; parsed.apiVersion = result.response.metadata.labels[EditResourceLabelName];
result = await this.dependencies.callForResource(createKubeApiURL(parsed)); result = await this.dependencies.requestKubeResource(createKubeApiURL(parsed));
} }
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {
@ -186,7 +194,18 @@ export class EditResourceModel {
const patches = createPatch(firstVersion, currentVersion); const patches = createPatch(firstVersion, currentVersion);
const selfLink = getEditSelfLinkFor(currentVersion); const selfLink = getEditSelfLinkFor(currentVersion);
const result = await this.dependencies.callForPatchResource(this.resource, patches);
if (!selfLink) {
this.dependencies.showErrorNotification((
<p>
{`Cannot save resource, unknown selfLink: "${currentVersion.metadata.selfLink}"`}
</p>
));
return null;
}
const result = await this.dependencies.requestPatchKubeResource(selfLink, patches);
if (!result.callWasSuccessful) { if (!result.callWasSuccessful) {
this.dependencies.showErrorNotification(( this.dependencies.showErrorNotification((

View File

@ -0,0 +1,46 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeObjectMetadata, KubeObjectScope } from "../../../../../common/k8s-api/kube-object";
import { KubeObject } from "../../../../../common/k8s-api/kube-object";
import type { AsyncResult } from "@k8slens/utilities";
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
import type { Writable } from "type-fest";
import type { KubeJsonApiData } from "../../../../../common/k8s-api/kube-json-api";
import { parseKubeApi } from "../../../../../common/k8s-api/kube-api-parse";
import apiKubeGetInjectable from "../../../../k8s/api-kube-get.injectable";
export type RequestKubeResource = (selfLink: string) => AsyncResult<KubeObject | undefined>;
const requestKubeResourceInjectable = getInjectable({
id: "request-kube-resource",
instantiate: (di): RequestKubeResource => {
const apiKubeGet = di.inject(apiKubeGetInjectable);
return async (selfLink) => {
const parsed = parseKubeApi(selfLink);
if (!parsed?.name) {
return { callWasSuccessful: false, error: "Invalid API path" };
}
try {
const rawData = await apiKubeGet(selfLink) as KubeJsonApiData<KubeObjectMetadata<KubeObjectScope>, unknown, unknown>;
(rawData.metadata as Writable<typeof rawData.metadata>).selfLink = selfLink;
return {
callWasSuccessful: true,
response: new KubeObject(rawData),
};
} catch (e) {
return { callWasSuccessful: false, error: getErrorMessage(e) };
}
};
},
});
export default requestKubeResourceInjectable;

View File

@ -0,0 +1,41 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { AsyncResult } from "@k8slens/utilities";
import type { JsonPatch } from "../../../../../common/k8s-api/kube-object.store";
import { getErrorMessage } from "../../../../../common/utils/get-error-message";
import { patchTypeHeaders } from "../../../../../common/k8s-api/kube-api";
import apiKubePatchInjectable from "../../../../k8s/api-kube-patch.injectable";
export type RequestPatchKubeResource = (selfLink: string, patch: JsonPatch) => AsyncResult<{ name: string; kind: string }>;
const requestPatchKubeResourceInjectable = getInjectable({
id: "request-patch-kube-resource",
instantiate: (di): RequestPatchKubeResource => {
const apiKubePatch = di.inject(apiKubePatchInjectable);
return async (selfLink, patch) => {
try {
const { metadata, kind } = await apiKubePatch(selfLink, { data: patch }, {
headers: {
"content-type": patchTypeHeaders.json,
},
});
return {
callWasSuccessful: true,
response: { name: metadata.name, kind },
};
} catch (e) {
return {
callWasSuccessful: false,
error: getErrorMessage(e),
};
}
};
},
});
export default requestPatchKubeResourceInjectable;

View File

@ -16,7 +16,6 @@ const showErrorNotificationInjectable = getInjectable({
return (message, customOpts = {}) => return (message, customOpts = {}) =>
notificationsStore.add({ notificationsStore.add({
status: NotificationStatus.ERROR, status: NotificationStatus.ERROR,
timeout: 5000,
message, message,
...customOpts, ...customOpts,
}); });

View File

@ -16,7 +16,6 @@ const showInfoNotificationInjectable = getInjectable({
return (message, customOpts = {}) => return (message, customOpts = {}) =>
notificationsStore.add({ notificationsStore.add({
status: NotificationStatus.INFO, status: NotificationStatus.INFO,
timeout: 5000,
message, message,
...customOpts, ...customOpts,
}); });

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeJsonApi } from "../../common/k8s-api/kube-json-api";
import apiKubeInjectable from "./api-kube.injectable";
export type ApiKubeGet = KubeJsonApi["get"];
const apiKubeGetInjectable = getInjectable({
id: "api-kube-get",
instantiate: (di): ApiKubeGet => {
const apiKube = di.inject(apiKubeInjectable);
return (...params) => apiKube.get(...params);
},
});
export default apiKubeGetInjectable;

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { getInjectable } from "@ogre-tools/injectable";
import type { KubeJsonApi } from "../../common/k8s-api/kube-json-api";
import apiKubeInjectable from "./api-kube.injectable";
export type ApiKubePatch = KubeJsonApi["patch"];
const apiKubePatchInjectable = getInjectable({
id: "api-kube-patch",
instantiate: (di): ApiKubePatch => {
const apiKube = di.inject(apiKubeInjectable);
return (...params) => apiKube.patch(...params);
},
});
export default apiKubePatchInjectable;