@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.
- package/package.json +45 -0
- package/src/external-substitutions.ts +71 -0
- package/src/flux.ts +233 -0
- package/src/generator.ts +311 -0
- package/src/index.ts +8 -0
- package/src/namespace.ts +121 -0
- package/src/output.ts +208 -0
- package/src/substitution.ts +140 -0
- package/src/types.ts +120 -0
- package/src/validation/cycle-detection.ts +111 -0
- package/src/validation/graph.ts +125 -0
- package/src/validation/index.ts +92 -0
- package/src/validation/reference.ts +126 -0
- package/src/validation/types.ts +107 -0
|
@@ -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
|
+
}
|