@f5-sales-demo/pi-resource-management 19.51.2

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.
@@ -0,0 +1,194 @@
1
+ import { stringify as yamlStringify } from "yaml";
2
+
3
+ const METADATA_KEEP_KEYS = new Set(["name", "namespace", "labels", "annotations", "description", "disable"]);
4
+
5
+ const SPEC_STRIP_KEYS = new Set([
6
+ "host_name",
7
+ "dns_info",
8
+ "state",
9
+ "auto_cert_info",
10
+ "internet_vip_info",
11
+ "cert_state",
12
+ "create_form",
13
+ "replace_form",
14
+ "status",
15
+ ]);
16
+
17
+ export interface MinimalExportFilter {
18
+ serverDefaults?: Record<string, unknown>;
19
+ serverDefaultFields?: string[];
20
+ minimumConfigFields?: string[];
21
+ oneofDefaultVariants?: Record<string, string>;
22
+ }
23
+
24
+ export interface ExportedManifest {
25
+ kind: string;
26
+ metadata: Record<string, unknown>;
27
+ spec: Record<string, unknown>;
28
+ }
29
+
30
+ export function toManifest(
31
+ apiResponse: Record<string, unknown>,
32
+ kind: string,
33
+ filter?: MinimalExportFilter,
34
+ ): ExportedManifest {
35
+ const objectData = apiResponse.object as Record<string, unknown> | undefined;
36
+ const getSpec = apiResponse.get_spec as Record<string, unknown> | undefined;
37
+
38
+ const rawMeta = (apiResponse.metadata ?? objectData?.metadata ?? getSpec?.metadata ?? {}) as Record<string, unknown>;
39
+ const metadata: Record<string, unknown> = {};
40
+
41
+ for (const key of Object.keys(rawMeta)) {
42
+ if (METADATA_KEEP_KEYS.has(key)) {
43
+ const val = rawMeta[key];
44
+ if (val !== undefined && val !== null) {
45
+ if (typeof val === "object" && !Array.isArray(val) && Object.keys(val as object).length === 0) continue;
46
+ metadata[key] = val;
47
+ }
48
+ }
49
+ }
50
+
51
+ const rawSpec = (apiResponse.spec ?? objectData?.spec ?? getSpec ?? {}) as Record<string, unknown>;
52
+ let spec = cleanSpec(rawSpec);
53
+ if (filter) {
54
+ spec = applyMinimalExportFilter(spec, filter);
55
+ }
56
+
57
+ return { kind, metadata, spec };
58
+ }
59
+
60
+ function cleanSpec(raw: Record<string, unknown>): Record<string, unknown> {
61
+ const result: Record<string, unknown> = {};
62
+ for (const [key, val] of Object.entries(raw)) {
63
+ if (SPEC_STRIP_KEYS.has(key)) continue;
64
+ const cleaned = cleanValue(val);
65
+ if (cleaned !== undefined) {
66
+ result[key] = cleaned;
67
+ }
68
+ }
69
+ return result;
70
+ }
71
+
72
+ function cleanValue(val: unknown): unknown {
73
+ if (val === null || val === undefined) return undefined;
74
+ if (Array.isArray(val)) {
75
+ if (val.length === 0) return undefined;
76
+ const cleaned = val.map(cleanValue).filter(v => v !== undefined);
77
+ return cleaned.length > 0 ? cleaned : undefined;
78
+ }
79
+ if (typeof val === "object") {
80
+ const obj = val as Record<string, unknown>;
81
+ const keys = Object.keys(obj);
82
+ if (keys.length === 0) return val;
83
+ const result: Record<string, unknown> = {};
84
+ let hasContent = false;
85
+ for (const [k, v] of Object.entries(obj)) {
86
+ const cleaned = cleanValue(v);
87
+ if (cleaned !== undefined) {
88
+ result[k] = cleaned;
89
+ hasContent = true;
90
+ }
91
+ }
92
+ return hasContent ? result : val;
93
+ }
94
+ return val;
95
+ }
96
+
97
+ function deepEqual(a: unknown, b: unknown): boolean {
98
+ if (a === b) return true;
99
+ if (a === null || b === null || a === undefined || b === undefined) return a === b;
100
+ if (typeof a !== typeof b) return false;
101
+ if (typeof a !== "object") return false;
102
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
103
+ if (Array.isArray(a) && Array.isArray(b)) {
104
+ if (a.length !== b.length) return false;
105
+ return a.every((item, i) => deepEqual(item, b[i]));
106
+ }
107
+ const aObj = a as Record<string, unknown>;
108
+ const bObj = b as Record<string, unknown>;
109
+ const aKeys = Object.keys(aObj);
110
+ const bKeys = Object.keys(bObj);
111
+ if (aKeys.length !== bKeys.length) return false;
112
+ return aKeys.every(key => key in bObj && deepEqual(aObj[key], bObj[key]));
113
+ }
114
+
115
+ function isTriviallyEmpty(val: unknown): boolean {
116
+ if (val === false || val === 0 || val === "") return true;
117
+ if (Array.isArray(val) && val.length === 0) return true;
118
+ if (typeof val === "object" && val !== null && !Array.isArray(val) && Object.keys(val as object).length === 0)
119
+ return true;
120
+ return false;
121
+ }
122
+
123
+ export function applyMinimalExportFilter(
124
+ spec: Record<string, unknown>,
125
+ filter: MinimalExportFilter | undefined,
126
+ ): Record<string, unknown> {
127
+ if (!filter) return spec;
128
+
129
+ const minimumSet = new Set(filter.minimumConfigFields ?? []);
130
+ const serverDefaults = filter.serverDefaults ?? {};
131
+ const serverDefaultFieldsSet = new Set(filter.serverDefaultFields ?? []);
132
+ const oneofDefaults = filter.oneofDefaultVariants ?? {};
133
+ const oneofDefaultKeys = new Set(Object.keys(oneofDefaults));
134
+
135
+ function filterObject(obj: Record<string, unknown>, prefix: string): Record<string, unknown> {
136
+ const result: Record<string, unknown> = {};
137
+
138
+ for (const [key, val] of Object.entries(obj)) {
139
+ const path = prefix ? `${prefix}.${key}` : key;
140
+
141
+ if (minimumSet.has(path)) {
142
+ result[key] = val;
143
+ continue;
144
+ }
145
+
146
+ if (path in serverDefaults && deepEqual(val, serverDefaults[path])) {
147
+ continue;
148
+ }
149
+
150
+ if (serverDefaultFieldsSet.has(path) && isTriviallyEmpty(val)) {
151
+ continue;
152
+ }
153
+
154
+ if (oneofDefaultKeys.has(key) && deepEqual(val, {})) {
155
+ continue;
156
+ }
157
+
158
+ if (typeof val === "object" && val !== null && !Array.isArray(val) && Object.keys(val as object).length > 0) {
159
+ const filtered = filterObject(val as Record<string, unknown>, path);
160
+ if (Object.keys(filtered).length > 0) {
161
+ result[key] = filtered;
162
+ }
163
+ continue;
164
+ }
165
+
166
+ result[key] = val;
167
+ }
168
+
169
+ return result;
170
+ }
171
+
172
+ return filterObject(spec, "");
173
+ }
174
+
175
+ export function toManifestList(apiListResponse: Record<string, unknown>, kind: string): ExportedManifest[] {
176
+ const items = Array.isArray(apiListResponse.items) ? (apiListResponse.items as Record<string, unknown>[]) : [];
177
+ return items.map(item => toManifest(item, kind));
178
+ }
179
+
180
+ export type ManifestOutputFormat = "json" | "yaml";
181
+
182
+ export function formatManifestOutput(manifests: ExportedManifest[], format: ManifestOutputFormat): string {
183
+ if (format === "yaml") {
184
+ if (manifests.length === 1) {
185
+ return yamlStringify(manifests[0]);
186
+ }
187
+ return manifests.map(m => yamlStringify(m)).join("---\n");
188
+ }
189
+
190
+ if (manifests.length === 1) {
191
+ return JSON.stringify(manifests[0], null, 2);
192
+ }
193
+ return JSON.stringify(manifests, null, 2);
194
+ }
@@ -0,0 +1,72 @@
1
+ import type { ResourceManifest } from "./types";
2
+
3
+ export class ManifestParseError extends Error {
4
+ readonly manifestIndex: number;
5
+ constructor(message: string, index: number) {
6
+ super(message);
7
+ this.name = "ManifestParseError";
8
+ this.manifestIndex = index;
9
+ }
10
+ }
11
+
12
+ export function parseManifests(objects: Record<string, unknown>[], sourcePath: string): ResourceManifest[] {
13
+ const manifests: ResourceManifest[] = [];
14
+ for (let i = 0; i < objects.length; i++) {
15
+ manifests.push(parseSingleManifest(objects[i], i, sourcePath));
16
+ }
17
+ return manifests;
18
+ }
19
+
20
+ function parseSingleManifest(obj: Record<string, unknown>, index: number, sourcePath: string): ResourceManifest {
21
+ const kind = obj.kind;
22
+ if (typeof kind !== "string" || !kind) {
23
+ throw new ManifestParseError(
24
+ `Manifest at index ${index} in ${sourcePath} is missing required field "kind".`,
25
+ index,
26
+ );
27
+ }
28
+
29
+ const metadata = obj.metadata;
30
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) {
31
+ throw new ManifestParseError(
32
+ `Manifest "${kind}" at index ${index} in ${sourcePath} is missing required field "metadata".`,
33
+ index,
34
+ );
35
+ }
36
+
37
+ const meta = metadata as Record<string, unknown>;
38
+ const name = meta.name;
39
+ if (typeof name !== "string" || !name) {
40
+ throw new ManifestParseError(
41
+ `Manifest "${kind}" at index ${index} in ${sourcePath} is missing required field "metadata.name".`,
42
+ index,
43
+ );
44
+ }
45
+
46
+ const spec = obj.spec;
47
+ if (spec !== undefined && (typeof spec !== "object" || spec === null || Array.isArray(spec))) {
48
+ throw new ManifestParseError(
49
+ `Manifest "${kind}/${name}" at index ${index} in ${sourcePath}: "spec" must be an object.`,
50
+ index,
51
+ );
52
+ }
53
+
54
+ return {
55
+ kind,
56
+ metadata: {
57
+ name,
58
+ namespace: typeof meta.namespace === "string" ? meta.namespace : undefined,
59
+ labels: isStringRecord(meta.labels) ? meta.labels : undefined,
60
+ annotations: isStringRecord(meta.annotations) ? meta.annotations : undefined,
61
+ description: typeof meta.description === "string" ? meta.description : undefined,
62
+ disable: typeof meta.disable === "boolean" ? meta.disable : undefined,
63
+ },
64
+ spec: (spec as Record<string, unknown>) ?? {},
65
+ rawObject: obj,
66
+ };
67
+ }
68
+
69
+ function isStringRecord(value: unknown): value is Record<string, string> {
70
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
71
+ return Object.values(value as Record<string, unknown>).every(v => typeof v === "string");
72
+ }
@@ -0,0 +1,109 @@
1
+ import { KindResolutionError } from "./kind-resolver";
2
+ import type {
3
+ KindResolver,
4
+ ManifestValidationResult,
5
+ ResolvedKind,
6
+ ResourceManifest,
7
+ ValidationError,
8
+ ValidationWarning,
9
+ } from "./types";
10
+
11
+ export function validateManifest(
12
+ manifest: ResourceManifest,
13
+ resolver: KindResolver,
14
+ namespaceOverride?: string,
15
+ ): { result: ManifestValidationResult; resolved?: ResolvedKind } {
16
+ const errors: ValidationError[] = [];
17
+ const warnings: ValidationWarning[] = [];
18
+
19
+ if (!manifest.kind) {
20
+ errors.push({ path: "kind", message: 'Required field "kind" is missing.', code: "MISSING_FIELD" });
21
+ }
22
+
23
+ if (!manifest.metadata.name) {
24
+ errors.push({
25
+ path: "metadata.name",
26
+ message: 'Required field "metadata.name" is missing.',
27
+ code: "MISSING_FIELD",
28
+ });
29
+ }
30
+
31
+ if (!manifest.metadata.namespace && !namespaceOverride) {
32
+ errors.push({
33
+ path: "metadata.namespace",
34
+ message: "No namespace specified. Use -n flag or set metadata.namespace in the manifest.",
35
+ code: "MISSING_FIELD",
36
+ });
37
+ }
38
+
39
+ let resolved: ResolvedKind | undefined;
40
+ if (manifest.kind) {
41
+ try {
42
+ resolved = resolver.resolveKind(manifest.kind);
43
+ } catch (err) {
44
+ if (err instanceof KindResolutionError) {
45
+ errors.push({ path: "kind", message: err.message, code: "UNKNOWN_KIND" });
46
+ } else {
47
+ errors.push({
48
+ path: "kind",
49
+ message: `Failed to resolve kind "${manifest.kind}": ${(err as Error).message}`,
50
+ code: "UNKNOWN_KIND",
51
+ });
52
+ }
53
+ }
54
+ }
55
+
56
+ if (resolved?.validation) {
57
+ const requiredFields = resolved.validation.create ?? resolved.validation.minimum_config ?? [];
58
+ for (const fieldPath of requiredFields) {
59
+ if (fieldPath.startsWith("metadata.")) continue;
60
+ const value = getNestedValue(manifest.rawObject, fieldPath);
61
+ if (value === undefined || value === null) {
62
+ errors.push({
63
+ path: fieldPath,
64
+ message: `Required field "${fieldPath}" is missing for ${manifest.kind} creation.`,
65
+ code: "MISSING_FIELD",
66
+ });
67
+ }
68
+ }
69
+ }
70
+
71
+ return { result: { valid: errors.length === 0, errors, warnings }, resolved };
72
+ }
73
+
74
+ export function validateManifests(
75
+ manifests: ResourceManifest[],
76
+ resolver: KindResolver,
77
+ namespaceOverride?: string,
78
+ ): { results: ManifestValidationResult[]; resolved: (ResolvedKind | undefined)[] } {
79
+ const results: ManifestValidationResult[] = [];
80
+ const resolved: (ResolvedKind | undefined)[] = [];
81
+ for (const manifest of manifests) {
82
+ const v = validateManifest(manifest, resolver, namespaceOverride);
83
+ results.push(v.result);
84
+ resolved.push(v.resolved);
85
+ }
86
+ return { results, resolved };
87
+ }
88
+
89
+ export function formatValidationErrors(manifest: ResourceManifest, result: ManifestValidationResult): string {
90
+ const lines: string[] = [];
91
+ lines.push(`Error: validation failed for ${manifest.kind || "unknown"} "${manifest.metadata.name || "unnamed"}"`);
92
+ for (const error of result.errors) {
93
+ lines.push(` - ${error.path}: ${error.message}`);
94
+ }
95
+ for (const warning of result.warnings) {
96
+ lines.push(` ! ${warning.path}: ${warning.message}`);
97
+ }
98
+ return lines.join("\n");
99
+ }
100
+
101
+ function getNestedValue(obj: Record<string, unknown>, path: string): unknown {
102
+ const parts = path.split(".");
103
+ let current: unknown = obj;
104
+ for (const part of parts) {
105
+ if (current == null || typeof current !== "object") return undefined;
106
+ current = (current as Record<string, unknown>)[part];
107
+ }
108
+ return current;
109
+ }
@@ -0,0 +1,209 @@
1
+ import { stringify as yamlStringify } from "yaml";
2
+ import { formatDiff } from "./diff-engine";
3
+ import type { OperationResult, ResourceDiff, ResourceManifest } from "./types";
4
+
5
+ export type OutputFormat = "json" | "yaml" | "table" | "wide";
6
+
7
+ export function formatOperationResult(
8
+ result: OperationResult,
9
+ manifest: ResourceManifest,
10
+ format: OutputFormat = "table",
11
+ ): string {
12
+ switch (result.status) {
13
+ case "created":
14
+ return formatMutationResult("created", manifest, result.resource, result.durationMs, format);
15
+ case "updated":
16
+ return formatMutationResult("configured", manifest, result.resource, result.durationMs, format, result.diff);
17
+ case "unchanged":
18
+ return `${manifest.kind}/${manifest.metadata.name} unchanged`;
19
+ case "deleted":
20
+ return `${result.kind}/${result.name} deleted (${result.durationMs}ms)`;
21
+ case "error":
22
+ return `Error: ${result.error.message}`;
23
+ case "dry-run":
24
+ if (result.action === "create") {
25
+ return `${manifest.kind}/${manifest.metadata.name} would be created (dry-run)`;
26
+ }
27
+ if (result.diff) {
28
+ return `${manifest.kind}/${manifest.metadata.name} would be updated (dry-run)\n${formatDiff(result.diff, manifest.kind, manifest.metadata.name)}`;
29
+ }
30
+ return `${manifest.kind}/${manifest.metadata.name} would be updated (dry-run)`;
31
+ }
32
+ }
33
+
34
+ function formatMutationResult(
35
+ verb: string,
36
+ manifest: ResourceManifest,
37
+ resource: Record<string, unknown>,
38
+ durationMs: number,
39
+ format: OutputFormat,
40
+ diff?: ResourceDiff,
41
+ ): string {
42
+ if (format === "json") {
43
+ return JSON.stringify(resource, null, 2);
44
+ }
45
+ if (format === "yaml") {
46
+ return yamlStringify(resource);
47
+ }
48
+
49
+ const parts: string[] = [];
50
+ parts.push(`${manifest.kind}/${manifest.metadata.name} ${verb} (${durationMs}ms)`);
51
+ if (diff?.hasDifferences) {
52
+ parts.push(formatDiff(diff, manifest.kind, manifest.metadata.name));
53
+ }
54
+ return parts.join("\n");
55
+ }
56
+
57
+ export function formatResourceList(items: Record<string, unknown>[], kind: string, format: OutputFormat): string {
58
+ if (format === "json") {
59
+ return JSON.stringify({ items }, null, 2);
60
+ }
61
+ if (format === "yaml") {
62
+ return yamlStringify({ items });
63
+ }
64
+
65
+ if (items.length === 0) {
66
+ return `No ${kind} resources found.`;
67
+ }
68
+
69
+ const rows: string[][] = [];
70
+ const headers = format === "wide" ? ["NAME", "NAMESPACE", "DISABLED", "AGE", "UID"] : ["NAME", "NAMESPACE", "AGE"];
71
+
72
+ for (const item of items) {
73
+ const name = String(item.name ?? "");
74
+ const namespace = String(item.namespace ?? "");
75
+ const disabled = item.disabled === true ? "true" : "false";
76
+ const sysMeta = item.system_metadata as Record<string, unknown> | undefined;
77
+ const createdAt = sysMeta?.creation_timestamp ?? item.creation_timestamp;
78
+ const age = typeof createdAt === "string" ? formatAge(createdAt) : "";
79
+ const uid = typeof sysMeta?.uid === "string" ? sysMeta.uid.slice(0, 8) : "";
80
+
81
+ if (format === "wide") {
82
+ rows.push([name, namespace, disabled, age, uid]);
83
+ } else {
84
+ rows.push([name, namespace, age]);
85
+ }
86
+ }
87
+
88
+ return formatTable(headers, rows);
89
+ }
90
+
91
+ export function formatResourceDetail(resource: Record<string, unknown>, kind: string, format: OutputFormat): string {
92
+ if (format === "json") {
93
+ return JSON.stringify(resource, null, 2);
94
+ }
95
+ if (format === "yaml") {
96
+ return yamlStringify(resource);
97
+ }
98
+
99
+ const lines: string[] = [];
100
+ const metadata = resource.metadata as Record<string, unknown> | undefined;
101
+ const sysMeta = resource.system_metadata as Record<string, unknown> | undefined;
102
+ const spec = resource.spec as Record<string, unknown> | undefined;
103
+
104
+ lines.push(`Name: ${metadata?.name ?? ""}`);
105
+ lines.push(`Kind: ${kind}`);
106
+ lines.push(`Namespace: ${metadata?.namespace ?? ""}`);
107
+
108
+ if (sysMeta) {
109
+ lines.push(`UID: ${sysMeta.uid ?? ""}`);
110
+ if (sysMeta.creation_timestamp) lines.push(`Created: ${sysMeta.creation_timestamp}`);
111
+ if (sysMeta.creator_id) lines.push(`Creator: ${sysMeta.creator_id}`);
112
+ }
113
+
114
+ if (metadata?.labels && Object.keys(metadata.labels as object).length > 0) {
115
+ lines.push(`Labels: ${formatLabels(metadata.labels as Record<string, string>)}`);
116
+ }
117
+
118
+ if (metadata?.description) lines.push(`Description: ${metadata.description}`);
119
+ if (metadata?.disable === true) lines.push(`Disabled: true`);
120
+
121
+ if (spec) {
122
+ lines.push("");
123
+ lines.push("Spec:");
124
+ const specJson = JSON.stringify(spec, null, 2);
125
+ for (const line of specJson.split("\n")) {
126
+ lines.push(` ${line}`);
127
+ }
128
+ }
129
+
130
+ const status = resource.status;
131
+ if (status) {
132
+ lines.push("");
133
+ lines.push("Status:");
134
+ const statusJson = JSON.stringify(status, null, 2);
135
+ for (const line of statusJson.split("\n")) {
136
+ lines.push(` ${line}`);
137
+ }
138
+ }
139
+
140
+ return lines.join("\n");
141
+ }
142
+
143
+ export function formatMultiOperationSummary(results: OperationResult[], _manifests: ResourceManifest[]): string {
144
+ let created = 0;
145
+ let updated = 0;
146
+ let unchanged = 0;
147
+ let errors = 0;
148
+ let dryRun = 0;
149
+
150
+ for (const result of results) {
151
+ switch (result.status) {
152
+ case "created":
153
+ created++;
154
+ break;
155
+ case "updated":
156
+ updated++;
157
+ break;
158
+ case "unchanged":
159
+ unchanged++;
160
+ break;
161
+ case "error":
162
+ errors++;
163
+ break;
164
+ case "dry-run":
165
+ dryRun++;
166
+ break;
167
+ }
168
+ }
169
+
170
+ const parts: string[] = [];
171
+ if (created > 0) parts.push(`${created} created`);
172
+ if (updated > 0) parts.push(`${updated} configured`);
173
+ if (unchanged > 0) parts.push(`${unchanged} unchanged`);
174
+ if (dryRun > 0) parts.push(`${dryRun} dry-run`);
175
+ if (errors > 0) parts.push(`${errors} error(s)`);
176
+
177
+ return parts.join(", ");
178
+ }
179
+
180
+ function formatTable(headers: string[], rows: string[][]): string {
181
+ const colWidths = headers.map((h, i) => Math.max(h.length, ...rows.map(r => (r[i] ?? "").length)));
182
+
183
+ const headerLine = headers.map((h, i) => h.padEnd(colWidths[i])).join(" ");
184
+ const dataLines = rows.map(row => row.map((cell, i) => cell.padEnd(colWidths[i])).join(" "));
185
+
186
+ return [headerLine, ...dataLines].join("\n");
187
+ }
188
+
189
+ function formatAge(timestamp: string): string {
190
+ const created = new Date(timestamp);
191
+ const now = new Date();
192
+ const diffMs = now.getTime() - created.getTime();
193
+
194
+ if (diffMs < 0) return "0s";
195
+ const seconds = Math.floor(diffMs / 1000);
196
+ if (seconds < 60) return `${seconds}s`;
197
+ const minutes = Math.floor(seconds / 60);
198
+ if (minutes < 60) return `${minutes}m`;
199
+ const hours = Math.floor(minutes / 60);
200
+ if (hours < 24) return `${hours}h`;
201
+ const days = Math.floor(hours / 24);
202
+ return `${days}d`;
203
+ }
204
+
205
+ function formatLabels(labels: Record<string, string>): string {
206
+ return Object.entries(labels)
207
+ .map(([k, v]) => `${k}=${v}`)
208
+ .join(", ");
209
+ }