@kustodian/generator 1.1.0 → 1.2.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 +5 -5
- package/src/flux.ts +34 -4
- package/src/generator.ts +18 -1
- package/src/kustomization-resolution.ts +133 -0
- package/src/preservation.ts +88 -0
- package/src/types.ts +20 -0
- package/src/validation/enablement.ts +111 -0
- package/src/validation/graph.ts +5 -0
- package/src/validation/index.ts +17 -3
- package/src/validation/reference.ts +56 -4
- package/src/validation/requirements.ts +108 -0
- package/src/validation/types.ts +31 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kustodian/generator",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Template processing and Flux generation for Kustodian",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"registry": "https://npm.pkg.github.com"
|
|
36
36
|
},
|
|
37
37
|
"dependencies": {
|
|
38
|
-
"@kustodian/core": "1.
|
|
39
|
-
"@kustodian/loader": "1.
|
|
40
|
-
"@kustodian/plugins": "1.0.
|
|
41
|
-
"@kustodian/schema": "1.
|
|
38
|
+
"@kustodian/core": "1.1.0",
|
|
39
|
+
"@kustodian/loader": "1.1.0",
|
|
40
|
+
"@kustodian/plugins": "1.0.1",
|
|
41
|
+
"@kustodian/schema": "1.3.0",
|
|
42
42
|
"yaml": "^2.8.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {}
|
package/src/flux.ts
CHANGED
|
@@ -2,15 +2,19 @@ import type {
|
|
|
2
2
|
ClusterType,
|
|
3
3
|
KustomizationType,
|
|
4
4
|
OciConfigType,
|
|
5
|
+
PreservationPolicyType,
|
|
6
|
+
DependencyRefType as SchemaDependencyRefType,
|
|
5
7
|
TemplateType,
|
|
6
8
|
} from '@kustodian/schema';
|
|
7
9
|
|
|
10
|
+
import { generate_preservation_patches, get_preserved_resource_types } from './preservation.js';
|
|
8
11
|
import type {
|
|
9
12
|
FluxKustomizationType,
|
|
10
13
|
FluxOCIRepositoryType,
|
|
11
14
|
ResolvedKustomizationType,
|
|
12
15
|
} from './types.js';
|
|
13
16
|
import { is_parse_error, parse_dependency_ref } from './validation/reference.js';
|
|
17
|
+
import { is_raw_dependency_ref } from './validation/types.js';
|
|
14
18
|
|
|
15
19
|
/**
|
|
16
20
|
* Default interval for Flux reconciliation.
|
|
@@ -45,14 +49,15 @@ export function generate_flux_path(
|
|
|
45
49
|
/**
|
|
46
50
|
* Generates the dependency references for a Flux Kustomization.
|
|
47
51
|
*
|
|
48
|
-
* Supports
|
|
52
|
+
* Supports three formats:
|
|
49
53
|
* - Within-template: `database` → uses current template name
|
|
50
54
|
* - Cross-template: `secrets/doppler` → uses explicit template name
|
|
55
|
+
* - Raw external: `{ raw: { name: 'legacy-infrastructure', namespace: 'gitops-system' } }`
|
|
51
56
|
*/
|
|
52
57
|
export function generate_depends_on(
|
|
53
58
|
template_name: string,
|
|
54
|
-
depends_on:
|
|
55
|
-
): Array<{ name: string }> | undefined {
|
|
59
|
+
depends_on: SchemaDependencyRefType[] | undefined,
|
|
60
|
+
): Array<{ name: string; namespace?: string }> | undefined {
|
|
56
61
|
if (!depends_on || depends_on.length === 0) {
|
|
57
62
|
return undefined;
|
|
58
63
|
}
|
|
@@ -61,8 +66,23 @@ export function generate_depends_on(
|
|
|
61
66
|
const parsed = parse_dependency_ref(dep);
|
|
62
67
|
if (is_parse_error(parsed)) {
|
|
63
68
|
// Invalid references are caught during validation, fall back to current behavior
|
|
64
|
-
|
|
69
|
+
// This should only happen with string refs
|
|
70
|
+
if (typeof dep === 'string') {
|
|
71
|
+
return { name: generate_flux_name(template_name, dep) };
|
|
72
|
+
}
|
|
73
|
+
// If somehow we have an invalid object ref, use a placeholder
|
|
74
|
+
return { name: 'invalid-reference' };
|
|
65
75
|
}
|
|
76
|
+
|
|
77
|
+
// Handle raw dependencies - pass through name and namespace directly
|
|
78
|
+
if (is_raw_dependency_ref(parsed)) {
|
|
79
|
+
return {
|
|
80
|
+
name: parsed.name,
|
|
81
|
+
namespace: parsed.namespace,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Handle string-based dependencies (within-template and cross-template)
|
|
66
86
|
const effective_template = parsed.template ?? template_name;
|
|
67
87
|
return { name: generate_flux_name(effective_template, parsed.kustomization) };
|
|
68
88
|
});
|
|
@@ -179,6 +199,7 @@ export function generate_flux_kustomization(
|
|
|
179
199
|
resolved: ResolvedKustomizationType,
|
|
180
200
|
source_repository_name = 'flux-system',
|
|
181
201
|
source_kind: 'GitRepository' | 'OCIRepository' = 'GitRepository',
|
|
202
|
+
preservation?: PreservationPolicyType,
|
|
182
203
|
): FluxKustomizationType {
|
|
183
204
|
const { template, kustomization, values, namespace } = resolved;
|
|
184
205
|
const name = generate_flux_name(template.metadata.name, kustomization.name);
|
|
@@ -196,6 +217,15 @@ export function generate_flux_kustomization(
|
|
|
196
217
|
},
|
|
197
218
|
};
|
|
198
219
|
|
|
220
|
+
// Add preservation patches if preservation is configured
|
|
221
|
+
if (preservation) {
|
|
222
|
+
const preserved_types = get_preserved_resource_types(preservation);
|
|
223
|
+
if (preserved_types.length > 0) {
|
|
224
|
+
const patches = generate_preservation_patches(preserved_types);
|
|
225
|
+
spec.patches = patches;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
199
229
|
// Add timeout if specified
|
|
200
230
|
if (kustomization.timeout) {
|
|
201
231
|
spec.timeout = kustomization.timeout;
|
package/src/generator.ts
CHANGED
|
@@ -17,6 +17,7 @@ import {
|
|
|
17
17
|
generate_health_checks,
|
|
18
18
|
resolve_kustomization,
|
|
19
19
|
} from './flux.js';
|
|
20
|
+
import { get_template_config, resolve_kustomization_state } from './kustomization-resolution.js';
|
|
20
21
|
import { generate_namespace_resources } from './namespace.js';
|
|
21
22
|
import { serialize_resource, serialize_resources, write_generation_result } from './output.js';
|
|
22
23
|
import type {
|
|
@@ -169,7 +170,7 @@ export function create_generator(
|
|
|
169
170
|
|
|
170
171
|
// Validate dependency graph before generation (unless skipped)
|
|
171
172
|
if (!skip_validation) {
|
|
172
|
-
const validation_result = validate_dependencies(templates);
|
|
173
|
+
const validation_result = validate_dependencies(cluster, templates);
|
|
173
174
|
if (!validation_result.success) {
|
|
174
175
|
return validation_result;
|
|
175
176
|
}
|
|
@@ -201,7 +202,22 @@ export function create_generator(
|
|
|
201
202
|
continue;
|
|
202
203
|
}
|
|
203
204
|
|
|
205
|
+
// Get template configuration from cluster for kustomization overrides
|
|
206
|
+
const template_config = get_template_config(cluster, resolved.template.metadata.name);
|
|
207
|
+
|
|
204
208
|
for (const kustomization of resolved.template.spec.kustomizations) {
|
|
209
|
+
// Resolve kustomization state (enabled + preservation)
|
|
210
|
+
const kustomization_state = resolve_kustomization_state(
|
|
211
|
+
kustomization,
|
|
212
|
+
template_config,
|
|
213
|
+
kustomization.name,
|
|
214
|
+
);
|
|
215
|
+
|
|
216
|
+
// Skip disabled kustomizations
|
|
217
|
+
if (!kustomization_state.enabled) {
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
205
221
|
const resolved_kustomization = resolve_kustomization(
|
|
206
222
|
resolved.template,
|
|
207
223
|
kustomization,
|
|
@@ -213,6 +229,7 @@ export function create_generator(
|
|
|
213
229
|
resolved_kustomization,
|
|
214
230
|
source_repository_name,
|
|
215
231
|
source_kind,
|
|
232
|
+
kustomization_state.preservation,
|
|
216
233
|
);
|
|
217
234
|
|
|
218
235
|
// Override namespace to configured value
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClusterType,
|
|
3
|
+
KustomizationType,
|
|
4
|
+
PreservationPolicyType,
|
|
5
|
+
TemplateConfigType,
|
|
6
|
+
} from '@kustodian/schema';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Resolved kustomization enablement and preservation state.
|
|
10
|
+
*/
|
|
11
|
+
export interface ResolvedKustomizationStateType {
|
|
12
|
+
enabled: boolean;
|
|
13
|
+
preservation: PreservationPolicyType;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resolves kustomization enablement state from template defaults and cluster overrides.
|
|
18
|
+
*
|
|
19
|
+
* Resolution order (last wins):
|
|
20
|
+
* 1. Template kustomization default (enabled field, defaults to true)
|
|
21
|
+
* 2. Cluster kustomization override (if specified)
|
|
22
|
+
*
|
|
23
|
+
* @param kustomization - Template kustomization definition
|
|
24
|
+
* @param template_config - Cluster template configuration (may be undefined)
|
|
25
|
+
* @param kustomization_name - Name of the kustomization to resolve
|
|
26
|
+
* @returns Resolved enablement state
|
|
27
|
+
*/
|
|
28
|
+
export function resolve_kustomization_enabled(
|
|
29
|
+
kustomization: KustomizationType,
|
|
30
|
+
template_config: TemplateConfigType | undefined,
|
|
31
|
+
kustomization_name: string,
|
|
32
|
+
): boolean {
|
|
33
|
+
// Start with template default (defaults to true via schema)
|
|
34
|
+
const template_default = kustomization.enabled ?? true;
|
|
35
|
+
|
|
36
|
+
// Check for cluster override
|
|
37
|
+
if (!template_config?.kustomizations) {
|
|
38
|
+
return template_default;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const override = template_config.kustomizations[kustomization_name];
|
|
42
|
+
if (override === undefined) {
|
|
43
|
+
return template_default;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Handle simple boolean override
|
|
47
|
+
if (typeof override === 'boolean') {
|
|
48
|
+
return override;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Handle complex override object
|
|
52
|
+
return override.enabled;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolves kustomization preservation policy from template defaults and cluster overrides.
|
|
57
|
+
*
|
|
58
|
+
* Resolution order (last wins):
|
|
59
|
+
* 1. Template kustomization preservation policy (defaults to 'stateful')
|
|
60
|
+
* 2. Cluster kustomization override preservation (if specified)
|
|
61
|
+
*
|
|
62
|
+
* @param kustomization - Template kustomization definition
|
|
63
|
+
* @param template_config - Cluster template configuration (may be undefined)
|
|
64
|
+
* @param kustomization_name - Name of the kustomization to resolve
|
|
65
|
+
* @returns Resolved preservation policy
|
|
66
|
+
*/
|
|
67
|
+
export function resolve_kustomization_preservation(
|
|
68
|
+
kustomization: KustomizationType,
|
|
69
|
+
template_config: TemplateConfigType | undefined,
|
|
70
|
+
kustomization_name: string,
|
|
71
|
+
): PreservationPolicyType {
|
|
72
|
+
// Start with template default
|
|
73
|
+
const template_default: PreservationPolicyType = kustomization.preservation ?? {
|
|
74
|
+
mode: 'stateful',
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Check for cluster override
|
|
78
|
+
if (!template_config?.kustomizations) {
|
|
79
|
+
return template_default;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const override = template_config.kustomizations[kustomization_name];
|
|
83
|
+
if (override === undefined || typeof override === 'boolean') {
|
|
84
|
+
return template_default;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Merge cluster override with template default
|
|
88
|
+
if (override.preservation) {
|
|
89
|
+
return {
|
|
90
|
+
mode: override.preservation.mode,
|
|
91
|
+
keep_resources: template_default.keep_resources,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return template_default;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolves complete kustomization state (enabled + preservation).
|
|
100
|
+
*
|
|
101
|
+
* @param kustomization - Template kustomization definition
|
|
102
|
+
* @param template_config - Cluster template configuration (may be undefined)
|
|
103
|
+
* @param kustomization_name - Name of the kustomization to resolve
|
|
104
|
+
* @returns Resolved kustomization state
|
|
105
|
+
*/
|
|
106
|
+
export function resolve_kustomization_state(
|
|
107
|
+
kustomization: KustomizationType,
|
|
108
|
+
template_config: TemplateConfigType | undefined,
|
|
109
|
+
kustomization_name: string,
|
|
110
|
+
): ResolvedKustomizationStateType {
|
|
111
|
+
return {
|
|
112
|
+
enabled: resolve_kustomization_enabled(kustomization, template_config, kustomization_name),
|
|
113
|
+
preservation: resolve_kustomization_preservation(
|
|
114
|
+
kustomization,
|
|
115
|
+
template_config,
|
|
116
|
+
kustomization_name,
|
|
117
|
+
),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Gets template configuration for a specific template from cluster spec.
|
|
123
|
+
*
|
|
124
|
+
* @param cluster - Cluster configuration
|
|
125
|
+
* @param template_name - Name of the template to find
|
|
126
|
+
* @returns Template configuration or undefined if not found
|
|
127
|
+
*/
|
|
128
|
+
export function get_template_config(
|
|
129
|
+
cluster: ClusterType,
|
|
130
|
+
template_name: string,
|
|
131
|
+
): TemplateConfigType | undefined {
|
|
132
|
+
return cluster.spec.templates?.find((t) => t.name === template_name);
|
|
133
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { PreservationPolicyType } from '@kustodian/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resource types that are considered stateful and should be preserved by default.
|
|
5
|
+
*
|
|
6
|
+
* This list defines which Kubernetes resources contain state that should not be
|
|
7
|
+
* accidentally deleted when disabling a kustomization.
|
|
8
|
+
*/
|
|
9
|
+
export const DEFAULT_STATEFUL_RESOURCES = ['PersistentVolumeClaim', 'Secret', 'ConfigMap'] as const;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Gets the list of resource types that should be preserved based on preservation policy.
|
|
13
|
+
*
|
|
14
|
+
* @param policy - Preservation policy from kustomization configuration
|
|
15
|
+
* @returns Array of Kubernetes resource kinds to preserve
|
|
16
|
+
*/
|
|
17
|
+
export function get_preserved_resource_types(policy: PreservationPolicyType): string[] {
|
|
18
|
+
switch (policy.mode) {
|
|
19
|
+
case 'none':
|
|
20
|
+
return [];
|
|
21
|
+
|
|
22
|
+
case 'stateful':
|
|
23
|
+
return [...DEFAULT_STATEFUL_RESOURCES];
|
|
24
|
+
|
|
25
|
+
case 'custom':
|
|
26
|
+
return policy.keep_resources ?? [];
|
|
27
|
+
|
|
28
|
+
default: {
|
|
29
|
+
// Exhaustiveness check
|
|
30
|
+
const _exhaustive: never = policy.mode;
|
|
31
|
+
throw new Error(`Unknown preservation mode: ${_exhaustive}`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Generates Flux Kustomization patches to label preserved resources.
|
|
38
|
+
*
|
|
39
|
+
* This function creates patches that add a `kustodian.io/preserve: "true"` label
|
|
40
|
+
* to resources that should be kept when a kustomization is disabled.
|
|
41
|
+
*
|
|
42
|
+
* The label-based approach works as follows:
|
|
43
|
+
* 1. Add labels to resources we want to preserve
|
|
44
|
+
* 2. Configure Flux to not prune resources with the preserve label
|
|
45
|
+
* 3. When kustomization is disabled, non-preserved resources are deleted
|
|
46
|
+
* 4. Preserved resources (PVCs, Secrets, etc.) remain in the cluster
|
|
47
|
+
*
|
|
48
|
+
* @param preserved_types - Resource types to keep (from get_preserved_resource_types)
|
|
49
|
+
* @returns Flux patch objects that add preservation labels
|
|
50
|
+
*/
|
|
51
|
+
export function generate_preservation_patches(preserved_types: string[]): Array<{
|
|
52
|
+
patch: string;
|
|
53
|
+
target: {
|
|
54
|
+
kind: string;
|
|
55
|
+
};
|
|
56
|
+
}> {
|
|
57
|
+
if (preserved_types.length === 0) {
|
|
58
|
+
return [];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return preserved_types.map((kind) => ({
|
|
62
|
+
patch: `
|
|
63
|
+
apiVersion: v1
|
|
64
|
+
kind: ${kind}
|
|
65
|
+
metadata:
|
|
66
|
+
labels:
|
|
67
|
+
kustodian.io/preserve: "true"
|
|
68
|
+
`,
|
|
69
|
+
target: {
|
|
70
|
+
kind,
|
|
71
|
+
},
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Checks if a resource type should be preserved based on the preservation policy.
|
|
77
|
+
*
|
|
78
|
+
* @param resource_kind - Kubernetes resource kind (e.g., 'Deployment', 'PersistentVolumeClaim')
|
|
79
|
+
* @param policy - Preservation policy
|
|
80
|
+
* @returns true if the resource should be preserved, false if it should be deleted
|
|
81
|
+
*/
|
|
82
|
+
export function should_preserve_resource(
|
|
83
|
+
resource_kind: string,
|
|
84
|
+
policy: PreservationPolicyType,
|
|
85
|
+
): boolean {
|
|
86
|
+
const preserved_types = get_preserved_resource_types(policy);
|
|
87
|
+
return preserved_types.includes(resource_kind);
|
|
88
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -84,6 +84,25 @@ export interface FluxOCIRepositoryType {
|
|
|
84
84
|
};
|
|
85
85
|
}
|
|
86
86
|
|
|
87
|
+
/**
|
|
88
|
+
* Flux Kustomization patch target selector.
|
|
89
|
+
*/
|
|
90
|
+
export interface FluxPatchTargetType {
|
|
91
|
+
kind: string;
|
|
92
|
+
name?: string;
|
|
93
|
+
namespace?: string;
|
|
94
|
+
labelSelector?: string;
|
|
95
|
+
annotationSelector?: string;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Flux Kustomization patch.
|
|
100
|
+
*/
|
|
101
|
+
export interface FluxPatchType {
|
|
102
|
+
patch: string;
|
|
103
|
+
target: FluxPatchTargetType;
|
|
104
|
+
}
|
|
105
|
+
|
|
87
106
|
/**
|
|
88
107
|
* Flux Kustomization resource type.
|
|
89
108
|
*/
|
|
@@ -125,5 +144,6 @@ export interface FluxKustomizationType {
|
|
|
125
144
|
/** CEL expression for when resource has failed */
|
|
126
145
|
failed?: string;
|
|
127
146
|
}>;
|
|
147
|
+
patches?: FluxPatchType[];
|
|
128
148
|
};
|
|
129
149
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import type { ClusterType, TemplateType } from '@kustodian/schema';
|
|
2
|
+
|
|
3
|
+
import { get_template_config, resolve_kustomization_state } from '../kustomization-resolution.js';
|
|
4
|
+
import { create_node_id } from './reference.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Disabled dependency error - an enabled kustomization depends on a disabled one.
|
|
8
|
+
*/
|
|
9
|
+
export interface DisabledDependencyErrorType {
|
|
10
|
+
readonly type: 'disabled_dependency';
|
|
11
|
+
/** Node ID of the enabled kustomization */
|
|
12
|
+
readonly source: string;
|
|
13
|
+
/** Node ID of the disabled dependency */
|
|
14
|
+
readonly target: string;
|
|
15
|
+
readonly message: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validates that no enabled kustomizations depend on disabled ones.
|
|
20
|
+
*
|
|
21
|
+
* This implements the "block by default" policy: you cannot disable a kustomization
|
|
22
|
+
* if other enabled kustomizations depend on it.
|
|
23
|
+
*
|
|
24
|
+
* @param cluster - Cluster configuration
|
|
25
|
+
* @param templates - Array of templates
|
|
26
|
+
* @returns Array of validation errors (empty if valid)
|
|
27
|
+
*/
|
|
28
|
+
export function validate_enablement_dependencies(
|
|
29
|
+
cluster: ClusterType,
|
|
30
|
+
templates: TemplateType[],
|
|
31
|
+
): DisabledDependencyErrorType[] {
|
|
32
|
+
const errors: DisabledDependencyErrorType[] = [];
|
|
33
|
+
|
|
34
|
+
// Build a map of kustomization ID to enabled state
|
|
35
|
+
const enablement_map = new Map<string, boolean>();
|
|
36
|
+
|
|
37
|
+
for (const template of templates) {
|
|
38
|
+
const template_config = get_template_config(cluster, template.metadata.name);
|
|
39
|
+
|
|
40
|
+
// Check if template itself is enabled
|
|
41
|
+
const template_enabled = template_config?.enabled ?? true;
|
|
42
|
+
if (!template_enabled) {
|
|
43
|
+
// All kustomizations in disabled templates are disabled
|
|
44
|
+
for (const kustomization of template.spec.kustomizations) {
|
|
45
|
+
const node_id = create_node_id(template.metadata.name, kustomization.name);
|
|
46
|
+
enablement_map.set(node_id, false);
|
|
47
|
+
}
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Template is enabled, check individual kustomizations
|
|
52
|
+
for (const kustomization of template.spec.kustomizations) {
|
|
53
|
+
const node_id = create_node_id(template.metadata.name, kustomization.name);
|
|
54
|
+
const state = resolve_kustomization_state(kustomization, template_config, kustomization.name);
|
|
55
|
+
enablement_map.set(node_id, state.enabled);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Check each enabled kustomization's dependencies
|
|
60
|
+
for (const template of templates) {
|
|
61
|
+
const template_config = get_template_config(cluster, template.metadata.name);
|
|
62
|
+
const template_enabled = template_config?.enabled ?? true;
|
|
63
|
+
|
|
64
|
+
if (!template_enabled) {
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
for (const kustomization of template.spec.kustomizations) {
|
|
69
|
+
const node_id = create_node_id(template.metadata.name, kustomization.name);
|
|
70
|
+
const state = resolve_kustomization_state(kustomization, template_config, kustomization.name);
|
|
71
|
+
|
|
72
|
+
// Skip if this kustomization is disabled
|
|
73
|
+
if (!state.enabled) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// This kustomization is enabled - check its dependencies
|
|
78
|
+
for (const dep of kustomization.depends_on ?? []) {
|
|
79
|
+
// Skip raw dependencies - they're external to kustodian
|
|
80
|
+
if (typeof dep !== 'string') {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Parse dependency to get target node ID
|
|
85
|
+
const dep_parts = dep.split('/');
|
|
86
|
+
let target_node_id: string;
|
|
87
|
+
|
|
88
|
+
if (dep_parts.length === 2) {
|
|
89
|
+
// Cross-template reference: template/kustomization
|
|
90
|
+
target_node_id = dep;
|
|
91
|
+
} else {
|
|
92
|
+
// Within-template reference: kustomization
|
|
93
|
+
target_node_id = create_node_id(template.metadata.name, dep);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Check if the dependency is disabled
|
|
97
|
+
const dep_enabled = enablement_map.get(target_node_id);
|
|
98
|
+
if (dep_enabled === false) {
|
|
99
|
+
errors.push({
|
|
100
|
+
type: 'disabled_dependency',
|
|
101
|
+
source: node_id,
|
|
102
|
+
target: target_node_id,
|
|
103
|
+
message: `Enabled kustomization '${node_id}' depends on disabled kustomization '${target_node_id}'. Either enable '${target_node_id}' or disable '${node_id}'.`,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return errors;
|
|
111
|
+
}
|
package/src/validation/graph.ts
CHANGED
|
@@ -62,6 +62,11 @@ export function build_dependency_graph(templates: TemplateType[]): BuildGraphRes
|
|
|
62
62
|
// Resolve to full node ID
|
|
63
63
|
const target_id = resolve_dependency_ref(parse_result, template_name);
|
|
64
64
|
|
|
65
|
+
// Skip raw dependencies - they're external and don't participate in graph validation
|
|
66
|
+
if (target_id === null) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
65
70
|
// Check for self-reference
|
|
66
71
|
if (target_id === node_id) {
|
|
67
72
|
errors.push({
|
package/src/validation/index.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { type ResultType, failure, success } from '@kustodian/core';
|
|
2
2
|
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
-
import type { TemplateType } from '@kustodian/schema';
|
|
3
|
+
import type { ClusterType, TemplateType } from '@kustodian/schema';
|
|
4
4
|
|
|
5
5
|
import { detect_cycles } from './cycle-detection.js';
|
|
6
|
+
import { validate_enablement_dependencies } from './enablement.js';
|
|
6
7
|
import { build_dependency_graph } from './graph.js';
|
|
7
8
|
import type { GraphValidationResultType } from './types.js';
|
|
8
9
|
|
|
@@ -19,6 +20,11 @@ export type {
|
|
|
19
20
|
MissingReferenceErrorType,
|
|
20
21
|
SelfReferenceErrorType,
|
|
21
22
|
} from './types.js';
|
|
23
|
+
export type {
|
|
24
|
+
RequirementValidationErrorType,
|
|
25
|
+
RequirementValidationResultType,
|
|
26
|
+
} from './requirements.js';
|
|
27
|
+
export type { DisabledDependencyErrorType } from './enablement.js';
|
|
22
28
|
|
|
23
29
|
// Re-export functions
|
|
24
30
|
export { build_dependency_graph, get_all_nodes, get_node } from './graph.js';
|
|
@@ -30,6 +36,8 @@ export {
|
|
|
30
36
|
parse_node_id,
|
|
31
37
|
resolve_dependency_ref,
|
|
32
38
|
} from './reference.js';
|
|
39
|
+
export { validate_template_requirements } from './requirements.js';
|
|
40
|
+
export { validate_enablement_dependencies } from './enablement.js';
|
|
33
41
|
|
|
34
42
|
/**
|
|
35
43
|
* Validates the dependency graph for a set of templates.
|
|
@@ -72,16 +80,22 @@ export function validate_dependency_graph(templates: TemplateType[]): GraphValid
|
|
|
72
80
|
* Returns a success with the topological order, or a failure with
|
|
73
81
|
* detailed error information.
|
|
74
82
|
*
|
|
83
|
+
* @param cluster - Cluster configuration
|
|
75
84
|
* @param templates - Array of templates to validate
|
|
76
85
|
* @returns Result with topological order on success, or error on failure
|
|
77
86
|
*/
|
|
78
87
|
export function validate_dependencies(
|
|
88
|
+
cluster: ClusterType,
|
|
79
89
|
templates: TemplateType[],
|
|
80
90
|
): ResultType<string[], KustodianErrorType> {
|
|
81
91
|
const result = validate_dependency_graph(templates);
|
|
92
|
+
const enablement_errors = validate_enablement_dependencies(cluster, templates);
|
|
93
|
+
|
|
94
|
+
// Combine all errors
|
|
95
|
+
const all_errors = [...result.errors, ...enablement_errors];
|
|
82
96
|
|
|
83
|
-
if (
|
|
84
|
-
const error_messages =
|
|
97
|
+
if (all_errors.length > 0) {
|
|
98
|
+
const error_messages = all_errors.map((e) => e.message);
|
|
85
99
|
return failure({
|
|
86
100
|
code: 'DEPENDENCY_VALIDATION_ERROR',
|
|
87
101
|
message: `Dependency validation failed:\n${error_messages.map((m) => ` - ${m}`).join('\n')}`,
|
|
@@ -1,4 +1,45 @@
|
|
|
1
|
-
import type { DependencyRefType
|
|
1
|
+
import type { DependencyRefType as SchemaDependencyRefType } from '@kustodian/schema';
|
|
2
|
+
import type {
|
|
3
|
+
DependencyRefType,
|
|
4
|
+
InvalidReferenceErrorType,
|
|
5
|
+
ParsedDependencyRefType,
|
|
6
|
+
RawDependencyRefType,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Checks if a dependency reference is a raw object reference.
|
|
11
|
+
*/
|
|
12
|
+
function is_raw_object(
|
|
13
|
+
ref: SchemaDependencyRefType,
|
|
14
|
+
): ref is { raw: { name: string; namespace: string } } {
|
|
15
|
+
return typeof ref === 'object' && 'raw' in ref;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parses a dependency reference from the schema.
|
|
20
|
+
*
|
|
21
|
+
* Supports three formats:
|
|
22
|
+
* - Within-template: `kustomization-name` (e.g., `operator`)
|
|
23
|
+
* - Cross-template: `template-name/kustomization-name` (e.g., `001-secrets/doppler`)
|
|
24
|
+
* - Raw external: `{ raw: { name: 'legacy-infrastructure', namespace: 'gitops-system' } }`
|
|
25
|
+
*
|
|
26
|
+
* @param ref - The dependency reference from schema (string or raw object)
|
|
27
|
+
* @returns Parsed dependency reference or error
|
|
28
|
+
*/
|
|
29
|
+
export function parse_dependency_ref(
|
|
30
|
+
ref: SchemaDependencyRefType,
|
|
31
|
+
): DependencyRefType | InvalidReferenceErrorType {
|
|
32
|
+
// Handle raw object references
|
|
33
|
+
if (is_raw_object(ref)) {
|
|
34
|
+
return {
|
|
35
|
+
name: ref.raw.name,
|
|
36
|
+
namespace: ref.raw.namespace,
|
|
37
|
+
} satisfies RawDependencyRefType;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Handle string references
|
|
41
|
+
return parse_string_dependency_ref(ref);
|
|
42
|
+
}
|
|
2
43
|
|
|
3
44
|
/**
|
|
4
45
|
* Parses a dependency reference string.
|
|
@@ -10,7 +51,9 @@ import type { DependencyRefType, InvalidReferenceErrorType } from './types.js';
|
|
|
10
51
|
* @param ref - The raw dependency reference string
|
|
11
52
|
* @returns Parsed dependency reference or error
|
|
12
53
|
*/
|
|
13
|
-
|
|
54
|
+
function parse_string_dependency_ref(
|
|
55
|
+
ref: string,
|
|
56
|
+
): ParsedDependencyRefType | InvalidReferenceErrorType {
|
|
14
57
|
const trimmed = ref.trim();
|
|
15
58
|
|
|
16
59
|
if (trimmed.length === 0) {
|
|
@@ -90,9 +133,18 @@ export function is_parse_error(
|
|
|
90
133
|
*
|
|
91
134
|
* @param ref - Parsed dependency reference
|
|
92
135
|
* @param current_template - The template containing the reference
|
|
93
|
-
* @returns Full node ID in format `template/kustomization`
|
|
136
|
+
* @returns Full node ID in format `template/kustomization` for string refs, or null for raw refs
|
|
94
137
|
*/
|
|
95
|
-
export function resolve_dependency_ref(
|
|
138
|
+
export function resolve_dependency_ref(
|
|
139
|
+
ref: DependencyRefType,
|
|
140
|
+
current_template: string,
|
|
141
|
+
): string | null {
|
|
142
|
+
// Raw dependencies don't resolve to node IDs - they're external
|
|
143
|
+
if ('name' in ref && 'namespace' in ref) {
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// String-based dependency references
|
|
96
148
|
const template = ref.template ?? current_template;
|
|
97
149
|
return `${template}/${ref.kustomization}`;
|
|
98
150
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { NodeSchemaType, TemplateRequirementType, TemplateType } from '@kustodian/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Validation error for a template requirement.
|
|
5
|
+
*/
|
|
6
|
+
export interface RequirementValidationErrorType {
|
|
7
|
+
template: string;
|
|
8
|
+
requirement: TemplateRequirementType;
|
|
9
|
+
message: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Result of validating template requirements against cluster nodes.
|
|
14
|
+
*/
|
|
15
|
+
export interface RequirementValidationResultType {
|
|
16
|
+
valid: boolean;
|
|
17
|
+
errors: RequirementValidationErrorType[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Checks if a node label matches the requirement.
|
|
22
|
+
*/
|
|
23
|
+
function node_matches_label_requirement(
|
|
24
|
+
node: NodeSchemaType,
|
|
25
|
+
key: string,
|
|
26
|
+
value?: string,
|
|
27
|
+
): boolean {
|
|
28
|
+
if (!node.labels) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const label_value = node.labels[key];
|
|
33
|
+
|
|
34
|
+
// Label must exist
|
|
35
|
+
if (label_value === undefined) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// If no specific value required, just presence is enough
|
|
40
|
+
if (value === undefined) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Check if label value matches
|
|
45
|
+
// Node labels can be string, boolean, or number, so convert both to strings for comparison
|
|
46
|
+
return String(label_value) === value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Validates node label requirements for a template against cluster nodes.
|
|
51
|
+
*/
|
|
52
|
+
function validate_node_label_requirements(
|
|
53
|
+
template: TemplateType,
|
|
54
|
+
nodes: NodeSchemaType[],
|
|
55
|
+
): RequirementValidationErrorType[] {
|
|
56
|
+
const errors: RequirementValidationErrorType[] = [];
|
|
57
|
+
|
|
58
|
+
if (!template.spec.requirements) {
|
|
59
|
+
return errors;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const requirement of template.spec.requirements) {
|
|
63
|
+
if (requirement.type !== 'nodeLabel') {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const matching_nodes = nodes.filter((node) =>
|
|
68
|
+
node_matches_label_requirement(node, requirement.key, requirement.value),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (matching_nodes.length < requirement.atLeast) {
|
|
72
|
+
const value_part = requirement.value !== undefined ? `=${requirement.value}` : '';
|
|
73
|
+
const message = `Template requires at least ${requirement.atLeast} node(s) with label '${requirement.key}${value_part}', but found ${matching_nodes.length}`;
|
|
74
|
+
|
|
75
|
+
errors.push({
|
|
76
|
+
template: template.metadata.name,
|
|
77
|
+
requirement,
|
|
78
|
+
message,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return errors;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Validates all template requirements against cluster nodes.
|
|
88
|
+
*
|
|
89
|
+
* @param templates - Templates to validate (only enabled ones should be passed)
|
|
90
|
+
* @param nodes - Cluster nodes to validate against
|
|
91
|
+
* @returns Validation result with any requirement errors
|
|
92
|
+
*/
|
|
93
|
+
export function validate_template_requirements(
|
|
94
|
+
templates: TemplateType[],
|
|
95
|
+
nodes: NodeSchemaType[],
|
|
96
|
+
): RequirementValidationResultType {
|
|
97
|
+
const all_errors: RequirementValidationErrorType[] = [];
|
|
98
|
+
|
|
99
|
+
for (const template of templates) {
|
|
100
|
+
const errors = validate_node_label_requirements(template, nodes);
|
|
101
|
+
all_errors.push(...errors);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
valid: all_errors.length === 0,
|
|
106
|
+
errors: all_errors,
|
|
107
|
+
};
|
|
108
|
+
}
|
package/src/validation/types.ts
CHANGED
|
@@ -13,9 +13,9 @@ export interface GraphNodeType {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
/**
|
|
16
|
-
* Parsed dependency reference.
|
|
16
|
+
* Parsed dependency reference for string-based dependencies.
|
|
17
17
|
*/
|
|
18
|
-
export interface
|
|
18
|
+
export interface ParsedDependencyRefType {
|
|
19
19
|
/** Template name (undefined for within-template refs) */
|
|
20
20
|
readonly template?: string;
|
|
21
21
|
/** Kustomization name */
|
|
@@ -24,6 +24,35 @@ export interface DependencyRefType {
|
|
|
24
24
|
readonly raw: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Raw external dependency reference.
|
|
29
|
+
*/
|
|
30
|
+
export interface RawDependencyRefType {
|
|
31
|
+
/** Flux Kustomization name */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/** Flux Kustomization namespace */
|
|
34
|
+
readonly namespace: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Union type for all parsed dependency references.
|
|
39
|
+
*/
|
|
40
|
+
export type DependencyRefType = ParsedDependencyRefType | RawDependencyRefType;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Type guard to check if a dependency reference is a raw reference.
|
|
44
|
+
*/
|
|
45
|
+
export function is_raw_dependency_ref(ref: DependencyRefType): ref is RawDependencyRefType {
|
|
46
|
+
return 'name' in ref && 'namespace' in ref;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Type guard to check if a dependency reference is a parsed string reference.
|
|
51
|
+
*/
|
|
52
|
+
export function is_parsed_dependency_ref(ref: DependencyRefType): ref is ParsedDependencyRefType {
|
|
53
|
+
return 'kustomization' in ref;
|
|
54
|
+
}
|
|
55
|
+
|
|
27
56
|
/**
|
|
28
57
|
* Cycle error - circular dependency detected.
|
|
29
58
|
*/
|