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:
commit
804f45422d
2
.github/workflows/cron-test.yaml
vendored
2
.github/workflows/cron-test.yaml
vendored
@ -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
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<D> {
|
||||
data?: PartialDeep<D>; // request body
|
||||
}
|
||||
@ -246,7 +270,7 @@ export class JsonApi<Data = JsonApiData, Params extends JsonApiParams<Data> = 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() {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
@ -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");
|
||||
|
||||
|
||||
@ -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> = K extends KubeObject<infer Metadata, infer Status, infer Spec>
|
||||
@ -552,14 +548,30 @@ export class KubeObject<
|
||||
}
|
||||
|
||||
constructor(data: KubeJsonApiData<Metadata, Status, Spec>) {
|
||||
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);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -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 type { AsyncFnMock } 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 { Namespace } from "../../../common/k8s-api/endpoints";
|
||||
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 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 type { CallForResource } from "../../../renderer/components/dock/edit-resource/edit-resource-model/call-for-resource/call-for-resource.injectable";
|
||||
import callForResourceInjectable 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 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", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let callForResourceMock: AsyncFnMock<CallForResource>;
|
||||
let callForPatchResourceMock: AsyncFnMock<CallForPatchResource>;
|
||||
let showSuccessNotificationMock: jest.Mock;
|
||||
let showErrorNotificationMock: jest.Mock;
|
||||
let apiKubePatchMock: AsyncFnMock<ApiKubePatch>;
|
||||
let apiKubeGetMock: AsyncFnMock<ApiKubeGet>;
|
||||
let showSuccessNotificationMock: jest.MockedFunction<ShowNotification>;
|
||||
let showErrorNotificationMock: jest.MockedFunction<ShowNotification>;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
@ -57,11 +62,11 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
|
||||
.mockReturnValueOnce("some-second-tab-id"),
|
||||
);
|
||||
|
||||
callForResourceMock = asyncFn();
|
||||
windowDi.override(callForResourceInjectable, () => callForResourceMock);
|
||||
apiKubePatchMock = asyncFn();
|
||||
windowDi.override(apiKubePatchInjectable, () => apiKubePatchMock);
|
||||
|
||||
callForPatchResourceMock = asyncFn();
|
||||
windowDi.override(callForPatchResourceInjectable, () => callForPatchResourceMock);
|
||||
apiKubeGetMock = asyncFn();
|
||||
windowDi.override(apiKubeGetInjectable, () => apiKubeGetMock);
|
||||
});
|
||||
|
||||
builder.afterWindowStart(() => {
|
||||
@ -156,16 +161,16 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
|
||||
});
|
||||
|
||||
it("calls for namespace", () => {
|
||||
expect(callForResourceMock).toHaveBeenCalledWith(
|
||||
expect(apiKubeGetMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-uid",
|
||||
);
|
||||
});
|
||||
|
||||
describe("when call for namespace resolves with namespace", () => {
|
||||
let someNamespace: Namespace;
|
||||
let someNamespaceData: KubeJsonApiData<BaseKubeJsonApiObjectMetadata<KubeObjectScope.Cluster>, unknown, unknown>;
|
||||
|
||||
beforeEach(async () => {
|
||||
someNamespace = new Namespace({
|
||||
someNamespaceData = ({
|
||||
apiVersion: "some-api-version",
|
||||
kind: "Namespace",
|
||||
|
||||
@ -173,16 +178,12 @@ describe("cluster/namespaces - edit namespace from new tab", () => {
|
||||
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",
|
||||
},
|
||||
});
|
||||
|
||||
await callForResourceMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: someNamespace,
|
||||
});
|
||||
await apiKubeGetMock.resolve(someNamespaceData);
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
@ -206,9 +207,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
|
||||
`);
|
||||
});
|
||||
|
||||
@ -226,15 +227,22 @@ metadata:
|
||||
});
|
||||
|
||||
it("calls for save with just the adding version label", () => {
|
||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||
someNamespace,
|
||||
[{
|
||||
op: "add",
|
||||
path: "/metadata/labels",
|
||||
value: {
|
||||
"k8slens-edit-resource-version": "some-api-version",
|
||||
expect(apiKubePatchMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-uid",
|
||||
{
|
||||
data: [{
|
||||
op: "add",
|
||||
path: "/metadata/labels",
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
await callForPatchResourceMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: { name: "some-name", kind: "Namespace" },
|
||||
await apiKubePatchMock.resolve({
|
||||
kind: "Namespace",
|
||||
metadata: {
|
||||
name: "some-name",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -309,12 +319,9 @@ metadata:
|
||||
});
|
||||
});
|
||||
|
||||
describe("when saving resolves with failure", () => {
|
||||
describe("when saving fails", () => {
|
||||
beforeEach(async () => {
|
||||
await callForPatchResourceMock.resolve({
|
||||
callWasSuccessful: false,
|
||||
error: "some-error",
|
||||
});
|
||||
await apiKubePatchMock.reject(new Error("some-error"));
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
@ -380,9 +387,11 @@ metadata:
|
||||
|
||||
describe("when saving resolves with success", () => {
|
||||
beforeEach(async () => {
|
||||
await callForPatchResourceMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: { name: "some-name", kind: "Namespace" },
|
||||
await apiKubePatchMock.resolve({
|
||||
kind: "Namespace",
|
||||
metadata: {
|
||||
name: "some-name",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -397,12 +406,9 @@ metadata:
|
||||
});
|
||||
});
|
||||
|
||||
describe("when saving resolves with failure", () => {
|
||||
describe("when saving failings", () => {
|
||||
beforeEach(async () => {
|
||||
await callForPatchResourceMock.resolve({
|
||||
callWasSuccessful: false,
|
||||
error: "Some error",
|
||||
});
|
||||
await apiKubePatchMock.reject(new Error("some-error"));
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
@ -415,6 +421,57 @@ metadata:
|
||||
).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", () => {
|
||||
@ -451,9 +508,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
|
||||
`,
|
||||
},
|
||||
});
|
||||
@ -474,9 +531,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
|
||||
`);
|
||||
});
|
||||
|
||||
@ -499,9 +556,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
|
||||
`,
|
||||
draft: `apiVersion: some-api-version
|
||||
kind: Namespace
|
||||
@ -509,9 +566,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
|
||||
`,
|
||||
});
|
||||
});
|
||||
@ -526,41 +583,46 @@ metadata:
|
||||
});
|
||||
|
||||
it("calls for save with changed configuration", () => {
|
||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||
someNamespace,
|
||||
[
|
||||
{
|
||||
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",
|
||||
expect(apiKubePatchMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-uid",
|
||||
{
|
||||
data: [
|
||||
{
|
||||
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: "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 () => {
|
||||
await callForPatchResourceMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
|
||||
response: {
|
||||
await apiKubePatchMock.resolve({
|
||||
kind: "Namespace",
|
||||
metadata: {
|
||||
name: "some-name",
|
||||
kind: "Namespace",
|
||||
},
|
||||
});
|
||||
|
||||
@ -585,7 +647,7 @@ metadata:
|
||||
});
|
||||
|
||||
|
||||
callForPatchResourceMock.mockClear();
|
||||
apiKubePatchMock.mockClear();
|
||||
|
||||
const saveButton = rendered.getByTestId(
|
||||
"save-edit-resource-from-tab-for-some-first-tab-id",
|
||||
@ -593,15 +655,22 @@ metadata:
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||
someNamespace,
|
||||
[
|
||||
{
|
||||
op: "add",
|
||||
path: "/metadata/someOtherAddedProperty",
|
||||
value: "some-other-new-value",
|
||||
expect(apiKubePatchMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-uid",
|
||||
{
|
||||
data: [
|
||||
{
|
||||
op: "add",
|
||||
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", () => {
|
||||
beforeEach(() => {
|
||||
callForResourceMock.mockClear();
|
||||
apiKubeGetMock.mockClear();
|
||||
|
||||
// TODO: Make implementation match the description
|
||||
const namespaceStub = new Namespace(someOtherNamespaceDataStub);
|
||||
@ -725,7 +794,7 @@ metadata:
|
||||
});
|
||||
|
||||
it("calls for second namespace", () => {
|
||||
expect(callForResourceMock).toHaveBeenCalledWith(
|
||||
expect(apiKubeGetMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-other-uid",
|
||||
);
|
||||
});
|
||||
@ -747,10 +816,7 @@ metadata:
|
||||
},
|
||||
});
|
||||
|
||||
await callForResourceMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: someOtherNamespace,
|
||||
});
|
||||
await apiKubeGetMock.resolve(someOtherNamespace);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
callForPatchResourceMock.mockClear();
|
||||
apiKubePatchMock.mockClear();
|
||||
|
||||
const saveButton = rendered.getByTestId(
|
||||
"save-edit-resource-from-tab-for-some-second-tab-id",
|
||||
@ -781,21 +847,28 @@ metadata:
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||
someOtherNamespace,
|
||||
[{
|
||||
op: "add",
|
||||
path: "/metadata/labels",
|
||||
value: {
|
||||
"k8slens-edit-resource-version": "some-api-version",
|
||||
expect(apiKubePatchMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-other-uid",
|
||||
{
|
||||
data: [{
|
||||
op: "add",
|
||||
path: "/metadata/labels",
|
||||
value: {
|
||||
"k8slens-edit-resource-version": "some-api-version",
|
||||
},
|
||||
}],
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
"content-type": "application/json-patch+json",
|
||||
},
|
||||
}],
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("when clicking dock tab for the first namespace", () => {
|
||||
beforeEach(() => {
|
||||
callForResourceMock.mockClear();
|
||||
apiKubeGetMock.mockClear();
|
||||
|
||||
const tab = rendered.getByTestId("dock-tab-for-some-first-tab-id");
|
||||
|
||||
@ -825,7 +898,7 @@ metadata:
|
||||
});
|
||||
|
||||
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", () => {
|
||||
@ -839,14 +912,14 @@ 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
|
||||
`);
|
||||
});
|
||||
|
||||
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(
|
||||
"save-edit-resource-from-tab-for-some-first-tab-id",
|
||||
@ -854,15 +927,22 @@ metadata:
|
||||
|
||||
fireEvent.click(saveButton);
|
||||
|
||||
expect(callForPatchResourceMock).toHaveBeenCalledWith(
|
||||
someNamespace,
|
||||
[ {
|
||||
op: "add",
|
||||
path: "/metadata/labels",
|
||||
value: {
|
||||
"k8slens-edit-resource-version": "some-api-version",
|
||||
expect(apiKubePatchMock).toHaveBeenCalledWith(
|
||||
"/apis/some-api-version/namespaces/some-uid",
|
||||
{
|
||||
data: [{
|
||||
op: "add",
|
||||
path: "/metadata/labels",
|
||||
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", () => {
|
||||
beforeEach(async () => {
|
||||
await callForResourceMock.resolve({
|
||||
callWasSuccessful: false,
|
||||
error: "some-error",
|
||||
});
|
||||
await apiKubeGetMock.reject(new Error("some-error-missing-namespace"));
|
||||
});
|
||||
|
||||
it("renders", () => {
|
||||
|
||||
@ -8,8 +8,8 @@ import type { ApplicationBuilder } from "../../../renderer/components/test-utils
|
||||
import { getApplicationBuilder } from "../../../renderer/components/test-utils/get-application-builder";
|
||||
import type { AsyncFnMock } 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 callForResourceInjectable 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 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 writeJsonFileInjectable from "../../../common/fs/write-json-file.injectable";
|
||||
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", () => {
|
||||
let builder: ApplicationBuilder;
|
||||
let callForNamespaceMock: AsyncFnMock<CallForResource>;
|
||||
let requestKubeResourceMock: AsyncFnMock<RequestKubeResource>;
|
||||
|
||||
beforeEach(() => {
|
||||
builder = getApplicationBuilder();
|
||||
|
||||
builder.setEnvironmentToClusterFrame();
|
||||
|
||||
callForNamespaceMock = asyncFn();
|
||||
requestKubeResourceMock = asyncFn();
|
||||
|
||||
builder.beforeWindowStart(({ windowDi }) => {
|
||||
windowDi.override(
|
||||
@ -32,7 +32,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
||||
() => "/some-directory-for-lens-local-storage",
|
||||
);
|
||||
|
||||
windowDi.override(callForResourceInjectable, () => callForNamespaceMock);
|
||||
windowDi.override(requestKubeResourceInjectable, () => requestKubeResourceMock);
|
||||
});
|
||||
|
||||
builder.afterWindowStart(() => {
|
||||
@ -97,7 +97,7 @@ describe("cluster/namespaces - edit namespaces from previously opened tab", () =
|
||||
});
|
||||
|
||||
it("calls for namespace", () => {
|
||||
expect(callForNamespaceMock).toHaveBeenCalledWith(
|
||||
expect(requestKubeResourceMock).toHaveBeenCalledWith(
|
||||
"/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.
|
||||
await act(async () => {
|
||||
await callForNamespaceMock.resolve({
|
||||
await requestKubeResourceMock.resolve({
|
||||
callWasSuccessful: true,
|
||||
response: someNamespace,
|
||||
});
|
||||
|
||||
@ -4,6 +4,7 @@
|
||||
*/
|
||||
import { getInjectable, createInstantiationTargetDecorator, instantiationDecoratorToken } from "@ogre-tools/injectable";
|
||||
import { pick } from "lodash";
|
||||
import { inspect } from "util";
|
||||
import { parseKubeApi } from "../../../common/k8s-api/kube-api-parse";
|
||||
import showDetailsInjectable from "../../../renderer/components/kube-detail-params/show-details.injectable";
|
||||
import emitTelemetryInjectable from "./emit-telemetry.injectable";
|
||||
@ -21,13 +22,15 @@ const telemetryDecoratorForShowDetailsInjectable = getInjectable({
|
||||
? {
|
||||
action: "open",
|
||||
...(() => {
|
||||
try {
|
||||
return {
|
||||
resource: pick(parseKubeApi(args[0]), "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"),
|
||||
};
|
||||
} catch (error) {
|
||||
return { error: `${error}` };
|
||||
const parsedApi = parseKubeApi(args[0]);
|
||||
|
||||
if (!parsedApi) {
|
||||
return { error: `invalid apiPath: ${inspect(args[0])}` };
|
||||
}
|
||||
|
||||
return {
|
||||
resource: pick(parsedApi, "apiPrefix", "apiVersion", "apiGroup", "namespace", "resource", "name"),
|
||||
};
|
||||
})(),
|
||||
}
|
||||
: {
|
||||
|
||||
@ -21,7 +21,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
|
||||
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);
|
||||
|
||||
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("");
|
||||
|
||||
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");
|
||||
|
||||
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");
|
||||
|
||||
expect(emitAppEventMock).toBeCalledWith({
|
||||
@ -77,7 +77,7 @@ describe("emit telemetry with params for calls to showDetails", () => {
|
||||
name: "show-details",
|
||||
params: {
|
||||
action: "open",
|
||||
error: "Error: invalid apiPath: /some-non-self-link-value",
|
||||
error: "invalid apiPath: 'some-non-self-link-value'",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -22,8 +22,16 @@ describe("NetworkPolicyDetails", () => {
|
||||
|
||||
it("should render w/o errors", () => {
|
||||
const policy = new NetworkPolicy({
|
||||
metadata: {} as never,
|
||||
spec: {} 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: {
|
||||
podSelector: {},
|
||||
},
|
||||
apiVersion: "networking.k8s.io/v1",
|
||||
kind: "NetworkPolicy",
|
||||
});
|
||||
@ -34,7 +42,13 @@ describe("NetworkPolicyDetails", () => {
|
||||
|
||||
it("should render egress nodeSelector", async () => {
|
||||
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: {
|
||||
egress: [{
|
||||
to: [{
|
||||
@ -58,7 +72,13 @@ describe("NetworkPolicyDetails", () => {
|
||||
|
||||
it("should not crash if egress nodeSelector doesn't have matchLabels", async () => {
|
||||
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: {
|
||||
egress: [{
|
||||
to: [{
|
||||
|
||||
@ -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.",
|
||||
);
|
||||
});
|
||||
@ -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;
|
||||
@ -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.",
|
||||
);
|
||||
});
|
||||
@ -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;
|
||||
@ -3,8 +3,8 @@
|
||||
* Licensed under MIT License. See LICENSE in root directory for more information.
|
||||
*/
|
||||
import { getInjectable, lifecycleEnum } from "@ogre-tools/injectable";
|
||||
import type { CallForResource } from "./call-for-resource/call-for-resource.injectable";
|
||||
import callForResourceInjectable from "./call-for-resource/call-for-resource.injectable";
|
||||
import type { RequestKubeResource } from "./request-kube-resource.injectable";
|
||||
import requestKubeResourceInjectable from "./request-kube-resource.injectable";
|
||||
import { waitUntilDefined } from "@k8slens/utilities";
|
||||
import editResourceTabStoreInjectable from "../store.injectable";
|
||||
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 yaml from "js-yaml";
|
||||
import assert from "assert";
|
||||
import type { CallForPatchResource } from "./call-for-patch-resource/call-for-patch-resource.injectable";
|
||||
import callForPatchResourceInjectable from "./call-for-patch-resource/call-for-patch-resource.injectable";
|
||||
import type { RequestPatchKubeResource } from "./request-patch-kube-resource.injectable";
|
||||
import requestPatchKubeResourceInjectable from "./request-patch-kube-resource.injectable";
|
||||
import { createPatch } from "rfc6902";
|
||||
import type { ShowNotification } from "../../../notifications";
|
||||
import showSuccessNotificationInjectable from "../../../notifications/show-success-notification.injectable";
|
||||
@ -28,8 +28,8 @@ const editResourceModelInjectable = getInjectable({
|
||||
const store = di.inject(editResourceTabStoreInjectable);
|
||||
|
||||
const model = new EditResourceModel({
|
||||
callForResource: di.inject(callForResourceInjectable),
|
||||
callForPatchResource: di.inject(callForPatchResourceInjectable),
|
||||
requestKubeResource: di.inject(requestKubeResourceInjectable),
|
||||
requestPatchKubeResource: di.inject(requestPatchKubeResourceInjectable),
|
||||
showSuccessNotification: di.inject(showSuccessNotificationInjectable),
|
||||
showErrorNotification: di.inject(showErrorNotificationInjectable),
|
||||
store,
|
||||
@ -50,8 +50,8 @@ const editResourceModelInjectable = getInjectable({
|
||||
export default editResourceModelInjectable;
|
||||
|
||||
interface Dependencies {
|
||||
callForResource: CallForResource;
|
||||
callForPatchResource: CallForPatchResource;
|
||||
requestKubeResource: RequestKubeResource;
|
||||
requestPatchKubeResource: RequestPatchKubeResource;
|
||||
waitForEditingResource: () => Promise<EditingResource>;
|
||||
showSuccessNotification: ShowNotification;
|
||||
showErrorNotification: ShowNotification;
|
||||
@ -59,17 +59,21 @@ interface Dependencies {
|
||||
readonly tabId: string;
|
||||
}
|
||||
|
||||
function getEditSelfLinkFor(object: RawKubeObject): string {
|
||||
function getEditSelfLinkFor(object: RawKubeObject): string | undefined {
|
||||
const lensVersionLabel = object.metadata.labels?.[EditResourceLabelName];
|
||||
|
||||
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({
|
||||
...parsedApi,
|
||||
apiVersion: `${parsedApi.apiGroup}/${parsedApi.apiVersion}`,
|
||||
apiVersion: lensVersionLabel,
|
||||
});
|
||||
}
|
||||
|
||||
@ -130,7 +134,7 @@ export class EditResourceModel {
|
||||
load = async (): Promise<void> => {
|
||||
await this.dependencies.waitForEditingResource();
|
||||
|
||||
let result = await this.dependencies.callForResource(this.selfLink);
|
||||
let result = await this.dependencies.requestKubeResource(this.selfLink);
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
return void this.dependencies.showErrorNotification(`Loading resource failed: ${result.error}`);
|
||||
@ -139,9 +143,13 @@ export class EditResourceModel {
|
||||
if (result?.response?.metadata.labels?.[EditResourceLabelName]) {
|
||||
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];
|
||||
|
||||
result = await this.dependencies.callForResource(createKubeApiURL(parsed));
|
||||
result = await this.dependencies.requestKubeResource(createKubeApiURL(parsed));
|
||||
}
|
||||
|
||||
if (!result.callWasSuccessful) {
|
||||
@ -186,7 +194,18 @@ export class EditResourceModel {
|
||||
|
||||
const patches = createPatch(firstVersion, 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) {
|
||||
this.dependencies.showErrorNotification((
|
||||
|
||||
@ -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;
|
||||
@ -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;
|
||||
@ -16,7 +16,6 @@ const showErrorNotificationInjectable = getInjectable({
|
||||
return (message, customOpts = {}) =>
|
||||
notificationsStore.add({
|
||||
status: NotificationStatus.ERROR,
|
||||
timeout: 5000,
|
||||
message,
|
||||
...customOpts,
|
||||
});
|
||||
|
||||
@ -16,7 +16,6 @@ const showInfoNotificationInjectable = getInjectable({
|
||||
return (message, customOpts = {}) =>
|
||||
notificationsStore.add({
|
||||
status: NotificationStatus.INFO,
|
||||
timeout: 5000,
|
||||
message,
|
||||
...customOpts,
|
||||
});
|
||||
|
||||
20
packages/core/src/renderer/k8s/api-kube-get.injectable.ts
Normal file
20
packages/core/src/renderer/k8s/api-kube-get.injectable.ts
Normal 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;
|
||||
20
packages/core/src/renderer/k8s/api-kube-patch.injectable.ts
Normal file
20
packages/core/src/renderer/k8s/api-kube-patch.injectable.ts
Normal 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;
|
||||
Loading…
Reference in New Issue
Block a user