@intentius/chant-lexicon-helm 0.0.16
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/README.md +22 -0
- package/dist/integrity.json +36 -0
- package/dist/manifest.json +37 -0
- package/dist/meta.json +208 -0
- package/dist/rules/chart-metadata.ts +64 -0
- package/dist/rules/helm-helpers.ts +64 -0
- package/dist/rules/no-hardcoded-image.ts +62 -0
- package/dist/rules/values-no-secrets.ts +82 -0
- package/dist/rules/whm101.ts +46 -0
- package/dist/rules/whm102.ts +33 -0
- package/dist/rules/whm103.ts +59 -0
- package/dist/rules/whm104.ts +35 -0
- package/dist/rules/whm105.ts +30 -0
- package/dist/rules/whm201.ts +36 -0
- package/dist/rules/whm202.ts +50 -0
- package/dist/rules/whm203.ts +39 -0
- package/dist/rules/whm204.ts +60 -0
- package/dist/rules/whm301.ts +41 -0
- package/dist/rules/whm302.ts +40 -0
- package/dist/rules/whm401.ts +57 -0
- package/dist/rules/whm402.ts +45 -0
- package/dist/rules/whm403.ts +45 -0
- package/dist/rules/whm404.ts +36 -0
- package/dist/rules/whm405.ts +53 -0
- package/dist/rules/whm406.ts +34 -0
- package/dist/rules/whm407.ts +83 -0
- package/dist/rules/whm501.ts +103 -0
- package/dist/rules/whm502.ts +94 -0
- package/dist/skills/chant-helm-chart-patterns.md +229 -0
- package/dist/skills/chant-helm-chart-security-patterns.md +192 -0
- package/dist/skills/chant-helm-create-chart.md +211 -0
- package/dist/types/index.d.ts +132 -0
- package/package.json +34 -0
- package/src/codegen/docs-cli.ts +4 -0
- package/src/codegen/docs.ts +483 -0
- package/src/codegen/generate-cli.ts +28 -0
- package/src/codegen/generate.ts +249 -0
- package/src/codegen/naming.ts +38 -0
- package/src/codegen/package.ts +64 -0
- package/src/composites/composites.test.ts +1050 -0
- package/src/composites/helm-batch-job.ts +209 -0
- package/src/composites/helm-crd-lifecycle.ts +184 -0
- package/src/composites/helm-cron-job.ts +177 -0
- package/src/composites/helm-daemon-set.ts +169 -0
- package/src/composites/helm-external-secret.ts +93 -0
- package/src/composites/helm-library.ts +51 -0
- package/src/composites/helm-microservice.ts +331 -0
- package/src/composites/helm-monitored-service.ts +252 -0
- package/src/composites/helm-namespace-env.ts +154 -0
- package/src/composites/helm-secure-ingress.ts +114 -0
- package/src/composites/helm-stateful-service.ts +213 -0
- package/src/composites/helm-web-app.ts +264 -0
- package/src/composites/helm-worker.ts +207 -0
- package/src/composites/index.ts +38 -0
- package/src/coverage.test.ts +21 -0
- package/src/coverage.ts +50 -0
- package/src/generated/index.d.ts +132 -0
- package/src/generated/index.ts +13 -0
- package/src/generated/lexicon-helm.json +208 -0
- package/src/helpers.test.ts +51 -0
- package/src/helpers.ts +100 -0
- package/src/import/generator.ts +285 -0
- package/src/import/import.test.ts +224 -0
- package/src/import/parser.ts +160 -0
- package/src/import/template-stripper.ts +123 -0
- package/src/index.ts +108 -0
- package/src/intrinsics.test.ts +380 -0
- package/src/intrinsics.ts +484 -0
- package/src/lint/post-synth/helm-helpers.ts +64 -0
- package/src/lint/post-synth/post-synth.test.ts +504 -0
- package/src/lint/post-synth/whm101.ts +46 -0
- package/src/lint/post-synth/whm102.ts +33 -0
- package/src/lint/post-synth/whm103.ts +59 -0
- package/src/lint/post-synth/whm104.ts +35 -0
- package/src/lint/post-synth/whm105.ts +30 -0
- package/src/lint/post-synth/whm201.ts +36 -0
- package/src/lint/post-synth/whm202.ts +50 -0
- package/src/lint/post-synth/whm203.ts +39 -0
- package/src/lint/post-synth/whm204.ts +60 -0
- package/src/lint/post-synth/whm301.ts +41 -0
- package/src/lint/post-synth/whm302.ts +40 -0
- package/src/lint/post-synth/whm401.ts +57 -0
- package/src/lint/post-synth/whm402.ts +45 -0
- package/src/lint/post-synth/whm403.ts +45 -0
- package/src/lint/post-synth/whm404.ts +36 -0
- package/src/lint/post-synth/whm405.ts +53 -0
- package/src/lint/post-synth/whm406.ts +34 -0
- package/src/lint/post-synth/whm407.ts +83 -0
- package/src/lint/post-synth/whm501.ts +103 -0
- package/src/lint/post-synth/whm502.ts +94 -0
- package/src/lint/rules/chart-metadata.ts +64 -0
- package/src/lint/rules/lint-rules.test.ts +97 -0
- package/src/lint/rules/no-hardcoded-image.ts +62 -0
- package/src/lint/rules/values-no-secrets.ts +82 -0
- package/src/lsp/completions.test.ts +72 -0
- package/src/lsp/completions.ts +20 -0
- package/src/lsp/hover.test.ts +46 -0
- package/src/lsp/hover.ts +46 -0
- package/src/package-cli.ts +28 -0
- package/src/plugin.test.ts +71 -0
- package/src/plugin.ts +206 -0
- package/src/resources.ts +77 -0
- package/src/serializer.test.ts +930 -0
- package/src/serializer.ts +835 -0
- package/src/skills/chart-patterns.md +229 -0
- package/src/skills/chart-security-patterns.md +192 -0
- package/src/skills/create-chart.md +211 -0
- package/src/validate-cli.ts +21 -0
- package/src/validate.test.ts +37 -0
- package/src/validate.ts +36 -0
|
@@ -0,0 +1,835 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helm chart serializer.
|
|
3
|
+
*
|
|
4
|
+
* Converts Chant declarables into a complete Helm chart directory structure
|
|
5
|
+
* returned as a SerializerResult with a files map:
|
|
6
|
+
*
|
|
7
|
+
* Chart.yaml, values.yaml, values.schema.json, .helmignore,
|
|
8
|
+
* templates/_helpers.tpl, templates/<resource>.yaml, templates/NOTES.txt,
|
|
9
|
+
* templates/tests/test-connection.yaml
|
|
10
|
+
*
|
|
11
|
+
* The serializer detects `__helm_tpl` markers in walked values and emits
|
|
12
|
+
* raw Go template expressions instead of YAML-quoting them. It also
|
|
13
|
+
* detects `__helm_if` markers to wrap entire resources in conditionals.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Declarable } from "@intentius/chant/declarable";
|
|
17
|
+
import { isPropertyDeclarable } from "@intentius/chant/declarable";
|
|
18
|
+
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
19
|
+
import type { Serializer, SerializerResult } from "@intentius/chant/serializer";
|
|
20
|
+
import type { LexiconOutput } from "@intentius/chant/lexicon-output";
|
|
21
|
+
import { walkValue, type SerializerVisitor } from "@intentius/chant/serializer-walker";
|
|
22
|
+
import { HELM_TPL_KEY, HELM_IF_KEY, HELM_RANGE_KEY, HELM_WITH_KEY, type HelmConditional } from "./intrinsics";
|
|
23
|
+
import { generateHelpers } from "./helpers";
|
|
24
|
+
|
|
25
|
+
// ── GVK resolution for K8s resources ──────────────────────
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Well-known K8s API group → apiVersion mappings.
|
|
29
|
+
* Used to resolve K8s entity types to apiVersion/kind for YAML emission.
|
|
30
|
+
*/
|
|
31
|
+
const API_GROUP_VERSIONS: Record<string, string> = {
|
|
32
|
+
Core: "v1",
|
|
33
|
+
Apps: "apps/v1",
|
|
34
|
+
Batch: "batch/v1",
|
|
35
|
+
Networking: "networking.k8s.io/v1",
|
|
36
|
+
Policy: "policy/v1",
|
|
37
|
+
Rbac: "rbac.authorization.k8s.io/v1",
|
|
38
|
+
Storage: "storage.k8s.io/v1",
|
|
39
|
+
Autoscaling: "autoscaling/v2",
|
|
40
|
+
Admissionregistration: "admissionregistration.k8s.io/v1",
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function resolveK8sGVK(entityType: string): { apiVersion: string; kind: string } | null {
|
|
44
|
+
const parts = entityType.split("::");
|
|
45
|
+
if (parts.length !== 3 || parts[0] !== "K8s") return null;
|
|
46
|
+
const group = parts[1];
|
|
47
|
+
const kind = parts[2];
|
|
48
|
+
const apiVersion = API_GROUP_VERSIONS[group];
|
|
49
|
+
if (!apiVersion) return null;
|
|
50
|
+
return { apiVersion, kind };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Helm visitor ──────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
function helmVisitor(): SerializerVisitor {
|
|
56
|
+
return {
|
|
57
|
+
attrRef: (name, _attr) => name,
|
|
58
|
+
resourceRef: (name) => name,
|
|
59
|
+
propertyDeclarable: (entity, walk) => {
|
|
60
|
+
if (!("props" in entity) || typeof entity.props !== "object" || entity.props === null) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
const props = entity.props as Record<string, unknown>;
|
|
64
|
+
const result: Record<string, unknown> = {};
|
|
65
|
+
for (const [key, value] of Object.entries(props)) {
|
|
66
|
+
if (value !== undefined) {
|
|
67
|
+
result[key] = walk(value);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ── YAML emission with Helm template support ──────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Emit an else chain, detecting nested `__helm_if` markers to produce
|
|
79
|
+
* `{{- else if <cond> }}` instead of `{{- else }}\n{{- if <cond> }}`.
|
|
80
|
+
*/
|
|
81
|
+
function emitElseChain(elseBody: unknown, indent: number): string {
|
|
82
|
+
if (typeof elseBody === "object" && elseBody !== null && HELM_IF_KEY in (elseBody as Record<string, unknown>)) {
|
|
83
|
+
const nested = elseBody as Record<string, unknown>;
|
|
84
|
+
const nestedCond = nested[HELM_IF_KEY] as string;
|
|
85
|
+
const nestedBody = nested.body;
|
|
86
|
+
const nestedElse = nested.else;
|
|
87
|
+
let result = `\n{{- else if ${nestedCond} }}\n${emitHelmYAML(nestedBody, indent)}`;
|
|
88
|
+
if (nestedElse !== undefined) {
|
|
89
|
+
result += emitElseChain(nestedElse, indent);
|
|
90
|
+
}
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
return `\n{{- else }}\n${emitHelmYAML(elseBody, indent)}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Emit a YAML value, detecting __helm_tpl markers and emitting them
|
|
98
|
+
* as raw Go template expressions.
|
|
99
|
+
*/
|
|
100
|
+
function emitHelmYAML(value: unknown, indent: number): string {
|
|
101
|
+
const prefix = " ".repeat(indent);
|
|
102
|
+
|
|
103
|
+
if (value === null || value === undefined) return "null";
|
|
104
|
+
if (typeof value === "boolean") return value ? "true" : "false";
|
|
105
|
+
if (typeof value === "number") return String(value);
|
|
106
|
+
|
|
107
|
+
if (typeof value === "string") {
|
|
108
|
+
// Check if this is a raw template expression that was already inlined
|
|
109
|
+
if (value.startsWith("{{") && value.endsWith("}}")) {
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
if (
|
|
113
|
+
value === "" || value === "true" || value === "false" || value === "null" ||
|
|
114
|
+
value === "yes" || value === "no" ||
|
|
115
|
+
value.includes(": ") || value.includes("#") ||
|
|
116
|
+
value.startsWith("*") || value.startsWith("&") || value.startsWith("!") ||
|
|
117
|
+
value.startsWith("{") || value.startsWith("[") ||
|
|
118
|
+
value.startsWith("'") || value.startsWith('"') || value.startsWith("$") ||
|
|
119
|
+
/^\d/.test(value)
|
|
120
|
+
) {
|
|
121
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (Array.isArray(value)) {
|
|
127
|
+
if (value.length === 0) return "[]";
|
|
128
|
+
const lines: string[] = [];
|
|
129
|
+
for (const item of value) {
|
|
130
|
+
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
131
|
+
const entries = Object.entries(item as Record<string, unknown>);
|
|
132
|
+
if (entries.length > 0) {
|
|
133
|
+
const [firstKey, firstVal] = entries[0];
|
|
134
|
+
const firstEmitted = emitHelmYAML(firstVal, indent + 2);
|
|
135
|
+
if (firstEmitted.startsWith("\n")) {
|
|
136
|
+
lines.push(`${prefix}- ${firstKey}:${firstEmitted}`);
|
|
137
|
+
} else {
|
|
138
|
+
lines.push(`${prefix}- ${firstKey}: ${firstEmitted}`);
|
|
139
|
+
}
|
|
140
|
+
for (let i = 1; i < entries.length; i++) {
|
|
141
|
+
const [key, val] = entries[i];
|
|
142
|
+
const emitted = emitHelmYAML(val, indent + 2);
|
|
143
|
+
if (emitted.startsWith("\n")) {
|
|
144
|
+
lines.push(`${prefix} ${key}:${emitted}`);
|
|
145
|
+
} else {
|
|
146
|
+
lines.push(`${prefix} ${key}: ${emitted}`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
lines.push(`${prefix}- ${emitHelmYAML(item, indent + 1).trimStart()}`);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return "\n" + lines.join("\n");
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (typeof value === "object") {
|
|
158
|
+
const obj = value as Record<string, unknown>;
|
|
159
|
+
|
|
160
|
+
// Detect __helm_tpl marker → emit raw template expression
|
|
161
|
+
if (HELM_TPL_KEY in obj && typeof obj[HELM_TPL_KEY] === "string") {
|
|
162
|
+
return obj[HELM_TPL_KEY] as string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Detect __helm_if marker → emit conditional block
|
|
166
|
+
if (HELM_IF_KEY in obj) {
|
|
167
|
+
const condition = obj[HELM_IF_KEY] as string;
|
|
168
|
+
const body = obj.body;
|
|
169
|
+
const elseBody = obj.else;
|
|
170
|
+
let result = `{{- if ${condition} }}\n${emitHelmYAML(body, indent)}`;
|
|
171
|
+
if (elseBody !== undefined) {
|
|
172
|
+
result += emitElseChain(elseBody, indent);
|
|
173
|
+
}
|
|
174
|
+
result += "\n{{- end }}";
|
|
175
|
+
return result;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Detect __helm_range marker → emit range loop
|
|
179
|
+
if (HELM_RANGE_KEY in obj) {
|
|
180
|
+
const list = obj[HELM_RANGE_KEY] as string;
|
|
181
|
+
const body = obj.body;
|
|
182
|
+
return `{{- range ${list} }}\n${emitHelmYAML(body, indent)}\n{{- end }}`;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Detect __helm_with marker → emit with scope
|
|
186
|
+
if (HELM_WITH_KEY in obj) {
|
|
187
|
+
const scope = obj[HELM_WITH_KEY] as string;
|
|
188
|
+
const body = obj.body;
|
|
189
|
+
return `{{- with ${scope} }}\n${emitHelmYAML(body, indent)}\n{{- end }}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const entries = Object.entries(obj);
|
|
193
|
+
if (entries.length === 0) return "{}";
|
|
194
|
+
const lines: string[] = [];
|
|
195
|
+
for (const [key, val] of entries) {
|
|
196
|
+
const emitted = emitHelmYAML(val, indent + 1);
|
|
197
|
+
if (emitted.startsWith("\n")) {
|
|
198
|
+
lines.push(`${prefix}${key}:${emitted}`);
|
|
199
|
+
} else {
|
|
200
|
+
lines.push(`${prefix}${key}: ${emitted}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return "\n" + lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return String(value);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Emit a top-level key-value pair in Helm YAML.
|
|
211
|
+
*/
|
|
212
|
+
function emitKeyValue(key: string, value: unknown): string {
|
|
213
|
+
const yamlStr = emitHelmYAML(value, 1);
|
|
214
|
+
if (yamlStr.startsWith("\n")) {
|
|
215
|
+
return `${key}:${yamlStr}`;
|
|
216
|
+
}
|
|
217
|
+
return `${key}: ${yamlStr}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── Serializer ────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Specless K8s types whose properties live directly on the manifest.
|
|
224
|
+
*/
|
|
225
|
+
const SPECLESS_TYPES = new Set([
|
|
226
|
+
"ConfigMap", "Secret", "Namespace", "ServiceAccount",
|
|
227
|
+
]);
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Convert a logical name to a kebab-case filename stem.
|
|
231
|
+
*/
|
|
232
|
+
function toFileName(name: string): string {
|
|
233
|
+
return name
|
|
234
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
235
|
+
.replace(/([A-Z])([A-Z][a-z])/g, "$1-$2")
|
|
236
|
+
.toLowerCase();
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Generate Chart.yaml content from Helm::Chart entity props.
|
|
241
|
+
*/
|
|
242
|
+
function emitChartYaml(props: Record<string, unknown>): string {
|
|
243
|
+
const orderedKeys = [
|
|
244
|
+
"apiVersion", "name", "version", "kubeVersion", "description", "type",
|
|
245
|
+
"keywords", "home", "sources", "icon", "maintainers", "deprecated",
|
|
246
|
+
"annotations", "condition", "tags", "appVersion",
|
|
247
|
+
];
|
|
248
|
+
const lines: string[] = [];
|
|
249
|
+
|
|
250
|
+
for (const key of orderedKeys) {
|
|
251
|
+
if (props[key] !== undefined) {
|
|
252
|
+
lines.push(emitKeyValue(key, props[key]));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
for (const [key, val] of Object.entries(props)) {
|
|
257
|
+
if (!orderedKeys.includes(key) && val !== undefined) {
|
|
258
|
+
lines.push(emitKeyValue(key, val));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
return lines.join("\n") + "\n";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Generate values.yaml content from Helm::Values entity props.
|
|
267
|
+
*/
|
|
268
|
+
function emitValuesYaml(props: Record<string, unknown>): string {
|
|
269
|
+
if (Object.keys(props).length === 0) return "{}\n";
|
|
270
|
+
const lines: string[] = [];
|
|
271
|
+
for (const [key, val] of Object.entries(props)) {
|
|
272
|
+
lines.push(emitKeyValue(key, val));
|
|
273
|
+
}
|
|
274
|
+
return lines.join("\n") + "\n";
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Description inference — static map from common Helm value key names.
|
|
279
|
+
*/
|
|
280
|
+
const KEY_DESCRIPTIONS: Record<string, string> = {
|
|
281
|
+
replicaCount: "Number of pod replicas",
|
|
282
|
+
image: "Container image configuration",
|
|
283
|
+
repository: "Image repository",
|
|
284
|
+
tag: "Image tag (empty defaults to Chart.appVersion)",
|
|
285
|
+
pullPolicy: "Image pull policy",
|
|
286
|
+
port: "Service port number",
|
|
287
|
+
enabled: "Whether this feature is enabled",
|
|
288
|
+
resources: "Container resource requests and limits",
|
|
289
|
+
service: "Kubernetes Service configuration",
|
|
290
|
+
type: "Service type",
|
|
291
|
+
ingress: "Ingress configuration",
|
|
292
|
+
className: "Ingress class name",
|
|
293
|
+
hosts: "Ingress host rules",
|
|
294
|
+
tls: "Ingress TLS configuration",
|
|
295
|
+
autoscaling: "Horizontal pod autoscaling configuration",
|
|
296
|
+
minReplicas: "Minimum number of replicas",
|
|
297
|
+
maxReplicas: "Maximum number of replicas",
|
|
298
|
+
targetCPUUtilizationPercentage: "Target CPU utilization for autoscaling",
|
|
299
|
+
targetMemoryUtilizationPercentage: "Target memory utilization for autoscaling",
|
|
300
|
+
serviceAccount: "Service account configuration",
|
|
301
|
+
create: "Whether to create the resource",
|
|
302
|
+
name: "Resource name override",
|
|
303
|
+
annotations: "Additional annotations",
|
|
304
|
+
nodeSelector: "Node selector constraints",
|
|
305
|
+
tolerations: "Pod tolerations",
|
|
306
|
+
affinity: "Pod affinity rules",
|
|
307
|
+
podSecurityContext: "Pod-level security context",
|
|
308
|
+
securityContext: "Container-level security context",
|
|
309
|
+
livenessProbe: "Liveness probe configuration",
|
|
310
|
+
readinessProbe: "Readiness probe configuration",
|
|
311
|
+
persistence: "Persistent storage configuration",
|
|
312
|
+
size: "Storage size",
|
|
313
|
+
storageClass: "Storage class name",
|
|
314
|
+
config: "Application configuration",
|
|
315
|
+
schedule: "Cron schedule expression",
|
|
316
|
+
fullnameOverride: "Override the full release name",
|
|
317
|
+
nameOverride: "Override the chart name",
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Enum detection — known string enums keyed by `parentKey.key` or just `key`.
|
|
322
|
+
*/
|
|
323
|
+
const KEY_ENUMS: Record<string, string[]> = {
|
|
324
|
+
pullPolicy: ["Always", "IfNotPresent", "Never"],
|
|
325
|
+
"service.type": ["ClusterIP", "NodePort", "LoadBalancer", "ExternalName"],
|
|
326
|
+
"ingress.pathType": ["Prefix", "Exact", "ImplementationSpecific"],
|
|
327
|
+
restartPolicy: ["Always", "OnFailure", "Never"],
|
|
328
|
+
"updateStrategy.type": ["RollingUpdate", "Recreate"],
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Numeric constraints keyed by key name.
|
|
333
|
+
*/
|
|
334
|
+
const KEY_NUMERIC_CONSTRAINTS: Record<string, { minimum?: number; maximum?: number }> = {
|
|
335
|
+
replicaCount: { minimum: 0 },
|
|
336
|
+
port: { minimum: 1, maximum: 65535 },
|
|
337
|
+
containerPort: { minimum: 1, maximum: 65535 },
|
|
338
|
+
minReplicas: { minimum: 1 },
|
|
339
|
+
maxReplicas: { minimum: 1 },
|
|
340
|
+
targetCPUUtilizationPercentage: { minimum: 1, maximum: 100 },
|
|
341
|
+
targetMemoryUtilizationPercentage: { minimum: 1, maximum: 100 },
|
|
342
|
+
};
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Generate values.schema.json from values defaults.
|
|
346
|
+
*/
|
|
347
|
+
function generateValuesSchema(props: Record<string, unknown>): string {
|
|
348
|
+
function inferType(
|
|
349
|
+
value: unknown,
|
|
350
|
+
includeDefault: boolean = true,
|
|
351
|
+
keyName?: string,
|
|
352
|
+
parentKeyName?: string,
|
|
353
|
+
): Record<string, unknown> {
|
|
354
|
+
if (value === null || value === undefined) return { type: "null" };
|
|
355
|
+
|
|
356
|
+
if (typeof value === "boolean") {
|
|
357
|
+
const schema: Record<string, unknown> = { type: "boolean" };
|
|
358
|
+
if (includeDefault) schema.default = value;
|
|
359
|
+
if (keyName && KEY_DESCRIPTIONS[keyName]) schema.description = KEY_DESCRIPTIONS[keyName];
|
|
360
|
+
return schema;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
if (typeof value === "number") {
|
|
364
|
+
const schema: Record<string, unknown> = {
|
|
365
|
+
type: Number.isInteger(value) ? "integer" : "number",
|
|
366
|
+
};
|
|
367
|
+
if (includeDefault) schema.default = value;
|
|
368
|
+
if (keyName && KEY_DESCRIPTIONS[keyName]) schema.description = KEY_DESCRIPTIONS[keyName];
|
|
369
|
+
if (keyName && KEY_NUMERIC_CONSTRAINTS[keyName]) {
|
|
370
|
+
const constraints = KEY_NUMERIC_CONSTRAINTS[keyName];
|
|
371
|
+
if (constraints.minimum !== undefined) schema.minimum = constraints.minimum;
|
|
372
|
+
if (constraints.maximum !== undefined) schema.maximum = constraints.maximum;
|
|
373
|
+
}
|
|
374
|
+
return schema;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (typeof value === "string") {
|
|
378
|
+
const schema: Record<string, unknown> = { type: "string" };
|
|
379
|
+
if (includeDefault && value !== "") schema.default = value;
|
|
380
|
+
if (keyName && KEY_DESCRIPTIONS[keyName]) schema.description = KEY_DESCRIPTIONS[keyName];
|
|
381
|
+
|
|
382
|
+
// Check enum — qualified key first, then bare key
|
|
383
|
+
const qualifiedKey = parentKeyName ? `${parentKeyName}.${keyName}` : undefined;
|
|
384
|
+
const enumValues = (qualifiedKey && KEY_ENUMS[qualifiedKey]) || (keyName && KEY_ENUMS[keyName]);
|
|
385
|
+
if (enumValues) schema.enum = enumValues;
|
|
386
|
+
|
|
387
|
+
return schema;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (Array.isArray(value)) {
|
|
391
|
+
const schema: Record<string, unknown> = {
|
|
392
|
+
type: "array",
|
|
393
|
+
items: value.length > 0 ? inferType(value[0], false) : {},
|
|
394
|
+
};
|
|
395
|
+
if (includeDefault && value.length > 0) schema.default = value;
|
|
396
|
+
if (keyName && KEY_DESCRIPTIONS[keyName]) schema.description = KEY_DESCRIPTIONS[keyName];
|
|
397
|
+
return schema;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
if (typeof value === "object") {
|
|
401
|
+
const obj = value as Record<string, unknown>;
|
|
402
|
+
const properties: Record<string, unknown> = {};
|
|
403
|
+
const requiredFields: string[] = [];
|
|
404
|
+
|
|
405
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
406
|
+
properties[k] = inferType(v, true, k, keyName);
|
|
407
|
+
// Non-null, non-empty-string defaults suggest the field is expected
|
|
408
|
+
if (v !== null && v !== undefined && v !== "" && v !== false) {
|
|
409
|
+
requiredFields.push(k);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const schema: Record<string, unknown> = {
|
|
414
|
+
type: "object",
|
|
415
|
+
properties,
|
|
416
|
+
};
|
|
417
|
+
if (keyName && KEY_DESCRIPTIONS[keyName]) schema.description = KEY_DESCRIPTIONS[keyName];
|
|
418
|
+
if (requiredFields.length > 0) {
|
|
419
|
+
schema.required = requiredFields;
|
|
420
|
+
}
|
|
421
|
+
return schema;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return {};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const topLevel = inferType(props, false, undefined, undefined) as Record<string, unknown>;
|
|
428
|
+
return JSON.stringify(
|
|
429
|
+
{ $schema: "http://json-schema.org/draft-07/schema#", ...topLevel },
|
|
430
|
+
null,
|
|
431
|
+
2,
|
|
432
|
+
) + "\n";
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Emit a K8s resource as a Helm template YAML file.
|
|
437
|
+
*/
|
|
438
|
+
function emitK8sTemplate(
|
|
439
|
+
name: string,
|
|
440
|
+
entityType: string,
|
|
441
|
+
props: Record<string, unknown>,
|
|
442
|
+
entityNames: Map<Declarable, string>,
|
|
443
|
+
hookAnnotations?: Record<string, string>,
|
|
444
|
+
): string {
|
|
445
|
+
let gvk = resolveK8sGVK(entityType);
|
|
446
|
+
|
|
447
|
+
// Fallback: extract apiVersion/kind from props for CRD-based resources
|
|
448
|
+
if (!gvk && props.apiVersion && props.kind) {
|
|
449
|
+
gvk = {
|
|
450
|
+
apiVersion: props.apiVersion as string,
|
|
451
|
+
kind: props.kind as string,
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
if (!gvk) return "";
|
|
455
|
+
|
|
456
|
+
const walked = walkValue(props, entityNames, helmVisitor()) as Record<string, unknown>;
|
|
457
|
+
if (!walked) return "";
|
|
458
|
+
|
|
459
|
+
const manifest: Record<string, unknown> = {
|
|
460
|
+
apiVersion: gvk.apiVersion,
|
|
461
|
+
kind: gvk.kind,
|
|
462
|
+
};
|
|
463
|
+
|
|
464
|
+
// Build metadata
|
|
465
|
+
const metadata: Record<string, unknown> = walked.metadata as Record<string, unknown> ?? {};
|
|
466
|
+
if (!metadata.name) {
|
|
467
|
+
metadata.name = toFileName(name);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Add hook annotations
|
|
471
|
+
if (hookAnnotations) {
|
|
472
|
+
const existing = (metadata.annotations ?? {}) as Record<string, unknown>;
|
|
473
|
+
metadata.annotations = { ...existing, ...hookAnnotations };
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
manifest.metadata = metadata;
|
|
477
|
+
|
|
478
|
+
// Build spec / specless body
|
|
479
|
+
if (SPECLESS_TYPES.has(gvk.kind)) {
|
|
480
|
+
for (const [key, value] of Object.entries(walked)) {
|
|
481
|
+
if (key !== "metadata") manifest[key] = value;
|
|
482
|
+
}
|
|
483
|
+
} else if (walked.spec !== undefined) {
|
|
484
|
+
manifest.spec = walked.spec;
|
|
485
|
+
for (const [key, value] of Object.entries(walked)) {
|
|
486
|
+
if (key !== "metadata" && key !== "spec") manifest[key] = value;
|
|
487
|
+
}
|
|
488
|
+
} else {
|
|
489
|
+
const spec: Record<string, unknown> = {};
|
|
490
|
+
for (const [key, value] of Object.entries(walked)) {
|
|
491
|
+
if (key !== "metadata") spec[key] = value;
|
|
492
|
+
}
|
|
493
|
+
if (Object.keys(spec).length > 0) manifest.spec = spec;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// Emit as YAML
|
|
497
|
+
const orderedKeys = ["apiVersion", "kind", "metadata", "spec"];
|
|
498
|
+
const lines: string[] = [];
|
|
499
|
+
|
|
500
|
+
for (const key of orderedKeys) {
|
|
501
|
+
if (manifest[key] !== undefined) {
|
|
502
|
+
lines.push(emitKeyValue(key, manifest[key]));
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
for (const [key, value] of Object.entries(manifest)) {
|
|
506
|
+
if (!orderedKeys.includes(key) && value !== undefined) {
|
|
507
|
+
lines.push(emitKeyValue(key, value));
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
return lines.join("\n") + "\n";
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Generate .helmignore content.
|
|
516
|
+
*/
|
|
517
|
+
function emitHelmignore(): string {
|
|
518
|
+
return `# Patterns to ignore when building packages.
|
|
519
|
+
.DS_Store
|
|
520
|
+
.git
|
|
521
|
+
.gitignore
|
|
522
|
+
.bzr
|
|
523
|
+
.bzrignore
|
|
524
|
+
.hg
|
|
525
|
+
.hgignore
|
|
526
|
+
.svn
|
|
527
|
+
*.swp
|
|
528
|
+
*.bak
|
|
529
|
+
*.tmp
|
|
530
|
+
*.orig
|
|
531
|
+
*~
|
|
532
|
+
.project
|
|
533
|
+
.idea
|
|
534
|
+
*.tmproj
|
|
535
|
+
.vscode
|
|
536
|
+
`;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ── Conditional / intrinsic detection ──────────────────────
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Check if a value is a HelmConditional (If wrapper).
|
|
543
|
+
*/
|
|
544
|
+
function isHelmConditional(value: unknown): value is HelmConditional {
|
|
545
|
+
if (typeof value !== "object" || value === null) return false;
|
|
546
|
+
return INTRINSIC_MARKER in value && "condition" in value && "body" in value;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* Check if a value is a Declarable with an entityType.
|
|
551
|
+
*/
|
|
552
|
+
function hasEntityType(value: unknown): value is Record<string, unknown> & { entityType: string } {
|
|
553
|
+
return typeof value === "object" && value !== null && "entityType" in value &&
|
|
554
|
+
typeof (value as Record<string, unknown>).entityType === "string";
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Emit a Chart.yaml dependencies block from HelmDependency property entities.
|
|
559
|
+
*/
|
|
560
|
+
/**
|
|
561
|
+
* Key mapping for dependency props that differ between JS and YAML.
|
|
562
|
+
*/
|
|
563
|
+
const DEP_KEY_MAP: Record<string, string> = {
|
|
564
|
+
importValues: "import-values",
|
|
565
|
+
};
|
|
566
|
+
|
|
567
|
+
function emitDependencies(deps: Record<string, unknown>[]): string {
|
|
568
|
+
const lines: string[] = ["dependencies:"];
|
|
569
|
+
for (const dep of deps) {
|
|
570
|
+
const orderedKeys = ["name", "version", "repository", "condition", "tags", "enabled", "importValues", "alias"];
|
|
571
|
+
const entries: [string, unknown][] = [];
|
|
572
|
+
for (const key of orderedKeys) {
|
|
573
|
+
if (dep[key] !== undefined) entries.push([DEP_KEY_MAP[key] ?? key, dep[key]]);
|
|
574
|
+
}
|
|
575
|
+
for (const [key, val] of Object.entries(dep)) {
|
|
576
|
+
if (!orderedKeys.includes(key) && val !== undefined) entries.push([key, val]);
|
|
577
|
+
}
|
|
578
|
+
if (entries.length > 0) {
|
|
579
|
+
const [firstKey, firstVal] = entries[0];
|
|
580
|
+
lines.push(` - ${firstKey}: ${emitHelmYAML(firstVal, 2).trimStart()}`);
|
|
581
|
+
for (let i = 1; i < entries.length; i++) {
|
|
582
|
+
const [key, val] = entries[i];
|
|
583
|
+
const emitted = emitHelmYAML(val, 2);
|
|
584
|
+
if (emitted.startsWith("\n")) {
|
|
585
|
+
lines.push(` ${key}:${emitted}`);
|
|
586
|
+
} else {
|
|
587
|
+
lines.push(` ${key}: ${emitted}`);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
return lines.join("\n") + "\n";
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Emit a Chart.yaml maintainers block from HelmMaintainer property entities.
|
|
597
|
+
*/
|
|
598
|
+
function emitMaintainers(maintainers: Record<string, unknown>[]): string {
|
|
599
|
+
const lines: string[] = ["maintainers:"];
|
|
600
|
+
for (const m of maintainers) {
|
|
601
|
+
const orderedKeys = ["name", "email", "url"];
|
|
602
|
+
const entries: [string, unknown][] = [];
|
|
603
|
+
for (const key of orderedKeys) {
|
|
604
|
+
if (m[key] !== undefined) entries.push([key, m[key]]);
|
|
605
|
+
}
|
|
606
|
+
for (const [key, val] of Object.entries(m)) {
|
|
607
|
+
if (!orderedKeys.includes(key) && val !== undefined) entries.push([key, val]);
|
|
608
|
+
}
|
|
609
|
+
if (entries.length > 0) {
|
|
610
|
+
const [firstKey, firstVal] = entries[0];
|
|
611
|
+
lines.push(` - ${firstKey}: ${emitHelmYAML(firstVal, 2).trimStart()}`);
|
|
612
|
+
for (let i = 1; i < entries.length; i++) {
|
|
613
|
+
const [key, val] = entries[i];
|
|
614
|
+
const emitted = emitHelmYAML(val, 2);
|
|
615
|
+
if (emitted.startsWith("\n")) {
|
|
616
|
+
lines.push(` ${key}:${emitted}`);
|
|
617
|
+
} else {
|
|
618
|
+
lines.push(` ${key}: ${emitted}`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
return lines.join("\n") + "\n";
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Helm chart serializer implementation.
|
|
628
|
+
*/
|
|
629
|
+
export const helmSerializer: Serializer = {
|
|
630
|
+
name: "helm",
|
|
631
|
+
rulePrefix: "WHM",
|
|
632
|
+
|
|
633
|
+
serialize(entities: Map<string, Declarable>, _outputs?: LexiconOutput[]): SerializerResult {
|
|
634
|
+
const entityNames = new Map<Declarable, string>();
|
|
635
|
+
for (const [name, entity] of entities) {
|
|
636
|
+
entityNames.set(entity, name);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const files: Record<string, string> = {};
|
|
640
|
+
|
|
641
|
+
let chartName = "my-chart";
|
|
642
|
+
let chartProps: Record<string, unknown> = {};
|
|
643
|
+
let valuesProps: Record<string, unknown> = {};
|
|
644
|
+
let hasValues = false;
|
|
645
|
+
let notesContent: string | undefined;
|
|
646
|
+
const dependencies: Record<string, unknown>[] = [];
|
|
647
|
+
const maintainers: Record<string, unknown>[] = [];
|
|
648
|
+
|
|
649
|
+
// First pass: extract Helm-specific resources and collect metadata
|
|
650
|
+
for (const [_name, entity] of entities) {
|
|
651
|
+
if (!hasEntityType(entity)) continue;
|
|
652
|
+
const entityType = entity.entityType;
|
|
653
|
+
|
|
654
|
+
if (entityType === "Helm::Chart") {
|
|
655
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
656
|
+
chartProps = props ?? {};
|
|
657
|
+
chartName = (chartProps.name as string) ?? chartName;
|
|
658
|
+
} else if (entityType === "Helm::Values") {
|
|
659
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
660
|
+
valuesProps = props ?? {};
|
|
661
|
+
hasValues = true;
|
|
662
|
+
} else if (entityType === "Helm::Notes") {
|
|
663
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
664
|
+
notesContent = (props?.content as string) ?? "";
|
|
665
|
+
} else if (entityType === "Helm::Dependency") {
|
|
666
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
667
|
+
if (props) dependencies.push(props);
|
|
668
|
+
} else if (entityType === "Helm::Maintainer") {
|
|
669
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
670
|
+
if (props) maintainers.push(props);
|
|
671
|
+
} else if (entityType === "Helm::CRD") {
|
|
672
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
673
|
+
if (props?.content) {
|
|
674
|
+
const filename = (props.filename as string) ?? `${toFileName(_name)}.yaml`;
|
|
675
|
+
files[`crds/${filename}`] = props.content as string;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// Emit Chart.yaml
|
|
681
|
+
if (!chartProps.apiVersion) chartProps.apiVersion = "v2";
|
|
682
|
+
if (!chartProps.name) chartProps.name = chartName;
|
|
683
|
+
if (!chartProps.version) chartProps.version = "0.1.0";
|
|
684
|
+
if (!chartProps.type) chartProps.type = "application";
|
|
685
|
+
|
|
686
|
+
// Inject collected maintainers into chart props for ordered emission
|
|
687
|
+
if (maintainers.length > 0) {
|
|
688
|
+
chartProps.maintainers = maintainers;
|
|
689
|
+
}
|
|
690
|
+
let chartYaml = emitChartYaml(chartProps);
|
|
691
|
+
if (dependencies.length > 0) {
|
|
692
|
+
chartYaml += emitDependencies(dependencies);
|
|
693
|
+
}
|
|
694
|
+
files["Chart.yaml"] = chartYaml;
|
|
695
|
+
|
|
696
|
+
// Emit values.yaml
|
|
697
|
+
files["values.yaml"] = emitValuesYaml(valuesProps);
|
|
698
|
+
|
|
699
|
+
// Emit values.schema.json if we have values
|
|
700
|
+
if (hasValues && Object.keys(valuesProps).length > 0) {
|
|
701
|
+
files["values.schema.json"] = generateValuesSchema(valuesProps);
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// Emit .helmignore
|
|
705
|
+
files[".helmignore"] = emitHelmignore();
|
|
706
|
+
|
|
707
|
+
// Emit _helpers.tpl
|
|
708
|
+
files["templates/_helpers.tpl"] = generateHelpers({ chartName });
|
|
709
|
+
|
|
710
|
+
// Second pass: emit K8s resources as templates
|
|
711
|
+
for (const [name, entity] of entities) {
|
|
712
|
+
const raw = entity as unknown;
|
|
713
|
+
|
|
714
|
+
// Handle resource-level If(condition, resource) — HelmConditional wrapping a Declarable
|
|
715
|
+
if (isHelmConditional(raw)) {
|
|
716
|
+
const conditional = raw;
|
|
717
|
+
const innerEntity = conditional.body;
|
|
718
|
+
if (hasEntityType(innerEntity) && (innerEntity.entityType as string).startsWith("K8s::")) {
|
|
719
|
+
const entityType = innerEntity.entityType as string;
|
|
720
|
+
const props = (innerEntity as Record<string, unknown>).props as Record<string, unknown>;
|
|
721
|
+
if (props) {
|
|
722
|
+
const fileName = toFileName(name);
|
|
723
|
+
const templateContent = emitK8sTemplate(name, entityType, props, entityNames);
|
|
724
|
+
if (templateContent) {
|
|
725
|
+
files[`templates/${fileName}.yaml`] =
|
|
726
|
+
`{{- if ${conditional.condition} }}\n${templateContent}{{- end }}\n`;
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
continue;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
if (!hasEntityType(raw)) continue;
|
|
734
|
+
const entityType = (raw as Record<string, unknown>).entityType as string;
|
|
735
|
+
|
|
736
|
+
if (isPropertyDeclarable(entity)) continue;
|
|
737
|
+
|
|
738
|
+
// Skip Helm-specific resources (already handled)
|
|
739
|
+
if (entityType.startsWith("Helm::")) continue;
|
|
740
|
+
|
|
741
|
+
// Handle K8s resources
|
|
742
|
+
if (entityType.startsWith("K8s::")) {
|
|
743
|
+
const props = (raw as Record<string, unknown>).props as Record<string, unknown>;
|
|
744
|
+
if (!props) continue;
|
|
745
|
+
|
|
746
|
+
const fileName = toFileName(name);
|
|
747
|
+
const templateContent = emitK8sTemplate(name, entityType, props, entityNames);
|
|
748
|
+
if (templateContent) {
|
|
749
|
+
files[`templates/${fileName}.yaml`] = templateContent;
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// Handle Helm::Test resources
|
|
755
|
+
for (const [name, entity] of entities) {
|
|
756
|
+
if (!hasEntityType(entity)) continue;
|
|
757
|
+
const entityType = (entity as Record<string, unknown>).entityType as string;
|
|
758
|
+
if (entityType === "Helm::Test") {
|
|
759
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
760
|
+
if (!props) continue;
|
|
761
|
+
|
|
762
|
+
const fileName = toFileName(name);
|
|
763
|
+
|
|
764
|
+
// If the test has a nested K8s Declarable as `resource`, use it
|
|
765
|
+
const resource = props.resource;
|
|
766
|
+
if (hasEntityType(resource) && (resource.entityType as string).startsWith("K8s::")) {
|
|
767
|
+
const resType = resource.entityType as string;
|
|
768
|
+
const resProps = (resource as Record<string, unknown>).props as Record<string, unknown>;
|
|
769
|
+
if (resProps) {
|
|
770
|
+
const templateContent = emitK8sTemplate(name, resType, resProps, entityNames, { "helm.sh/hook": "test" });
|
|
771
|
+
if (templateContent) {
|
|
772
|
+
files[`templates/tests/${fileName}.yaml`] = templateContent;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
} else {
|
|
776
|
+
// Treat props directly as Pod spec
|
|
777
|
+
const templateContent = emitK8sTemplate(
|
|
778
|
+
name,
|
|
779
|
+
"K8s::Core::Pod",
|
|
780
|
+
props,
|
|
781
|
+
entityNames,
|
|
782
|
+
{ "helm.sh/hook": "test" },
|
|
783
|
+
);
|
|
784
|
+
if (templateContent) {
|
|
785
|
+
files[`templates/tests/${fileName}.yaml`] = templateContent;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Handle Helm::Hook property wrappers (property kind, but we iterate all)
|
|
792
|
+
for (const [name, entity] of entities) {
|
|
793
|
+
if (!hasEntityType(entity)) continue;
|
|
794
|
+
const entityType = (entity as Record<string, unknown>).entityType as string;
|
|
795
|
+
if (entityType === "Helm::Hook") {
|
|
796
|
+
const props = (entity as Record<string, unknown>).props as Record<string, unknown>;
|
|
797
|
+
if (!props) continue;
|
|
798
|
+
|
|
799
|
+
const hook = props.hook as string;
|
|
800
|
+
const weight = props.weight as number | undefined;
|
|
801
|
+
const deletePolicy = props.deletePolicy as string | undefined;
|
|
802
|
+
const resource = props.resource as Declarable | undefined;
|
|
803
|
+
|
|
804
|
+
if (resource && hasEntityType(resource)) {
|
|
805
|
+
const resType = resource.entityType as string;
|
|
806
|
+
const resProps = (resource as Record<string, unknown>).props as Record<string, unknown>;
|
|
807
|
+
if (resProps && resType) {
|
|
808
|
+
const hookAnnotations: Record<string, string> = {
|
|
809
|
+
"helm.sh/hook": hook,
|
|
810
|
+
};
|
|
811
|
+
if (weight !== undefined) hookAnnotations["helm.sh/hook-weight"] = String(weight);
|
|
812
|
+
if (deletePolicy) hookAnnotations["helm.sh/hook-delete-policy"] = deletePolicy;
|
|
813
|
+
|
|
814
|
+
const fileName = toFileName(name);
|
|
815
|
+
const templateContent = emitK8sTemplate(name, resType, resProps, entityNames, hookAnnotations);
|
|
816
|
+
if (templateContent) {
|
|
817
|
+
files[`templates/${fileName}.yaml`] = templateContent;
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// Emit NOTES.txt
|
|
825
|
+
if (notesContent) {
|
|
826
|
+
files["templates/NOTES.txt"] = notesContent;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
// Primary content is Chart.yaml (used as the build output identifier)
|
|
830
|
+
return {
|
|
831
|
+
primary: files["Chart.yaml"],
|
|
832
|
+
files,
|
|
833
|
+
};
|
|
834
|
+
},
|
|
835
|
+
};
|