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:
parent
a86b306a48
commit
23d5d40d62
@ -19,11 +19,19 @@
|
|||||||
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
* 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 { forRemoteCluster, KubeApi } from "../kube-api";
|
||||||
import { KubeJsonApi } from "../kube-json-api";
|
import { KubeJsonApi } from "../kube-json-api";
|
||||||
import { KubeObject } from "../kube-object";
|
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", () => {
|
describe("forRemoteCluster", () => {
|
||||||
it("builds api client for KubeObject", async () => {
|
it("builds api client for KubeObject", async () => {
|
||||||
const api = forRemoteCluster({
|
const api = forRemoteCluster({
|
||||||
@ -33,7 +41,7 @@ describe("forRemoteCluster", () => {
|
|||||||
user: {
|
user: {
|
||||||
token: "daa",
|
token: "daa",
|
||||||
},
|
},
|
||||||
}, Pod);
|
}, TestKubeObject);
|
||||||
|
|
||||||
expect(api).toBeInstanceOf(KubeApi);
|
expect(api).toBeInstanceOf(KubeApi);
|
||||||
});
|
});
|
||||||
@ -46,9 +54,9 @@ describe("forRemoteCluster", () => {
|
|||||||
user: {
|
user: {
|
||||||
token: "daa",
|
token: "daa",
|
||||||
},
|
},
|
||||||
}, Pod, PodsApi);
|
}, TestKubeObject, TestKubeApi);
|
||||||
|
|
||||||
expect(api).toBeInstanceOf(PodsApi);
|
expect(api).toBeInstanceOf(TestKubeApi);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls right api endpoint", async () => {
|
it("calls right api endpoint", async () => {
|
||||||
@ -59,7 +67,7 @@ describe("forRemoteCluster", () => {
|
|||||||
user: {
|
user: {
|
||||||
token: "daa",
|
token: "daa",
|
||||||
},
|
},
|
||||||
}, Pod);
|
}, TestKubeObject);
|
||||||
|
|
||||||
(fetch as any).mockResponse(async (request: any) => {
|
(fetch as any).mockResponse(async (request: any) => {
|
||||||
expect(request.url).toEqual("https://127.0.0.1:6443/api/v1/pods");
|
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.apiPrefix).toEqual("/apis");
|
||||||
expect(kubeApi.apiGroup).toEqual("extensions");
|
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");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -37,6 +37,7 @@ import { noop } from "../utils";
|
|||||||
import type { RequestInit } from "node-fetch";
|
import type { RequestInit } from "node-fetch";
|
||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
import { Agent, AgentOptions } from "https";
|
import { Agent, AgentOptions } from "https";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export interface IKubeApiOptions<T extends KubeObject> {
|
export interface IKubeApiOptions<T extends KubeObject> {
|
||||||
/**
|
/**
|
||||||
@ -207,6 +208,14 @@ export type KubeApiWatchOptions = {
|
|||||||
retry?: boolean;
|
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> {
|
export class KubeApi<T extends KubeObject> {
|
||||||
readonly kind: string;
|
readonly kind: string;
|
||||||
readonly apiBase: string;
|
readonly apiBase: string;
|
||||||
@ -475,6 +484,24 @@ export class KubeApi<T extends KubeObject> {
|
|||||||
return parsed;
|
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" }) {
|
async delete({ name = "", namespace = "default" }) {
|
||||||
await this.checkPreferredVersion();
|
await this.checkPreferredVersion();
|
||||||
const apiUrl = this.getUrl({ namespace, name });
|
const apiUrl = this.getUrl({ namespace, name });
|
||||||
|
|||||||
@ -295,16 +295,30 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
async patch(item: T, patch: Patch): Promise<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> {
|
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) {
|
async remove(item: T) {
|
||||||
await item.delete();
|
await this.api.delete({ name: item.getName(), namespace: item.getNs() });
|
||||||
this.items.remove(item);
|
|
||||||
this.selectedItemsIds.delete(item.getId());
|
this.selectedItemsIds.delete(item.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -306,6 +306,9 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
return JSON.parse(JSON.stringify(this));
|
return JSON.parse(JSON.stringify(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated use KubeApi.patch instead
|
||||||
|
*/
|
||||||
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
|
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
|
||||||
for (const op of patch) {
|
for (const op of patch) {
|
||||||
if (KubeObject.nonEditablePaths.has(op.path)) {
|
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).
|
* 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
|
* As fields such as `resourceVersion` will probably out of date. This is a
|
||||||
* common race condition.
|
* common race condition.
|
||||||
|
*
|
||||||
|
* @deprecated use KubeApi.update instead
|
||||||
*/
|
*/
|
||||||
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
||||||
// use unified resource-applier api for updating all k8s objects
|
// 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) {
|
delete(params?: JsonApiParams) {
|
||||||
return apiKube.del(this.selfLink, params);
|
return apiKube.del(this.selfLink, params);
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user