@kustodian/generator 1.0.1 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kustodian/generator",
3
- "version": "1.0.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": "workspace:*",
39
- "@kustodian/loader": "workspace:*",
40
- "@kustodian/plugins": "workspace:*",
41
- "@kustodian/schema": "workspace:*",
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 both within-template and cross-template references:
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: string[] | undefined,
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
- return { name: generate_flux_name(template_name, dep) };
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
  });
@@ -80,13 +100,49 @@ export function generate_health_checks(
80
100
  }
81
101
 
82
102
  return kustomization.health_checks.map((check) => ({
83
- apiVersion: 'apps/v1',
103
+ apiVersion: check.api_version ?? 'apps/v1',
84
104
  kind: check.kind,
85
105
  name: check.name,
86
106
  namespace: check.namespace ?? namespace,
87
107
  }));
88
108
  }
89
109
 
110
+ /**
111
+ * Generates custom health checks with CEL expressions for a Flux Kustomization.
112
+ */
113
+ export function generate_custom_health_checks(
114
+ kustomization: KustomizationType,
115
+ namespace: string,
116
+ ): FluxKustomizationType['spec']['customHealthChecks'] {
117
+ if (!kustomization.health_check_exprs || kustomization.health_check_exprs.length === 0) {
118
+ return undefined;
119
+ }
120
+
121
+ return kustomization.health_check_exprs.map((check) => {
122
+ const healthCheck: {
123
+ apiVersion: string;
124
+ kind: string;
125
+ namespace?: string;
126
+ current?: string;
127
+ failed?: string;
128
+ } = {
129
+ apiVersion: check.api_version,
130
+ kind: check.kind,
131
+ namespace: check.namespace ?? namespace,
132
+ };
133
+
134
+ if (check.current !== undefined) {
135
+ healthCheck.current = check.current;
136
+ }
137
+
138
+ if (check.failed !== undefined) {
139
+ healthCheck.failed = check.failed;
140
+ }
141
+
142
+ return healthCheck;
143
+ });
144
+ }
145
+
90
146
  /**
91
147
  * Generates a Flux OCIRepository resource.
92
148
  */
@@ -143,6 +199,7 @@ export function generate_flux_kustomization(
143
199
  resolved: ResolvedKustomizationType,
144
200
  source_repository_name = 'flux-system',
145
201
  source_kind: 'GitRepository' | 'OCIRepository' = 'GitRepository',
202
+ preservation?: PreservationPolicyType,
146
203
  ): FluxKustomizationType {
147
204
  const { template, kustomization, values, namespace } = resolved;
148
205
  const name = generate_flux_name(template.metadata.name, kustomization.name);
@@ -160,6 +217,15 @@ export function generate_flux_kustomization(
160
217
  },
161
218
  };
162
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
+
163
229
  // Add timeout if specified
164
230
  if (kustomization.timeout) {
165
231
  spec.timeout = kustomization.timeout;
@@ -191,6 +257,12 @@ export function generate_flux_kustomization(
191
257
  spec.healthChecks = health_checks;
192
258
  }
193
259
 
260
+ // Add custom health checks with CEL expressions
261
+ const custom_health_checks = generate_custom_health_checks(kustomization, namespace);
262
+ if (custom_health_checks) {
263
+ spec.customHealthChecks = custom_health_checks;
264
+ }
265
+
194
266
  return {
195
267
  apiVersion: 'kustomize.toolkit.fluxcd.io/v1',
196
268
  kind: 'Kustomization',
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
  */
@@ -116,5 +135,15 @@ export interface FluxKustomizationType {
116
135
  name: string;
117
136
  namespace: string;
118
137
  }>;
138
+ customHealthChecks?: Array<{
139
+ apiVersion: string;
140
+ kind: string;
141
+ namespace?: string;
142
+ /** CEL expression for when resource is healthy/current */
143
+ current?: string;
144
+ /** CEL expression for when resource has failed */
145
+ failed?: string;
146
+ }>;
147
+ patches?: FluxPatchType[];
119
148
  };
120
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
+ }
@@ -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({
@@ -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 (!result.valid) {
84
- const error_messages = result.errors.map((e) => e.message);
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, InvalidReferenceErrorType } from './types.js';
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
- export function parse_dependency_ref(ref: string): DependencyRefType | InvalidReferenceErrorType {
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(ref: DependencyRefType, current_template: string): string {
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
+ }
@@ -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 DependencyRefType {
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
  */