diff --git a/packages/core/src/common/k8s-api/__tests__/stateful-set.api.test.ts b/packages/core/src/common/k8s-api/__tests__/stateful-set.api.test.ts index 9b13e09f2e..f99d866e75 100644 --- a/packages/core/src/common/k8s-api/__tests__/stateful-set.api.test.ts +++ b/packages/core/src/common/k8s-api/__tests__/stateful-set.api.test.ts @@ -9,33 +9,35 @@ import apiKubeInjectable from "../../../renderer/k8s/api-kube.injectable"; import type { StatefulSetApi } from "../endpoints"; import statefulSetApiInjectable from "../endpoints/stateful-set.api.injectable"; import type { KubeJsonApi } from "../kube-json-api"; +import type { AsyncFnMock } from "@async-fn/jest"; +import asyncFn from "@async-fn/jest"; +import { flushPromises } from "@k8slens/test-utils"; describe("StatefulSetApi", () => { let statefulSetApi: StatefulSetApi; - let kubeJsonApi: jest.Mocked; + let kubeJsonApiPatchMock: AsyncFnMock; + let kubeJsonApiGetMock: AsyncFnMock; beforeEach(() => { const di = getDiForUnitTesting(); di.override(storesAndApisCanBeCreatedInjectable, () => true); - kubeJsonApi = { - getResponse: jest.fn(), - get: jest.fn(), - post: jest.fn(), - put: jest.fn(), - patch: jest.fn(), - del: jest.fn(), - } as never; - di.override(apiKubeInjectable, () => kubeJsonApi); + kubeJsonApiPatchMock = asyncFn(); + kubeJsonApiGetMock = asyncFn(); + di.override(apiKubeInjectable, () => ({ + get: kubeJsonApiGetMock, + patch: kubeJsonApiPatchMock, + } as Partial as KubeJsonApi)); statefulSetApi = di.inject(statefulSetApiInjectable); }); describe("scale", () => { - it("requests Kubernetes API with PATCH verb and correct amount of replicas", () => { - statefulSetApi.scale({ namespace: "default", name: "statefulset-1" }, 5); + it("requests Kubernetes API with PATCH verb and correct amount of replicas", async () => { + const req = statefulSetApi.scale({ namespace: "default", name: "statefulset-1" }, 5); - expect(kubeJsonApi.patch).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", { + await flushPromises(); + expect(kubeJsonApiPatchMock).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale", { data: { spec: { replicas: 5, @@ -47,6 +49,19 @@ describe("StatefulSetApi", () => { "content-type": "application/merge-patch+json", }, }); + + await kubeJsonApiPatchMock.resolve({}); + await req; + }); + + it("requests Kubernetes API with GET verb and correct sub-resource", async () => { + const req = statefulSetApi.getReplicas({ namespace: "default", name: "statefulset-1" }); + + await flushPromises(); + expect(kubeJsonApiGetMock).toHaveBeenCalledWith("/apis/apps/v1/namespaces/default/statefulsets/statefulset-1/scale"); + await kubeJsonApiGetMock.resolve({ status: { replicas: 10 }}); + + expect(await req).toBe(10); }); }); }); diff --git a/packages/core/src/common/k8s-api/endpoints/deployment.api.ts b/packages/core/src/common/k8s-api/endpoints/deployment.api.ts index d715612739..7cfbe4cb01 100644 --- a/packages/core/src/common/k8s-api/endpoints/deployment.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/deployment.api.ts @@ -8,7 +8,6 @@ import moment from "moment"; import type { DerivedKubeApiOptions, KubeApiDependencies, NamespacedResourceDescriptor } from "../kube-api"; import { KubeApi } from "../kube-api"; import { Deployment } from "@k8slens/kube-object"; -import { hasTypedProperty, isNumber, isObject } from "@k8slens/utilities"; export class DeploymentApi extends KubeApi { constructor(deps: KubeApiDependencies, opts?: DerivedKubeApiOptions) { @@ -18,33 +17,14 @@ export class DeploymentApi extends KubeApi { }); } - protected getScaleApiUrl(params: NamespacedResourceDescriptor) { - return `${this.formatUrlForNotListing(params)}/scale`; - } - async getReplicas(params: NamespacedResourceDescriptor): Promise { - const { status } = await this.request.get(this.getScaleApiUrl(params)); + const { status } = await this.getResourceScale(params); - if (isObject(status) && hasTypedProperty(status, "replicas", isNumber)) { - return status.replicas; - } - - return 0; + return status.replicas; } scale(params: NamespacedResourceDescriptor, replicas: number) { - return this.request.patch(this.getScaleApiUrl(params), { - data: { - spec: { - replicas, - }, - }, - }, - { - headers: { - "content-type": "application/merge-patch+json", - }, - }); + return this.scaleResource(params, { spec: { replicas }}); } restart(params: NamespacedResourceDescriptor) { diff --git a/packages/core/src/common/k8s-api/endpoints/replica-set.api.ts b/packages/core/src/common/k8s-api/endpoints/replica-set.api.ts index b3a81de7a4..3c9152759b 100644 --- a/packages/core/src/common/k8s-api/endpoints/replica-set.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/replica-set.api.ts @@ -15,24 +15,13 @@ export class ReplicaSetApi extends KubeApi { }); } - protected getScaleApiUrl(params: NamespacedResourceDescriptor) { - return `${this.formatUrlForNotListing(params)}/scale`; - } - async getReplicas(params: NamespacedResourceDescriptor): Promise { - const { status } = await this.request.get(this.getScaleApiUrl(params)); + const { status } = await this.getResourceScale(params); - return (status as { replicas: number })?.replicas; + return status.replicas; } scale(params: NamespacedResourceDescriptor, replicas: number) { - return this.request.put(this.getScaleApiUrl(params), { - data: { - metadata: params, - spec: { - replicas, - }, - }, - }); + return this.scaleResource(params, { spec: { replicas }}); } } diff --git a/packages/core/src/common/k8s-api/endpoints/replication-controller.api.ts b/packages/core/src/common/k8s-api/endpoints/replication-controller.api.ts index e7ef4d506d..98f29e58a1 100644 --- a/packages/core/src/common/k8s-api/endpoints/replication-controller.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/replication-controller.api.ts @@ -25,17 +25,6 @@ export class ReplicationControllerApi extends KubeApi { } scale(params: NamespacedResourceDescriptor, replicas: number): Promise { - return this.request.patch(this.getScaleApiUrl(params), { - data: { - metadata: params, - spec: { - replicas, - }, - }, - }, { - headers: { - "content-type": "application/strategic-merge-patch+json", - }, - }); + return this.scaleResource(params, { spec: { replicas }}); } } diff --git a/packages/core/src/common/k8s-api/endpoints/stateful-set.api.ts b/packages/core/src/common/k8s-api/endpoints/stateful-set.api.ts index 5531455d2c..fcdd2c7b79 100644 --- a/packages/core/src/common/k8s-api/endpoints/stateful-set.api.ts +++ b/packages/core/src/common/k8s-api/endpoints/stateful-set.api.ts @@ -17,23 +17,14 @@ export class StatefulSetApi extends KubeApi { }); } - protected getScaleApiUrl(params: NamespacedResourceDescriptor) { - return `${this.formatUrlForNotListing(params)}/scale`; - } - async getReplicas(params: NamespacedResourceDescriptor): Promise { - const apiUrl = this.getScaleApiUrl(params); - const { status = 0 } = await this.request.get(apiUrl) as { status?: number }; + const { status } = await this.getResourceScale(params); - return status; + return status.replicas; } scale(params: NamespacedResourceDescriptor, replicas: number) { - return this.patch(params, { - spec: { - replicas, - }, - }, "merge"); + return this.scaleResource(params, { spec: { replicas }}); } restart(params: NamespacedResourceDescriptor) { diff --git a/packages/core/src/common/k8s-api/kube-api.ts b/packages/core/src/common/k8s-api/kube-api.ts index 585f845b49..90d53bf966 100644 --- a/packages/core/src/common/k8s-api/kube-api.ts +++ b/packages/core/src/common/k8s-api/kube-api.ts @@ -8,7 +8,7 @@ import { merge } from "lodash"; import { stringify } from "querystring"; import { createKubeApiURL, parseKubeApi } from "./kube-api-parse"; -import type { KubeObjectConstructor, KubeJsonApiDataFor, KubeObjectMetadata, KubeJsonApiData, KubeObject, KubeObjectScope } from "@k8slens/kube-object"; +import type { KubeObjectConstructor, KubeJsonApiDataFor, KubeObjectMetadata, KubeJsonApiData, KubeObject, KubeObjectScope, Scale } from "@k8slens/kube-object"; import { isJsonApiData, isJsonApiDataList, isPartialJsonApiData, KubeStatus, isKubeStatusData } from "@k8slens/kube-object"; import byline from "byline"; import type { IKubeWatchEvent } from "./kube-watch-event"; @@ -22,6 +22,7 @@ import type { PartialDeep } from "type-fest"; import type { Logger } from "../logger"; import { matches } from "lodash/fp"; import { makeObservable, observable } from "mobx"; +import type { ScaleCreateOptions } from "@k8slens/kube-object/src/types/scale"; /** * The options used for creating a `KubeApi` @@ -213,6 +214,10 @@ export interface ResourceDescriptor { namespace?: string; } +export interface SubResourceDescriptor { + subResource: string; +} + export type SpecificResourceDescriptor = { /** * The name of the kubernetes resource @@ -592,6 +597,37 @@ export class KubeApi< return parsed; } + /** + * An internal method for requesting the `/scale` sub-resource if it exists + */ + protected async getResourceScale(desc: ResourceDescriptor): Promise { + await this.checkPreferredVersion(); + const apiUrl = this.formatUrlForNotListing(desc); + + const res = await this.request.get(`${apiUrl}/scale`); + + return res as Scale; + } + + + /** + * An internal method for requesting the `/scale` sub-resource if it exists + */ + protected async scaleResource(desc: ResourceDescriptor, data: ScaleCreateOptions): Promise { + await this.checkPreferredVersion(); + const apiUrl = this.formatUrlForNotListing(desc); + + const res = await this.request.patch(`${apiUrl}/scale`, { + data: { ...data }, + }, { + headers: { + "content-type": patchTypeHeaders.merge, + }, + }); + + return res as Scale; + } + async patch(desc: ResourceDescriptor, data: PartialDeep): Promise; async patch(desc: ResourceDescriptor, data: PartialDeep, strategy: "strategic" | "merge"): Promise; async patch(desc: ResourceDescriptor, data: Patch, strategy: "json"): Promise; diff --git a/packages/kube-object/src/specifics/replication-controller.ts b/packages/kube-object/src/specifics/replication-controller.ts index 4c9876b11e..af87bd2b64 100644 --- a/packages/kube-object/src/specifics/replication-controller.ts +++ b/packages/kube-object/src/specifics/replication-controller.ts @@ -3,28 +3,10 @@ * Licensed under MIT License. See LICENSE in root directory for more information. */ -import type { - KubeObjectMetadata, - KubeObjectStatus, - NamespaceScopedMetadata, - BaseKubeObjectCondition, -} from "../api-types"; +import type { KubeObjectStatus, NamespaceScopedMetadata, BaseKubeObjectCondition } from "../api-types"; import { KubeObject } from "../kube-object"; import type { PodTemplateSpec } from "../types/pod-template-spec"; -export interface Scale { - apiVersion: "autoscaling/v1"; - kind: "Scale"; - metadata: KubeObjectMetadata; - spec: { - replicas: number; - }; - status: { - replicas: number; - selector: string; - }; -} - export interface ReplicationControllerSpec { /** * Minimum number of seconds for which a newly created pod should be ready without any of its container crashing, diff --git a/packages/kube-object/src/types/scale.ts b/packages/kube-object/src/types/scale.ts new file mode 100644 index 0000000000..187474c8c4 --- /dev/null +++ b/packages/kube-object/src/types/scale.ts @@ -0,0 +1,27 @@ +/** + * Copyright (c) OpenLens Authors. All rights reserved. + * Licensed under MIT License. See LICENSE in root directory for more information. + */ + +import type { KubeObjectMetadata } from "../api-types"; + +export interface Scale { + apiVersion: "autoscaling/v1"; + kind: "Scale"; + metadata: KubeObjectMetadata; + spec: { + replicas: number; + }; + status: { + replicas: number; + selector: string; + }; +} + +export interface ScaleCreateOptions { + apiVersion?: "autoscaling/v1"; + kind?: "Scale"; + spec: { + replicas: number; + }; +}