Use JSON patch for editing components (#3674)

This commit is contained in:
Sebastian Malton authored and GitHub committed 2021-10-04 16:28:19 -04:00
1 parent 8b1de233f2
commit 4705a66632
18 files changed
+242 -106

No files matched your search

@@ -22,19 +22,27 @@
import jsYaml from "js-yaml";
import type { KubeJsonApiData } from "../kube-json-api";
import { apiBase } from "../index";
import type { Patch } from "rfc6902";
export const resourceApplierApi = {
annotations: [
"kubectl.kubernetes.io/last-applied-configuration"
],
export const annotations = [
"kubectl.kubernetes.io/last-applied-configuration"
];
async update(resource: object | string): Promise<KubeJsonApiData | null> {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
const [data = null] = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
return data;
export async function update(resource: object | string): Promise<KubeJsonApiData> {
if (typeof resource === "string") {
resource = jsYaml.safeLoad(resource);
}
};
return apiBase.post<KubeJsonApiData>("/stack", { data: resource });
}
export async function patch(name: string, kind: string, ns: string, patch: Patch): Promise<KubeJsonApiData> {
return apiBase.patch<KubeJsonApiData>("/stack", {
data: {
name,
kind,
ns,
patch,
},
});
}
+16 -5
View File
@@ -32,6 +32,7 @@ import { parseKubeApi } from "./kube-api-parse";
import type { KubeJsonApiData } from "./kube-json-api";
import type { RequestInit } from "node-fetch";
import AbortController from "abort-controller";
import type { Patch } from "rfc6902";
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
namespaces: string[];
@@ -279,19 +280,29 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
return newItem;
}
async update(item: T, data: Partial<T>): Promise<T> {
const rawItem = await item.update(data);
private postUpdate(rawItem: KubeJsonApiData): T {
const newItem = new this.api.objectConstructor(rawItem);
const index = this.items.findIndex(item => item.getId() === newItem.getId());
ensureObjectSelfLink(this.api, newItem);
const index = this.items.findIndex(item => item.getId() === newItem.getId());
this.items.splice(index, 1, newItem);
if (index < 0) {
this.items.push(newItem);
} else {
this.items[index] = newItem;
}
return newItem;
}
async patch(item: T, patch: Patch): Promise<T> {
return this.postUpdate(await item.patch(patch));
}
async update(item: T, data: Partial<T>): Promise<T> {
return this.postUpdate(await item.update(data));
}
async remove(item: T) {
await item.delete();
this.items.remove(item);
+43 -16
View File
@@ -27,9 +27,9 @@ import { autoBind, formatDuration } from "../utils";
import type { ItemObject } from "../item.store";
import { apiKube } from "./index";
import type { JsonApiParams } from "./json-api";
import { resourceApplierApi } from "./endpoints/resource-applier.api";
import * as resourceApplierApi from "./endpoints/resource-applier.api";
import { hasOptionalProperty, hasTypedProperty, isObject, isString, bindPredicate, isTypedArray, isRecord } from "../../common/utils/type-narrowing";
import _ from "lodash";
import type { Patch } from "rfc6902";
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
kind?: string;
@@ -191,18 +191,28 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
}
protected static readonly nonEditableFields = [
"apiVersion",
"kind",
"metadata.name",
"metadata.selfLink",
"metadata.resourceVersion",
"metadata.uid",
"managedFields",
"status",
/**
* These must be RFC6902 compliant paths
*/
private static readonly nonEditiablePathPrefixes = [
"/metadata/managedFields",
"/status",
];
private static readonly nonEditablePaths = new Set([
"/apiVersion",
"/kind",
"/metadata/name",
"/metadata/selfLink",
"/metadata/resourceVersion",
"/metadata/uid",
...KubeObject.nonEditiablePathPrefixes,
]);
constructor(data: KubeJsonApiData) {
if (typeof data !== "object") {
throw new TypeError(`Cannot create a KubeObject from ${typeof data}`);
}
Object.assign(this, data);
autoBind(this);
}
@@ -286,14 +296,31 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
return JSON.parse(JSON.stringify(this));
}
// use unified resource-applier api for updating all k8s objects
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
for (const field of KubeObject.nonEditableFields) {
if (!_.isEqual(_.get(this, field), _.get(data, field))) {
throw new Error(`Failed to update Kube Object: ${field} has been modified`);
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
for (const op of patch) {
if (KubeObject.nonEditablePaths.has(op.path)) {
throw new Error(`Failed to update ${this.kind}: JSON pointer ${op.path} has been modified`);
}
for (const pathPrefix of KubeObject.nonEditiablePathPrefixes) {
if (op.path.startsWith(`${pathPrefix}/`)) {
throw new Error(`Failed to update ${this.kind}: Child JSON pointer of ${op.path} has been modified`);
}
}
}
return resourceApplierApi.patch(this.getName(), this.kind, this.getNs(), patch);
}
/**
* Perform a full update (or more specifically a replace)
*
* 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.
*/
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
// use unified resource-applier api for updating all k8s objects
return resourceApplierApi.update({
...this.toPlainObject(),
...data,