mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Support using Eviction API where applicable (#7372)
* Support using Eviction API when deleting Pods and Deployments, fix #5602 Signed-off-by: Roman <ixrock@gmail.com> * fix: `KubeStatus` message/status/info full explanation Signed-off-by: Roman <ixrock@gmail.com> * added some tests for `PodApi.evict(resourceDescriptor)` Signed-off-by: Roman <ixrock@gmail.com> * revert props.editable & props.removable for `KubeObjectMenu` Signed-off-by: Roman <ixrock@gmail.com> * revert props.editable & props.removable for `KubeObjectMenu` -- missing parts Signed-off-by: Roman <ixrock@gmail.com> --------- Signed-off-by: Roman <ixrock@gmail.com>
This commit is contained in:
parent
a6e0da1297
commit
3931c90d30
@ -11,20 +11,31 @@ import { Deployment, Pod, PodApi } from "../endpoints";
|
|||||||
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
import { getDiForUnitTesting } from "../../../renderer/getDiForUnitTesting";
|
||||||
import type { Fetch } from "../../fetch/fetch.injectable";
|
import type { Fetch } from "../../fetch/fetch.injectable";
|
||||||
import fetchInjectable from "../../fetch/fetch.injectable";
|
import fetchInjectable from "../../fetch/fetch.injectable";
|
||||||
import type { CreateKubeApiForRemoteCluster } from "../create-kube-api-for-remote-cluster.injectable";
|
import type {
|
||||||
import createKubeApiForRemoteClusterInjectable from "../create-kube-api-for-remote-cluster.injectable";
|
CreateKubeApiForRemoteCluster,
|
||||||
|
} from "../create-kube-api-for-remote-cluster.injectable";
|
||||||
|
import createKubeApiForRemoteClusterInjectable
|
||||||
|
from "../create-kube-api-for-remote-cluster.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 { flushPromises } from "@k8slens/test-utils";
|
import { flushPromises } from "@k8slens/test-utils";
|
||||||
import createKubeJsonApiInjectable from "../create-kube-json-api.injectable";
|
import createKubeJsonApiInjectable from "../create-kube-json-api.injectable";
|
||||||
import type { IKubeWatchEvent } from "../kube-watch-event";
|
import type { IKubeWatchEvent } from "../kube-watch-event";
|
||||||
import type { KubeJsonApiDataFor } from "../kube-object";
|
import type { KubeJsonApiDataFor, KubeStatusData } from "../kube-object";
|
||||||
import setupAutoRegistrationInjectable from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable";
|
import setupAutoRegistrationInjectable
|
||||||
import { createMockResponseFromStream, createMockResponseFromString } from "../../../test-utils/mock-responses";
|
from "../../../renderer/before-frame-starts/runnables/setup-auto-registration.injectable";
|
||||||
import storesAndApisCanBeCreatedInjectable from "../../../renderer/stores-apis-can-be-created.injectable";
|
import {
|
||||||
import directoryForUserDataInjectable from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
createMockResponseFromStream,
|
||||||
import hostedClusterInjectable from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
|
createMockResponseFromString,
|
||||||
import directoryForKubeConfigsInjectable from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
} from "../../../test-utils/mock-responses";
|
||||||
|
import storesAndApisCanBeCreatedInjectable
|
||||||
|
from "../../../renderer/stores-apis-can-be-created.injectable";
|
||||||
|
import directoryForUserDataInjectable
|
||||||
|
from "../../app-paths/directory-for-user-data/directory-for-user-data.injectable";
|
||||||
|
import hostedClusterInjectable
|
||||||
|
from "../../../renderer/cluster-frame-context/hosted-cluster.injectable";
|
||||||
|
import directoryForKubeConfigsInjectable
|
||||||
|
from "../../app-paths/directory-for-kube-configs/directory-for-kube-configs.injectable";
|
||||||
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
|
import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable";
|
||||||
import type { DiContainer } from "@ogre-tools/injectable";
|
import type { DiContainer } from "@ogre-tools/injectable";
|
||||||
import deploymentApiInjectable from "../endpoints/deployment.api.injectable";
|
import deploymentApiInjectable from "../endpoints/deployment.api.injectable";
|
||||||
@ -450,6 +461,54 @@ describe("KubeApi", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("eviction-api as better replacement for pod.delete() request", () => {
|
||||||
|
let evictRequest: Promise<string>;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
evictRequest = api.evict({ name: "foo", namespace: "test" });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("requests evicting a pod in given namespace", () => {
|
||||||
|
expect(fetchMock.mock.lastCall).toMatchObject([
|
||||||
|
"http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo/eviction",
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
},
|
||||||
|
method: "post",
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should resolve the call with >=200 <300 http code", async () => {
|
||||||
|
fetchMock.resolveSpecific(
|
||||||
|
["http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo/eviction"],
|
||||||
|
createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo/eviction", JSON.stringify({
|
||||||
|
apiVersion: "policy/v1",
|
||||||
|
kind: "Status",
|
||||||
|
code: 201,
|
||||||
|
status: "all good",
|
||||||
|
} as KubeStatusData)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(await evictRequest).toBe("201: all good");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should throw in case of error", async () => {
|
||||||
|
fetchMock.resolveSpecific(
|
||||||
|
["http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo/eviction"],
|
||||||
|
createMockResponseFromString("http://127.0.0.1:9999/api-kube/api/v1/namespaces/test/pods/foo/eviction", JSON.stringify({
|
||||||
|
apiVersion: "policy/v1",
|
||||||
|
kind: "Status",
|
||||||
|
code: 500,
|
||||||
|
status: "something went wrong",
|
||||||
|
} as KubeStatusData)),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(async () => evictRequest).rejects.toBe("500: something went wrong");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("deleting namespaces (cluser scoped resource)", () => {
|
describe("deleting namespaces (cluser scoped resource)", () => {
|
||||||
|
|||||||
@ -3,13 +3,24 @@
|
|||||||
* 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 type { DerivedKubeApiOptions, KubeApiDependencies, ResourceDescriptor } from "../kube-api";
|
import type {
|
||||||
|
DeleteResourceDescriptor,
|
||||||
|
DerivedKubeApiOptions,
|
||||||
|
KubeApiDependencies,
|
||||||
|
ResourceDescriptor,
|
||||||
|
} from "../kube-api";
|
||||||
import { KubeApi } from "../kube-api";
|
import { KubeApi } from "../kube-api";
|
||||||
import type { RequireExactlyOne } from "type-fest";
|
import type { RequireExactlyOne } from "type-fest";
|
||||||
import type { KubeObjectMetadata, LocalObjectReference, Affinity, Toleration, NamespaceScopedMetadata } from "../kube-object";
|
import type {
|
||||||
|
Affinity,
|
||||||
|
KubeObjectMetadata, KubeStatusData,
|
||||||
|
LocalObjectReference,
|
||||||
|
NamespaceScopedMetadata,
|
||||||
|
Toleration,
|
||||||
|
} from "../kube-object";
|
||||||
|
import { isKubeStatusData, KubeObject, KubeStatus } from "../kube-object";
|
||||||
import type { SecretReference } from "./secret.api";
|
import type { SecretReference } from "./secret.api";
|
||||||
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claim.api";
|
import type { PersistentVolumeClaimSpec } from "./persistent-volume-claim.api";
|
||||||
import { KubeObject } from "../kube-object";
|
|
||||||
import { isDefined } from "@k8slens/utilities";
|
import { isDefined } from "@k8slens/utilities";
|
||||||
import type { PodSecurityContext } from "./types/pod-security-context";
|
import type { PodSecurityContext } from "./types/pod-security-context";
|
||||||
import type { Probe } from "./types/probe";
|
import type { Probe } from "./types/probe";
|
||||||
@ -24,6 +35,38 @@ export class PodApi extends KubeApi<Pod> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async evict(resource: DeleteResourceDescriptor) {
|
||||||
|
await this.checkPreferredVersion();
|
||||||
|
const apiUrl = this.formatUrlForNotListing(resource);
|
||||||
|
let response: KubeStatusData;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await this.request.post<KubeStatusData>(`${apiUrl}/eviction`, {
|
||||||
|
data: {
|
||||||
|
apiVersion: "policy/v1",
|
||||||
|
kind: "Eviction",
|
||||||
|
metadata: {
|
||||||
|
...resource,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
response = err as KubeStatusData;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isKubeStatusData(response)) {
|
||||||
|
const status = new KubeStatus(response);
|
||||||
|
|
||||||
|
if (status.code >= 200 && status.code < 300) {
|
||||||
|
return status.getExplanation();
|
||||||
|
} else {
|
||||||
|
throw status.getExplanation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
async getLogs(params: ResourceDescriptor, query?: PodLogsQuery): Promise<string> {
|
async getLogs(params: ResourceDescriptor, query?: PodLogsQuery): Promise<string> {
|
||||||
const path = `${this.getUrl(params)}/log`;
|
const path = `${this.getUrl(params)}/log`;
|
||||||
|
|
||||||
|
|||||||
@ -582,6 +582,15 @@ export class KubeApi<
|
|||||||
return parsed;
|
return parsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Some k8s resources might implement special "delete" (e.g. pod.api)
|
||||||
|
* See also: https://kubernetes.io/docs/concepts/scheduling-eviction/api-eviction/
|
||||||
|
* By default should work same as KubeObject.remove()
|
||||||
|
*/
|
||||||
|
async evict(desc: DeleteResourceDescriptor): Promise<KubeStatus | KubeObject | unknown> {
|
||||||
|
return this.delete(desc);
|
||||||
|
}
|
||||||
|
|
||||||
async delete({ propagationPolicy = "Background", ...desc }: DeleteResourceDescriptor) {
|
async delete({ propagationPolicy = "Background", ...desc }: DeleteResourceDescriptor) {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
const apiUrl = this.formatUrlForNotListing(desc);
|
const apiUrl = this.formatUrlForNotListing(desc);
|
||||||
|
|||||||
@ -387,7 +387,9 @@ export class KubeObjectStore<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async remove(item: K) {
|
async remove(item: K) {
|
||||||
await this.api.delete({ name: item.getName(), namespace: item.getNs() });
|
// Some k8s apis might implement special more fine-grained "delete" request for resources (e.g. pod.api.ts)
|
||||||
|
// See also: https://kubernetes.io/docs/concepts/scheduling-eviction/api-eviction/
|
||||||
|
await this.api.evict({ name: item.getName(), namespace: item.getNs() });
|
||||||
this.selectedItemsIds.delete(item.getId());
|
this.selectedItemsIds.delete(item.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -6,17 +6,35 @@
|
|||||||
// Base class for all kubernetes objects
|
// Base class for all kubernetes objects
|
||||||
|
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata } from "./kube-json-api";
|
import type {
|
||||||
import { formatDuration, hasOptionalTypedProperty, hasTypedProperty, isObject, isString, isNumber, bindPredicate, isTypedArray, isRecord } from "@k8slens/utilities";
|
KubeJsonApiData,
|
||||||
|
KubeJsonApiDataList,
|
||||||
|
KubeJsonApiListMetadata,
|
||||||
|
} from "./kube-json-api";
|
||||||
|
import {
|
||||||
|
formatDuration,
|
||||||
|
hasOptionalTypedProperty,
|
||||||
|
hasTypedProperty,
|
||||||
|
isObject,
|
||||||
|
isString,
|
||||||
|
isNumber,
|
||||||
|
bindPredicate,
|
||||||
|
isTypedArray,
|
||||||
|
isRecord,
|
||||||
|
} from "@k8slens/utilities";
|
||||||
import type { ItemObject } from "../item.store";
|
import type { ItemObject } from "../item.store";
|
||||||
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 from "./endpoints/resource-applier.api/request-patch.injectable";
|
import requestKubeObjectPatchInjectable
|
||||||
|
from "./endpoints/resource-applier.api/request-patch.injectable";
|
||||||
import { apiKubeInjectionToken } from "./api-kube";
|
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 { 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";
|
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>
|
||||||
@ -203,13 +221,17 @@ export interface BaseKubeJsonApiObjectMetadata<Namespaced extends KubeObjectScop
|
|||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type KubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = BaseKubeJsonApiObjectMetadata<Namespaced> & (
|
export type KubeJsonApiObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> =
|
||||||
|
BaseKubeJsonApiObjectMetadata<Namespaced>
|
||||||
|
& (
|
||||||
Namespaced extends KubeObjectScope.Namespace
|
Namespaced extends KubeObjectScope.Namespace
|
||||||
? { readonly namespace: string }
|
? { readonly namespace: string }
|
||||||
: {}
|
: {}
|
||||||
);
|
);
|
||||||
|
|
||||||
export type KubeObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> = KubeJsonApiObjectMetadata<Namespaced> & {
|
export type KubeObjectMetadata<Namespaced extends KubeObjectScope = KubeObjectScope> =
|
||||||
|
KubeJsonApiObjectMetadata<Namespaced>
|
||||||
|
& {
|
||||||
readonly selfLink: string;
|
readonly selfLink: string;
|
||||||
readonly uid: string;
|
readonly uid: string;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
@ -225,6 +247,25 @@ export interface KubeStatusData {
|
|||||||
code: number;
|
code: number;
|
||||||
message?: string;
|
message?: string;
|
||||||
reason?: string;
|
reason?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EvictionObject {
|
||||||
|
kind: "Eviction";
|
||||||
|
apiVersion: string | "policy/v1";
|
||||||
|
metadata: Partial<KubeObjectMetadata>;
|
||||||
|
deleteOptions?: {
|
||||||
|
kind?: string;
|
||||||
|
apiVersion?: string;
|
||||||
|
dryRun?: string[];
|
||||||
|
gracePeriodSeconds?: number;
|
||||||
|
orphanDependents?: boolean;
|
||||||
|
propagationPolicy?: string;
|
||||||
|
preconditions?: {
|
||||||
|
resourceVersion: string;
|
||||||
|
uid: string;
|
||||||
|
}[];
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isKubeStatusData(object: unknown): object is KubeStatusData {
|
export function isKubeStatusData(object: unknown): object is KubeStatusData {
|
||||||
@ -232,8 +273,11 @@ export function isKubeStatusData(object: unknown): object is KubeStatusData {
|
|||||||
&& hasTypedProperty(object, "kind", isString)
|
&& hasTypedProperty(object, "kind", isString)
|
||||||
&& hasTypedProperty(object, "apiVersion", isString)
|
&& hasTypedProperty(object, "apiVersion", isString)
|
||||||
&& hasTypedProperty(object, "code", isNumber)
|
&& hasTypedProperty(object, "code", isNumber)
|
||||||
&& hasOptionalTypedProperty(object, "message", isString)
|
&& (
|
||||||
&& hasOptionalTypedProperty(object, "reason", isString)
|
hasOptionalTypedProperty(object, "message", isString)
|
||||||
|
|| hasOptionalTypedProperty(object, "reason", isString)
|
||||||
|
|| hasOptionalTypedProperty(object, "status", isString)
|
||||||
|
)
|
||||||
&& object.kind === "Status";
|
&& object.kind === "Status";
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -241,14 +285,22 @@ export class KubeStatus {
|
|||||||
public readonly kind = "Status";
|
public readonly kind = "Status";
|
||||||
public readonly apiVersion: string;
|
public readonly apiVersion: string;
|
||||||
public readonly code: number;
|
public readonly code: number;
|
||||||
public readonly message: string;
|
public readonly message?: string;
|
||||||
public readonly reason: string;
|
public readonly reason?: string;
|
||||||
|
public readonly status?: string;
|
||||||
|
|
||||||
constructor(data: KubeStatusData) {
|
constructor(data: KubeStatusData) {
|
||||||
this.apiVersion = data.apiVersion;
|
this.apiVersion = data.apiVersion;
|
||||||
this.code = data.code;
|
this.code = data.code;
|
||||||
this.message = data.message || "";
|
this.message = data.message || "";
|
||||||
this.reason = data.reason || "";
|
this.reason = data.reason || "";
|
||||||
|
this.status = data.status || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
getExplanation(): string {
|
||||||
|
const { code, message, reason, status } = this;
|
||||||
|
|
||||||
|
return `${code}: ${message || reason || status}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user