diff --git a/packages/core/src/common/k8s-api/endpoints/pod.api.ts b/packages/core/src/common/k8s-api/endpoints/pod.api.ts index 1db41957b2..45bd31a222 100644 --- a/packages/core/src/common/k8s-api/endpoints/pod.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/pod.api.ts @@ -3,13 +3,24 @@ * 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 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 { PersistentVolumeClaimSpec } from "./persistent-volume-claim.api"; -import { KubeObject } from "../kube-object"; import { isDefined } from "@k8slens/utilities"; import type { PodSecurityContext } from "./types/pod-security-context"; import type { Probe } from "./types/probe"; @@ -24,6 +35,38 @@ export class PodApi extends KubeApi { }); } + async evict(resource: DeleteResourceDescriptor) { + await this.checkPreferredVersion(); + const apiUrl = this.formatUrlForNotListing(resource); + let response: KubeStatusData; + + try { + response = await this.request.post(`${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.getMessage(); + } else { + throw status.getMessage(); + } + } + + return response; + } + async getLogs(params: ResourceDescriptor, query?: PodLogsQuery): Promise { const path = `${this.getUrl(params)}/log`; diff --git a/packages/core/src/common/k8s-api/kube-api.ts b/packages/core/src/common/k8s-api/kube-api.ts index 95210478a1..94fc24be7d 100644 --- a/packages/core/src/common/k8s-api/kube-api.ts +++ b/packages/core/src/common/k8s-api/kube-api.ts @@ -583,6 +583,15 @@ export class KubeApi< 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 { + return this.delete(desc); + } + async delete({ propagationPolicy = "Background", ...desc }: DeleteResourceDescriptor) { await this.checkPreferredVersion(); const apiUrl = this.formatUrlForNotListing(desc); diff --git a/packages/core/src/common/k8s-api/kube-object.store.ts b/packages/core/src/common/k8s-api/kube-object.store.ts index f26b05283e..408794e44b 100644 --- a/packages/core/src/common/k8s-api/kube-object.store.ts +++ b/packages/core/src/common/k8s-api/kube-object.store.ts @@ -388,7 +388,9 @@ export abstract class KubeObjectStore< } 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()); } diff --git a/packages/core/src/common/k8s-api/kube-object.ts b/packages/core/src/common/k8s-api/kube-object.ts index e700710788..b06e887b01 100644 --- a/packages/core/src/common/k8s-api/kube-object.ts +++ b/packages/core/src/common/k8s-api/kube-object.ts @@ -6,17 +6,35 @@ // Base class for all kubernetes objects import moment from "moment"; -import type { KubeJsonApiData, KubeJsonApiDataList, KubeJsonApiListMetadata } from "./kube-json-api"; -import { formatDuration, hasOptionalTypedProperty, hasTypedProperty, isObject, isString, isNumber, bindPredicate, isTypedArray, isRecord } from "@k8slens/utilities"; +import type { + 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 { Patch } from "rfc6902"; import assert from "assert"; import type { JsonObject } from "type-fest"; -import requestKubeObjectPatchInjectable from "./endpoints/resource-applier.api/request-patch.injectable"; +import requestKubeObjectPatchInjectable + from "./endpoints/resource-applier.api/request-patch.injectable"; import { apiKubeInjectionToken } from "./api-kube"; -import requestKubeObjectCreationInjectable from "./endpoints/resource-applier.api/request-update.injectable"; +import requestKubeObjectCreationInjectable + from "./endpoints/resource-applier.api/request-update.injectable"; import { dump } from "js-yaml"; -import { getLegacyGlobalDiForExtensionApi } from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; +import { + getLegacyGlobalDiForExtensionApi, +} from "../../extensions/as-legacy-globals-for-extension-api/legacy-global-di-for-extension-api"; import autoBind from "auto-bind"; export type KubeJsonApiDataFor = K extends KubeObject @@ -203,13 +221,17 @@ export interface BaseKubeJsonApiObjectMetadata = BaseKubeJsonApiObjectMetadata & ( +export type KubeJsonApiObjectMetadata = + BaseKubeJsonApiObjectMetadata + & ( Namespaced extends KubeObjectScope.Namespace ? { readonly namespace: string } : {} -); + ); -export type KubeObjectMetadata = KubeJsonApiObjectMetadata & { +export type KubeObjectMetadata = + KubeJsonApiObjectMetadata + & { readonly selfLink: string; readonly uid: string; readonly name: string; @@ -225,6 +247,25 @@ export interface KubeStatusData { code: number; message?: string; reason?: string; + status?: string; +} + +export interface EvictionObject { + kind: "Eviction"; + apiVersion: string | "policy/v1"; + metadata: Partial; + 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 { @@ -232,8 +273,11 @@ export function isKubeStatusData(object: unknown): object is KubeStatusData { && hasTypedProperty(object, "kind", isString) && hasTypedProperty(object, "apiVersion", isString) && 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"; } @@ -241,14 +285,22 @@ export class KubeStatus { public readonly kind = "Status"; public readonly apiVersion: string; public readonly code: number; - public readonly message: string; - public readonly reason: string; + public readonly message?: string; + public readonly reason?: string; + public readonly status?: string; constructor(data: KubeStatusData) { this.apiVersion = data.apiVersion; this.code = data.code; this.message = data.message || ""; this.reason = data.reason || ""; + this.status = data.status || ""; + } + + getMessage() { + const { code, message, reason, status } = this; + + return `${code}: ${message ?? reason ?? status}`; } } diff --git a/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx b/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx index 8535006f46..5c0f47fb68 100644 --- a/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx +++ b/packages/core/src/renderer/components/kube-object-menu/kube-object-menu.tsx @@ -31,8 +31,6 @@ import { observer } from "mobx-react"; export interface KubeObjectMenuProps extends MenuActionsProps { object: TKubeObject; - editable?: boolean; - removable?: boolean; } interface Dependencies { @@ -86,8 +84,6 @@ class NonInjectedKubeObjectMenu extends React.Component private emitOnContextMenuOpen(object: KubeObject) { const { apiManager, - editable, - removable, hideDetails, createEditResourceTab, withConfirmation, @@ -98,8 +94,8 @@ class NonInjectedKubeObjectMenu extends React.Component } = this.props; const store = apiManager.getStore(object.selfLink); - const isEditable = editable ?? (Boolean(store?.patch) || Boolean(updateAction)); - const isRemovable = removable ?? (Boolean(store?.remove) || Boolean(removeAction)); + const isEditable = Boolean(updateAction ?? store?.patch); + const isRemovable = Boolean(removeAction ?? store?.remove); runInAction(() => { this.menuItems.clear(); @@ -177,8 +173,6 @@ class NonInjectedKubeObjectMenu extends React.Component render() { const { className, - editable, - removable, object, removeAction, // This is here so we don't pass it down to `` removeConfirmationMessage, // This is here so we don't pass it down to ``