@kustodian/nodes 1.1.0 → 2.0.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.
@@ -1,3 +1,5 @@
1
- export * from './types.js';
1
+ export * from './kubectl-labeler.js';
2
2
  export * from './labeler.js';
3
3
  export * from './profile.js';
4
+ export * from './types.js';
5
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,509 @@
1
+ // ../core/dist/index.js
2
+ function create_error(code, message, cause) {
3
+ return { code, message, cause };
4
+ }
5
+ var ErrorCodes = {
6
+ UNKNOWN: "UNKNOWN",
7
+ INVALID_ARGUMENT: "INVALID_ARGUMENT",
8
+ NOT_FOUND: "NOT_FOUND",
9
+ ALREADY_EXISTS: "ALREADY_EXISTS",
10
+ FILE_NOT_FOUND: "FILE_NOT_FOUND",
11
+ FILE_READ_ERROR: "FILE_READ_ERROR",
12
+ FILE_WRITE_ERROR: "FILE_WRITE_ERROR",
13
+ DIRECTORY_NOT_FOUND: "DIRECTORY_NOT_FOUND",
14
+ PARSE_ERROR: "PARSE_ERROR",
15
+ YAML_PARSE_ERROR: "YAML_PARSE_ERROR",
16
+ JSON_PARSE_ERROR: "JSON_PARSE_ERROR",
17
+ VALIDATION_ERROR: "VALIDATION_ERROR",
18
+ SCHEMA_VALIDATION_ERROR: "SCHEMA_VALIDATION_ERROR",
19
+ CONFIG_NOT_FOUND: "CONFIG_NOT_FOUND",
20
+ CONFIG_INVALID: "CONFIG_INVALID",
21
+ TEMPLATE_NOT_FOUND: "TEMPLATE_NOT_FOUND",
22
+ CLUSTER_NOT_FOUND: "CLUSTER_NOT_FOUND",
23
+ PROFILE_NOT_FOUND: "PROFILE_NOT_FOUND",
24
+ NETWORK_ERROR: "NETWORK_ERROR",
25
+ CONNECTION_REFUSED: "CONNECTION_REFUSED",
26
+ TIMEOUT: "TIMEOUT",
27
+ SSH_CONNECTION_ERROR: "SSH_CONNECTION_ERROR",
28
+ SSH_AUTH_ERROR: "SSH_AUTH_ERROR",
29
+ SSH_COMMAND_ERROR: "SSH_COMMAND_ERROR",
30
+ K8S_CONNECTION_ERROR: "K8S_CONNECTION_ERROR",
31
+ K8S_API_ERROR: "K8S_API_ERROR",
32
+ NODE_NOT_READY: "NODE_NOT_READY",
33
+ BOOTSTRAP_ERROR: "BOOTSTRAP_ERROR",
34
+ BOOTSTRAP_STATE_ERROR: "BOOTSTRAP_STATE_ERROR",
35
+ CLUSTER_PROVIDER_ERROR: "CLUSTER_PROVIDER_ERROR",
36
+ PLUGIN_NOT_FOUND: "PLUGIN_NOT_FOUND",
37
+ PLUGIN_LOAD_ERROR: "PLUGIN_LOAD_ERROR",
38
+ PLUGIN_EXECUTION_ERROR: "PLUGIN_EXECUTION_ERROR",
39
+ DEPENDENCY_CYCLE: "DEPENDENCY_CYCLE",
40
+ DEPENDENCY_MISSING: "DEPENDENCY_MISSING",
41
+ DEPENDENCY_SELF_REFERENCE: "DEPENDENCY_SELF_REFERENCE",
42
+ DEPENDENCY_VALIDATION_ERROR: "DEPENDENCY_VALIDATION_ERROR",
43
+ SECRET_CLI_NOT_FOUND: "SECRET_CLI_NOT_FOUND",
44
+ SECRET_NOT_FOUND: "SECRET_NOT_FOUND",
45
+ SECRET_AUTH_ERROR: "SECRET_AUTH_ERROR",
46
+ SECRET_TIMEOUT: "SECRET_TIMEOUT",
47
+ SOURCE_FETCH_ERROR: "SOURCE_FETCH_ERROR",
48
+ SOURCE_AUTH_ERROR: "SOURCE_AUTH_ERROR",
49
+ SOURCE_TIMEOUT: "SOURCE_TIMEOUT",
50
+ SOURCE_CHECKSUM_MISMATCH: "SOURCE_CHECKSUM_MISMATCH",
51
+ SOURCE_VERSION_NOT_FOUND: "SOURCE_VERSION_NOT_FOUND",
52
+ CACHE_READ_ERROR: "CACHE_READ_ERROR",
53
+ CACHE_WRITE_ERROR: "CACHE_WRITE_ERROR",
54
+ CACHE_CORRUPT: "CACHE_CORRUPT"
55
+ };
56
+ var Errors = {
57
+ unknown(message, cause) {
58
+ return create_error(ErrorCodes.UNKNOWN, message, cause);
59
+ },
60
+ invalid_argument(argument, reason) {
61
+ return create_error(ErrorCodes.INVALID_ARGUMENT, `Invalid argument '${argument}': ${reason}`);
62
+ },
63
+ not_found(resource, identifier) {
64
+ return create_error(ErrorCodes.NOT_FOUND, `${resource} '${identifier}' not found`);
65
+ },
66
+ already_exists(resource, identifier) {
67
+ return create_error(ErrorCodes.ALREADY_EXISTS, `${resource} '${identifier}' already exists`);
68
+ },
69
+ file_not_found(path) {
70
+ return create_error(ErrorCodes.FILE_NOT_FOUND, `File not found: ${path}`);
71
+ },
72
+ file_read_error(path, cause) {
73
+ return create_error(ErrorCodes.FILE_READ_ERROR, `Failed to read file: ${path}`, cause);
74
+ },
75
+ file_write_error(path, cause) {
76
+ return create_error(ErrorCodes.FILE_WRITE_ERROR, `Failed to write file: ${path}`, cause);
77
+ },
78
+ parse_error(format, message, cause) {
79
+ return create_error(ErrorCodes.PARSE_ERROR, `Failed to parse ${format}: ${message}`, cause);
80
+ },
81
+ yaml_parse_error(message, cause) {
82
+ return create_error(ErrorCodes.YAML_PARSE_ERROR, `YAML parse error: ${message}`, cause);
83
+ },
84
+ validation_error(message, cause) {
85
+ return create_error(ErrorCodes.VALIDATION_ERROR, message, cause);
86
+ },
87
+ schema_validation_error(errors) {
88
+ return create_error(ErrorCodes.SCHEMA_VALIDATION_ERROR, `Schema validation failed:
89
+ ${errors.map((e) => ` - ${e}`).join(`
90
+ `)}`);
91
+ },
92
+ config_not_found(config_type, path) {
93
+ return create_error(ErrorCodes.CONFIG_NOT_FOUND, `${config_type} configuration not found at: ${path}`);
94
+ },
95
+ template_not_found(name) {
96
+ return create_error(ErrorCodes.TEMPLATE_NOT_FOUND, `Template '${name}' not found`);
97
+ },
98
+ cluster_not_found(name) {
99
+ return create_error(ErrorCodes.CLUSTER_NOT_FOUND, `Cluster '${name}' not found`);
100
+ },
101
+ profile_not_found(name) {
102
+ return create_error(ErrorCodes.PROFILE_NOT_FOUND, `Node profile '${name}' not found`);
103
+ },
104
+ ssh_connection_error(host, cause) {
105
+ return create_error(ErrorCodes.SSH_CONNECTION_ERROR, `Failed to connect to ${host} via SSH`, cause);
106
+ },
107
+ ssh_auth_error(host, cause) {
108
+ return create_error(ErrorCodes.SSH_AUTH_ERROR, `SSH authentication failed for ${host}`, cause);
109
+ },
110
+ bootstrap_error(message, cause) {
111
+ return create_error(ErrorCodes.BOOTSTRAP_ERROR, `Bootstrap failed: ${message}`, cause);
112
+ },
113
+ plugin_not_found(name) {
114
+ return create_error(ErrorCodes.PLUGIN_NOT_FOUND, `Plugin '${name}' not found`);
115
+ },
116
+ plugin_load_error(name, cause) {
117
+ return create_error(ErrorCodes.PLUGIN_LOAD_ERROR, `Failed to load plugin '${name}'`, cause);
118
+ },
119
+ dependency_cycle(cycle) {
120
+ const cycle_str = cycle.join(" → ");
121
+ return create_error(ErrorCodes.DEPENDENCY_CYCLE, `Dependency cycle detected: ${cycle_str}`);
122
+ },
123
+ dependency_missing(source, target) {
124
+ return create_error(ErrorCodes.DEPENDENCY_MISSING, `Kustomization '${source}' depends on '${target}' which does not exist`);
125
+ },
126
+ dependency_self_reference(node) {
127
+ return create_error(ErrorCodes.DEPENDENCY_SELF_REFERENCE, `Kustomization '${node}' cannot depend on itself`);
128
+ },
129
+ dependency_validation_error(errors) {
130
+ return create_error(ErrorCodes.DEPENDENCY_VALIDATION_ERROR, `Dependency validation failed:
131
+ ${errors.map((e) => ` - ${e}`).join(`
132
+ `)}`);
133
+ },
134
+ secret_cli_not_found(provider, cli_name) {
135
+ return create_error(ErrorCodes.SECRET_CLI_NOT_FOUND, `${provider} CLI (${cli_name}) not found. Please install it first.`);
136
+ },
137
+ secret_not_found(provider, ref) {
138
+ return create_error(ErrorCodes.SECRET_NOT_FOUND, `Secret not found in ${provider}: ${ref}`);
139
+ },
140
+ secret_auth_error(provider, cause) {
141
+ return create_error(ErrorCodes.SECRET_AUTH_ERROR, `${provider} authentication failed. Check your credentials.`, cause);
142
+ },
143
+ secret_timeout(provider, timeout) {
144
+ return create_error(ErrorCodes.SECRET_TIMEOUT, `${provider} operation timed out after ${timeout}ms`);
145
+ },
146
+ source_fetch_error(source, cause) {
147
+ return create_error(ErrorCodes.SOURCE_FETCH_ERROR, `Failed to fetch template source '${source}'`, cause);
148
+ },
149
+ source_auth_error(source, cause) {
150
+ return create_error(ErrorCodes.SOURCE_AUTH_ERROR, `Authentication failed for source '${source}'`, cause);
151
+ },
152
+ source_timeout(source, timeout) {
153
+ return create_error(ErrorCodes.SOURCE_TIMEOUT, `Source '${source}' operation timed out after ${timeout}ms`);
154
+ },
155
+ source_checksum_mismatch(source, expected, actual) {
156
+ return create_error(ErrorCodes.SOURCE_CHECKSUM_MISMATCH, `Checksum mismatch for '${source}': expected ${expected}, got ${actual}`);
157
+ },
158
+ source_version_not_found(source, version) {
159
+ return create_error(ErrorCodes.SOURCE_VERSION_NOT_FOUND, `Version '${version}' not found for source '${source}'`);
160
+ },
161
+ cache_read_error(path, cause) {
162
+ return create_error(ErrorCodes.CACHE_READ_ERROR, `Failed to read cache at: ${path}`, cause);
163
+ },
164
+ cache_write_error(path, cause) {
165
+ return create_error(ErrorCodes.CACHE_WRITE_ERROR, `Failed to write cache at: ${path}`, cause);
166
+ },
167
+ cache_corrupt(path) {
168
+ return create_error(ErrorCodes.CACHE_CORRUPT, `Cache is corrupt at: ${path}`);
169
+ }
170
+ };
171
+ function success(value) {
172
+ return { success: true, value };
173
+ }
174
+ function failure(error) {
175
+ return { success: false, error };
176
+ }
177
+ function is_success(result) {
178
+ return result.success;
179
+ }
180
+
181
+ // src/types.ts
182
+ var DEFAULT_LABEL_PREFIX = "kustodian.io";
183
+ function format_label_key(key, prefix = DEFAULT_LABEL_PREFIX) {
184
+ if (key.includes("/")) {
185
+ return key;
186
+ }
187
+ return `${prefix}/${key}`;
188
+ }
189
+ function format_label_value(value) {
190
+ if (typeof value === "boolean") {
191
+ return value ? "true" : "false";
192
+ }
193
+ return String(value);
194
+ }
195
+ function format_node_labels(labels, prefix = DEFAULT_LABEL_PREFIX) {
196
+ if (!labels) {
197
+ return {};
198
+ }
199
+ const result = {};
200
+ for (const [key, value] of Object.entries(labels)) {
201
+ const formatted_key = format_label_key(key, prefix);
202
+ result[formatted_key] = format_label_value(value);
203
+ }
204
+ return result;
205
+ }
206
+ function get_node_ssh_config(node, default_ssh) {
207
+ return {
208
+ ...default_ssh,
209
+ ...node.ssh
210
+ };
211
+ }
212
+ function is_controller(node) {
213
+ return node.role === "controller" || node.role === "controller+worker";
214
+ }
215
+ function is_worker(node) {
216
+ return node.role === "worker" || node.role === "controller+worker";
217
+ }
218
+ function get_controllers(nodes) {
219
+ return nodes.filter(is_controller);
220
+ }
221
+ function get_workers(nodes) {
222
+ return nodes.filter(is_worker);
223
+ }
224
+ function get_primary_controller(nodes) {
225
+ return nodes.find(is_controller);
226
+ }
227
+
228
+ // src/labeler.ts
229
+ function calculate_label_changes(node, current_labels, prefix) {
230
+ const changes = [];
231
+ const desired_labels = format_node_labels(node.labels, prefix);
232
+ for (const [key, value] of Object.entries(desired_labels)) {
233
+ const current_value = current_labels[key];
234
+ if (current_value === undefined) {
235
+ changes.push({
236
+ node: node.name,
237
+ key,
238
+ value,
239
+ operation: "add"
240
+ });
241
+ } else if (current_value !== value) {
242
+ changes.push({
243
+ node: node.name,
244
+ key,
245
+ value,
246
+ operation: "update"
247
+ });
248
+ }
249
+ }
250
+ for (const key of Object.keys(current_labels)) {
251
+ if (key.startsWith(`${prefix}/`) && !(key in desired_labels)) {
252
+ changes.push({
253
+ node: node.name,
254
+ key,
255
+ operation: "remove"
256
+ });
257
+ }
258
+ }
259
+ return changes;
260
+ }
261
+ function calculate_all_label_changes(node_list, current_labels_by_node) {
262
+ const prefix = node_list.label_prefix ?? "kustodian.io";
263
+ const all_changes = [];
264
+ for (const node of node_list.nodes) {
265
+ const current_labels = current_labels_by_node.get(node.name) ?? {};
266
+ const changes = calculate_label_changes(node, current_labels, prefix);
267
+ all_changes.push(...changes);
268
+ }
269
+ return all_changes;
270
+ }
271
+ function format_label_change(change) {
272
+ switch (change.operation) {
273
+ case "add":
274
+ return `[+] ${change.node}: ${change.key}=${change.value}`;
275
+ case "update":
276
+ return `[~] ${change.node}: ${change.key}=${change.value}`;
277
+ case "remove":
278
+ return `[-] ${change.node}: ${change.key}`;
279
+ }
280
+ }
281
+ function group_changes_by_node(changes) {
282
+ const grouped = new Map;
283
+ for (const change of changes) {
284
+ const node_changes = grouped.get(change.node) ?? [];
285
+ node_changes.push(change);
286
+ grouped.set(change.node, node_changes);
287
+ }
288
+ return grouped;
289
+ }
290
+ function create_dry_run_result(changes) {
291
+ return {
292
+ changes,
293
+ applied: 0,
294
+ skipped: changes.length
295
+ };
296
+ }
297
+ function create_mock_labeler(labels_by_node) {
298
+ return {
299
+ async get_labels(node_name) {
300
+ const labels = labels_by_node.get(node_name);
301
+ if (!labels) {
302
+ return success({});
303
+ }
304
+ return success(labels);
305
+ },
306
+ async apply_change(_change) {
307
+ return success(undefined);
308
+ },
309
+ async sync_labels(node_list, options) {
310
+ const changes = calculate_all_label_changes(node_list, labels_by_node);
311
+ if (options?.dry_run) {
312
+ return success(create_dry_run_result(changes));
313
+ }
314
+ return success({
315
+ changes,
316
+ applied: changes.length,
317
+ skipped: 0
318
+ });
319
+ }
320
+ };
321
+ }
322
+
323
+ // src/kubectl-labeler.ts
324
+ function create_kubectl_labeler(kubectl) {
325
+ return {
326
+ async get_labels(node_name) {
327
+ const get_result = await kubectl.get({ kind: "Node", name: node_name });
328
+ if (!is_success(get_result)) {
329
+ return get_result;
330
+ }
331
+ const nodes = get_result.value;
332
+ if (nodes.length === 0) {
333
+ return failure(Errors.not_found("Node", node_name));
334
+ }
335
+ const node = nodes[0];
336
+ if (!node) {
337
+ return failure(Errors.not_found("Node", node_name));
338
+ }
339
+ const labels = node.metadata.labels ?? {};
340
+ return success(labels);
341
+ },
342
+ async apply_change(change) {
343
+ if (change.operation === "remove") {
344
+ const remove_key = `${change.key}-`;
345
+ const label_result2 = await kubectl.label(change.node, { [remove_key]: "" });
346
+ if (!is_success(label_result2)) {
347
+ return failure({
348
+ code: "KUBECTL_LABEL_ERROR",
349
+ message: `Failed to remove label ${change.key} from node ${change.node}: ${label_result2.error.message}`
350
+ });
351
+ }
352
+ return success(undefined);
353
+ }
354
+ if (!change.value) {
355
+ return failure(Errors.validation_error(`Label change for ${change.key} requires a value for ${change.operation} operation`));
356
+ }
357
+ const label_result = await kubectl.label(change.node, {
358
+ [change.key]: change.value
359
+ });
360
+ if (!is_success(label_result)) {
361
+ return failure({
362
+ code: "KUBECTL_LABEL_ERROR",
363
+ message: `Failed to ${change.operation} label ${change.key}=${change.value} on node ${change.node}: ${label_result.error.message}`
364
+ });
365
+ }
366
+ return success(undefined);
367
+ },
368
+ async sync_labels(node_list, options = {}) {
369
+ const current_labels_by_node = new Map;
370
+ for (const node of node_list.nodes) {
371
+ const labels_result = await this.get_labels(node.name);
372
+ if (!is_success(labels_result)) {
373
+ return failure({
374
+ code: "KUBECTL_LABEL_ERROR",
375
+ message: `Failed to get labels for node ${node.name}: ${labels_result.error.message}`
376
+ });
377
+ }
378
+ current_labels_by_node.set(node.name, labels_result.value);
379
+ }
380
+ const changes = calculate_all_label_changes(node_list, current_labels_by_node);
381
+ if (options.dry_run) {
382
+ return success(create_dry_run_result(changes));
383
+ }
384
+ const grouped_changes = group_changes_by_node(changes);
385
+ let applied = 0;
386
+ let failed = 0;
387
+ for (const [node_name, node_changes] of grouped_changes) {
388
+ for (const change of node_changes) {
389
+ const apply_result = await this.apply_change(change);
390
+ if (is_success(apply_result)) {
391
+ applied++;
392
+ } else {
393
+ failed++;
394
+ console.warn(` ⚠ Failed to apply change on ${node_name}: ${apply_result.error.message}`);
395
+ }
396
+ }
397
+ }
398
+ return success({
399
+ changes,
400
+ applied,
401
+ skipped: failed
402
+ });
403
+ }
404
+ };
405
+ }
406
+ // src/profile.ts
407
+ function merge_taints(profile_taints, node_taints) {
408
+ if (!profile_taints && !node_taints) {
409
+ return;
410
+ }
411
+ if (!profile_taints) {
412
+ return node_taints;
413
+ }
414
+ if (!node_taints) {
415
+ return profile_taints;
416
+ }
417
+ const node_taint_keys = new Set(node_taints.map((t) => `${t.key}:${t.effect}`));
418
+ const filtered_profile_taints = profile_taints.filter((t) => !node_taint_keys.has(`${t.key}:${t.effect}`));
419
+ return [...filtered_profile_taints, ...node_taints];
420
+ }
421
+ function resolve_node_profile(node, profile) {
422
+ if (!profile) {
423
+ const { profile: _, ...resolved2 } = node;
424
+ return resolved2;
425
+ }
426
+ const merged_labels = profile.labels || node.labels ? { ...profile.labels, ...node.labels } : undefined;
427
+ const merged_taints = merge_taints(profile.taints, node.taints);
428
+ const merged_annotations = profile.annotations || node.annotations ? { ...profile.annotations, ...node.annotations } : undefined;
429
+ const resolved = {
430
+ name: node.name,
431
+ role: node.role,
432
+ address: node.address
433
+ };
434
+ if (node.ssh !== undefined) {
435
+ resolved.ssh = node.ssh;
436
+ }
437
+ if (merged_labels !== undefined) {
438
+ resolved.labels = merged_labels;
439
+ }
440
+ if (merged_taints !== undefined) {
441
+ resolved.taints = merged_taints;
442
+ }
443
+ if (merged_annotations !== undefined) {
444
+ resolved.annotations = merged_annotations;
445
+ }
446
+ return resolved;
447
+ }
448
+ function resolve_all_node_profiles(nodes, profiles) {
449
+ const resolved = [];
450
+ const errors = [];
451
+ for (const node of nodes) {
452
+ if (node.profile) {
453
+ const profile = profiles.get(node.profile);
454
+ if (!profile) {
455
+ errors.push(`Node '${node.name}' references unknown profile '${node.profile}'`);
456
+ const { profile: _, ...node_without_profile } = node;
457
+ resolved.push(node_without_profile);
458
+ } else {
459
+ resolved.push(resolve_node_profile(node, profile));
460
+ }
461
+ } else {
462
+ resolved.push(resolve_node_profile(node, undefined));
463
+ }
464
+ }
465
+ return { resolved, errors };
466
+ }
467
+ function get_referenced_profiles(nodes) {
468
+ const profiles = new Set;
469
+ for (const node of nodes) {
470
+ if (node.profile) {
471
+ profiles.add(node.profile);
472
+ }
473
+ }
474
+ return profiles;
475
+ }
476
+ function validate_profile_references(nodes, profiles) {
477
+ const errors = [];
478
+ const referenced = get_referenced_profiles(nodes);
479
+ for (const profile_name of referenced) {
480
+ if (!profiles.has(profile_name)) {
481
+ const nodes_using = nodes.filter((n) => n.profile === profile_name).map((n) => n.name);
482
+ errors.push(`Profile '${profile_name}' not found, referenced by nodes: ${nodes_using.join(", ")}`);
483
+ }
484
+ }
485
+ return errors;
486
+ }
487
+ export {
488
+ validate_profile_references,
489
+ resolve_node_profile,
490
+ resolve_all_node_profiles,
491
+ is_worker,
492
+ is_controller,
493
+ group_changes_by_node,
494
+ get_workers,
495
+ get_referenced_profiles,
496
+ get_primary_controller,
497
+ get_node_ssh_config,
498
+ get_controllers,
499
+ format_node_labels,
500
+ format_label_value,
501
+ format_label_key,
502
+ format_label_change,
503
+ create_mock_labeler,
504
+ create_kubectl_labeler,
505
+ create_dry_run_result,
506
+ calculate_label_changes,
507
+ calculate_all_label_changes,
508
+ DEFAULT_LABEL_PREFIX
509
+ };
@@ -0,0 +1,7 @@
1
+ import type { KubectlClientType } from '@kustodian/k8s';
2
+ import { type NodeLabelerType } from './labeler.js';
3
+ /**
4
+ * Creates a node labeler that uses kubectl to manage node labels.
5
+ */
6
+ export declare function create_kubectl_labeler(kubectl: KubectlClientType): NodeLabelerType;
7
+ //# sourceMappingURL=kubectl-labeler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"kubectl-labeler.d.ts","sourceRoot":"","sources":["../src/kubectl-labeler.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,gBAAgB,CAAC;AAExD,OAAO,EACL,KAAK,eAAe,EAIrB,MAAM,cAAc,CAAC;AAEtB;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,OAAO,EAAE,iBAAiB,GAAG,eAAe,CA2GlF"}
@@ -0,0 +1,74 @@
1
+ import type { KustodianErrorType } from '@kustodian/core';
2
+ import { type ResultType } from '@kustodian/core';
3
+ import { type NodeListType, type NodeType } from './types.js';
4
+ /**
5
+ * Label operation types.
6
+ */
7
+ export type LabelOperationType = 'add' | 'update' | 'remove';
8
+ /**
9
+ * A single label change to be applied.
10
+ */
11
+ export interface LabelChangeType {
12
+ node: string;
13
+ key: string;
14
+ value?: string;
15
+ operation: LabelOperationType;
16
+ }
17
+ /**
18
+ * Options for the label sync operation.
19
+ */
20
+ export interface LabelSyncOptionsType {
21
+ dry_run?: boolean;
22
+ verify?: boolean;
23
+ }
24
+ /**
25
+ * Result of a label sync operation.
26
+ */
27
+ export interface LabelSyncResultType {
28
+ changes: LabelChangeType[];
29
+ applied: number;
30
+ skipped: number;
31
+ }
32
+ /**
33
+ * Calculates the label changes needed for a node.
34
+ */
35
+ export declare function calculate_label_changes(node: NodeType, current_labels: Record<string, string>, prefix: string): LabelChangeType[];
36
+ /**
37
+ * Calculates all label changes for a node list.
38
+ */
39
+ export declare function calculate_all_label_changes(node_list: NodeListType, current_labels_by_node: Map<string, Record<string, string>>): LabelChangeType[];
40
+ /**
41
+ * Formats a label change for display.
42
+ */
43
+ export declare function format_label_change(change: LabelChangeType): string;
44
+ /**
45
+ * Groups label changes by node.
46
+ */
47
+ export declare function group_changes_by_node(changes: LabelChangeType[]): Map<string, LabelChangeType[]>;
48
+ /**
49
+ * Creates a dry-run result from label changes.
50
+ */
51
+ export declare function create_dry_run_result(changes: LabelChangeType[]): LabelSyncResultType;
52
+ /**
53
+ * Node labeler service interface.
54
+ * This is implemented by the actual Kubernetes client.
55
+ */
56
+ export interface NodeLabelerType {
57
+ /**
58
+ * Gets the current labels for a node.
59
+ */
60
+ get_labels(node_name: string): Promise<ResultType<Record<string, string>, KustodianErrorType>>;
61
+ /**
62
+ * Applies a label change to a node.
63
+ */
64
+ apply_change(change: LabelChangeType): Promise<ResultType<void, KustodianErrorType>>;
65
+ /**
66
+ * Syncs labels for all nodes in the list.
67
+ */
68
+ sync_labels(node_list: NodeListType, options?: LabelSyncOptionsType): Promise<ResultType<LabelSyncResultType, KustodianErrorType>>;
69
+ }
70
+ /**
71
+ * Creates a mock labeler for testing.
72
+ */
73
+ export declare function create_mock_labeler(labels_by_node: Map<string, Record<string, string>>): NodeLabelerType;
74
+ //# sourceMappingURL=labeler.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"labeler.d.ts","sourceRoot":"","sources":["../src/labeler.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,KAAK,UAAU,EAAW,MAAM,iBAAiB,CAAC;AAE3D,OAAO,EAAE,KAAK,YAAY,EAAE,KAAK,QAAQ,EAAsB,MAAM,YAAY,CAAC;AAElF;;GAEG;AACH,MAAM,MAAM,kBAAkB,GAAG,KAAK,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAE7D;;GAEG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,kBAAkB,CAAC;CAC/B;AAED;;GAEG;AACH,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED;;GAEG;AACH,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,QAAQ,EACd,cAAc,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EACtC,MAAM,EAAE,MAAM,GACb,eAAe,EAAE,CAqCnB;AAED;;GAEG;AACH,wBAAgB,2BAA2B,CACzC,SAAS,EAAE,YAAY,EACvB,sBAAsB,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAC1D,eAAe,EAAE,CAWnB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,eAAe,GAAG,MAAM,CASnE;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,eAAe,EAAE,CAAC,CAUhG;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,eAAe,EAAE,GAAG,mBAAmB,CAMrF;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;OAEG;IACH,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAE/F;;OAEG;IACH,YAAY,CAAC,MAAM,EAAE,eAAe,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,kBAAkB,CAAC,CAAC,CAAC;IAErF;;OAEG;IACH,WAAW,CACT,SAAS,EAAE,YAAY,EACvB,OAAO,CAAC,EAAE,oBAAoB,GAC7B,OAAO,CAAC,UAAU,CAAC,mBAAmB,EAAE,kBAAkB,CAAC,CAAC,CAAC;CACjE;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CACjC,cAAc,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,GAClD,eAAe,CA6BjB"}
@@ -0,0 +1,58 @@
1
+ import type { NodeType, TaintType } from './types.js';
2
+ /**
3
+ * Node profile type for profile resolution.
4
+ * This mirrors the NodeProfileType from @kustodian/schema but is defined here
5
+ * to avoid circular dependencies.
6
+ */
7
+ export interface NodeProfileType {
8
+ name: string;
9
+ display_name?: string;
10
+ description?: string;
11
+ labels?: Record<string, string | boolean | number>;
12
+ taints?: TaintType[];
13
+ annotations?: Record<string, string>;
14
+ }
15
+ /**
16
+ * Resolved node type with profile values merged.
17
+ * The profile field is removed after resolution.
18
+ */
19
+ export type ResolvedNodeType = Omit<NodeType, 'profile'>;
20
+ /**
21
+ * Resolves a node with its profile configuration.
22
+ * Profile values are merged with node values, where node values take precedence.
23
+ *
24
+ * Merge strategy:
25
+ * - labels: Profile labels are base, node labels override/extend
26
+ * - taints: Node taints override profile taints with same key+effect, others are combined
27
+ * - annotations: Profile annotations are base, node annotations override/extend
28
+ * - ssh: Not inherited from profile (kept as node-specific)
29
+ *
30
+ * @param node - The node to resolve
31
+ * @param profile - The profile to apply, or undefined if no profile
32
+ * @returns The resolved node with profile values merged
33
+ */
34
+ export declare function resolve_node_profile(node: NodeType, profile: NodeProfileType | undefined): ResolvedNodeType;
35
+ /**
36
+ * Resolves all nodes in a list with their profiles.
37
+ *
38
+ * @param nodes - The nodes to resolve
39
+ * @param profiles - Map of profile name to profile
40
+ * @returns Object with resolved nodes and any missing profile errors
41
+ */
42
+ export declare function resolve_all_node_profiles(nodes: NodeType[], profiles: Map<string, NodeProfileType>): {
43
+ resolved: ResolvedNodeType[];
44
+ errors: string[];
45
+ };
46
+ /**
47
+ * Collects all unique profile names referenced by nodes.
48
+ */
49
+ export declare function get_referenced_profiles(nodes: NodeType[]): Set<string>;
50
+ /**
51
+ * Validates that all referenced profiles exist.
52
+ *
53
+ * @param nodes - The nodes to check
54
+ * @param profiles - Map of available profiles
55
+ * @returns Array of error messages for missing profiles
56
+ */
57
+ export declare function validate_profile_references(nodes: NodeType[], profiles: Map<string, NodeProfileType>): string[];
58
+ //# sourceMappingURL=profile.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile.d.ts","sourceRoot":"","sources":["../src/profile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAEtD;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;;GAGG;AACH,MAAM,MAAM,gBAAgB,GAAG,IAAI,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC;AAkCzD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,QAAQ,EACd,OAAO,EAAE,eAAe,GAAG,SAAS,GACnC,gBAAgB,CAwClB;AAED;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,KAAK,EAAE,QAAQ,EAAE,EACjB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GACrC;IAAE,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAAE,CAqBpD;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,GAAG,CAAC,MAAM,CAAC,CAQtE;AAED;;;;;;GAMG;AACH,wBAAgB,2BAA2B,CACzC,KAAK,EAAE,QAAQ,EAAE,EACjB,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,eAAe,CAAC,GACrC,MAAM,EAAE,CAcV"}
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Node role in the cluster.
3
+ */
4
+ export type NodeRoleType = 'controller' | 'worker' | 'controller+worker';
5
+ /**
6
+ * SSH configuration for connecting to a node.
7
+ */
8
+ export interface SshConfigType {
9
+ user?: string;
10
+ key_path?: string;
11
+ known_hosts_path?: string;
12
+ port?: number;
13
+ }
14
+ /**
15
+ * Kubernetes taint configuration.
16
+ */
17
+ export interface TaintType {
18
+ key: string;
19
+ value?: string;
20
+ effect: 'NoSchedule' | 'PreferNoSchedule' | 'NoExecute';
21
+ }
22
+ /**
23
+ * Node definition with its configuration.
24
+ */
25
+ export interface NodeType {
26
+ name: string;
27
+ role: NodeRoleType;
28
+ address: string;
29
+ profile?: string;
30
+ ssh?: SshConfigType;
31
+ labels?: Record<string, string | boolean | number>;
32
+ taints?: TaintType[];
33
+ annotations?: Record<string, string>;
34
+ }
35
+ /**
36
+ * Node list with default SSH configuration.
37
+ */
38
+ export interface NodeListType {
39
+ cluster: string;
40
+ label_prefix?: string;
41
+ ssh?: SshConfigType;
42
+ nodes: NodeType[];
43
+ }
44
+ /**
45
+ * Default label prefix.
46
+ */
47
+ export declare const DEFAULT_LABEL_PREFIX = "kustodian.io";
48
+ /**
49
+ * Formats a label key with the configured prefix.
50
+ */
51
+ export declare function format_label_key(key: string, prefix?: string): string;
52
+ /**
53
+ * Formats a label value as a string.
54
+ */
55
+ export declare function format_label_value(value: string | boolean | number): string;
56
+ /**
57
+ * Formats all labels for a node with the configured prefix.
58
+ */
59
+ export declare function format_node_labels(labels: Record<string, string | boolean | number> | undefined, prefix?: string): Record<string, string>;
60
+ /**
61
+ * Gets the SSH configuration for a node, merging with defaults.
62
+ */
63
+ export declare function get_node_ssh_config(node: NodeType, default_ssh?: SshConfigType): SshConfigType;
64
+ /**
65
+ * Checks if a node has a controller role.
66
+ */
67
+ export declare function is_controller(node: NodeType): boolean;
68
+ /**
69
+ * Checks if a node has a worker role.
70
+ */
71
+ export declare function is_worker(node: NodeType): boolean;
72
+ /**
73
+ * Gets all controller nodes from a node list.
74
+ */
75
+ export declare function get_controllers(nodes: NodeType[]): NodeType[];
76
+ /**
77
+ * Gets all worker nodes from a node list.
78
+ */
79
+ export declare function get_workers(nodes: NodeType[]): NodeType[];
80
+ /**
81
+ * Gets the primary controller node (first controller).
82
+ */
83
+ export declare function get_primary_controller(nodes: NodeType[]): NodeType | undefined;
84
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG,YAAY,GAAG,QAAQ,GAAG,mBAAmB,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,GAAG,kBAAkB,GAAG,WAAW,CAAC;CACzD;AAED;;GAEG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,CAAC;IACnD,MAAM,CAAC,EAAE,SAAS,EAAE,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACtC;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB;AAED;;GAEG;AACH,eAAO,MAAM,oBAAoB,iBAAiB,CAAC;AAEnD;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,MAA6B,GAAG,MAAM,CAM3F;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,GAAG,MAAM,CAK3E;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,GAAG,MAAM,CAAC,GAAG,SAAS,EAC7D,MAAM,GAAE,MAA6B,GACpC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAWxB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,QAAQ,EAAE,WAAW,CAAC,EAAE,aAAa,GAAG,aAAa,CAK9F;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAErD;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,CAEjD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAE7D;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,EAAE,CAEzD;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,QAAQ,EAAE,GAAG,QAAQ,GAAG,SAAS,CAE9E"}
package/package.json CHANGED
@@ -1,23 +1,25 @@
1
1
  {
2
2
  "name": "@kustodian/nodes",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "Node definitions, roles, and labeling for Kustodian",
5
5
  "type": "module",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./src/index.ts",
11
- "import": "./src/index.ts"
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
12
  }
13
13
  },
14
14
  "files": [
15
- "src"
15
+ "dist"
16
16
  ],
17
17
  "scripts": {
18
18
  "test": "bun test",
19
19
  "test:watch": "bun test --watch",
20
- "typecheck": "bun run tsc --noEmit"
20
+ "typecheck": "bun run tsc --noEmit",
21
+ "build": "bun build src/index.ts --outdir dist --target node --format esm && tsc --emitDeclarationOnly --outDir dist",
22
+ "prepublishOnly": "bun run build"
21
23
  },
22
24
  "keywords": [
23
25
  "kustodian",
@@ -32,11 +34,13 @@
32
34
  "directory": "packages/nodes"
33
35
  },
34
36
  "publishConfig": {
35
- "registry": "https://npm.pkg.github.com"
37
+ "access": "public",
38
+ "registry": "https://registry.npmjs.org"
36
39
  },
37
40
  "dependencies": {
38
- "@kustodian/core": "1.1.0",
39
- "@kustodian/schema": "1.3.0"
41
+ "@kustodian/core": "2.0.0",
42
+ "@kustodian/k8s": "2.0.0",
43
+ "@kustodian/schema": "2.0.0"
40
44
  },
41
45
  "devDependencies": {}
42
46
  }
package/src/labeler.ts DELETED
@@ -1,201 +0,0 @@
1
- import { type ResultType, success } from '@kustodian/core';
2
- import type { KustodianErrorType } from '@kustodian/core';
3
-
4
- import { type NodeListType, type NodeType, format_node_labels } from './types.js';
5
-
6
- /**
7
- * Label operation types.
8
- */
9
- export type LabelOperationType = 'add' | 'update' | 'remove';
10
-
11
- /**
12
- * A single label change to be applied.
13
- */
14
- export interface LabelChangeType {
15
- node: string;
16
- key: string;
17
- value?: string;
18
- operation: LabelOperationType;
19
- }
20
-
21
- /**
22
- * Options for the label sync operation.
23
- */
24
- export interface LabelSyncOptionsType {
25
- dry_run?: boolean;
26
- verify?: boolean;
27
- }
28
-
29
- /**
30
- * Result of a label sync operation.
31
- */
32
- export interface LabelSyncResultType {
33
- changes: LabelChangeType[];
34
- applied: number;
35
- skipped: number;
36
- }
37
-
38
- /**
39
- * Calculates the label changes needed for a node.
40
- */
41
- export function calculate_label_changes(
42
- node: NodeType,
43
- current_labels: Record<string, string>,
44
- prefix: string,
45
- ): LabelChangeType[] {
46
- const changes: LabelChangeType[] = [];
47
- const desired_labels = format_node_labels(node.labels, prefix);
48
-
49
- // Find labels to add or update
50
- for (const [key, value] of Object.entries(desired_labels)) {
51
- const current_value = current_labels[key];
52
-
53
- if (current_value === undefined) {
54
- changes.push({
55
- node: node.name,
56
- key,
57
- value,
58
- operation: 'add',
59
- });
60
- } else if (current_value !== value) {
61
- changes.push({
62
- node: node.name,
63
- key,
64
- value,
65
- operation: 'update',
66
- });
67
- }
68
- }
69
-
70
- // Find labels to remove (labels with prefix that aren't in desired)
71
- for (const key of Object.keys(current_labels)) {
72
- if (key.startsWith(`${prefix}/`) && !(key in desired_labels)) {
73
- changes.push({
74
- node: node.name,
75
- key,
76
- operation: 'remove',
77
- });
78
- }
79
- }
80
-
81
- return changes;
82
- }
83
-
84
- /**
85
- * Calculates all label changes for a node list.
86
- */
87
- export function calculate_all_label_changes(
88
- node_list: NodeListType,
89
- current_labels_by_node: Map<string, Record<string, string>>,
90
- ): LabelChangeType[] {
91
- const prefix = node_list.label_prefix ?? 'kustodian.io';
92
- const all_changes: LabelChangeType[] = [];
93
-
94
- for (const node of node_list.nodes) {
95
- const current_labels = current_labels_by_node.get(node.name) ?? {};
96
- const changes = calculate_label_changes(node, current_labels, prefix);
97
- all_changes.push(...changes);
98
- }
99
-
100
- return all_changes;
101
- }
102
-
103
- /**
104
- * Formats a label change for display.
105
- */
106
- export function format_label_change(change: LabelChangeType): string {
107
- switch (change.operation) {
108
- case 'add':
109
- return `[+] ${change.node}: ${change.key}=${change.value}`;
110
- case 'update':
111
- return `[~] ${change.node}: ${change.key}=${change.value}`;
112
- case 'remove':
113
- return `[-] ${change.node}: ${change.key}`;
114
- }
115
- }
116
-
117
- /**
118
- * Groups label changes by node.
119
- */
120
- export function group_changes_by_node(changes: LabelChangeType[]): Map<string, LabelChangeType[]> {
121
- const grouped = new Map<string, LabelChangeType[]>();
122
-
123
- for (const change of changes) {
124
- const node_changes = grouped.get(change.node) ?? [];
125
- node_changes.push(change);
126
- grouped.set(change.node, node_changes);
127
- }
128
-
129
- return grouped;
130
- }
131
-
132
- /**
133
- * Creates a dry-run result from label changes.
134
- */
135
- export function create_dry_run_result(changes: LabelChangeType[]): LabelSyncResultType {
136
- return {
137
- changes,
138
- applied: 0,
139
- skipped: changes.length,
140
- };
141
- }
142
-
143
- /**
144
- * Node labeler service interface.
145
- * This is implemented by the actual Kubernetes client.
146
- */
147
- export interface NodeLabelerType {
148
- /**
149
- * Gets the current labels for a node.
150
- */
151
- get_labels(node_name: string): Promise<ResultType<Record<string, string>, KustodianErrorType>>;
152
-
153
- /**
154
- * Applies a label change to a node.
155
- */
156
- apply_change(change: LabelChangeType): Promise<ResultType<void, KustodianErrorType>>;
157
-
158
- /**
159
- * Syncs labels for all nodes in the list.
160
- */
161
- sync_labels(
162
- node_list: NodeListType,
163
- options?: LabelSyncOptionsType,
164
- ): Promise<ResultType<LabelSyncResultType, KustodianErrorType>>;
165
- }
166
-
167
- /**
168
- * Creates a mock labeler for testing.
169
- */
170
- export function create_mock_labeler(
171
- labels_by_node: Map<string, Record<string, string>>,
172
- ): NodeLabelerType {
173
- return {
174
- async get_labels(node_name) {
175
- const labels = labels_by_node.get(node_name);
176
- if (!labels) {
177
- return success({});
178
- }
179
- return success(labels);
180
- },
181
-
182
- async apply_change(_change) {
183
- return success(undefined);
184
- },
185
-
186
- async sync_labels(node_list, options) {
187
- const changes = calculate_all_label_changes(node_list, labels_by_node);
188
-
189
- if (options?.dry_run) {
190
- return success(create_dry_run_result(changes));
191
- }
192
-
193
- // In mock, all changes are "applied"
194
- return success({
195
- changes,
196
- applied: changes.length,
197
- skipped: 0,
198
- });
199
- },
200
- };
201
- }
package/src/profile.ts DELETED
@@ -1,184 +0,0 @@
1
- import type { NodeType, TaintType } from './types.js';
2
-
3
- /**
4
- * Node profile type for profile resolution.
5
- * This mirrors the NodeProfileType from @kustodian/schema but is defined here
6
- * to avoid circular dependencies.
7
- */
8
- export interface NodeProfileType {
9
- name: string;
10
- display_name?: string;
11
- description?: string;
12
- labels?: Record<string, string | boolean | number>;
13
- taints?: TaintType[];
14
- annotations?: Record<string, string>;
15
- }
16
-
17
- /**
18
- * Resolved node type with profile values merged.
19
- * The profile field is removed after resolution.
20
- */
21
- export type ResolvedNodeType = Omit<NodeType, 'profile'>;
22
-
23
- /**
24
- * Merges taints from profile and node.
25
- * Node taints are appended after profile taints.
26
- * Duplicate taints (same key+effect) from node override profile taints.
27
- */
28
- function merge_taints(
29
- profile_taints: TaintType[] | undefined,
30
- node_taints: TaintType[] | undefined,
31
- ): TaintType[] | undefined {
32
- if (!profile_taints && !node_taints) {
33
- return undefined;
34
- }
35
-
36
- if (!profile_taints) {
37
- return node_taints;
38
- }
39
-
40
- if (!node_taints) {
41
- return profile_taints;
42
- }
43
-
44
- // Create a map of node taints keyed by key+effect for deduplication
45
- const node_taint_keys = new Set(node_taints.map((t) => `${t.key}:${t.effect}`));
46
-
47
- // Filter out profile taints that are overridden by node taints
48
- const filtered_profile_taints = profile_taints.filter(
49
- (t) => !node_taint_keys.has(`${t.key}:${t.effect}`),
50
- );
51
-
52
- return [...filtered_profile_taints, ...node_taints];
53
- }
54
-
55
- /**
56
- * Resolves a node with its profile configuration.
57
- * Profile values are merged with node values, where node values take precedence.
58
- *
59
- * Merge strategy:
60
- * - labels: Profile labels are base, node labels override/extend
61
- * - taints: Node taints override profile taints with same key+effect, others are combined
62
- * - annotations: Profile annotations are base, node annotations override/extend
63
- * - ssh: Not inherited from profile (kept as node-specific)
64
- *
65
- * @param node - The node to resolve
66
- * @param profile - The profile to apply, or undefined if no profile
67
- * @returns The resolved node with profile values merged
68
- */
69
- export function resolve_node_profile(
70
- node: NodeType,
71
- profile: NodeProfileType | undefined,
72
- ): ResolvedNodeType {
73
- if (!profile) {
74
- // No profile to apply, return node without profile field
75
- const { profile: _, ...resolved } = node;
76
- return resolved;
77
- }
78
-
79
- // Merge labels: profile as base, node overrides
80
- const merged_labels =
81
- profile.labels || node.labels ? { ...profile.labels, ...node.labels } : undefined;
82
-
83
- // Merge taints: deduplicate by key+effect, node wins
84
- const merged_taints = merge_taints(profile.taints, node.taints);
85
-
86
- // Merge annotations: profile as base, node overrides
87
- const merged_annotations =
88
- profile.annotations || node.annotations
89
- ? { ...profile.annotations, ...node.annotations }
90
- : undefined;
91
-
92
- const resolved: ResolvedNodeType = {
93
- name: node.name,
94
- role: node.role,
95
- address: node.address,
96
- };
97
-
98
- if (node.ssh !== undefined) {
99
- resolved.ssh = node.ssh;
100
- }
101
- if (merged_labels !== undefined) {
102
- resolved.labels = merged_labels;
103
- }
104
- if (merged_taints !== undefined) {
105
- resolved.taints = merged_taints;
106
- }
107
- if (merged_annotations !== undefined) {
108
- resolved.annotations = merged_annotations;
109
- }
110
-
111
- return resolved;
112
- }
113
-
114
- /**
115
- * Resolves all nodes in a list with their profiles.
116
- *
117
- * @param nodes - The nodes to resolve
118
- * @param profiles - Map of profile name to profile
119
- * @returns Object with resolved nodes and any missing profile errors
120
- */
121
- export function resolve_all_node_profiles(
122
- nodes: NodeType[],
123
- profiles: Map<string, NodeProfileType>,
124
- ): { resolved: ResolvedNodeType[]; errors: string[] } {
125
- const resolved: ResolvedNodeType[] = [];
126
- const errors: string[] = [];
127
-
128
- for (const node of nodes) {
129
- if (node.profile) {
130
- const profile = profiles.get(node.profile);
131
- if (!profile) {
132
- errors.push(`Node '${node.name}' references unknown profile '${node.profile}'`);
133
- // Still resolve the node without the profile
134
- const { profile: _, ...node_without_profile } = node;
135
- resolved.push(node_without_profile);
136
- } else {
137
- resolved.push(resolve_node_profile(node, profile));
138
- }
139
- } else {
140
- resolved.push(resolve_node_profile(node, undefined));
141
- }
142
- }
143
-
144
- return { resolved, errors };
145
- }
146
-
147
- /**
148
- * Collects all unique profile names referenced by nodes.
149
- */
150
- export function get_referenced_profiles(nodes: NodeType[]): Set<string> {
151
- const profiles = new Set<string>();
152
- for (const node of nodes) {
153
- if (node.profile) {
154
- profiles.add(node.profile);
155
- }
156
- }
157
- return profiles;
158
- }
159
-
160
- /**
161
- * Validates that all referenced profiles exist.
162
- *
163
- * @param nodes - The nodes to check
164
- * @param profiles - Map of available profiles
165
- * @returns Array of error messages for missing profiles
166
- */
167
- export function validate_profile_references(
168
- nodes: NodeType[],
169
- profiles: Map<string, NodeProfileType>,
170
- ): string[] {
171
- const errors: string[] = [];
172
- const referenced = get_referenced_profiles(nodes);
173
-
174
- for (const profile_name of referenced) {
175
- if (!profiles.has(profile_name)) {
176
- const nodes_using = nodes.filter((n) => n.profile === profile_name).map((n) => n.name);
177
- errors.push(
178
- `Profile '${profile_name}' not found, referenced by nodes: ${nodes_using.join(', ')}`,
179
- );
180
- }
181
- }
182
-
183
- return errors;
184
- }
package/src/types.ts DELETED
@@ -1,137 +0,0 @@
1
- /**
2
- * Node role in the cluster.
3
- */
4
- export type NodeRoleType = 'controller' | 'worker' | 'controller+worker';
5
-
6
- /**
7
- * SSH configuration for connecting to a node.
8
- */
9
- export interface SshConfigType {
10
- user?: string;
11
- key_path?: string;
12
- known_hosts_path?: string;
13
- port?: number;
14
- }
15
-
16
- /**
17
- * Kubernetes taint configuration.
18
- */
19
- export interface TaintType {
20
- key: string;
21
- value?: string;
22
- effect: 'NoSchedule' | 'PreferNoSchedule' | 'NoExecute';
23
- }
24
-
25
- /**
26
- * Node definition with its configuration.
27
- */
28
- export interface NodeType {
29
- name: string;
30
- role: NodeRoleType;
31
- address: string;
32
- profile?: string;
33
- ssh?: SshConfigType;
34
- labels?: Record<string, string | boolean | number>;
35
- taints?: TaintType[];
36
- annotations?: Record<string, string>;
37
- }
38
-
39
- /**
40
- * Node list with default SSH configuration.
41
- */
42
- export interface NodeListType {
43
- cluster: string;
44
- label_prefix?: string;
45
- ssh?: SshConfigType;
46
- nodes: NodeType[];
47
- }
48
-
49
- /**
50
- * Default label prefix.
51
- */
52
- export const DEFAULT_LABEL_PREFIX = 'kustodian.io';
53
-
54
- /**
55
- * Formats a label key with the configured prefix.
56
- */
57
- export function format_label_key(key: string, prefix: string = DEFAULT_LABEL_PREFIX): string {
58
- // If key already has a prefix (contains /), return as-is
59
- if (key.includes('/')) {
60
- return key;
61
- }
62
- return `${prefix}/${key}`;
63
- }
64
-
65
- /**
66
- * Formats a label value as a string.
67
- */
68
- export function format_label_value(value: string | boolean | number): string {
69
- if (typeof value === 'boolean') {
70
- return value ? 'true' : 'false';
71
- }
72
- return String(value);
73
- }
74
-
75
- /**
76
- * Formats all labels for a node with the configured prefix.
77
- */
78
- export function format_node_labels(
79
- labels: Record<string, string | boolean | number> | undefined,
80
- prefix: string = DEFAULT_LABEL_PREFIX,
81
- ): Record<string, string> {
82
- if (!labels) {
83
- return {};
84
- }
85
-
86
- const result: Record<string, string> = {};
87
- for (const [key, value] of Object.entries(labels)) {
88
- const formatted_key = format_label_key(key, prefix);
89
- result[formatted_key] = format_label_value(value);
90
- }
91
- return result;
92
- }
93
-
94
- /**
95
- * Gets the SSH configuration for a node, merging with defaults.
96
- */
97
- export function get_node_ssh_config(node: NodeType, default_ssh?: SshConfigType): SshConfigType {
98
- return {
99
- ...default_ssh,
100
- ...node.ssh,
101
- };
102
- }
103
-
104
- /**
105
- * Checks if a node has a controller role.
106
- */
107
- export function is_controller(node: NodeType): boolean {
108
- return node.role === 'controller' || node.role === 'controller+worker';
109
- }
110
-
111
- /**
112
- * Checks if a node has a worker role.
113
- */
114
- export function is_worker(node: NodeType): boolean {
115
- return node.role === 'worker' || node.role === 'controller+worker';
116
- }
117
-
118
- /**
119
- * Gets all controller nodes from a node list.
120
- */
121
- export function get_controllers(nodes: NodeType[]): NodeType[] {
122
- return nodes.filter(is_controller);
123
- }
124
-
125
- /**
126
- * Gets all worker nodes from a node list.
127
- */
128
- export function get_workers(nodes: NodeType[]): NodeType[] {
129
- return nodes.filter(is_worker);
130
- }
131
-
132
- /**
133
- * Gets the primary controller node (first controller).
134
- */
135
- export function get_primary_controller(nodes: NodeType[]): NodeType | undefined {
136
- return nodes.find(is_controller);
137
- }