@kustodian/generator 1.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.
@@ -0,0 +1,125 @@
1
+ import type { TemplateType } from '@kustodian/schema';
2
+
3
+ import {
4
+ create_node_id,
5
+ is_parse_error,
6
+ parse_dependency_ref,
7
+ resolve_dependency_ref,
8
+ } from './reference.js';
9
+ import type { BuildGraphResultType, GraphNodeType, GraphValidationErrorType } from './types.js';
10
+
11
+ /**
12
+ * Builds a dependency graph from templates.
13
+ *
14
+ * Uses a two-pass algorithm:
15
+ * 1. First pass: Create nodes for all kustomizations
16
+ * 2. Second pass: Resolve dependencies and validate references
17
+ *
18
+ * @param templates - Array of templates to build graph from
19
+ * @returns Graph nodes and any errors encountered during building
20
+ */
21
+ export function build_dependency_graph(templates: TemplateType[]): BuildGraphResultType {
22
+ const nodes = new Map<string, GraphNodeType>();
23
+ const errors: GraphValidationErrorType[] = [];
24
+
25
+ // First pass: Create all nodes
26
+ for (const template of templates) {
27
+ const template_name = template.metadata.name;
28
+
29
+ for (const kustomization of template.spec.kustomizations) {
30
+ const node_id = create_node_id(template_name, kustomization.name);
31
+
32
+ nodes.set(node_id, {
33
+ id: node_id,
34
+ template: template_name,
35
+ kustomization: kustomization.name,
36
+ dependencies: [],
37
+ });
38
+ }
39
+ }
40
+
41
+ // Second pass: Resolve dependencies
42
+ for (const template of templates) {
43
+ const template_name = template.metadata.name;
44
+
45
+ for (const kustomization of template.spec.kustomizations) {
46
+ const node_id = create_node_id(template_name, kustomization.name);
47
+ const resolved_dependencies: string[] = [];
48
+
49
+ for (const dep of kustomization.depends_on ?? []) {
50
+ // Parse the dependency reference
51
+ const parse_result = parse_dependency_ref(dep);
52
+
53
+ if (is_parse_error(parse_result)) {
54
+ // Add source to the error
55
+ errors.push({
56
+ ...parse_result,
57
+ source: node_id,
58
+ });
59
+ continue;
60
+ }
61
+
62
+ // Resolve to full node ID
63
+ const target_id = resolve_dependency_ref(parse_result, template_name);
64
+
65
+ // Check for self-reference
66
+ if (target_id === node_id) {
67
+ errors.push({
68
+ type: 'self_reference',
69
+ node: node_id,
70
+ message: `Kustomization '${node_id}' cannot depend on itself`,
71
+ });
72
+ continue;
73
+ }
74
+
75
+ // Check if target exists
76
+ if (!nodes.has(target_id)) {
77
+ errors.push({
78
+ type: 'missing_reference',
79
+ source: node_id,
80
+ target: target_id,
81
+ message: `Kustomization '${node_id}' depends on '${target_id}' which does not exist`,
82
+ });
83
+ continue;
84
+ }
85
+
86
+ resolved_dependencies.push(target_id);
87
+ }
88
+
89
+ // Update node with resolved dependencies
90
+ // We need to create a new node since GraphNodeType has readonly properties
91
+ if (resolved_dependencies.length > 0) {
92
+ const existing_node = nodes.get(node_id);
93
+ if (existing_node) {
94
+ nodes.set(node_id, {
95
+ ...existing_node,
96
+ dependencies: resolved_dependencies,
97
+ });
98
+ }
99
+ }
100
+ }
101
+ }
102
+
103
+ return { nodes, errors };
104
+ }
105
+
106
+ /**
107
+ * Gets all nodes from the graph as an array.
108
+ *
109
+ * @param nodes - Map of graph nodes
110
+ * @returns Array of all nodes
111
+ */
112
+ export function get_all_nodes(nodes: Map<string, GraphNodeType>): GraphNodeType[] {
113
+ return Array.from(nodes.values());
114
+ }
115
+
116
+ /**
117
+ * Gets a node by its ID.
118
+ *
119
+ * @param nodes - Map of graph nodes
120
+ * @param id - Node ID to look up
121
+ * @returns The node or undefined if not found
122
+ */
123
+ export function get_node(nodes: Map<string, GraphNodeType>, id: string): GraphNodeType | undefined {
124
+ return nodes.get(id);
125
+ }
@@ -0,0 +1,92 @@
1
+ import { type ResultType, failure, success } from '@kustodian/core';
2
+ import type { KustodianErrorType } from '@kustodian/core';
3
+ import type { TemplateType } from '@kustodian/schema';
4
+
5
+ import { detect_cycles } from './cycle-detection.js';
6
+ import { build_dependency_graph } from './graph.js';
7
+ import type { GraphValidationResultType } from './types.js';
8
+
9
+ // Re-export types
10
+ export type {
11
+ BuildGraphResultType,
12
+ CycleDetectionResultType,
13
+ CycleErrorType,
14
+ DependencyRefType,
15
+ GraphNodeType,
16
+ GraphValidationErrorType,
17
+ GraphValidationResultType,
18
+ InvalidReferenceErrorType,
19
+ MissingReferenceErrorType,
20
+ SelfReferenceErrorType,
21
+ } from './types.js';
22
+
23
+ // Re-export functions
24
+ export { build_dependency_graph, get_all_nodes, get_node } from './graph.js';
25
+ export { detect_cycles, has_cycles } from './cycle-detection.js';
26
+ export {
27
+ create_node_id,
28
+ is_parse_error,
29
+ parse_dependency_ref,
30
+ parse_node_id,
31
+ resolve_dependency_ref,
32
+ } from './reference.js';
33
+
34
+ /**
35
+ * Validates the dependency graph for a set of templates.
36
+ *
37
+ * Performs the following validations:
38
+ * 1. Reference validation: Ensures all `depends_on` references point to existing kustomizations
39
+ * 2. Self-reference detection: Detects kustomizations that depend on themselves
40
+ * 3. Cycle detection: Detects circular dependencies using DFS
41
+ * 4. Topological sorting: Computes deployment order if the graph is valid
42
+ *
43
+ * @param templates - Array of templates to validate
44
+ * @returns Detailed validation result including errors and deployment order
45
+ */
46
+ export function validate_dependency_graph(templates: TemplateType[]): GraphValidationResultType {
47
+ // Build the dependency graph (validates references)
48
+ const { nodes, errors } = build_dependency_graph(templates);
49
+
50
+ // Detect cycles in the graph
51
+ const { cycles, topological_order } = detect_cycles(nodes);
52
+
53
+ // Combine all errors
54
+ const all_errors = [...errors, ...cycles];
55
+
56
+ const result: GraphValidationResultType = {
57
+ valid: all_errors.length === 0,
58
+ errors: all_errors,
59
+ };
60
+
61
+ if (topological_order !== null) {
62
+ return { ...result, topological_order };
63
+ }
64
+
65
+ return result;
66
+ }
67
+
68
+ /**
69
+ * Validates the dependency graph and returns a Result type.
70
+ *
71
+ * This is the main integration point for the generator pipeline.
72
+ * Returns a success with the topological order, or a failure with
73
+ * detailed error information.
74
+ *
75
+ * @param templates - Array of templates to validate
76
+ * @returns Result with topological order on success, or error on failure
77
+ */
78
+ export function validate_dependencies(
79
+ templates: TemplateType[],
80
+ ): ResultType<string[], KustodianErrorType> {
81
+ const result = validate_dependency_graph(templates);
82
+
83
+ if (!result.valid) {
84
+ const error_messages = result.errors.map((e) => e.message);
85
+ return failure({
86
+ code: 'DEPENDENCY_VALIDATION_ERROR',
87
+ message: `Dependency validation failed:\n${error_messages.map((m) => ` - ${m}`).join('\n')}`,
88
+ });
89
+ }
90
+
91
+ return success(result.topological_order ?? []);
92
+ }
@@ -0,0 +1,126 @@
1
+ import type { DependencyRefType, InvalidReferenceErrorType } from './types.js';
2
+
3
+ /**
4
+ * Parses a dependency reference string.
5
+ *
6
+ * Supports two formats:
7
+ * - Within-template: `kustomization-name` (e.g., `operator`)
8
+ * - Cross-template: `template-name/kustomization-name` (e.g., `001-secrets/doppler`)
9
+ *
10
+ * @param ref - The raw dependency reference string
11
+ * @returns Parsed dependency reference or error
12
+ */
13
+ export function parse_dependency_ref(ref: string): DependencyRefType | InvalidReferenceErrorType {
14
+ const trimmed = ref.trim();
15
+
16
+ if (trimmed.length === 0) {
17
+ return {
18
+ type: 'invalid_reference',
19
+ source: '',
20
+ reference: ref,
21
+ message: 'Empty dependency reference',
22
+ };
23
+ }
24
+
25
+ const parts = trimmed.split('/');
26
+
27
+ if (parts.length === 1) {
28
+ // Within-template reference: `kustomization-name`
29
+ const kustomization = parts[0];
30
+ if (kustomization === undefined) {
31
+ return {
32
+ type: 'invalid_reference',
33
+ source: '',
34
+ reference: ref,
35
+ message: `Invalid dependency reference: '${ref}'`,
36
+ };
37
+ }
38
+ return {
39
+ kustomization,
40
+ raw: ref,
41
+ };
42
+ }
43
+
44
+ if (parts.length === 2) {
45
+ // Cross-template reference: `template-name/kustomization-name`
46
+ const template = parts[0];
47
+ const kustomization = parts[1];
48
+
49
+ if (
50
+ template === undefined ||
51
+ kustomization === undefined ||
52
+ template.length === 0 ||
53
+ kustomization.length === 0
54
+ ) {
55
+ return {
56
+ type: 'invalid_reference',
57
+ source: '',
58
+ reference: ref,
59
+ message: `Invalid dependency reference format: '${ref}' - both template and kustomization names must be non-empty`,
60
+ };
61
+ }
62
+
63
+ return {
64
+ template,
65
+ kustomization,
66
+ raw: ref,
67
+ };
68
+ }
69
+
70
+ // More than one slash is invalid
71
+ return {
72
+ type: 'invalid_reference',
73
+ source: '',
74
+ reference: ref,
75
+ message: `Invalid dependency reference format: '${ref}' - expected 'kustomization' or 'template/kustomization'`,
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Type guard to check if parse result is an error.
81
+ */
82
+ export function is_parse_error(
83
+ result: DependencyRefType | InvalidReferenceErrorType,
84
+ ): result is InvalidReferenceErrorType {
85
+ return 'type' in result && result.type === 'invalid_reference';
86
+ }
87
+
88
+ /**
89
+ * Resolves a dependency reference to a full node ID.
90
+ *
91
+ * @param ref - Parsed dependency reference
92
+ * @param current_template - The template containing the reference
93
+ * @returns Full node ID in format `template/kustomization`
94
+ */
95
+ export function resolve_dependency_ref(ref: DependencyRefType, current_template: string): string {
96
+ const template = ref.template ?? current_template;
97
+ return `${template}/${ref.kustomization}`;
98
+ }
99
+
100
+ /**
101
+ * Creates a node ID from template and kustomization names.
102
+ *
103
+ * @param template - Template name
104
+ * @param kustomization - Kustomization name
105
+ * @returns Node ID in format `template/kustomization`
106
+ */
107
+ export function create_node_id(template: string, kustomization: string): string {
108
+ return `${template}/${kustomization}`;
109
+ }
110
+
111
+ /**
112
+ * Parses a node ID into its components.
113
+ *
114
+ * @param node_id - Node ID in format `template/kustomization`
115
+ * @returns Object with template and kustomization names
116
+ */
117
+ export function parse_node_id(node_id: string): { template: string; kustomization: string } {
118
+ const slash_index = node_id.indexOf('/');
119
+ if (slash_index === -1) {
120
+ return { template: '', kustomization: node_id };
121
+ }
122
+ return {
123
+ template: node_id.slice(0, slash_index),
124
+ kustomization: node_id.slice(slash_index + 1),
125
+ };
126
+ }
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Node in the dependency graph.
3
+ */
4
+ export interface GraphNodeType {
5
+ /** Unique identifier: `${template}/${kustomization}` */
6
+ readonly id: string;
7
+ /** Template name */
8
+ readonly template: string;
9
+ /** Kustomization name */
10
+ readonly kustomization: string;
11
+ /** Resolved dependency IDs */
12
+ readonly dependencies: string[];
13
+ }
14
+
15
+ /**
16
+ * Parsed dependency reference.
17
+ */
18
+ export interface DependencyRefType {
19
+ /** Template name (undefined for within-template refs) */
20
+ readonly template?: string;
21
+ /** Kustomization name */
22
+ readonly kustomization: string;
23
+ /** Original reference string */
24
+ readonly raw: string;
25
+ }
26
+
27
+ /**
28
+ * Cycle error - circular dependency detected.
29
+ */
30
+ export interface CycleErrorType {
31
+ readonly type: 'cycle';
32
+ /** Array of node IDs forming the cycle */
33
+ readonly cycle: string[];
34
+ readonly message: string;
35
+ }
36
+
37
+ /**
38
+ * Missing reference error - dependency target doesn't exist.
39
+ */
40
+ export interface MissingReferenceErrorType {
41
+ readonly type: 'missing_reference';
42
+ /** Node ID that has the reference */
43
+ readonly source: string;
44
+ /** Missing target reference */
45
+ readonly target: string;
46
+ readonly message: string;
47
+ }
48
+
49
+ /**
50
+ * Self-reference error - kustomization depends on itself.
51
+ */
52
+ export interface SelfReferenceErrorType {
53
+ readonly type: 'self_reference';
54
+ readonly node: string;
55
+ readonly message: string;
56
+ }
57
+
58
+ /**
59
+ * Invalid reference format error.
60
+ */
61
+ export interface InvalidReferenceErrorType {
62
+ readonly type: 'invalid_reference';
63
+ readonly source: string;
64
+ readonly reference: string;
65
+ readonly message: string;
66
+ }
67
+
68
+ /**
69
+ * Union of all graph validation error types.
70
+ */
71
+ export type GraphValidationErrorType =
72
+ | CycleErrorType
73
+ | MissingReferenceErrorType
74
+ | SelfReferenceErrorType
75
+ | InvalidReferenceErrorType;
76
+
77
+ /**
78
+ * Result of graph validation.
79
+ */
80
+ export interface GraphValidationResultType {
81
+ /** Whether the graph is valid (no errors) */
82
+ readonly valid: boolean;
83
+ /** All validation errors found */
84
+ readonly errors: GraphValidationErrorType[];
85
+ /** Deployment order if valid (topologically sorted) */
86
+ readonly topological_order?: string[];
87
+ }
88
+
89
+ /**
90
+ * Result of building the dependency graph.
91
+ */
92
+ export interface BuildGraphResultType {
93
+ /** All nodes in the graph */
94
+ readonly nodes: Map<string, GraphNodeType>;
95
+ /** Errors encountered during graph building */
96
+ readonly errors: GraphValidationErrorType[];
97
+ }
98
+
99
+ /**
100
+ * Result of cycle detection.
101
+ */
102
+ export interface CycleDetectionResultType {
103
+ /** All cycles found */
104
+ readonly cycles: CycleErrorType[];
105
+ /** Topological order if no cycles found, null otherwise */
106
+ readonly topological_order: string[] | null;
107
+ }