@intentius/chant-lexicon-helm 0.0.24 → 0.1.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/dist/integrity.json +6 -4
- package/dist/manifest.json +1 -1
- package/dist/rules/values-no-helm-tpl.ts +92 -0
- package/dist/rules/whm005-no-empty-wrapper.ts +54 -0
- package/dist/skills/chant-helm-patterns.md +52 -0
- package/dist/skills/chant-helm.md +71 -22
- package/package.json +6 -3
- package/src/codegen/docs.ts +3 -2
- package/src/index.ts +4 -1
- package/src/intrinsics.ts +53 -0
- package/src/lint/post-synth/post-synth.test.ts +43 -0
- package/src/lint/post-synth/whm005-no-empty-wrapper.ts +54 -0
- package/src/lint/rules/lint-rules.test.ts +35 -0
- package/src/lint/rules/values-no-helm-tpl.ts +92 -0
- package/src/plugin.test.ts +4 -2
- package/src/resources.ts +20 -0
- package/src/serializer.test.ts +113 -2
- package/src/serializer.ts +149 -13
- package/src/skills/chant-helm-patterns.md +52 -0
- package/src/skills/chant-helm.md +71 -22
package/dist/integrity.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"algorithm": "xxhash64",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
4
|
+
"manifest.json": "8230c04a29b03431",
|
|
5
5
|
"meta.json": "b7aab243e162dfaf",
|
|
6
6
|
"types/index.d.ts": "e8011da51963f058",
|
|
7
7
|
"rules/no-hardcoded-image.ts": "c75aa9c33016c3b9",
|
|
8
8
|
"rules/values-no-secrets.ts": "213276bfe1fb8ff7",
|
|
9
|
+
"rules/values-no-helm-tpl.ts": "ed23236546936fd0",
|
|
9
10
|
"rules/chart-metadata.ts": "d0cff2b78fed78d3",
|
|
10
11
|
"rules/whm103.ts": "a777a13f2eaf1831",
|
|
11
12
|
"rules/whm104.ts": "e2d644da2a9dc605",
|
|
@@ -25,12 +26,13 @@
|
|
|
25
26
|
"rules/whm401.ts": "fe72c3c450a12f93",
|
|
26
27
|
"rules/whm105.ts": "8977ec834713b90c",
|
|
27
28
|
"rules/whm403.ts": "729d10edb48b0d03",
|
|
29
|
+
"rules/whm005-no-empty-wrapper.ts": "1a1baea985f0bd86",
|
|
28
30
|
"rules/whm405.ts": "ec82442f9120e5e0",
|
|
29
31
|
"rules/whm202.ts": "dbe1cbb3237be84f",
|
|
30
32
|
"rules/whm501.ts": "e9a9f2b4e034a51f",
|
|
31
|
-
"skills/chant-helm.md": "
|
|
32
|
-
"skills/chant-helm-patterns.md": "
|
|
33
|
+
"skills/chant-helm.md": "6476c552b9235085",
|
|
34
|
+
"skills/chant-helm-patterns.md": "cc9bfe5595f22710",
|
|
33
35
|
"skills/chant-helm-security.md": "a7b950513dac7d37"
|
|
34
36
|
},
|
|
35
|
-
"composite": "
|
|
37
|
+
"composite": "8872cd0d6c961e4d"
|
|
36
38
|
}
|
package/dist/manifest.json
CHANGED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM004: HelmTpl Expression Has No Effect in Values Constructor
|
|
3
|
+
*
|
|
4
|
+
* Detects Values constructor props that use `v.xxx` (the `values` proxy)
|
|
5
|
+
* or any HelmTpl-like expression. values.yaml is static YAML — it is NOT
|
|
6
|
+
* processed as a Go template by Helm. These expressions silently become ''.
|
|
7
|
+
*
|
|
8
|
+
* Bad: new Values({ host: v.pgHost })
|
|
9
|
+
* Good: new Values({ host: runtimeSlot("Cloud SQL private IP") })
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
|
|
13
|
+
import * as ts from "typescript";
|
|
14
|
+
|
|
15
|
+
export const valuesNoHelmTplRule: LintRule = {
|
|
16
|
+
id: "WHM004",
|
|
17
|
+
severity: "warning",
|
|
18
|
+
category: "correctness",
|
|
19
|
+
description:
|
|
20
|
+
"HelmTpl expression has no effect in values.yaml — use runtimeSlot() for deploy-time values",
|
|
21
|
+
|
|
22
|
+
check(context: LintContext): LintDiagnostic[] {
|
|
23
|
+
const { sourceFile } = context;
|
|
24
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
25
|
+
|
|
26
|
+
function visit(node: ts.Node): void {
|
|
27
|
+
if (
|
|
28
|
+
ts.isNewExpression(node) &&
|
|
29
|
+
ts.isIdentifier(node.expression) &&
|
|
30
|
+
node.expression.text === "Values" &&
|
|
31
|
+
node.arguments &&
|
|
32
|
+
node.arguments.length > 0
|
|
33
|
+
) {
|
|
34
|
+
const arg = node.arguments[0];
|
|
35
|
+
if (ts.isObjectLiteralExpression(arg)) {
|
|
36
|
+
checkObjectLiteral(arg, [], sourceFile, diagnostics);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
ts.forEachChild(node, visit);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
visit(sourceFile);
|
|
43
|
+
return diagnostics;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the root identifier name of a property access / call chain.
|
|
49
|
+
* v.foo → "v"; values.x.pipe("fn") → "values"; runtimeSlot() → "runtimeSlot"
|
|
50
|
+
*/
|
|
51
|
+
function getRootIdentifier(node: ts.Node): string | null {
|
|
52
|
+
if (ts.isIdentifier(node)) return node.text;
|
|
53
|
+
if (ts.isPropertyAccessExpression(node)) return getRootIdentifier(node.expression);
|
|
54
|
+
if (ts.isCallExpression(node)) return getRootIdentifier(node.expression);
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isHelmTplExpr(node: ts.Node): boolean {
|
|
59
|
+
const root = getRootIdentifier(node);
|
|
60
|
+
return root === "v" || root === "values";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function checkObjectLiteral(
|
|
64
|
+
obj: ts.ObjectLiteralExpression,
|
|
65
|
+
path: string[],
|
|
66
|
+
sourceFile: ts.SourceFile,
|
|
67
|
+
diagnostics: LintDiagnostic[],
|
|
68
|
+
): void {
|
|
69
|
+
for (const prop of obj.properties) {
|
|
70
|
+
if (!ts.isPropertyAssignment(prop)) continue;
|
|
71
|
+
|
|
72
|
+
const keyName = ts.isIdentifier(prop.name) || ts.isStringLiteral(prop.name)
|
|
73
|
+
? prop.name.text
|
|
74
|
+
: undefined;
|
|
75
|
+
const propPath = keyName ? [...path, keyName] : path;
|
|
76
|
+
|
|
77
|
+
if (isHelmTplExpr(prop.initializer)) {
|
|
78
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(prop.getStart());
|
|
79
|
+
const pathStr = propPath.join(".");
|
|
80
|
+
diagnostics.push({
|
|
81
|
+
file: sourceFile.fileName,
|
|
82
|
+
line: line + 1,
|
|
83
|
+
column: character + 1,
|
|
84
|
+
ruleId: "WHM004",
|
|
85
|
+
severity: "warning",
|
|
86
|
+
message: `HelmTpl expression has no effect in values.yaml (values.yaml is not a Go template). Use runtimeSlot() for deploy-time values or a static default.${pathStr ? ` (path: ${pathStr})` : ""}`,
|
|
87
|
+
});
|
|
88
|
+
} else if (ts.isObjectLiteralExpression(prop.initializer)) {
|
|
89
|
+
checkObjectLiteral(prop.initializer, propPath, sourceFile, diagnostics);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM005: Chart Has Sub-chart Dependencies But Generates No Templates
|
|
3
|
+
*
|
|
4
|
+
* A chart with HelmDependency entries but no templates/*.yaml files generates
|
|
5
|
+
* an empty templates/ directory. Deploying it requires `helm dependency build`
|
|
6
|
+
* as a non-obvious prerequisite.
|
|
7
|
+
*
|
|
8
|
+
* If you only need value overrides for an upstream chart, deploy it directly:
|
|
9
|
+
* helm upgrade upstream-chart -f values-override.yaml
|
|
10
|
+
* and use ValuesOverride to generate the override file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
|
+
import { getChartFiles } from "./helm-helpers";
|
|
15
|
+
|
|
16
|
+
export const whm005: PostSynthCheck = {
|
|
17
|
+
id: "WHM005",
|
|
18
|
+
description:
|
|
19
|
+
"Chart with sub-chart dependencies but no templates should deploy upstream chart directly",
|
|
20
|
+
|
|
21
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
23
|
+
|
|
24
|
+
for (const [, output] of ctx.outputs) {
|
|
25
|
+
const files = getChartFiles(output);
|
|
26
|
+
const chartYaml = files["Chart.yaml"];
|
|
27
|
+
if (!chartYaml) continue;
|
|
28
|
+
|
|
29
|
+
// Check for non-empty dependencies block
|
|
30
|
+
const hasDependencies = /^dependencies:/m.test(chartYaml);
|
|
31
|
+
if (!hasDependencies) continue;
|
|
32
|
+
|
|
33
|
+
// Check for template files (excluding _helpers.tpl and NOTES.txt)
|
|
34
|
+
const hasTemplates = Object.keys(files).some((path) => {
|
|
35
|
+
if (!path.startsWith("templates/")) return false;
|
|
36
|
+
const filename = path.slice("templates/".length);
|
|
37
|
+
if (filename === "_helpers.tpl" || filename === "NOTES.txt") return false;
|
|
38
|
+
return filename.endsWith(".yaml") || filename.endsWith(".yml");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!hasTemplates) {
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
checkId: "WHM005",
|
|
44
|
+
severity: "warning",
|
|
45
|
+
message:
|
|
46
|
+
"Chart has sub-chart dependencies but generates no templates. Deploying this chart requires 'helm dependency build' first. If you only need value overrides for an upstream chart, deploy it directly with 'helm upgrade upstream-chart -f values-override.yaml' and use ValuesOverride to generate the override file.",
|
|
47
|
+
lexicon: "helm",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return diagnostics;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -215,6 +215,58 @@ ChartRef.Name // {{ .Chart.Name }}
|
|
|
215
215
|
ChartRef.Version // {{ .Chart.Version }}
|
|
216
216
|
```
|
|
217
217
|
|
|
218
|
+
## Deploying Upstream Charts with Value Overrides
|
|
219
|
+
|
|
220
|
+
When you need to deploy an upstream chart (like `gitlab/gitlab`) with custom values, avoid wrapper charts with no templates. Instead:
|
|
221
|
+
|
|
222
|
+
1. Use `runtimeSlot()` in `new Values({...})` for deploy-time values (DB IPs, bucket names, replicas)
|
|
223
|
+
2. Use `ValuesOverride` for static values shared across all environments (disabled bundled services, shared secret refs)
|
|
224
|
+
3. Deploy the upstream chart directly with `-f` flags
|
|
225
|
+
|
|
226
|
+
```typescript
|
|
227
|
+
import { Values, ValuesOverride, runtimeSlot } from "@intentius/chant-lexicon-helm";
|
|
228
|
+
|
|
229
|
+
// Runtime slots → values-runtime-slots.yaml (deploy-time checklist)
|
|
230
|
+
export const vals = new Values({
|
|
231
|
+
global: {
|
|
232
|
+
psql: { host: runtimeSlot("Cloud SQL private IP") },
|
|
233
|
+
redis: { host: runtimeSlot("Memorystore host") },
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Static overrides → values-base.yaml (shared across all deployments)
|
|
238
|
+
export const baseOverride = new ValuesOverride({
|
|
239
|
+
filename: "values-base",
|
|
240
|
+
values: {
|
|
241
|
+
postgresql: { install: false },
|
|
242
|
+
redis: { install: false },
|
|
243
|
+
certmanager: { install: false },
|
|
244
|
+
"nginx-ingress": { enabled: false },
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Outputs:
|
|
250
|
+
- `chart-dir/values.yaml` — defaults; runtime slots appear as `''`
|
|
251
|
+
- `chart-dir/values-base.yaml` — generated static overrides
|
|
252
|
+
- `chart-dir/values-runtime-slots.yaml` — deploy-time slots with descriptions as comments
|
|
253
|
+
|
|
254
|
+
Deploy:
|
|
255
|
+
```bash
|
|
256
|
+
# chant build generates chart-dir/ including values-base.yaml
|
|
257
|
+
chant build
|
|
258
|
+
|
|
259
|
+
# Fill in runtime-slot values (one per environment)
|
|
260
|
+
# values-prod.yaml contains: global.psql.host, global.redis.host, etc.
|
|
261
|
+
|
|
262
|
+
helm upgrade --install my-release upstream/chart \
|
|
263
|
+
-f chart-dir/values-base.yaml \
|
|
264
|
+
-f values-prod.yaml \
|
|
265
|
+
--wait
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
**WHM005** warns when a chart has `HelmDependency` entries but generates no templates — this is the "empty wrapper" anti-pattern that requires `helm dependency build` as a non-obvious prerequisite.
|
|
269
|
+
|
|
218
270
|
## Template Functions
|
|
219
271
|
|
|
220
272
|
```typescript
|
|
@@ -65,6 +65,53 @@ import { If, values } from "@intentius/chant-lexicon-helm";
|
|
|
65
65
|
export const ingress = If(values.ingress.enabled, new Ingress({ ... }));
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
### Runtime values and value overrides
|
|
69
|
+
|
|
70
|
+
Use `runtimeSlot()` for deploy-time values that cannot be known at build time (database IPs, bucket names, etc.):
|
|
71
|
+
|
|
72
|
+
```typescript
|
|
73
|
+
import { Values, runtimeSlot } from "@intentius/chant-lexicon-helm";
|
|
74
|
+
|
|
75
|
+
export const vals = new Values({
|
|
76
|
+
global: {
|
|
77
|
+
psql: {
|
|
78
|
+
host: runtimeSlot("Cloud SQL private IP"), // → '' in values.yaml
|
|
79
|
+
},
|
|
80
|
+
redis: {
|
|
81
|
+
host: runtimeSlot("Memorystore persistent host"),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
`runtimeSlot()` generates two outputs:
|
|
88
|
+
- `values.yaml` — the field emits `''` (empty placeholder, safe for `helm template`)
|
|
89
|
+
- `values-runtime-slots.yaml` — lists only the slots with descriptions as YAML comments, for use as a deploy-time checklist
|
|
90
|
+
|
|
91
|
+
**WHM004** fires when `v.xxx` (the values proxy) is used inside `new Values({...})` — values.yaml is not a Go template, so `{{ .Values.x }}` would silently become `''`. Use `runtimeSlot()` instead.
|
|
92
|
+
|
|
93
|
+
Use `ValuesOverride` for static configuration shared across all deployments, like disabling bundled services:
|
|
94
|
+
|
|
95
|
+
```typescript
|
|
96
|
+
import { ValuesOverride } from "@intentius/chant-lexicon-helm";
|
|
97
|
+
|
|
98
|
+
export const baseOverride = new ValuesOverride({
|
|
99
|
+
filename: "values-base", // → generates chart-dir/values-base.yaml
|
|
100
|
+
values: {
|
|
101
|
+
postgresql: { install: false },
|
|
102
|
+
redis: { install: false },
|
|
103
|
+
certmanager: { install: false },
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
Pass the generated file at deploy time:
|
|
109
|
+
```bash
|
|
110
|
+
helm upgrade --install my-release chart/
|
|
111
|
+
-f chart/values-base.yaml # generated by ValuesOverride
|
|
112
|
+
-f values-prod.yaml # runtime overrides (from values-runtime-slots.yaml)
|
|
113
|
+
```
|
|
114
|
+
|
|
68
115
|
### Built-in objects
|
|
69
116
|
|
|
70
117
|
```typescript
|
|
@@ -218,28 +265,30 @@ const lifecycle = HelmCRDLifecycle({
|
|
|
218
265
|
|
|
219
266
|
## Lint rules
|
|
220
267
|
|
|
221
|
-
| Rule | Description |
|
|
222
|
-
|
|
223
|
-
| WHM001 | Chart must have name, version, apiVersion |
|
|
224
|
-
| WHM002 | Values should not contain bare secrets |
|
|
225
|
-
| WHM003 | Container images should use values references |
|
|
226
|
-
|
|
|
227
|
-
|
|
|
228
|
-
|
|
|
229
|
-
|
|
|
230
|
-
|
|
|
231
|
-
|
|
|
232
|
-
|
|
|
233
|
-
|
|
|
234
|
-
|
|
|
235
|
-
|
|
|
236
|
-
|
|
|
237
|
-
|
|
|
238
|
-
|
|
|
239
|
-
|
|
|
240
|
-
|
|
|
241
|
-
|
|
|
242
|
-
|
|
|
268
|
+
| Rule | Phase | Description |
|
|
269
|
+
|------|-------|-------------|
|
|
270
|
+
| WHM001 | pre-synth | Chart must have name, version, apiVersion |
|
|
271
|
+
| WHM002 | pre-synth | Values should not contain bare secrets |
|
|
272
|
+
| WHM003 | pre-synth | Container images should use values references |
|
|
273
|
+
| WHM004 | pre-synth | HelmTpl (`v.xxx`) has no effect in Values — use `runtimeSlot()` |
|
|
274
|
+
| WHM005 | post-synth | Chart with dependencies but no templates — deploy upstream chart directly |
|
|
275
|
+
| WHM101 | post-synth | Chart.yaml has valid apiVersion (v2) |
|
|
276
|
+
| WHM102 | post-synth | values.schema.json present when Values used |
|
|
277
|
+
| WHM103 | post-synth | Go template syntax valid (balanced braces) |
|
|
278
|
+
| WHM104 | post-synth | NOTES.txt exists for application charts |
|
|
279
|
+
| WHM105 | post-synth | _helpers.tpl exists |
|
|
280
|
+
| WHM201 | post-synth | Resources have standard Helm labels |
|
|
281
|
+
| WHM301 | post-synth | At least one test for application charts |
|
|
282
|
+
| WHM302 | post-synth | Resource limits set |
|
|
283
|
+
| WHM401 | post-synth | Image uses :latest tag or no tag |
|
|
284
|
+
| WHM402 | post-synth | runAsNonRoot not set |
|
|
285
|
+
| WHM403 | post-synth | readOnlyRootFilesystem not set |
|
|
286
|
+
| WHM404 | post-synth | privileged: true detected |
|
|
287
|
+
| WHM405 | post-synth | Resource spec missing cpu/memory |
|
|
288
|
+
| WHM406 | post-synth | CRD lifecycle limitation |
|
|
289
|
+
| WHM407 | post-synth | Secret with inline data |
|
|
290
|
+
| WHM501 | post-synth | Unused values keys |
|
|
291
|
+
| WHM502 | post-synth | Deprecated K8s API versions |
|
|
243
292
|
|
|
244
293
|
## OCI registry workflow
|
|
245
294
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-helm",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "Helm chart lexicon for chant — declarative IaC in TypeScript",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://intentius.io/chant",
|
|
@@ -43,10 +43,13 @@
|
|
|
43
43
|
"prepack": "bun run generate && bun run bundle && bun run validate"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@intentius/chant": "0.0
|
|
47
|
-
"@intentius/chant-lexicon-k8s": "0.0.22"
|
|
46
|
+
"@intentius/chant-lexicon-k8s": "0.1.0"
|
|
48
47
|
},
|
|
49
48
|
"devDependencies": {
|
|
49
|
+
"@intentius/chant": "0.1.0",
|
|
50
50
|
"typescript": "^5.9.3"
|
|
51
|
+
},
|
|
52
|
+
"peerDependencies": {
|
|
53
|
+
"@intentius/chant": "^0.1.0"
|
|
51
54
|
}
|
|
52
55
|
}
|
package/src/codegen/docs.ts
CHANGED
|
@@ -102,6 +102,7 @@ export async function generateDocs(opts?: { verbose?: boolean }): Promise<void>
|
|
|
102
102
|
description: "Typed constructors for parameterized Helm charts",
|
|
103
103
|
distDir: join(pkgDir, "dist"),
|
|
104
104
|
outDir: join(pkgDir, "docs"),
|
|
105
|
+
basePath: process.env.DOCS_BASE_PATH ?? "/chant/lexicons/helm/",
|
|
105
106
|
overview,
|
|
106
107
|
outputFormat,
|
|
107
108
|
serviceFromType,
|
|
@@ -474,8 +475,8 @@ When you scaffold a new project with \`chant init --lexicon helm\`, the skill is
|
|
|
474
475
|
],
|
|
475
476
|
};
|
|
476
477
|
|
|
477
|
-
const result =
|
|
478
|
-
writeDocsSite(
|
|
478
|
+
const result = docsPipeline(config);
|
|
479
|
+
writeDocsSite(config, result);
|
|
479
480
|
|
|
480
481
|
if (opts?.verbose) {
|
|
481
482
|
console.error(`Generated ${result.pages.length} documentation pages`);
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export { helmSerializer } from "./serializer";
|
|
|
5
5
|
export { helmPlugin } from "./plugin";
|
|
6
6
|
|
|
7
7
|
// Resources
|
|
8
|
-
export { Chart, Values, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
|
|
8
|
+
export { Chart, Values, ValuesOverride, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
|
|
9
9
|
|
|
10
10
|
// Intrinsics
|
|
11
11
|
export {
|
|
@@ -14,6 +14,9 @@ export {
|
|
|
14
14
|
HELM_IF_KEY,
|
|
15
15
|
HELM_RANGE_KEY,
|
|
16
16
|
HELM_WITH_KEY,
|
|
17
|
+
RuntimeSlot,
|
|
18
|
+
RUNTIME_SLOT_KEY,
|
|
19
|
+
runtimeSlot,
|
|
17
20
|
values,
|
|
18
21
|
Release,
|
|
19
22
|
ChartRef,
|
package/src/intrinsics.ts
CHANGED
|
@@ -473,6 +473,59 @@ export function argoWave(wave: number): Record<string, string> {
|
|
|
473
473
|
return { "argocd.argoproj.io/sync-wave": String(wave) };
|
|
474
474
|
}
|
|
475
475
|
|
|
476
|
+
// ── RuntimeSlot ───────────────────────────────────────────
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* JSON marker key used by the serializer to detect runtime slot placeholders.
|
|
480
|
+
* Distinguished from HelmTpl: RuntimeSlot appears in values.yaml as '' AND
|
|
481
|
+
* is collected into values-runtime-slots.yaml with its description as a comment.
|
|
482
|
+
*/
|
|
483
|
+
export const RUNTIME_SLOT_KEY = "__runtime_slot" as const;
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* A deploy-time value placeholder in a Values object.
|
|
487
|
+
*
|
|
488
|
+
* Marks fields that cannot be known at build time (DB IP, bucket name, etc.)
|
|
489
|
+
* and must be supplied via a `-f` override file when running `helm upgrade`.
|
|
490
|
+
*
|
|
491
|
+
* Two outputs are generated:
|
|
492
|
+
* - `values.yaml` — the field emits `''` (empty placeholder)
|
|
493
|
+
* - `values-runtime-slots.yaml` — lists only the RuntimeSlot fields with
|
|
494
|
+
* their descriptions as YAML comments, for use as a deploy-time checklist
|
|
495
|
+
*
|
|
496
|
+
* ```ts
|
|
497
|
+
* new Values({
|
|
498
|
+
* global: {
|
|
499
|
+
* psql: { host: runtimeSlot("Cloud SQL private IP") },
|
|
500
|
+
* }
|
|
501
|
+
* })
|
|
502
|
+
* ```
|
|
503
|
+
*/
|
|
504
|
+
export class RuntimeSlot implements Intrinsic {
|
|
505
|
+
readonly [INTRINSIC_MARKER] = true as const;
|
|
506
|
+
readonly description: string;
|
|
507
|
+
|
|
508
|
+
constructor(description = "") {
|
|
509
|
+
this.description = description;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
toJSON(): { [RUNTIME_SLOT_KEY]: string } {
|
|
513
|
+
return { [RUNTIME_SLOT_KEY]: this.description };
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Mark a Values field as a deploy-time runtime slot.
|
|
519
|
+
*
|
|
520
|
+
* Emits `''` in values.yaml and generates an entry in values-runtime-slots.yaml.
|
|
521
|
+
*
|
|
522
|
+
* @param description Human-readable description of what to provide.
|
|
523
|
+
* Emitted as a YAML comment in values-runtime-slots.yaml.
|
|
524
|
+
*/
|
|
525
|
+
export function runtimeSlot(description = ""): RuntimeSlot {
|
|
526
|
+
return new RuntimeSlot(description);
|
|
527
|
+
}
|
|
528
|
+
|
|
476
529
|
// ── Helpers ───────────────────────────────────────────────
|
|
477
530
|
|
|
478
531
|
/**
|
|
@@ -18,6 +18,7 @@ import { whm406 } from "./whm406";
|
|
|
18
18
|
import { whm407 } from "./whm407";
|
|
19
19
|
import { whm501 } from "./whm501";
|
|
20
20
|
import { whm502 } from "./whm502";
|
|
21
|
+
import { whm005 } from "./whm005-no-empty-wrapper";
|
|
21
22
|
|
|
22
23
|
function makeCtx(files: Record<string, string>): PostSynthContext {
|
|
23
24
|
const result: SerializerResult = { primary: files["Chart.yaml"] ?? "", files };
|
|
@@ -36,6 +37,48 @@ function makeCtx(files: Record<string, string>): PostSynthContext {
|
|
|
36
37
|
};
|
|
37
38
|
}
|
|
38
39
|
|
|
40
|
+
describe("WHM005: noEmptyWrapperChart", () => {
|
|
41
|
+
test("warns when chart has HelmDependency but no templates", () => {
|
|
42
|
+
const ctx = makeCtx({
|
|
43
|
+
"Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
|
|
44
|
+
"Chart.yaml.deps": "",
|
|
45
|
+
"templates/_helpers.tpl": "{{/* helpers */}}",
|
|
46
|
+
});
|
|
47
|
+
// Inject dependencies block
|
|
48
|
+
const files = {
|
|
49
|
+
"Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ndependencies:\n - name: gitlab\n version: 8.7.2\n repository: https://charts.gitlab.io\n",
|
|
50
|
+
"templates/_helpers.tpl": "{{/* helpers */}}",
|
|
51
|
+
};
|
|
52
|
+
const ctx2 = makeCtx(files);
|
|
53
|
+
const diags = whm005.check(ctx2);
|
|
54
|
+
expect(diags).toHaveLength(1);
|
|
55
|
+
expect(diags[0].checkId).toBe("WHM005");
|
|
56
|
+
expect(diags[0].severity).toBe("warning");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("passes when chart has dependencies and templates", () => {
|
|
60
|
+
const ctx = makeCtx({
|
|
61
|
+
"Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\ndependencies:\n - name: gitlab\n version: 8.7.2\n repository: https://charts.gitlab.io\n",
|
|
62
|
+
"templates/_helpers.tpl": "{{/* helpers */}}",
|
|
63
|
+
"templates/deployment.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
|
|
64
|
+
});
|
|
65
|
+
expect(whm005.check(ctx)).toHaveLength(0);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("passes when chart has no dependencies", () => {
|
|
69
|
+
const ctx = makeCtx({
|
|
70
|
+
"Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
|
|
71
|
+
"templates/deployment.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
|
|
72
|
+
});
|
|
73
|
+
expect(whm005.check(ctx)).toHaveLength(0);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("passes when no Chart.yaml present", () => {
|
|
77
|
+
const ctx = makeCtx({});
|
|
78
|
+
expect(whm005.check(ctx)).toHaveLength(0);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
39
82
|
describe("WHM101: Chart.yaml validation", () => {
|
|
40
83
|
test("passes with valid Chart.yaml", () => {
|
|
41
84
|
const ctx = makeCtx({
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM005: Chart Has Sub-chart Dependencies But Generates No Templates
|
|
3
|
+
*
|
|
4
|
+
* A chart with HelmDependency entries but no templates/*.yaml files generates
|
|
5
|
+
* an empty templates/ directory. Deploying it requires `helm dependency build`
|
|
6
|
+
* as a non-obvious prerequisite.
|
|
7
|
+
*
|
|
8
|
+
* If you only need value overrides for an upstream chart, deploy it directly:
|
|
9
|
+
* helm upgrade upstream-chart -f values-override.yaml
|
|
10
|
+
* and use ValuesOverride to generate the override file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
14
|
+
import { getChartFiles } from "./helm-helpers";
|
|
15
|
+
|
|
16
|
+
export const whm005: PostSynthCheck = {
|
|
17
|
+
id: "WHM005",
|
|
18
|
+
description:
|
|
19
|
+
"Chart with sub-chart dependencies but no templates should deploy upstream chart directly",
|
|
20
|
+
|
|
21
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
22
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
23
|
+
|
|
24
|
+
for (const [, output] of ctx.outputs) {
|
|
25
|
+
const files = getChartFiles(output);
|
|
26
|
+
const chartYaml = files["Chart.yaml"];
|
|
27
|
+
if (!chartYaml) continue;
|
|
28
|
+
|
|
29
|
+
// Check for non-empty dependencies block
|
|
30
|
+
const hasDependencies = /^dependencies:/m.test(chartYaml);
|
|
31
|
+
if (!hasDependencies) continue;
|
|
32
|
+
|
|
33
|
+
// Check for template files (excluding _helpers.tpl and NOTES.txt)
|
|
34
|
+
const hasTemplates = Object.keys(files).some((path) => {
|
|
35
|
+
if (!path.startsWith("templates/")) return false;
|
|
36
|
+
const filename = path.slice("templates/".length);
|
|
37
|
+
if (filename === "_helpers.tpl" || filename === "NOTES.txt") return false;
|
|
38
|
+
return filename.endsWith(".yaml") || filename.endsWith(".yml");
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!hasTemplates) {
|
|
42
|
+
diagnostics.push({
|
|
43
|
+
checkId: "WHM005",
|
|
44
|
+
severity: "warning",
|
|
45
|
+
message:
|
|
46
|
+
"Chart has sub-chart dependencies but generates no templates. Deploying this chart requires 'helm dependency build' first. If you only need value overrides for an upstream chart, deploy it directly with 'helm upgrade upstream-chart -f values-override.yaml' and use ValuesOverride to generate the override file.",
|
|
47
|
+
lexicon: "helm",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return diagnostics;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -4,6 +4,7 @@ import type { LintContext } from "@intentius/chant/lint/rule";
|
|
|
4
4
|
import { chartMetadataRule } from "./chart-metadata";
|
|
5
5
|
import { valuesNoSecretsRule } from "./values-no-secrets";
|
|
6
6
|
import { noHardcodedImageRule } from "./no-hardcoded-image";
|
|
7
|
+
import { valuesNoHelmTplRule } from "./values-no-helm-tpl";
|
|
7
8
|
|
|
8
9
|
function makeContext(code: string): LintContext {
|
|
9
10
|
const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
|
|
@@ -70,6 +71,40 @@ describe("WHM002: valuesNoSecretsRule", () => {
|
|
|
70
71
|
});
|
|
71
72
|
});
|
|
72
73
|
|
|
74
|
+
describe("WHM004: valuesNoHelmTplRule", () => {
|
|
75
|
+
test("warns when Values prop uses v.xxx", () => {
|
|
76
|
+
const ctx = makeContext(`new Values({ host: v.myHost });`);
|
|
77
|
+
const diags = valuesNoHelmTplRule.check(ctx);
|
|
78
|
+
expect(diags).toHaveLength(1);
|
|
79
|
+
expect(diags[0].ruleId).toBe("WHM004");
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test("warns on nested v.xxx", () => {
|
|
83
|
+
const ctx = makeContext(`new Values({ global: { hosts: { domain: v.cellDomain } } });`);
|
|
84
|
+
expect(valuesNoHelmTplRule.check(ctx).length).toBeGreaterThan(0);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("passes when Values props are static", () => {
|
|
88
|
+
const ctx = makeContext(`new Values({ host: "localhost" });`);
|
|
89
|
+
expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(0);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("passes for runtimeSlot() calls", () => {
|
|
93
|
+
const ctx = makeContext(`new Values({ host: runtimeSlot("Cloud SQL IP") });`);
|
|
94
|
+
expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(0);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("does not fire on non-Values constructors", () => {
|
|
98
|
+
const ctx = makeContext(`new Deployment({ image: v.image });`);
|
|
99
|
+
expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(0);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("warns on v.xxx pipe chain", () => {
|
|
103
|
+
const ctx = makeContext(`new Values({ host: v.myHost.pipe("quote") });`);
|
|
104
|
+
expect(valuesNoHelmTplRule.check(ctx)).toHaveLength(1);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
73
108
|
describe("WHM003: noHardcodedImageRule", () => {
|
|
74
109
|
test("warns on hardcoded image with tag", () => {
|
|
75
110
|
const ctx = makeContext(`new Deployment({ spec: { containers: [{ image: "nginx:1.19" }] } });`);
|