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

Implement KubeApi.patch (#4325)

* implement KubeApi.patch

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* cleanup tests

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>

* keep it backward compatible

Signed-off-by: Jari Kolehmainen <jari.kolehmainen@gmail.com>
This commit is contained in:
Jari Kolehmainen 2021-11-12 11:15:33 +02:00 committed by GitHub
parent a86b306a48
commit 23d5d40d62
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 125 additions and 9 deletions

View File

@ -19,11 +19,19 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import { Pod, PodsApi } from "../endpoints/pods.api";
import type { Request } from "node-fetch";
import { forRemoteCluster, KubeApi } from "../kube-api";
import { KubeJsonApi } from "../kube-json-api";
import { KubeObject } from "../kube-object";
class TestKubeObject extends KubeObject {
static kind = "Pod";
static namespaced = true;
static apiBase = "/api/v1/pods";
}
class TestKubeApi extends KubeApi<TestKubeObject> {}
describe("forRemoteCluster", () => {
it("builds api client for KubeObject", async () => {
const api = forRemoteCluster({
@ -33,7 +41,7 @@ describe("forRemoteCluster", () => {
user: {
token: "daa",
},
}, Pod);
}, TestKubeObject);
expect(api).toBeInstanceOf(KubeApi);
});
@ -46,9 +54,9 @@ describe("forRemoteCluster", () => {
user: {
token: "daa",
},
}, Pod, PodsApi);
}, TestKubeObject, TestKubeApi);
expect(api).toBeInstanceOf(PodsApi);
expect(api).toBeInstanceOf(TestKubeApi);
});
it("calls right api endpoint", async () => {
@ -59,7 +67,7 @@ describe("forRemoteCluster", () => {
user: {
token: "daa",
},
}, Pod);
}, TestKubeObject);
(fetch as any).mockResponse(async (request: any) => {
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
@ -167,4 +175,63 @@ describe("KubeApi", () => {
expect(kubeApi.apiPrefix).toEqual("/apis");
expect(kubeApi.apiGroup).toEqual("extensions");
});
describe("patch", () => {
let api: TestKubeApi;
beforeEach(() => {
api = new TestKubeApi({
request,
objectConstructor: TestKubeObject,
});
});
it("sends strategic patch by default", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("strategic-merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
return {};
});
await api.patch({ name: "test", namespace: "default" }, {
spec: { replicas: 2 },
});
});
it("allows to use merge patch", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("merge-patch");
expect(request.body.toString()).toEqual(JSON.stringify({ spec: { replicas: 2 }}));
return {};
});
await api.patch({ name: "test", namespace: "default" }, {
spec: { replicas: 2 },
}, "merge");
});
it("allows to use json patch", async () => {
expect.hasAssertions();
(fetch as any).mockResponse(async (request: Request) => {
expect(request.method).toEqual("PATCH");
expect(request.headers.get("content-type")).toMatch("json-patch");
expect(request.body.toString()).toEqual(JSON.stringify([{ op: "replace", path: "/spec/replicas", value: 2 }]));
return {};
});
await api.patch({ name: "test", namespace: "default" }, [
{ op: "replace", path: "/spec/replicas", value: 2 },
], "json");
});
});
});

View File

@ -37,6 +37,7 @@ import { noop } from "../utils";
import type { RequestInit } from "node-fetch";
import AbortController from "abort-controller";
import { Agent, AgentOptions } from "https";
import type { Patch } from "rfc6902";
export interface IKubeApiOptions<T extends KubeObject> {
/**
@ -207,6 +208,14 @@ export type KubeApiWatchOptions = {
retry?: boolean;
};
export type KubeApiPatchType = "merge" | "json" | "strategic";
const patchTypeHeaders: Record<KubeApiPatchType, string> = {
"merge": "application/merge-patch+json",
"json": "application/json-patch+json",
"strategic": "application/strategic-merge-patch+json",
};
export class KubeApi<T extends KubeObject> {
readonly kind: string;
readonly apiBase: string;
@ -475,6 +484,24 @@ export class KubeApi<T extends KubeObject> {
return parsed;
}
async patch({ name = "", namespace = "default" } = {}, data?: Partial<T> | Patch, strategy: KubeApiPatchType = "strategic"): Promise<T | null> {
await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name });
const res = await this.request.patch(apiUrl, { data }, {
headers: {
"content-type": patchTypeHeaders[strategy],
},
});
const parsed = this.parseResponse(res);
if (Array.isArray(parsed)) {
throw new Error(`PATCH request to ${apiUrl} returned an array: ${JSON.stringify(parsed)}`);
}
return parsed;
}
async delete({ name = "", namespace = "default" }) {
await this.checkPreferredVersion();
const apiUrl = this.getUrl({ namespace, name });

View File

@ -295,16 +295,30 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
}
async patch(item: T, patch: Patch): Promise<T> {
return this.postUpdate(await item.patch(patch));
return this.postUpdate(
await this.api.patch(
{
name: item.getName(), namespace: item.getNs(),
},
patch,
"json",
),
);
}
async update(item: T, data: Partial<T>): Promise<T> {
return this.postUpdate(await item.update(data));
return this.postUpdate(
await this.api.update(
{
name: item.getName(), namespace: item.getNs(),
},
data,
),
);
}
async remove(item: T) {
await item.delete();
this.items.remove(item);
await this.api.delete({ name: item.getName(), namespace: item.getNs() });
this.selectedItemsIds.delete(item.getId());
}

View File

@ -306,6 +306,9 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return JSON.parse(JSON.stringify(this));
}
/**
* @deprecated use KubeApi.patch instead
*/
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
for (const op of patch) {
if (KubeObject.nonEditablePaths.has(op.path)) {
@ -328,6 +331,8 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
* Note: this is brittle if `data` is not actually partial (but instead whole).
* As fields such as `resourceVersion` will probably out of date. This is a
* common race condition.
*
* @deprecated use KubeApi.update instead
*/
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
// use unified resource-applier api for updating all k8s objects
@ -337,6 +342,9 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
});
}
/**
* @deprecated use KubeApi.delete instead
*/
delete(params?: JsonApiParams) {
return apiKube.del(this.selfLink, params);
}