mirror of
https://github.com/lensapp/lens.git
synced 2025-05-20 05:10:56 +00:00
Use JSON patch for editing components
Signed-off-by: Sebastian Malton <sebastian@malton.name>
This commit is contained in:
parent
8bbaf8c59e
commit
c73876170e
@ -237,6 +237,7 @@
|
|||||||
"readable-stream": "^3.6.0",
|
"readable-stream": "^3.6.0",
|
||||||
"request": "^2.88.2",
|
"request": "^2.88.2",
|
||||||
"request-promise-native": "^1.0.9",
|
"request-promise-native": "^1.0.9",
|
||||||
|
"rfc6902": "^4.0.2",
|
||||||
"semver": "^7.3.2",
|
"semver": "^7.3.2",
|
||||||
"serializr": "^2.0.5",
|
"serializr": "^2.0.5",
|
||||||
"shell-env": "^3.0.1",
|
"shell-env": "^3.0.1",
|
||||||
|
|||||||
@ -22,19 +22,27 @@
|
|||||||
import jsYaml from "js-yaml";
|
import jsYaml from "js-yaml";
|
||||||
import type { KubeJsonApiData } from "../kube-json-api";
|
import type { KubeJsonApiData } from "../kube-json-api";
|
||||||
import { apiBase } from "../index";
|
import { apiBase } from "../index";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export const resourceApplierApi = {
|
export const annotations = [
|
||||||
annotations: [
|
"kubectl.kubernetes.io/last-applied-configuration"
|
||||||
"kubectl.kubernetes.io/last-applied-configuration"
|
];
|
||||||
],
|
|
||||||
|
|
||||||
async update(resource: object | string): Promise<KubeJsonApiData | null> {
|
export async function update(resource: object | string): Promise<KubeJsonApiData> {
|
||||||
if (typeof resource === "string") {
|
if (typeof resource === "string") {
|
||||||
resource = jsYaml.safeLoad(resource);
|
resource = jsYaml.safeLoad(resource);
|
||||||
}
|
|
||||||
|
|
||||||
const [data = null] = await apiBase.post<KubeJsonApiData[]>("/stack", { data: resource });
|
|
||||||
|
|
||||||
return data;
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -32,6 +32,7 @@ import { parseKubeApi } from "./kube-api-parse";
|
|||||||
import type { KubeJsonApiData } from "./kube-json-api";
|
import type { KubeJsonApiData } from "./kube-json-api";
|
||||||
import type { RequestInit } from "node-fetch";
|
import type { RequestInit } from "node-fetch";
|
||||||
import AbortController from "abort-controller";
|
import AbortController from "abort-controller";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
|
||||||
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
export interface KubeObjectStoreLoadingParams<K extends KubeObject> {
|
||||||
namespaces: string[];
|
namespaces: string[];
|
||||||
@ -279,17 +280,21 @@ export abstract class KubeObjectStore<T extends KubeObject> extends ItemStore<T>
|
|||||||
return newItem;
|
return newItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
async update(item: T, data: Partial<T>): Promise<T> {
|
private postUpdate(rawItem: KubeJsonApiData): T {
|
||||||
const rawItem = await item.update(data);
|
|
||||||
const newItem = new this.api.objectConstructor(rawItem);
|
const newItem = new this.api.objectConstructor(rawItem);
|
||||||
|
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
||||||
|
|
||||||
ensureObjectSelfLink(this.api, newItem);
|
ensureObjectSelfLink(this.api, newItem);
|
||||||
|
|
||||||
const index = this.items.findIndex(item => item.getId() === newItem.getId());
|
return this.items[index] = newItem;
|
||||||
|
}
|
||||||
|
|
||||||
this.items.splice(index, 1, newItem);
|
async patch(item: T, patch: Patch): Promise<T> {
|
||||||
|
return this.postUpdate(await item.patch(patch));
|
||||||
|
}
|
||||||
|
|
||||||
return newItem;
|
async update(item: T, data: Partial<T>): Promise<T> {
|
||||||
|
return this.postUpdate(await item.update(data));
|
||||||
}
|
}
|
||||||
|
|
||||||
async remove(item: T) {
|
async remove(item: T) {
|
||||||
|
|||||||
@ -27,9 +27,9 @@ import { autoBind, formatDuration } from "../utils";
|
|||||||
import type { ItemObject } from "../item.store";
|
import type { ItemObject } from "../item.store";
|
||||||
import { apiKube } from "./index";
|
import { apiKube } from "./index";
|
||||||
import type { JsonApiParams } from "./json-api";
|
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 { 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) & {
|
export type KubeObjectConstructor<K extends KubeObject> = (new (data: KubeJsonApiData | any) => K) & {
|
||||||
kind?: string;
|
kind?: string;
|
||||||
@ -191,16 +191,22 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
return Object.entries(labels).map(([name, value]) => `${name}=${value}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected static readonly nonEditableFields = [
|
/**
|
||||||
"apiVersion",
|
* These must be RFC6902 complient paths
|
||||||
"kind",
|
*/
|
||||||
"metadata.name",
|
private static readonly nonEditiablePathPrefixes = [
|
||||||
"metadata.selfLink",
|
"/metadata/managedFields",
|
||||||
"metadata.resourceVersion",
|
"/status",
|
||||||
"metadata.uid",
|
|
||||||
"managedFields",
|
|
||||||
"status",
|
|
||||||
];
|
];
|
||||||
|
private static readonly nonEditablePaths = new Set([
|
||||||
|
"/apiVersion",
|
||||||
|
"/kind",
|
||||||
|
"/metadata/name",
|
||||||
|
"/metadata/selfLink",
|
||||||
|
"/metadata/resourceVersion",
|
||||||
|
"/metadata/uid",
|
||||||
|
...KubeObject.nonEditiablePathPrefixes,
|
||||||
|
]);
|
||||||
|
|
||||||
constructor(data: KubeJsonApiData) {
|
constructor(data: KubeJsonApiData) {
|
||||||
Object.assign(this, data);
|
Object.assign(this, data);
|
||||||
@ -286,14 +292,30 @@ export class KubeObject<Metadata extends KubeObjectMetadata = KubeObjectMetadata
|
|||||||
return JSON.parse(JSON.stringify(this));
|
return JSON.parse(JSON.stringify(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
// use unified resource-applier api for updating all k8s objects
|
async patch(patch: Patch): Promise<KubeJsonApiData | null> {
|
||||||
async update(data: Partial<this>): Promise<KubeJsonApiData | null> {
|
for (const op of patch) {
|
||||||
for (const field of KubeObject.nonEditableFields) {
|
if (KubeObject.nonEditablePaths.has(op.path)) {
|
||||||
if (!_.isEqual(_.get(this, field), _.get(data, field))) {
|
throw new Error(`Failed to update ${this.kind}: JSON pointer ${op.path} has been modified`);
|
||||||
throw new Error(`Failed to update Kube Object: ${field} 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 a object spread with the current
|
||||||
|
* fields as the `resourceVersion` maybe 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({
|
return resourceApplierApi.update({
|
||||||
...this.toPlainObject(),
|
...this.toPlainObject(),
|
||||||
...data,
|
...data,
|
||||||
|
|||||||
@ -22,55 +22,97 @@
|
|||||||
import type { Cluster } from "./cluster";
|
import type { Cluster } from "./cluster";
|
||||||
import type { KubernetesObject } from "@kubernetes/client-node";
|
import type { KubernetesObject } from "@kubernetes/client-node";
|
||||||
import { exec } from "child_process";
|
import { exec } from "child_process";
|
||||||
import fs from "fs";
|
import fs from "fs-extra";
|
||||||
import * as yaml from "js-yaml";
|
import * as yaml from "js-yaml";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import * as tempy from "tempy";
|
import * as tempy from "tempy";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { appEventBus } from "../common/event-bus";
|
import { appEventBus } from "../common/event-bus";
|
||||||
import { cloneJsonObject } from "../common/utils";
|
import { cloneJsonObject } from "../common/utils";
|
||||||
|
import type { Patch } from "rfc6902";
|
||||||
|
import { promiseExecFile } from "./promise-exec";
|
||||||
|
|
||||||
export class ResourceApplier {
|
export class ResourceApplier {
|
||||||
constructor(protected cluster: Cluster) {
|
constructor(protected cluster: Cluster) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Patch a kube resource's manifest, throwing any error that occurs.
|
||||||
|
* @param name The name of the kube resource
|
||||||
|
* @param kind The kind of the kube resource
|
||||||
|
* @param patch The list of JSON operations
|
||||||
|
* @param ns The optional namespace of the kube resource
|
||||||
|
*/
|
||||||
|
async patch(name: string, kind: string, patch: Patch, ns?: string): Promise<string> {
|
||||||
|
appEventBus.emit({ name: "resource", action: "patch" });
|
||||||
|
|
||||||
|
const kubectl = await this.cluster.ensureKubectl();
|
||||||
|
const kubectlPath = await kubectl.getPath();
|
||||||
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
const args = [
|
||||||
|
"--kubeconfig", proxyKubeconfigPath,
|
||||||
|
"patch",
|
||||||
|
kind,
|
||||||
|
name,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (ns) {
|
||||||
|
args.push("--namespace", ns);
|
||||||
|
}
|
||||||
|
|
||||||
|
args.push(
|
||||||
|
"--type", "json",
|
||||||
|
"--patch", JSON.stringify(patch),
|
||||||
|
"-o", "yaml"
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout } = await promiseExecFile(kubectlPath, args);
|
||||||
|
|
||||||
|
return stdout;
|
||||||
|
} catch (error) {
|
||||||
|
throw error.stderr ?? error;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async apply(resource: KubernetesObject | any): Promise<string> {
|
async apply(resource: KubernetesObject | any): Promise<string> {
|
||||||
resource = this.sanitizeObject(resource);
|
resource = this.sanitizeObject(resource);
|
||||||
appEventBus.emit({ name: "resource", action: "apply" });
|
appEventBus.emit({ name: "resource", action: "apply" });
|
||||||
|
|
||||||
return await this.kubectlApply(yaml.safeDump(resource));
|
return this.kubectlApply(yaml.safeDump(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async kubectlApply(content: string): Promise<string> {
|
protected async kubectlApply(content: string): Promise<string> {
|
||||||
const kubectl = await this.cluster.ensureKubectl();
|
const kubectl = await this.cluster.ensureKubectl();
|
||||||
const kubectlPath = await kubectl.getPath();
|
const kubectlPath = await kubectl.getPath();
|
||||||
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
const proxyKubeconfigPath = await this.cluster.getProxyKubeconfigPath();
|
||||||
|
const fileName = tempy.file({ name: "resource.yaml" });
|
||||||
|
const args = [
|
||||||
|
"apply",
|
||||||
|
"--kubeconfig", proxyKubeconfigPath,
|
||||||
|
"-o", "json",
|
||||||
|
"-f", fileName,
|
||||||
|
];
|
||||||
|
|
||||||
return new Promise<string>((resolve, reject) => {
|
await fs.writeFile(fileName, content);
|
||||||
const fileName = tempy.file({ name: "resource.yaml" });
|
|
||||||
|
|
||||||
fs.writeFileSync(fileName, content);
|
logger.debug(`shooting manifests with with ${kubectlPath}`, { args });
|
||||||
const cmd = `"${kubectlPath}" apply --kubeconfig "${proxyKubeconfigPath}" -o json -f "${fileName}"`;
|
|
||||||
|
|
||||||
logger.debug(`shooting manifests with: ${cmd}`);
|
const execEnv = { ...process.env };
|
||||||
const execEnv: NodeJS.ProcessEnv = Object.assign({}, process.env);
|
const httpsProxy = this.cluster.preferences?.httpsProxy;
|
||||||
const httpsProxy = this.cluster.preferences?.httpsProxy;
|
|
||||||
|
|
||||||
if (httpsProxy) {
|
if (httpsProxy) {
|
||||||
execEnv["HTTPS_PROXY"] = httpsProxy;
|
execEnv["HTTPS_PROXY"] = httpsProxy;
|
||||||
}
|
}
|
||||||
exec(cmd, { env: execEnv },
|
|
||||||
(error, stdout, stderr) => {
|
|
||||||
if (stderr != "") {
|
|
||||||
fs.unlinkSync(fileName);
|
|
||||||
reject(stderr);
|
|
||||||
|
|
||||||
return;
|
try {
|
||||||
}
|
const { stdout } = await promiseExecFile(kubectlPath, args);
|
||||||
fs.unlinkSync(fileName);
|
|
||||||
resolve(JSON.parse(stdout));
|
return stdout;
|
||||||
});
|
} catch (error) {
|
||||||
});
|
throw error?.stderr ?? error;
|
||||||
|
} finally {
|
||||||
|
await fs.unlink(fileName);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise<string> {
|
public async kubectlApplyAll(resources: string[], extraArgs = ["-o", "json"]): Promise<string> {
|
||||||
|
|||||||
@ -198,5 +198,6 @@ export class Router {
|
|||||||
|
|
||||||
// Resource Applier API
|
// Resource Applier API
|
||||||
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
|
this.router.add({ method: "post", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.applyResource);
|
||||||
|
this.router.add({ method: "patch", path: `${apiPrefix}/stack` }, ResourceApplierApiRoute.patchResource);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,19 @@ export class ResourceApplierApiRoute {
|
|||||||
try {
|
try {
|
||||||
const resource = await new ResourceApplier(cluster).apply(payload);
|
const resource = await new ResourceApplier(cluster).apply(payload);
|
||||||
|
|
||||||
respondJson(response, [resource], 200);
|
respondJson(response, resource, 200);
|
||||||
|
} catch (error) {
|
||||||
|
respondText(response, error, 422);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async patchResource(request: LensApiRequest) {
|
||||||
|
const { response, cluster, payload } = request;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const resource = await new ResourceApplier(cluster).patch(payload.name, payload.kind, payload.ns, payload.patch);
|
||||||
|
|
||||||
|
respondJson(response, resource, 200);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
respondText(response, error, 422);
|
respondText(response, error, 422);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -112,6 +112,7 @@ const dummyDeployment: Deployment = {
|
|||||||
toPlainObject: jest.fn(),
|
toPlainObject: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<DeploymentScaleDialog />", () => {
|
describe("<DeploymentScaleDialog />", () => {
|
||||||
|
|||||||
@ -107,6 +107,7 @@ const dummyReplicaSet: ReplicaSet = {
|
|||||||
toPlainObject: jest.fn(),
|
toPlainObject: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<ReplicaSetScaleDialog />", () => {
|
describe("<ReplicaSetScaleDialog />", () => {
|
||||||
|
|||||||
@ -117,6 +117,7 @@ const dummyStatefulSet: StatefulSet = {
|
|||||||
toPlainObject: jest.fn(),
|
toPlainObject: jest.fn(),
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
delete: jest.fn(),
|
delete: jest.fn(),
|
||||||
|
patch: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("<StatefulSetScaleDialog />", () => {
|
describe("<StatefulSetScaleDialog />", () => {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ import { createResourceStore } from "./create-resource.store";
|
|||||||
import type { DockTab } from "./dock.store";
|
import type { DockTab } from "./dock.store";
|
||||||
import { EditorPanel } from "./editor-panel";
|
import { EditorPanel } from "./editor-panel";
|
||||||
import { InfoPanel } from "./info-panel";
|
import { InfoPanel } from "./info-panel";
|
||||||
import { resourceApplierApi } from "../../../common/k8s-api/endpoints/resource-applier.api";
|
import * as resourceApplierApi from "../../../common/k8s-api/endpoints/resource-applier.api";
|
||||||
import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
import type { JsonApiErrorParsed } from "../../../common/k8s-api/json-api";
|
||||||
import { Notifications } from "../notifications";
|
import { Notifications } from "../notifications";
|
||||||
import { monacoModelsManager } from "./monaco-model-manager";
|
import { monacoModelsManager } from "./monaco-model-manager";
|
||||||
|
|||||||
@ -31,6 +31,7 @@ import {monacoModelsManager} from "./monaco-model-manager";
|
|||||||
export interface EditingResource {
|
export interface EditingResource {
|
||||||
resource: string; // resource path, e.g. /api/v1/namespaces/default
|
resource: string; // resource path, e.g. /api/v1/namespaces/default
|
||||||
draft?: string; // edited draft in yaml
|
draft?: string; // edited draft in yaml
|
||||||
|
firstDraft?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EditResourceStore extends DockTabStore<EditingResource> {
|
export class EditResourceStore extends DockTabStore<EditingResource> {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ import "./edit-resource.scss";
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { action, computed, makeObservable, observable } from "mobx";
|
import { action, computed, makeObservable, observable } from "mobx";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import jsYaml from "js-yaml";
|
import yaml from "js-yaml";
|
||||||
import type { DockTab } from "./dock.store";
|
import type { DockTab } from "./dock.store";
|
||||||
import { cssNames } from "../../utils";
|
import { cssNames } from "../../utils";
|
||||||
import { editResourceStore } from "./edit-resource.store";
|
import { editResourceStore } from "./edit-resource.store";
|
||||||
@ -33,6 +33,7 @@ import { Badge } from "../badge";
|
|||||||
import { EditorPanel } from "./editor-panel";
|
import { EditorPanel } from "./editor-panel";
|
||||||
import { Spinner } from "../spinner";
|
import { Spinner } from "../spinner";
|
||||||
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
import type { KubeObject } from "../../../common/k8s-api/kube-object";
|
||||||
|
import { createPatch } from "rfc6902";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -71,16 +72,17 @@ export class EditResource extends React.Component<Props> {
|
|||||||
return draft;
|
return draft;
|
||||||
}
|
}
|
||||||
|
|
||||||
return jsYaml.safeDump(this.resource.toPlainObject()); // dump resource first time
|
return yaml.safeDump(this.resource.toPlainObject()); // dump resource first time
|
||||||
}
|
}
|
||||||
|
|
||||||
@action
|
@action
|
||||||
saveDraft(draft: string | object) {
|
saveDraft(draft: string | object) {
|
||||||
if (typeof draft === "object") {
|
if (typeof draft === "object") {
|
||||||
draft = draft ? jsYaml.safeDump(draft) : undefined;
|
draft = draft ? yaml.safeDump(draft) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
editResourceStore.setData(this.tabId, {
|
editResourceStore.setData(this.tabId, {
|
||||||
|
firstDraft: draft, // this must be before the next line
|
||||||
...editResourceStore.getData(this.tabId),
|
...editResourceStore.getData(this.tabId),
|
||||||
draft,
|
draft,
|
||||||
});
|
});
|
||||||
@ -96,15 +98,14 @@ export class EditResource extends React.Component<Props> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const store = editResourceStore.getStore(this.tabId);
|
const store = editResourceStore.getStore(this.tabId);
|
||||||
const updatedResource: KubeObject = await store.update(this.resource, jsYaml.safeLoad(this.draft));
|
const currentEdit = yaml.safeLoad(this.draft);
|
||||||
|
const firstVersion = yaml.safeLoad(editResourceStore.getData(this.tabId).firstDraft ?? this.draft);
|
||||||
this.saveDraft(updatedResource.toPlainObject()); // update with new resourceVersion to avoid further errors on save
|
const patches = createPatch(firstVersion, currentEdit);
|
||||||
const resourceType = updatedResource.kind;
|
const updatedResource = await store.patch(this.resource, patches);
|
||||||
const resourceName = updatedResource.getName();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p>
|
<p>
|
||||||
{resourceType} <b>{resourceName}</b> updated.
|
{updatedResource.kind} <b>{updatedResource.getName()}</b> updated.
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user