@f5-sales-demo/pi-resource-management 20.3.2 → 20.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-resource-management",
4
- "version": "20.3.2",
4
+ "version": "20.4.0",
5
5
  "description": "Shared resource management for F5 XC manifest parsing, validation, diff, and CRUD operations",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Robin Mordasiewicz",
package/src/arg-parser.ts CHANGED
@@ -2,7 +2,7 @@ import type { ParsedExportArgs, ParsedResourceArgs } from "./types";
2
2
 
3
3
  const VALID_OUTPUT_FORMATS = new Set(["json", "yaml", "table", "wide"]);
4
4
  const VALID_EXPORT_FORMATS = new Set(["json", "yaml"]);
5
- const VALID_DRY_RUN_MODES = new Set(["client", "server"]);
5
+ const VALID_DRY_RUN_MODES = new Set(["client"]);
6
6
 
7
7
  export function parseResourceArgs(argsString: string): ParsedResourceArgs | { error: string } {
8
8
  const tokens = argsString.split(/\s+/).filter(Boolean);
@@ -11,7 +11,6 @@ export function parseResourceArgs(argsString: string): ParsedResourceArgs | { er
11
11
  let outputFormat: ParsedResourceArgs["outputFormat"] = "table";
12
12
  let dryRun: ParsedResourceArgs["dryRun"];
13
13
  let recursive = false;
14
- let force = false;
15
14
  const positionals: string[] = [];
16
15
 
17
16
  for (let i = 0; i < tokens.length; i++) {
@@ -44,7 +43,7 @@ export function parseResourceArgs(argsString: string): ParsedResourceArgs | { er
44
43
  if (token.includes("=")) {
45
44
  const mode = token.split("=")[1];
46
45
  if (!VALID_DRY_RUN_MODES.has(mode)) {
47
- return { error: `Invalid --dry-run mode: "${mode}". Must be "client" or "server".` };
46
+ return { error: `Invalid --dry-run mode: "${mode}". Must be "client".` };
48
47
  }
49
48
  dryRun = mode as ParsedResourceArgs["dryRun"];
50
49
  } else {
@@ -52,8 +51,6 @@ export function parseResourceArgs(argsString: string): ParsedResourceArgs | { er
52
51
  }
53
52
  } else if (token === "-R" || token === "--recursive") {
54
53
  recursive = true;
55
- } else if (token === "--force") {
56
- force = true;
57
54
  } else if (token.startsWith("-")) {
58
55
  return { error: `Unknown flag: "${token}".` };
59
56
  } else {
@@ -67,7 +64,6 @@ export function parseResourceArgs(argsString: string): ParsedResourceArgs | { er
67
64
  outputFormat,
68
65
  dryRun,
69
66
  recursive,
70
- force,
71
67
  kind: positionals[0],
72
68
  name: positionals[1],
73
69
  };
package/src/index.ts CHANGED
@@ -9,6 +9,8 @@ export type { ExportedManifest, ManifestOutputFormat, MinimalExportFilter } from
9
9
  export { applyMinimalExportFilter, formatManifestOutput, toManifest, toManifestList } from "./manifest-export";
10
10
  export { ManifestParseError, parseManifests } from "./manifest-parser";
11
11
  export { formatValidationErrors, validateManifest, validateManifests } from "./manifest-validator";
12
+ export type { RunResourceOperationOptions } from "./operation-runner";
13
+ export { formatResourceOperationReport, readManifestInputs, runResourceOperation } from "./operation-runner";
12
14
  export {
13
15
  formatMultiOperationSummary,
14
16
  formatOperationResult,
@@ -26,6 +28,7 @@ export type {
26
28
  HttpTransportRequest,
27
29
  HttpTransportResponse,
28
30
  KindResolver,
31
+ ManifestOperationInput,
29
32
  ManifestValidationResult,
30
33
  OperationResult,
31
34
  ParsedExportArgs,
@@ -36,6 +39,10 @@ export type {
36
39
  ResourceError,
37
40
  ResourceErrorKind,
38
41
  ResourceManifest,
42
+ ResourceOperation,
43
+ ResourceOperationCounts,
44
+ ResourceOperationItem,
45
+ ResourceOperationReport,
39
46
  ValidationError,
40
47
  ValidationErrorCode,
41
48
  ValidationWarning,
@@ -12,6 +12,7 @@ export function validateManifest(
12
12
  manifest: ResourceManifest,
13
13
  resolver: KindResolver,
14
14
  namespaceOverride?: string,
15
+ options: { operation?: "create" | "update" | "identity" } = {},
15
16
  ): { result: ManifestValidationResult; resolved?: ResolvedKind } {
16
17
  const errors: ValidationError[] = [];
17
18
  const warnings: ValidationWarning[] = [];
@@ -53,15 +54,18 @@ export function validateManifest(
53
54
  }
54
55
  }
55
56
 
56
- if (resolved?.validation) {
57
- const requiredFields = resolved.validation.create ?? resolved.validation.minimum_config ?? [];
57
+ if (resolved?.validation && options.operation !== "identity") {
58
+ const requiredFields =
59
+ options.operation === "update"
60
+ ? (resolved.validation.update ?? resolved.validation.minimum_config ?? [])
61
+ : (resolved.validation.create ?? resolved.validation.minimum_config ?? []);
58
62
  for (const fieldPath of requiredFields) {
59
63
  if (fieldPath.startsWith("metadata.")) continue;
60
64
  const value = getNestedValue(manifest.rawObject, fieldPath);
61
65
  if (value === undefined || value === null) {
62
66
  errors.push({
63
67
  path: fieldPath,
64
- message: `Required field "${fieldPath}" is missing for ${manifest.kind} creation.`,
68
+ message: `Required field "${fieldPath}" is missing for ${manifest.kind} ${options.operation ?? "create"}.`,
65
69
  code: "MISSING_FIELD",
66
70
  });
67
71
  }
@@ -75,11 +79,12 @@ export function validateManifests(
75
79
  manifests: ResourceManifest[],
76
80
  resolver: KindResolver,
77
81
  namespaceOverride?: string,
82
+ options: { operation?: "create" | "update" | "identity" } = {},
78
83
  ): { results: ManifestValidationResult[]; resolved: (ResolvedKind | undefined)[] } {
79
84
  const results: ManifestValidationResult[] = [];
80
85
  const resolved: (ResolvedKind | undefined)[] = [];
81
86
  for (const manifest of manifests) {
82
- const v = validateManifest(manifest, resolver, namespaceOverride);
87
+ const v = validateManifest(manifest, resolver, namespaceOverride, options);
83
88
  results.push(v.result);
84
89
  resolved.push(v.resolved);
85
90
  }
@@ -0,0 +1,371 @@
1
+ import { stringify as yamlStringify } from "yaml";
2
+ import { formatDiff } from "./diff-engine";
3
+ import { readManifestFiles } from "./file-reader";
4
+ import { toManifestList } from "./manifest-export";
5
+ import { parseManifests } from "./manifest-parser";
6
+ import { validateManifest } from "./manifest-validator";
7
+ import type { ResourceClient } from "./resource-client";
8
+ import type {
9
+ KindResolver,
10
+ ManifestOperationInput,
11
+ OperationResult,
12
+ ResourceError,
13
+ ResourceOperation,
14
+ ResourceOperationItem,
15
+ ResourceOperationReport,
16
+ } from "./types";
17
+
18
+ export interface RunResourceOperationOptions {
19
+ operation: ResourceOperation;
20
+ kindResolver: KindResolver;
21
+ client?: ResourceClient;
22
+ inputs?: ManifestOperationInput[];
23
+ kind?: string;
24
+ name?: string;
25
+ all?: boolean;
26
+ namespaceOverride?: string;
27
+ defaultNamespace?: string;
28
+ dryRun?: "client";
29
+ }
30
+
31
+ const MANIFEST_OPERATIONS = new Set<ResourceOperation>(["apply", "create", "update", "delete", "diff", "validate"]);
32
+
33
+ export async function readManifestInputs(filenames: string[], recursive: boolean): Promise<ManifestOperationInput[]> {
34
+ const files = await readManifestFiles(filenames, recursive);
35
+ const inputs: ManifestOperationInput[] = [];
36
+ let index = 0;
37
+ for (const file of files) {
38
+ for (const manifest of parseManifests(file.objects, file.sourcePath)) {
39
+ inputs.push({ index, sourcePath: file.sourcePath, manifest });
40
+ index++;
41
+ }
42
+ }
43
+ return inputs;
44
+ }
45
+
46
+ export async function runResourceOperation(options: RunResourceOperationOptions): Promise<ResourceOperationReport> {
47
+ if (options.operation === "get" && options.inputs !== undefined) return runManifestGet(options);
48
+ if (MANIFEST_OPERATIONS.has(options.operation)) return runManifestOperation(options);
49
+ if (!options.client) return reportForError(options.operation, "API credentials are required for this operation.");
50
+ if (options.operation === "get") return runGet(options);
51
+ return runExport(options);
52
+ }
53
+
54
+ async function runManifestGet(options: RunResourceOperationOptions): Promise<ResourceOperationReport> {
55
+ const inputs = options.inputs ?? [];
56
+ if (inputs.length === 0) return reportForError("get", "No resource manifests were provided.");
57
+
58
+ const validations = inputs.map(input => {
59
+ const namespace = options.namespaceOverride ?? input.manifest.metadata.namespace ?? options.defaultNamespace;
60
+ return validateManifest(input.manifest, options.kindResolver, namespace, { operation: "identity" });
61
+ });
62
+ if (validations.some(validation => !validation.result.valid || !validation.resolved)) {
63
+ return buildReport(
64
+ "get",
65
+ inputs.map((input, index) => {
66
+ const validation = validations[index].result;
67
+ if (validation.valid && validations[index].resolved) {
68
+ return itemFromInput(input, options, {
69
+ status: "skipped",
70
+ error: validationError("Batch validation failed; no operations were executed."),
71
+ });
72
+ }
73
+ return itemFromInput(input, options, {
74
+ status: "error",
75
+ validation,
76
+ error: validationError(validation.errors.map(error => `${error.path}: ${error.message}`).join("; ")),
77
+ });
78
+ }),
79
+ );
80
+ }
81
+ if (!options.client) return reportForError("get", "API credentials are required for this operation.");
82
+
83
+ const results: ResourceOperationItem[] = [];
84
+ for (let index = 0; index < inputs.length; index++) {
85
+ const input = inputs[index];
86
+ try {
87
+ const result = await options.client.get(
88
+ validations[index].resolved!,
89
+ input.manifest.metadata.name,
90
+ options.namespaceOverride ?? input.manifest.metadata.namespace ?? options.defaultNamespace,
91
+ );
92
+ results.push(
93
+ result.error
94
+ ? itemFromInput(input, options, { status: "error", error: result.error })
95
+ : itemFromInput(input, options, { status: "found", resource: result.resource }),
96
+ );
97
+ } catch (error) {
98
+ results.push(itemFromInput(input, options, { status: "error", error: unexpectedError(error) }));
99
+ }
100
+ }
101
+ return buildReport("get", results);
102
+ }
103
+
104
+ async function runManifestOperation(options: RunResourceOperationOptions): Promise<ResourceOperationReport> {
105
+ const inputs = options.inputs ?? [];
106
+ if (inputs.length === 0) return reportForError(options.operation, "No resource manifests were provided.");
107
+
108
+ const validationMode =
109
+ options.operation === "delete" ? "identity" : options.operation === "update" ? "update" : "create";
110
+ const validations = inputs.map(input => {
111
+ const namespace = options.namespaceOverride ?? input.manifest.metadata.namespace ?? options.defaultNamespace;
112
+ return validateManifest(input.manifest, options.kindResolver, namespace, { operation: validationMode });
113
+ });
114
+ const invalid = validations.some(validation => !validation.result.valid || !validation.resolved);
115
+ if (invalid) {
116
+ const results = inputs.map((input, index): ResourceOperationItem => {
117
+ const validation = validations[index].result;
118
+ if (validation.valid && validations[index].resolved) {
119
+ return itemFromInput(input, options, {
120
+ status: "skipped",
121
+ error: validationError("Batch validation failed; no operations were executed."),
122
+ });
123
+ }
124
+ return itemFromInput(input, options, {
125
+ status: "error",
126
+ validation,
127
+ error: validationError(validation.errors.map(error => `${error.path}: ${error.message}`).join("; ")),
128
+ });
129
+ });
130
+ return buildReport(options.operation, results);
131
+ }
132
+
133
+ if (options.operation === "validate") {
134
+ return buildReport(
135
+ options.operation,
136
+ inputs.map(input => itemFromInput(input, options, { status: "valid" })),
137
+ );
138
+ }
139
+ if (!options.client) return reportForError(options.operation, "API credentials are required for this operation.");
140
+
141
+ const results: ResourceOperationItem[] = [];
142
+ for (let index = 0; index < inputs.length; index++) {
143
+ const input = inputs[index];
144
+ const resolved = validations[index].resolved!;
145
+ try {
146
+ if (options.operation === "diff") {
147
+ const diff = await options.client.diff(input.manifest, resolved, options.namespaceOverride);
148
+ if (diff.error) {
149
+ results.push(itemFromInput(input, options, { status: "error", error: diff.error }));
150
+ } else {
151
+ results.push(
152
+ itemFromInput(input, options, {
153
+ status: diff.isNew ? "new" : diff.diff?.hasDifferences ? "different" : "identical",
154
+ diff: diff.diff,
155
+ isNew: diff.isNew,
156
+ }),
157
+ );
158
+ }
159
+ continue;
160
+ }
161
+
162
+ let operationResult: OperationResult;
163
+ if (options.operation === "apply") {
164
+ operationResult = await options.client.apply(
165
+ input.manifest,
166
+ resolved,
167
+ options.namespaceOverride,
168
+ options.dryRun,
169
+ );
170
+ } else if (options.operation === "create") {
171
+ operationResult = await options.client.create(
172
+ input.manifest,
173
+ resolved,
174
+ options.namespaceOverride,
175
+ options.dryRun,
176
+ );
177
+ } else if (options.operation === "update") {
178
+ operationResult = await options.client.update(
179
+ input.manifest,
180
+ resolved,
181
+ options.namespaceOverride,
182
+ options.dryRun,
183
+ );
184
+ } else {
185
+ operationResult = await options.client.delete(
186
+ input.manifest.kind,
187
+ input.manifest.metadata.name,
188
+ resolved,
189
+ options.namespaceOverride ?? input.manifest.metadata.namespace,
190
+ options.dryRun,
191
+ );
192
+ }
193
+ results.push(operationResultToItem(input, options, operationResult));
194
+ } catch (error) {
195
+ results.push(itemFromInput(input, options, { status: "error", error: unexpectedError(error) }));
196
+ }
197
+ }
198
+ return buildReport(options.operation, results);
199
+ }
200
+
201
+ async function runGet(options: RunResourceOperationOptions): Promise<ResourceOperationReport> {
202
+ if (!options.kind) return reportForError("get", "A resource kind is required.");
203
+ try {
204
+ const resolved = options.kindResolver.resolveKind(options.kind);
205
+ const result = await options.client!.get(
206
+ resolved,
207
+ options.name,
208
+ options.namespaceOverride ?? options.defaultNamespace,
209
+ );
210
+ if (result.error)
211
+ return buildReport("get", [
212
+ { index: 0, kind: options.kind, name: options.name, status: "error", error: result.error },
213
+ ]);
214
+ return buildReport("get", [
215
+ {
216
+ index: 0,
217
+ kind: options.kind,
218
+ name: options.name,
219
+ status: options.name ? "found" : "listed",
220
+ resource: result.resource,
221
+ items: result.items,
222
+ },
223
+ ]);
224
+ } catch (error) {
225
+ return reportForError("get", (error as Error).message);
226
+ }
227
+ }
228
+
229
+ async function runExport(options: RunResourceOperationOptions): Promise<ResourceOperationReport> {
230
+ try {
231
+ if (options.all) {
232
+ const result = await options.client!.exportAll(
233
+ options.kindResolver,
234
+ options.namespaceOverride ?? options.defaultNamespace,
235
+ );
236
+ const items: ResourceOperationItem[] = result.manifests.map((manifest, index) => ({
237
+ index,
238
+ kind: manifest.kind,
239
+ name: String(manifest.metadata.name ?? ""),
240
+ status: "exported",
241
+ manifest,
242
+ }));
243
+ for (const entry of result.errors) {
244
+ items.push({ index: items.length, kind: entry.kind, status: "error", error: entry.error });
245
+ }
246
+ return buildReport("export", items);
247
+ }
248
+ if (!options.kind) return reportForError("export", "A resource kind or --all is required.");
249
+ const resolved = options.kindResolver.resolveKind(options.kind);
250
+ if (options.name) {
251
+ const result = await options.client!.exportOne(
252
+ options.kind,
253
+ resolved,
254
+ options.name,
255
+ options.namespaceOverride ?? options.defaultNamespace,
256
+ );
257
+ if (result.error)
258
+ return buildReport("export", [
259
+ { index: 0, kind: options.kind, name: options.name, status: "error", error: result.error },
260
+ ]);
261
+ return buildReport("export", [
262
+ { index: 0, kind: options.kind, name: options.name, status: "exported", manifest: result.manifest },
263
+ ]);
264
+ }
265
+ const result = await options.client!.get(
266
+ resolved,
267
+ undefined,
268
+ options.namespaceOverride ?? options.defaultNamespace,
269
+ );
270
+ if (result.error)
271
+ return buildReport("export", [{ index: 0, kind: options.kind, status: "error", error: result.error }]);
272
+ const manifests = toManifestList({ items: result.items ?? [] }, options.kind);
273
+ return buildReport(
274
+ "export",
275
+ manifests.map((manifest, index) => ({
276
+ index,
277
+ kind: options.kind,
278
+ name: String(manifest.metadata.name ?? ""),
279
+ status: "exported",
280
+ manifest,
281
+ })),
282
+ );
283
+ } catch (error) {
284
+ return reportForError("export", (error as Error).message);
285
+ }
286
+ }
287
+
288
+ function operationResultToItem(
289
+ input: ManifestOperationInput,
290
+ options: RunResourceOperationOptions,
291
+ result: OperationResult,
292
+ ): ResourceOperationItem {
293
+ if (result.status === "error") return itemFromInput(input, options, { status: "error", error: result.error });
294
+ if (result.status === "deleted")
295
+ return itemFromInput(input, options, { status: result.status, durationMs: result.durationMs });
296
+ if (result.status === "dry-run")
297
+ return itemFromInput(input, options, { status: result.status, action: result.action, diff: result.diff });
298
+ if (result.status === "updated")
299
+ return itemFromInput(input, options, {
300
+ status: result.status,
301
+ durationMs: result.durationMs,
302
+ resource: result.resource,
303
+ diff: result.diff,
304
+ });
305
+ if (result.status === "created")
306
+ return itemFromInput(input, options, {
307
+ status: result.status,
308
+ durationMs: result.durationMs,
309
+ resource: result.resource,
310
+ });
311
+ return itemFromInput(input, options, { status: result.status, resource: result.resource });
312
+ }
313
+
314
+ function itemFromInput(
315
+ input: ManifestOperationInput,
316
+ options: RunResourceOperationOptions,
317
+ extra: Partial<ResourceOperationItem> & Pick<ResourceOperationItem, "status">,
318
+ ): ResourceOperationItem {
319
+ return {
320
+ index: input.index,
321
+ sourcePath: input.sourcePath,
322
+ kind: input.manifest.kind,
323
+ name: input.manifest.metadata.name,
324
+ namespace: options.namespaceOverride ?? input.manifest.metadata.namespace ?? options.defaultNamespace,
325
+ ...extra,
326
+ };
327
+ }
328
+
329
+ function validationError(message: string): ResourceError {
330
+ return { kind: "validation", message };
331
+ }
332
+
333
+ function unexpectedError(error: unknown): ResourceError {
334
+ return { kind: "api", message: error instanceof Error ? error.message : String(error) };
335
+ }
336
+
337
+ function reportForError(operation: ResourceOperation, message: string): ResourceOperationReport {
338
+ return buildReport(operation, [{ index: 0, status: "error", error: validationError(message) }]);
339
+ }
340
+
341
+ function buildReport(operation: ResourceOperation, results: ResourceOperationItem[]): ResourceOperationReport {
342
+ const failed = results.filter(result => result.status === "error" || result.status === "skipped").length;
343
+ const counts = {
344
+ total: results.length,
345
+ succeeded: results.length - failed,
346
+ failed,
347
+ } as ResourceOperationReport["counts"];
348
+ for (const result of results) counts[result.status] = (counts[result.status] ?? 0) + 1;
349
+ return { schemaVersion: 1, operation, success: failed === 0, counts, results };
350
+ }
351
+
352
+ export function formatResourceOperationReport(
353
+ report: ResourceOperationReport,
354
+ format: "json" | "yaml" | "table" | "wide" = "table",
355
+ ): string {
356
+ if (format === "json") return JSON.stringify(report, null, 2);
357
+ if (format === "yaml") return yamlStringify(report);
358
+ const lines = report.results.map(result => {
359
+ const identity =
360
+ result.kind && result.name
361
+ ? `${result.kind}/${result.name}`
362
+ : (result.kind ?? result.sourcePath ?? "resource");
363
+ if (result.error) return `${identity} error: ${result.error.message}`;
364
+ if (result.diff?.hasDifferences) {
365
+ return `${identity} ${result.status}\n${formatDiff(result.diff, result.kind ?? "resource", result.name ?? "unknown")}`;
366
+ }
367
+ return `${identity} ${result.status}`;
368
+ });
369
+ lines.push(`${report.counts.succeeded} succeeded, ${report.counts.failed} failed, ${report.counts.total} total`);
370
+ return lines.join("\n");
371
+ }
@@ -24,6 +24,9 @@ export function formatOperationResult(
24
24
  if (result.action === "create") {
25
25
  return `${manifest.kind}/${manifest.metadata.name} would be created (dry-run)`;
26
26
  }
27
+ if (result.action === "delete") {
28
+ return `${manifest.kind}/${manifest.metadata.name} would be deleted (dry-run)`;
29
+ }
27
30
  if (result.diff) {
28
31
  return `${manifest.kind}/${manifest.metadata.name} would be updated (dry-run)\n${formatDiff(result.diff, manifest.kind, manifest.metadata.name)}`;
29
32
  }
@@ -117,7 +117,7 @@ export class ResourceClient {
117
117
  manifest: ResourceManifest,
118
118
  resolved: ResolvedKind,
119
119
  namespaceOverride?: string,
120
- dryRun?: "client" | "server",
120
+ dryRun?: "client",
121
121
  ): Promise<OperationResult> {
122
122
  const namespace = this.#resolveNamespace(manifest, namespaceOverride);
123
123
  const name = manifest.metadata.name;
@@ -147,7 +147,7 @@ export class ResourceClient {
147
147
  manifest: ResourceManifest,
148
148
  resolved: ResolvedKind,
149
149
  namespaceOverride?: string,
150
- dryRun?: "client" | "server",
150
+ dryRun?: "client",
151
151
  ): Promise<OperationResult> {
152
152
  if (dryRun) return { status: "dry-run", action: "create" };
153
153
 
@@ -156,12 +156,35 @@ export class ResourceClient {
156
156
  return this.#createResource(manifest, resolved, namespace, startMs);
157
157
  }
158
158
 
159
+ async update(
160
+ manifest: ResourceManifest,
161
+ resolved: ResolvedKind,
162
+ namespaceOverride?: string,
163
+ dryRun?: "client",
164
+ ): Promise<OperationResult> {
165
+ const namespace = this.#resolveNamespace(manifest, namespaceOverride);
166
+ const name = manifest.metadata.name;
167
+ const getUrl = this.#buildUrl(resolved.paths.get, namespace, name);
168
+ const startMs = performance.now();
169
+ const existing = await this.#fetchResource(getUrl);
170
+
171
+ if (existing.error) return { status: "error", error: existing.error };
172
+
173
+ const diff = this.#computeManifestDiff(existing.body!, manifest);
174
+ if (!diff.hasDifferences) return { status: "unchanged", resource: existing.body! };
175
+ if (dryRun) return { status: "dry-run", action: "update", diff };
176
+ return this.#updateResource(manifest, resolved, namespace, diff, startMs);
177
+ }
178
+
159
179
  async delete(
160
180
  kind: string,
161
181
  name: string,
162
182
  resolved: ResolvedKind,
163
183
  namespaceOverride?: string,
184
+ dryRun?: "client",
164
185
  ): Promise<OperationResult> {
186
+ if (dryRun) return { status: "dry-run", action: "delete" };
187
+
165
188
  const namespace = namespaceOverride ?? this.#defaultNamespace;
166
189
  const url = this.#buildUrl(resolved.paths.delete, namespace, name);
167
190
 
@@ -306,12 +329,7 @@ export class ResourceClient {
306
329
  const result = await this.#fetch(url, "POST", body);
307
330
  const durationMs = Math.round(performance.now() - startMs);
308
331
 
309
- if (result.error) {
310
- if (result.error.httpStatus === 409) {
311
- return this.#updateResource(manifest, resolved, namespace, undefined, startMs);
312
- }
313
- return { status: "error", error: result.error };
314
- }
332
+ if (result.error) return { status: "error", error: result.error };
315
333
 
316
334
  return { status: "created", resource: result.body ?? {}, durationMs };
317
335
  }
package/src/types.ts CHANGED
@@ -58,9 +58,8 @@ export interface ParsedResourceArgs {
58
58
  filenames: string[];
59
59
  namespace?: string;
60
60
  outputFormat: "json" | "yaml" | "table" | "wide";
61
- dryRun?: "client" | "server";
61
+ dryRun?: "client";
62
62
  recursive: boolean;
63
- force: boolean;
64
63
  kind?: string;
65
64
  name?: string;
66
65
  }
@@ -141,7 +140,7 @@ export type OperationResult =
141
140
  | { status: "unchanged"; resource: Record<string, unknown> }
142
141
  | { status: "deleted"; name: string; kind: string; durationMs: number }
143
142
  | { status: "error"; error: ResourceError }
144
- | { status: "dry-run"; action: "create" | "update"; diff?: ResourceDiff };
143
+ | { status: "dry-run"; action: "create" | "update" | "delete"; diff?: ResourceDiff };
145
144
 
146
145
  export interface HttpTransportRequest {
147
146
  method: "GET" | "POST" | "PUT" | "DELETE";
@@ -163,11 +162,53 @@ export interface ResourceClientOptions {
163
162
  apiUrl: string;
164
163
  apiToken: string;
165
164
  namespace: string;
166
- dryRun?: "client" | "server";
165
+ dryRun?: "client";
167
166
  resolvePayloadVars?: (json: string) => string;
168
167
  transport?: HttpTransport;
169
168
  }
170
169
 
170
+ export type ResourceOperation = "apply" | "create" | "update" | "get" | "delete" | "diff" | "export" | "validate";
171
+
172
+ export interface ManifestOperationInput {
173
+ index: number;
174
+ sourcePath: string;
175
+ manifest: ResourceManifest;
176
+ }
177
+
178
+ export interface ResourceOperationItem {
179
+ index: number;
180
+ sourcePath?: string;
181
+ kind?: string;
182
+ name?: string;
183
+ namespace?: string;
184
+ status: string;
185
+ action?: "create" | "update" | "delete";
186
+ durationMs?: number;
187
+ resource?: Record<string, unknown>;
188
+ items?: Record<string, unknown>[];
189
+ manifest?: { kind: string; metadata: Record<string, unknown>; spec: Record<string, unknown> };
190
+ manifests?: Array<{ kind: string; metadata: Record<string, unknown>; spec: Record<string, unknown> }>;
191
+ diff?: ResourceDiff;
192
+ isNew?: boolean;
193
+ validation?: ManifestValidationResult;
194
+ error?: ResourceError;
195
+ }
196
+
197
+ export interface ResourceOperationCounts {
198
+ total: number;
199
+ succeeded: number;
200
+ failed: number;
201
+ [status: string]: number;
202
+ }
203
+
204
+ export interface ResourceOperationReport {
205
+ schemaVersion: 1;
206
+ operation: ResourceOperation;
207
+ success: boolean;
208
+ counts: ResourceOperationCounts;
209
+ results: ResourceOperationItem[];
210
+ }
211
+
171
212
  export interface KindResolver {
172
213
  resolveKind(kind: string): ResolvedKind;
173
214
  getAllKnownKinds(): string[];