@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
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kustodian/generator",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Template processing and Flux generation for Kustodian",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.ts",
|
|
7
|
+
"types": "./src/index.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./src/index.ts",
|
|
11
|
+
"import": "./src/index.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"src"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "bun test",
|
|
19
|
+
"test:watch": "bun test --watch",
|
|
20
|
+
"typecheck": "bun run tsc --noEmit"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"kustodian",
|
|
24
|
+
"generator",
|
|
25
|
+
"flux"
|
|
26
|
+
],
|
|
27
|
+
"author": "Luca Silverentand <luca@onezero.company>",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "https://github.com/lucasilverentand/kustodian.git",
|
|
32
|
+
"directory": "packages/generator"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"registry": "https://npm.pkg.github.com"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@kustodian/core": "workspace:*",
|
|
39
|
+
"@kustodian/loader": "workspace:*",
|
|
40
|
+
"@kustodian/plugins": "workspace:*",
|
|
41
|
+
"@kustodian/schema": "workspace:*",
|
|
42
|
+
"yaml": "^2.8.2"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {}
|
|
45
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
DopplerSubstitutionType,
|
|
3
|
+
OnePasswordSubstitutionType,
|
|
4
|
+
SubstitutionType,
|
|
5
|
+
TemplateType,
|
|
6
|
+
} from '@kustodian/schema';
|
|
7
|
+
import {
|
|
8
|
+
is_doppler_substitution,
|
|
9
|
+
is_onepassword_substitution,
|
|
10
|
+
} from '@kustodian/schema';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts 1Password substitutions from templates.
|
|
14
|
+
*/
|
|
15
|
+
export function extract_onepassword_substitutions(
|
|
16
|
+
templates: TemplateType[],
|
|
17
|
+
): OnePasswordSubstitutionType[] {
|
|
18
|
+
const substitutions: OnePasswordSubstitutionType[] = [];
|
|
19
|
+
|
|
20
|
+
for (const template of templates) {
|
|
21
|
+
for (const kustomization of template.spec.kustomizations) {
|
|
22
|
+
for (const sub of kustomization.substitutions ?? []) {
|
|
23
|
+
if (is_onepassword_substitution(sub)) {
|
|
24
|
+
substitutions.push(sub);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return substitutions;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Extracts Doppler substitutions from templates.
|
|
35
|
+
*/
|
|
36
|
+
export function extract_doppler_substitutions(
|
|
37
|
+
templates: TemplateType[],
|
|
38
|
+
): DopplerSubstitutionType[] {
|
|
39
|
+
const substitutions: DopplerSubstitutionType[] = [];
|
|
40
|
+
|
|
41
|
+
for (const template of templates) {
|
|
42
|
+
for (const kustomization of template.spec.kustomizations) {
|
|
43
|
+
for (const sub of kustomization.substitutions ?? []) {
|
|
44
|
+
if (is_doppler_substitution(sub)) {
|
|
45
|
+
substitutions.push(sub);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return substitutions;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Extracts all external substitutions (1Password and Doppler) from templates.
|
|
56
|
+
*/
|
|
57
|
+
export function extract_external_substitutions(
|
|
58
|
+
templates: TemplateType[],
|
|
59
|
+
): { onepassword: OnePasswordSubstitutionType[]; doppler: DopplerSubstitutionType[] } {
|
|
60
|
+
return {
|
|
61
|
+
onepassword: extract_onepassword_substitutions(templates),
|
|
62
|
+
doppler: extract_doppler_substitutions(templates),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Checks if a substitution requires external resolution.
|
|
68
|
+
*/
|
|
69
|
+
export function is_external_substitution(sub: SubstitutionType): boolean {
|
|
70
|
+
return is_onepassword_substitution(sub) || is_doppler_substitution(sub);
|
|
71
|
+
}
|
package/src/flux.ts
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ClusterType,
|
|
3
|
+
KustomizationType,
|
|
4
|
+
OciConfigType,
|
|
5
|
+
TemplateType,
|
|
6
|
+
} from '@kustodian/schema';
|
|
7
|
+
|
|
8
|
+
import type {
|
|
9
|
+
FluxKustomizationType,
|
|
10
|
+
FluxOCIRepositoryType,
|
|
11
|
+
ResolvedKustomizationType,
|
|
12
|
+
} from './types.js';
|
|
13
|
+
import { is_parse_error, parse_dependency_ref } from './validation/reference.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Default interval for Flux reconciliation.
|
|
17
|
+
*/
|
|
18
|
+
export const DEFAULT_INTERVAL = '10m';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Default timeout for Flux reconciliation.
|
|
22
|
+
*/
|
|
23
|
+
export const DEFAULT_TIMEOUT = '5m';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Generates a Flux Kustomization name from template and kustomization.
|
|
27
|
+
*/
|
|
28
|
+
export function generate_flux_name(template_name: string, kustomization_name: string): string {
|
|
29
|
+
return `${template_name}-${kustomization_name}`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Generates the path for a Flux Kustomization.
|
|
34
|
+
*/
|
|
35
|
+
export function generate_flux_path(
|
|
36
|
+
template_name: string,
|
|
37
|
+
kustomization_path: string,
|
|
38
|
+
base_path = './templates',
|
|
39
|
+
): string {
|
|
40
|
+
// Normalize the path
|
|
41
|
+
const normalized = kustomization_path.replace(/^\.\//, '');
|
|
42
|
+
return `${base_path}/${template_name}/${normalized}`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Generates the dependency references for a Flux Kustomization.
|
|
47
|
+
*
|
|
48
|
+
* Supports both within-template and cross-template references:
|
|
49
|
+
* - Within-template: `database` → uses current template name
|
|
50
|
+
* - Cross-template: `secrets/doppler` → uses explicit template name
|
|
51
|
+
*/
|
|
52
|
+
export function generate_depends_on(
|
|
53
|
+
template_name: string,
|
|
54
|
+
depends_on: string[] | undefined,
|
|
55
|
+
): Array<{ name: string }> | undefined {
|
|
56
|
+
if (!depends_on || depends_on.length === 0) {
|
|
57
|
+
return undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return depends_on.map((dep) => {
|
|
61
|
+
const parsed = parse_dependency_ref(dep);
|
|
62
|
+
if (is_parse_error(parsed)) {
|
|
63
|
+
// Invalid references are caught during validation, fall back to current behavior
|
|
64
|
+
return { name: generate_flux_name(template_name, dep) };
|
|
65
|
+
}
|
|
66
|
+
const effective_template = parsed.template ?? template_name;
|
|
67
|
+
return { name: generate_flux_name(effective_template, parsed.kustomization) };
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Generates health checks for a Flux Kustomization.
|
|
73
|
+
*/
|
|
74
|
+
export function generate_health_checks(
|
|
75
|
+
kustomization: KustomizationType,
|
|
76
|
+
namespace: string,
|
|
77
|
+
): FluxKustomizationType['spec']['healthChecks'] {
|
|
78
|
+
if (!kustomization.health_checks || kustomization.health_checks.length === 0) {
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return kustomization.health_checks.map((check) => ({
|
|
83
|
+
apiVersion: 'apps/v1',
|
|
84
|
+
kind: check.kind,
|
|
85
|
+
name: check.name,
|
|
86
|
+
namespace: check.namespace ?? namespace,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Generates a Flux OCIRepository resource.
|
|
92
|
+
*/
|
|
93
|
+
export function generate_flux_oci_repository(
|
|
94
|
+
cluster: ClusterType,
|
|
95
|
+
oci_config: OciConfigType,
|
|
96
|
+
repository_name: string,
|
|
97
|
+
flux_namespace: string,
|
|
98
|
+
): FluxOCIRepositoryType {
|
|
99
|
+
const url = `oci://${oci_config.registry}/${oci_config.repository}`;
|
|
100
|
+
|
|
101
|
+
const ref: FluxOCIRepositoryType['spec']['ref'] = {};
|
|
102
|
+
switch (oci_config.tag_strategy) {
|
|
103
|
+
case 'cluster':
|
|
104
|
+
ref.tag = cluster.metadata.name;
|
|
105
|
+
break;
|
|
106
|
+
case 'manual':
|
|
107
|
+
ref.tag = oci_config.tag || 'latest';
|
|
108
|
+
break;
|
|
109
|
+
default:
|
|
110
|
+
ref.tag = 'latest'; // CI will update this
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const spec: FluxOCIRepositoryType['spec'] = {
|
|
114
|
+
interval: DEFAULT_INTERVAL,
|
|
115
|
+
url,
|
|
116
|
+
ref,
|
|
117
|
+
provider: oci_config.provider || 'generic',
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
if (oci_config.secret_ref) {
|
|
121
|
+
spec.secretRef = { name: oci_config.secret_ref };
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (oci_config.insecure) {
|
|
125
|
+
spec.insecure = true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
apiVersion: 'source.toolkit.fluxcd.io/v1',
|
|
130
|
+
kind: 'OCIRepository',
|
|
131
|
+
metadata: {
|
|
132
|
+
name: repository_name,
|
|
133
|
+
namespace: flux_namespace,
|
|
134
|
+
},
|
|
135
|
+
spec,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Generates a Flux Kustomization resource.
|
|
141
|
+
*/
|
|
142
|
+
export function generate_flux_kustomization(
|
|
143
|
+
resolved: ResolvedKustomizationType,
|
|
144
|
+
source_repository_name = 'flux-system',
|
|
145
|
+
source_kind: 'GitRepository' | 'OCIRepository' = 'GitRepository',
|
|
146
|
+
): FluxKustomizationType {
|
|
147
|
+
const { template, kustomization, values, namespace } = resolved;
|
|
148
|
+
const name = generate_flux_name(template.metadata.name, kustomization.name);
|
|
149
|
+
const path = generate_flux_path(template.metadata.name, kustomization.path);
|
|
150
|
+
|
|
151
|
+
const spec: FluxKustomizationType['spec'] = {
|
|
152
|
+
interval: DEFAULT_INTERVAL,
|
|
153
|
+
targetNamespace: namespace,
|
|
154
|
+
path,
|
|
155
|
+
prune: kustomization.prune ?? true,
|
|
156
|
+
wait: kustomization.wait ?? true,
|
|
157
|
+
sourceRef: {
|
|
158
|
+
kind: source_kind,
|
|
159
|
+
name: source_repository_name,
|
|
160
|
+
},
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// Add timeout if specified
|
|
164
|
+
if (kustomization.timeout) {
|
|
165
|
+
spec.timeout = kustomization.timeout;
|
|
166
|
+
} else {
|
|
167
|
+
spec.timeout = DEFAULT_TIMEOUT;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Add retry interval if specified
|
|
171
|
+
if (kustomization.retry_interval) {
|
|
172
|
+
spec.retryInterval = kustomization.retry_interval;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// Add dependencies
|
|
176
|
+
const depends_on = generate_depends_on(template.metadata.name, kustomization.depends_on);
|
|
177
|
+
if (depends_on) {
|
|
178
|
+
spec.dependsOn = depends_on;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Add substitutions if there are values
|
|
182
|
+
if (Object.keys(values).length > 0) {
|
|
183
|
+
spec.postBuild = {
|
|
184
|
+
substitute: values,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Add health checks
|
|
189
|
+
const health_checks = generate_health_checks(kustomization, namespace);
|
|
190
|
+
if (health_checks) {
|
|
191
|
+
spec.healthChecks = health_checks;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
apiVersion: 'kustomize.toolkit.fluxcd.io/v1',
|
|
196
|
+
kind: 'Kustomization',
|
|
197
|
+
metadata: {
|
|
198
|
+
name,
|
|
199
|
+
namespace: 'flux-system',
|
|
200
|
+
},
|
|
201
|
+
spec,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Resolves a kustomization with values from the cluster config.
|
|
207
|
+
*/
|
|
208
|
+
export function resolve_kustomization(
|
|
209
|
+
template: TemplateType,
|
|
210
|
+
kustomization: KustomizationType,
|
|
211
|
+
cluster_values: Record<string, string> = {},
|
|
212
|
+
): ResolvedKustomizationType {
|
|
213
|
+
// Collect values with defaults
|
|
214
|
+
const values: Record<string, string> = {};
|
|
215
|
+
|
|
216
|
+
for (const sub of kustomization.substitutions ?? []) {
|
|
217
|
+
// Check cluster-provided value first, then fall back to default
|
|
218
|
+
const value = cluster_values[sub.name] ?? sub.default;
|
|
219
|
+
if (value !== undefined) {
|
|
220
|
+
values[sub.name] = value;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Determine namespace
|
|
225
|
+
const namespace = kustomization.namespace?.default ?? 'default';
|
|
226
|
+
|
|
227
|
+
return {
|
|
228
|
+
template,
|
|
229
|
+
kustomization,
|
|
230
|
+
values,
|
|
231
|
+
namespace,
|
|
232
|
+
};
|
|
233
|
+
}
|
package/src/generator.ts
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import { type ResultType, success } from '@kustodian/core';
|
|
2
|
+
import type { KustodianErrorType } from '@kustodian/core';
|
|
3
|
+
import type {
|
|
4
|
+
GeneratedResourceType,
|
|
5
|
+
LegacyPluginRegistryType,
|
|
6
|
+
PluginContextType,
|
|
7
|
+
} from '@kustodian/plugins';
|
|
8
|
+
import type { ClusterType, TemplateConfigType, TemplateType } from '@kustodian/schema';
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
DEFAULT_INTERVAL,
|
|
12
|
+
DEFAULT_TIMEOUT,
|
|
13
|
+
generate_depends_on,
|
|
14
|
+
generate_flux_kustomization,
|
|
15
|
+
generate_flux_name,
|
|
16
|
+
generate_flux_oci_repository,
|
|
17
|
+
generate_health_checks,
|
|
18
|
+
resolve_kustomization,
|
|
19
|
+
} from './flux.js';
|
|
20
|
+
import { generate_namespace_resources } from './namespace.js';
|
|
21
|
+
import { serialize_resource, serialize_resources, write_generation_result } from './output.js';
|
|
22
|
+
import type {
|
|
23
|
+
FluxKustomizationType,
|
|
24
|
+
GenerateOptionsType,
|
|
25
|
+
GeneratedKustomizationType,
|
|
26
|
+
GenerationResultType,
|
|
27
|
+
ResolvedKustomizationType,
|
|
28
|
+
ResolvedTemplateType,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
import { validate_dependencies } from './validation/index.js';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Options for the Generator.
|
|
34
|
+
*/
|
|
35
|
+
export interface GeneratorOptionsType {
|
|
36
|
+
flux_namespace?: string;
|
|
37
|
+
git_repository_name?: string;
|
|
38
|
+
base_path?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generator hook phases.
|
|
43
|
+
*/
|
|
44
|
+
export type GeneratorHookPhaseType =
|
|
45
|
+
| 'before_generate'
|
|
46
|
+
| 'after_resolve_template'
|
|
47
|
+
| 'after_generate_kustomization'
|
|
48
|
+
| 'after_generate';
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Hook handler function type.
|
|
52
|
+
*/
|
|
53
|
+
export type GeneratorHookHandlerType = (
|
|
54
|
+
phase: GeneratorHookPhaseType,
|
|
55
|
+
context: GeneratorHookContextType,
|
|
56
|
+
) => void | Promise<void>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Context passed to generator hooks.
|
|
60
|
+
*/
|
|
61
|
+
export interface GeneratorHookContextType {
|
|
62
|
+
cluster: ClusterType;
|
|
63
|
+
templates?: ResolvedTemplateType[];
|
|
64
|
+
kustomization?: ResolvedKustomizationType;
|
|
65
|
+
flux_kustomization?: FluxKustomizationType;
|
|
66
|
+
result?: GenerationResultType;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Generator class for processing templates into Flux resources.
|
|
71
|
+
*/
|
|
72
|
+
export interface GeneratorType {
|
|
73
|
+
/**
|
|
74
|
+
* Registers a hook handler.
|
|
75
|
+
*/
|
|
76
|
+
on_hook(handler: GeneratorHookHandlerType): void;
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Resolves templates for a cluster.
|
|
80
|
+
*/
|
|
81
|
+
resolve_templates(cluster: ClusterType, templates: TemplateType[]): ResolvedTemplateType[];
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Generates Flux resources for a cluster.
|
|
85
|
+
*/
|
|
86
|
+
generate(
|
|
87
|
+
cluster: ClusterType,
|
|
88
|
+
templates: TemplateType[],
|
|
89
|
+
options?: GenerateOptionsType,
|
|
90
|
+
): Promise<ResultType<GenerationResultType, KustodianErrorType>>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Generates resources from plugins.
|
|
94
|
+
*/
|
|
95
|
+
generate_plugin_resources(
|
|
96
|
+
cluster: ClusterType,
|
|
97
|
+
templates: ResolvedTemplateType[],
|
|
98
|
+
): ResultType<GeneratedResourceType[], KustodianErrorType>;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Writes generation result to disk.
|
|
102
|
+
*/
|
|
103
|
+
write(result: GenerationResultType): Promise<ResultType<string[], KustodianErrorType>>;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Creates a new Generator instance.
|
|
108
|
+
*/
|
|
109
|
+
export function create_generator(
|
|
110
|
+
options: GeneratorOptionsType = {},
|
|
111
|
+
registry?: LegacyPluginRegistryType,
|
|
112
|
+
): GeneratorType {
|
|
113
|
+
const flux_namespace = options.flux_namespace ?? 'flux-system';
|
|
114
|
+
const git_repository_name = options.git_repository_name ?? 'flux-system';
|
|
115
|
+
// base_path is available for future template path customization
|
|
116
|
+
const _base_path = options.base_path ?? './templates';
|
|
117
|
+
void _base_path; // Suppress unused variable warning
|
|
118
|
+
|
|
119
|
+
const hooks: GeneratorHookHandlerType[] = [];
|
|
120
|
+
|
|
121
|
+
async function run_hooks(
|
|
122
|
+
phase: GeneratorHookPhaseType,
|
|
123
|
+
context: GeneratorHookContextType,
|
|
124
|
+
): Promise<void> {
|
|
125
|
+
for (const handler of hooks) {
|
|
126
|
+
await handler(phase, context);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function get_template_values(
|
|
131
|
+
cluster: ClusterType,
|
|
132
|
+
template_name: string,
|
|
133
|
+
): Record<string, string> {
|
|
134
|
+
const template_config = cluster.spec.templates?.find(
|
|
135
|
+
(t: TemplateConfigType) => t.name === template_name,
|
|
136
|
+
);
|
|
137
|
+
return template_config?.values ?? {};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function is_template_enabled(cluster: ClusterType, template_name: string): boolean {
|
|
141
|
+
const template_config = cluster.spec.templates?.find(
|
|
142
|
+
(t: TemplateConfigType) => t.name === template_name,
|
|
143
|
+
);
|
|
144
|
+
// Default to enabled if not specified
|
|
145
|
+
return template_config?.enabled ?? true;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return {
|
|
149
|
+
on_hook(handler) {
|
|
150
|
+
hooks.push(handler);
|
|
151
|
+
},
|
|
152
|
+
|
|
153
|
+
resolve_templates(cluster, templates) {
|
|
154
|
+
return templates.map((template) => {
|
|
155
|
+
const values = get_template_values(cluster, template.metadata.name);
|
|
156
|
+
const enabled = is_template_enabled(cluster, template.metadata.name);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
template,
|
|
160
|
+
values,
|
|
161
|
+
enabled,
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
async generate(cluster, templates, generate_options = {}) {
|
|
167
|
+
const output_dir = generate_options.output_dir ?? './output';
|
|
168
|
+
const skip_validation = generate_options.skip_validation ?? false;
|
|
169
|
+
|
|
170
|
+
// Validate dependency graph before generation (unless skipped)
|
|
171
|
+
if (!skip_validation) {
|
|
172
|
+
const validation_result = validate_dependencies(templates);
|
|
173
|
+
if (!validation_result.success) {
|
|
174
|
+
return validation_result;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Detect source kind and repository name
|
|
179
|
+
const source_kind = cluster.spec.oci ? 'OCIRepository' : 'GitRepository';
|
|
180
|
+
const source_repository_name = git_repository_name;
|
|
181
|
+
|
|
182
|
+
// Run before_generate hook
|
|
183
|
+
await run_hooks('before_generate', { cluster });
|
|
184
|
+
|
|
185
|
+
// Resolve templates
|
|
186
|
+
const resolved_templates = this.resolve_templates(cluster, templates);
|
|
187
|
+
|
|
188
|
+
// Run after_resolve_template hook for each
|
|
189
|
+
for (const resolved of resolved_templates) {
|
|
190
|
+
await run_hooks('after_resolve_template', {
|
|
191
|
+
cluster,
|
|
192
|
+
templates: [resolved],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// Generate kustomizations
|
|
197
|
+
const generated_kustomizations: GeneratedKustomizationType[] = [];
|
|
198
|
+
|
|
199
|
+
for (const resolved of resolved_templates) {
|
|
200
|
+
if (!resolved.enabled) {
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const kustomization of resolved.template.spec.kustomizations) {
|
|
205
|
+
const resolved_kustomization = resolve_kustomization(
|
|
206
|
+
resolved.template,
|
|
207
|
+
kustomization,
|
|
208
|
+
resolved.values,
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
// Generate Flux resource with configurable namespace
|
|
212
|
+
const flux_kustomization = generate_flux_kustomization(
|
|
213
|
+
resolved_kustomization,
|
|
214
|
+
source_repository_name,
|
|
215
|
+
source_kind,
|
|
216
|
+
);
|
|
217
|
+
|
|
218
|
+
// Override namespace to configured value
|
|
219
|
+
flux_kustomization.metadata.namespace = flux_namespace;
|
|
220
|
+
|
|
221
|
+
// Run after_generate_kustomization hook
|
|
222
|
+
await run_hooks('after_generate_kustomization', {
|
|
223
|
+
cluster,
|
|
224
|
+
kustomization: resolved_kustomization,
|
|
225
|
+
flux_kustomization,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
generated_kustomizations.push({
|
|
229
|
+
name: flux_kustomization.metadata.name,
|
|
230
|
+
template: resolved.template.metadata.name,
|
|
231
|
+
path: flux_kustomization.spec.path,
|
|
232
|
+
flux_kustomization,
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const result: GenerationResultType = {
|
|
238
|
+
cluster: cluster.metadata.name,
|
|
239
|
+
output_dir,
|
|
240
|
+
kustomizations: generated_kustomizations,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// Generate OCIRepository if using OCI mode
|
|
244
|
+
if (cluster.spec.oci) {
|
|
245
|
+
result.oci_repository = generate_flux_oci_repository(
|
|
246
|
+
cluster,
|
|
247
|
+
cluster.spec.oci,
|
|
248
|
+
source_repository_name,
|
|
249
|
+
flux_namespace,
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Run after_generate hook
|
|
254
|
+
await run_hooks('after_generate', {
|
|
255
|
+
cluster,
|
|
256
|
+
templates: resolved_templates,
|
|
257
|
+
result,
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
return success(result);
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
generate_plugin_resources(cluster, templates) {
|
|
264
|
+
if (!registry) {
|
|
265
|
+
return success([]);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const all_resources: GeneratedResourceType[] = [];
|
|
269
|
+
|
|
270
|
+
// Generate resources from plugins
|
|
271
|
+
for (const generator of registry.get_resource_generators()) {
|
|
272
|
+
for (const resolved of templates) {
|
|
273
|
+
if (!resolved.enabled) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const ctx: PluginContextType = {
|
|
278
|
+
cluster,
|
|
279
|
+
template: resolved.template,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const result = generator.generate(ctx);
|
|
283
|
+
if (!result.success) {
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
all_resources.push(...result.value);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return success(all_resources);
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
async write(result) {
|
|
295
|
+
return write_generation_result(result);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Re-export commonly used utilities
|
|
301
|
+
export { serialize_resource, serialize_resources };
|
|
302
|
+
export { generate_namespace_resources };
|
|
303
|
+
export {
|
|
304
|
+
DEFAULT_INTERVAL,
|
|
305
|
+
DEFAULT_TIMEOUT,
|
|
306
|
+
generate_depends_on,
|
|
307
|
+
generate_flux_kustomization,
|
|
308
|
+
generate_flux_name,
|
|
309
|
+
generate_health_checks,
|
|
310
|
+
resolve_kustomization,
|
|
311
|
+
};
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './types.js';
|
|
2
|
+
export * from './flux.js';
|
|
3
|
+
export * from './substitution.js';
|
|
4
|
+
export * from './namespace.js';
|
|
5
|
+
export * from './output.js';
|
|
6
|
+
export * from './generator.js';
|
|
7
|
+
export * from './validation/index.js';
|
|
8
|
+
export * from './external-substitutions.js';
|