@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,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM407: `kind: Secret` with inline `data:` values and no ExternalSecret/SealedSecret.
|
|
3
|
+
*
|
|
4
|
+
* Warns when secrets contain inline data values (not `.Values` refs) and the chart
|
|
5
|
+
* does not use ExternalSecret or SealedSecret resources.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { getChartFiles } from "./helm-helpers";
|
|
10
|
+
|
|
11
|
+
export const whm407: PostSynthCheck = {
|
|
12
|
+
id: "WHM407",
|
|
13
|
+
description: "Secrets with inline data should use ExternalSecret or SealedSecret",
|
|
14
|
+
|
|
15
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
16
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
17
|
+
|
|
18
|
+
for (const [, output] of ctx.outputs) {
|
|
19
|
+
const files = getChartFiles(output);
|
|
20
|
+
|
|
21
|
+
// Check if chart uses ExternalSecret or SealedSecret anywhere
|
|
22
|
+
const allContent = Object.values(files).join("\n");
|
|
23
|
+
const hasExternalSecret = allContent.includes("ExternalSecret") || allContent.includes("SealedSecret");
|
|
24
|
+
|
|
25
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
26
|
+
if (!filename.startsWith("templates/") || filename.endsWith("_helpers.tpl") || filename.endsWith("NOTES.txt")) continue;
|
|
27
|
+
if (filename.includes("tests/")) continue;
|
|
28
|
+
|
|
29
|
+
if (!content.includes("kind: Secret")) continue;
|
|
30
|
+
|
|
31
|
+
// Scan for inline data values in data: or stringData: sections
|
|
32
|
+
const lines = content.split("\n");
|
|
33
|
+
let inData = false;
|
|
34
|
+
let dataIndent = -1;
|
|
35
|
+
let hasLiteralValues = false;
|
|
36
|
+
|
|
37
|
+
for (const line of lines) {
|
|
38
|
+
const trimmed = line.trim();
|
|
39
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
40
|
+
|
|
41
|
+
const indent = line.length - line.trimStart().length;
|
|
42
|
+
|
|
43
|
+
if (trimmed === "data:" || trimmed === "stringData:") {
|
|
44
|
+
inData = true;
|
|
45
|
+
dataIndent = indent;
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (inData) {
|
|
50
|
+
// If we've returned to the same or lower indent level, exit data section
|
|
51
|
+
if (indent <= dataIndent && trimmed) {
|
|
52
|
+
inData = false;
|
|
53
|
+
dataIndent = -1;
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// Check if value is a literal (not a template expression)
|
|
57
|
+
if (!trimmed.includes("{{") && !trimmed.includes(".Values")) {
|
|
58
|
+
const colonIdx = trimmed.indexOf(":");
|
|
59
|
+
if (colonIdx > 0 && trimmed.length > colonIdx + 1 && trimmed[colonIdx + 1] === " ") {
|
|
60
|
+
const val = trimmed.slice(colonIdx + 1).trim();
|
|
61
|
+
if (val) {
|
|
62
|
+
hasLiteralValues = true;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (hasLiteralValues && !hasExternalSecret) {
|
|
70
|
+
diagnostics.push({
|
|
71
|
+
checkId: "WHM407",
|
|
72
|
+
severity: "warning",
|
|
73
|
+
message: `${filename}: Secret with inline data values — consider using ExternalSecret or SealedSecret for secret management`,
|
|
74
|
+
entity: filename,
|
|
75
|
+
lexicon: "helm",
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return diagnostics;
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM501: Unused values keys.
|
|
3
|
+
*
|
|
4
|
+
* Parses values.yaml key paths and scans all template files for
|
|
5
|
+
* `.Values.` references. Keys defined but never referenced produce
|
|
6
|
+
* an info diagnostic.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
10
|
+
import { getChartFiles } from "./helm-helpers";
|
|
11
|
+
|
|
12
|
+
/** Keys that are always implicitly used (e.g. in _helpers.tpl). */
|
|
13
|
+
const IMPLICIT_KEYS = new Set(["nameOverride", "fullnameOverride"]);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract all top-level and nested key paths from values.yaml content.
|
|
17
|
+
* Uses indentation to track nesting (2-space indent per level).
|
|
18
|
+
*/
|
|
19
|
+
function extractValuePaths(content: string): string[] {
|
|
20
|
+
const paths: string[] = [];
|
|
21
|
+
const stack: string[] = [];
|
|
22
|
+
let prevIndent = -1;
|
|
23
|
+
|
|
24
|
+
for (const line of content.split("\n")) {
|
|
25
|
+
const trimmed = line.trimStart();
|
|
26
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
27
|
+
|
|
28
|
+
const indent = line.length - trimmed.length;
|
|
29
|
+
const match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:/);
|
|
30
|
+
if (!match) continue;
|
|
31
|
+
|
|
32
|
+
const key = match[1];
|
|
33
|
+
|
|
34
|
+
// Adjust stack based on indentation
|
|
35
|
+
while (stack.length > 0 && indent <= prevIndent) {
|
|
36
|
+
stack.pop();
|
|
37
|
+
prevIndent -= 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
stack.push(key);
|
|
41
|
+
paths.push(stack.join("."));
|
|
42
|
+
prevIndent = indent;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return paths;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export const whm501: PostSynthCheck = {
|
|
49
|
+
id: "WHM501",
|
|
50
|
+
description: "Detect values keys that are defined but never referenced in templates",
|
|
51
|
+
|
|
52
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
53
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
54
|
+
|
|
55
|
+
for (const [, output] of ctx.outputs) {
|
|
56
|
+
const files = getChartFiles(output);
|
|
57
|
+
const valuesContent = files["values.yaml"];
|
|
58
|
+
if (!valuesContent || valuesContent.trim() === "{}" || valuesContent.trim() === "") continue;
|
|
59
|
+
|
|
60
|
+
const definedPaths = extractValuePaths(valuesContent);
|
|
61
|
+
if (definedPaths.length === 0) continue;
|
|
62
|
+
|
|
63
|
+
// Collect all .Values references from templates
|
|
64
|
+
const referencedPaths = new Set<string>();
|
|
65
|
+
const valuesRegex = /\.Values\.([a-zA-Z0-9_.]+)/g;
|
|
66
|
+
|
|
67
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
68
|
+
if (!filename.startsWith("templates/")) continue;
|
|
69
|
+
let match;
|
|
70
|
+
while ((match = valuesRegex.exec(content)) !== null) {
|
|
71
|
+
referencedPaths.add(match[1]);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const path of definedPaths) {
|
|
76
|
+
if (IMPLICIT_KEYS.has(path)) continue;
|
|
77
|
+
|
|
78
|
+
// Check if this path or any child is referenced
|
|
79
|
+
const isReferenced = referencedPaths.has(path) ||
|
|
80
|
+
[...referencedPaths].some((ref) => ref.startsWith(path + "."));
|
|
81
|
+
|
|
82
|
+
// Check if any parent of this path is referenced (parent consumed entirely)
|
|
83
|
+
const parts = path.split(".");
|
|
84
|
+
const parentReferenced = parts.some((_, i) => {
|
|
85
|
+
if (i === parts.length - 1) return false;
|
|
86
|
+
return referencedPaths.has(parts.slice(0, i + 1).join("."));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (!isReferenced && !parentReferenced) {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
checkId: "WHM501",
|
|
92
|
+
severity: "info",
|
|
93
|
+
message: `values.yaml defines "${path}" but it is never referenced in templates`,
|
|
94
|
+
entity: "values.yaml",
|
|
95
|
+
lexicon: "helm",
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return diagnostics;
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM502: K8s API version/kind validation.
|
|
3
|
+
*
|
|
4
|
+
* Checks for deprecated or invalid Kubernetes API versions in templates
|
|
5
|
+
* and suggests replacements.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
9
|
+
import { getChartFiles } from "./helm-helpers";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Deprecated API versions with their replacements.
|
|
13
|
+
*/
|
|
14
|
+
const DEPRECATED_APIS: Record<string, { replacement: string; kinds: string[] }> = {
|
|
15
|
+
"extensions/v1beta1": {
|
|
16
|
+
replacement: "networking.k8s.io/v1",
|
|
17
|
+
kinds: ["Ingress"],
|
|
18
|
+
},
|
|
19
|
+
"networking.k8s.io/v1beta1": {
|
|
20
|
+
replacement: "networking.k8s.io/v1",
|
|
21
|
+
kinds: ["Ingress", "IngressClass"],
|
|
22
|
+
},
|
|
23
|
+
"apps/v1beta1": {
|
|
24
|
+
replacement: "apps/v1",
|
|
25
|
+
kinds: ["Deployment", "StatefulSet"],
|
|
26
|
+
},
|
|
27
|
+
"apps/v1beta2": {
|
|
28
|
+
replacement: "apps/v1",
|
|
29
|
+
kinds: ["Deployment", "StatefulSet", "DaemonSet", "ReplicaSet"],
|
|
30
|
+
},
|
|
31
|
+
"rbac.authorization.k8s.io/v1beta1": {
|
|
32
|
+
replacement: "rbac.authorization.k8s.io/v1",
|
|
33
|
+
kinds: ["ClusterRole", "ClusterRoleBinding", "Role", "RoleBinding"],
|
|
34
|
+
},
|
|
35
|
+
"admissionregistration.k8s.io/v1beta1": {
|
|
36
|
+
replacement: "admissionregistration.k8s.io/v1",
|
|
37
|
+
kinds: ["MutatingWebhookConfiguration", "ValidatingWebhookConfiguration"],
|
|
38
|
+
},
|
|
39
|
+
"batch/v1beta1": {
|
|
40
|
+
replacement: "batch/v1",
|
|
41
|
+
kinds: ["CronJob"],
|
|
42
|
+
},
|
|
43
|
+
"policy/v1beta1": {
|
|
44
|
+
replacement: "policy/v1",
|
|
45
|
+
kinds: ["PodDisruptionBudget", "PodSecurityPolicy"],
|
|
46
|
+
},
|
|
47
|
+
"autoscaling/v2beta1": {
|
|
48
|
+
replacement: "autoscaling/v2",
|
|
49
|
+
kinds: ["HorizontalPodAutoscaler"],
|
|
50
|
+
},
|
|
51
|
+
"autoscaling/v2beta2": {
|
|
52
|
+
replacement: "autoscaling/v2",
|
|
53
|
+
kinds: ["HorizontalPodAutoscaler"],
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const whm502: PostSynthCheck = {
|
|
58
|
+
id: "WHM502",
|
|
59
|
+
description: "Detect deprecated or invalid Kubernetes API versions",
|
|
60
|
+
|
|
61
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
62
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
63
|
+
|
|
64
|
+
for (const [, output] of ctx.outputs) {
|
|
65
|
+
const files = getChartFiles(output);
|
|
66
|
+
|
|
67
|
+
for (const [filename, content] of Object.entries(files)) {
|
|
68
|
+
if (!filename.startsWith("templates/") || filename.endsWith("_helpers.tpl") || filename.endsWith("NOTES.txt")) continue;
|
|
69
|
+
|
|
70
|
+
// Extract apiVersion from template
|
|
71
|
+
const apiVersionMatch = content.match(/apiVersion:\s*(.+)/);
|
|
72
|
+
if (!apiVersionMatch) continue;
|
|
73
|
+
|
|
74
|
+
const apiVersion = apiVersionMatch[1].trim();
|
|
75
|
+
|
|
76
|
+
// Skip template expressions
|
|
77
|
+
if (apiVersion.includes("{{")) continue;
|
|
78
|
+
|
|
79
|
+
const deprecation = DEPRECATED_APIS[apiVersion];
|
|
80
|
+
if (deprecation) {
|
|
81
|
+
diagnostics.push({
|
|
82
|
+
checkId: "WHM502",
|
|
83
|
+
severity: "warning",
|
|
84
|
+
message: `${filename}: apiVersion "${apiVersion}" is deprecated — use "${deprecation.replacement}" instead`,
|
|
85
|
+
entity: filename,
|
|
86
|
+
lexicon: "helm",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return diagnostics;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-helm-patterns
|
|
3
|
+
description: Common Helm chart patterns and best practices using chant
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Common Helm Chart Patterns
|
|
8
|
+
|
|
9
|
+
## Standard Chart Layout
|
|
10
|
+
|
|
11
|
+
A chant Helm project compiles TypeScript into the standard Helm directory structure:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
my-chart/
|
|
15
|
+
Chart.yaml ← chart metadata (name, version, apiVersion: v2)
|
|
16
|
+
values.yaml ← default configuration values
|
|
17
|
+
templates/
|
|
18
|
+
deployment.yaml
|
|
19
|
+
service.yaml
|
|
20
|
+
ingress.yaml
|
|
21
|
+
_helpers.tpl ← named templates (fullname, labels, etc.)
|
|
22
|
+
NOTES.txt ← post-install instructions
|
|
23
|
+
charts/ ← subcharts (dependencies)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
The source of truth is `src/`. Never edit generated files in `templates/` by hand.
|
|
27
|
+
|
|
28
|
+
## Parameterization via Values Proxy
|
|
29
|
+
|
|
30
|
+
The `values` proxy turns property access into `{{ .Values.x }}` template directives:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { values } from "@intentius/chant-lexicon-helm";
|
|
34
|
+
|
|
35
|
+
// Simple access
|
|
36
|
+
values.replicaCount // → {{ .Values.replicaCount }}
|
|
37
|
+
|
|
38
|
+
// Nested access
|
|
39
|
+
values.image.repository // → {{ .Values.image.repository }}
|
|
40
|
+
values.image.tag // → {{ .Values.image.tag }}
|
|
41
|
+
|
|
42
|
+
// Use in resource definitions
|
|
43
|
+
export const deployment = new Deployment({
|
|
44
|
+
spec: {
|
|
45
|
+
replicas: values.replicaCount,
|
|
46
|
+
template: {
|
|
47
|
+
spec: {
|
|
48
|
+
containers: [{
|
|
49
|
+
name: "app",
|
|
50
|
+
image: printf("%s:%s", values.image.repository, values.image.tag),
|
|
51
|
+
}],
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Default values are set in the `Values` resource, which becomes `values.yaml`.
|
|
59
|
+
|
|
60
|
+
## Conditional Resources with If()
|
|
61
|
+
|
|
62
|
+
Wrap resources in `If()` to gate them on a values flag:
|
|
63
|
+
|
|
64
|
+
```typescript
|
|
65
|
+
import { If, values } from "@intentius/chant-lexicon-helm";
|
|
66
|
+
import { Ingress } from "@intentius/chant-lexicon-k8s";
|
|
67
|
+
|
|
68
|
+
// Only rendered when .Values.ingress.enabled is true
|
|
69
|
+
export const ingress = If(values.ingress.enabled, new Ingress({
|
|
70
|
+
metadata: { name: include("my-app.fullname") },
|
|
71
|
+
spec: {
|
|
72
|
+
rules: [{ host: values.ingress.hostname }],
|
|
73
|
+
},
|
|
74
|
+
}));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
`If()` wraps the entire template output in `{{- if .Values.ingress.enabled }}...{{- end }}`.
|
|
78
|
+
|
|
79
|
+
## Helper Templates with include()
|
|
80
|
+
|
|
81
|
+
Reference named templates from `_helpers.tpl`:
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { include } from "@intentius/chant-lexicon-helm";
|
|
85
|
+
|
|
86
|
+
export const deployment = new Deployment({
|
|
87
|
+
metadata: {
|
|
88
|
+
name: include("my-app.fullname"),
|
|
89
|
+
labels: include("my-app.labels"),
|
|
90
|
+
},
|
|
91
|
+
spec: {
|
|
92
|
+
selector: {
|
|
93
|
+
matchLabels: include("my-app.selectorLabels"),
|
|
94
|
+
},
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
The composites (`HelmWebApp`, `HelmMicroservice`, etc.) generate `_helpers.tpl` automatically with standard named templates for fullname, labels, and selector labels.
|
|
100
|
+
|
|
101
|
+
## Dependency Management with HelmDependency
|
|
102
|
+
|
|
103
|
+
Declare subchart dependencies that go into `Chart.yaml`:
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
import { HelmDependency } from "@intentius/chant-lexicon-helm";
|
|
107
|
+
|
|
108
|
+
export const redisDep = HelmDependency({
|
|
109
|
+
name: "redis",
|
|
110
|
+
version: "17.x",
|
|
111
|
+
repository: "https://charts.bitnami.com/bitnami",
|
|
112
|
+
condition: "redis.enabled",
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
export const postgresqlDep = HelmDependency({
|
|
116
|
+
name: "postgresql",
|
|
117
|
+
version: "12.x",
|
|
118
|
+
repository: "https://charts.bitnami.com/bitnami",
|
|
119
|
+
condition: "postgresql.enabled",
|
|
120
|
+
});
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
After building, run `helm dependency update` to fetch the subcharts.
|
|
124
|
+
|
|
125
|
+
## Multi-Environment Configuration
|
|
126
|
+
|
|
127
|
+
Use separate values files per environment and compose them at deploy time:
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
// src/infra.ts — base chart with parameterized defaults
|
|
131
|
+
export const app = HelmWebApp({
|
|
132
|
+
name: "my-app",
|
|
133
|
+
port: 3000,
|
|
134
|
+
replicas: 1, // default for dev
|
|
135
|
+
imageTag: "latest", // overridden per env
|
|
136
|
+
});
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# Deploy with environment-specific overrides
|
|
141
|
+
helm install my-app . -f values.yaml -f values-staging.yaml
|
|
142
|
+
helm install my-app . -f values.yaml -f values-production.yaml
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Structure values files as overlays: `values.yaml` (defaults) -> `values-staging.yaml` (overrides) -> `values-production.yaml` (overrides). Later files win.
|
|
146
|
+
|
|
147
|
+
## Library Charts with HelmLibrary
|
|
148
|
+
|
|
149
|
+
Create reusable chart libraries that other charts can import:
|
|
150
|
+
|
|
151
|
+
```typescript
|
|
152
|
+
import { HelmLibrary } from "@intentius/chant-lexicon-helm";
|
|
153
|
+
|
|
154
|
+
export const library = HelmLibrary({
|
|
155
|
+
name: "common-templates",
|
|
156
|
+
version: "1.0.0",
|
|
157
|
+
templates: {
|
|
158
|
+
"common.labels": labelsTemplate,
|
|
159
|
+
"common.annotations": annotationsTemplate,
|
|
160
|
+
"common.fullname": fullnameTemplate,
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Library charts produce no templates of their own. They are included as dependencies by application charts, which use `include()` to reference the shared named templates.
|
|
166
|
+
|
|
167
|
+
## Testing Patterns with HelmTest
|
|
168
|
+
|
|
169
|
+
Add Helm test pods that run on `helm test`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { HelmTest } from "@intentius/chant-lexicon-helm";
|
|
173
|
+
|
|
174
|
+
export const connectivityTest = HelmTest({
|
|
175
|
+
name: "test-connection",
|
|
176
|
+
image: "busybox:1.36",
|
|
177
|
+
command: ["wget", "--spider", "http://my-app:3000/healthz"],
|
|
178
|
+
});
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
This generates a pod in `templates/tests/` with the `helm.sh/hook: test` annotation. Run tests after install:
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
helm install my-app .
|
|
185
|
+
helm test my-app
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Lint rule WHM301** fires when an application chart has no test resources.
|
|
189
|
+
|
|
190
|
+
## Composites Quick Reference
|
|
191
|
+
|
|
192
|
+
| Composite | Resources created |
|
|
193
|
+
|---------------------|-------------------------------------------------------------|
|
|
194
|
+
| `HelmWebApp` | Chart, Values, Deployment, Service, Ingress?, HPA?, SA? |
|
|
195
|
+
| `HelmMicroservice` | Chart, Values, Deployment, Service, SA, ConfigMap?, PDB?, HPA?, Ingress? |
|
|
196
|
+
| `HelmWorker` | Chart, Values, Deployment, SA, HPA?, PDB? |
|
|
197
|
+
| `HelmCronJob` | Chart, Values, CronJob |
|
|
198
|
+
| `HelmStatefulSet` | Chart, Values, StatefulSet, Service |
|
|
199
|
+
| `HelmDaemonSet` | Chart, Values, DaemonSet, SA? |
|
|
200
|
+
| `HelmLibrary` | Chart (type: library), named templates |
|
|
201
|
+
|
|
202
|
+
All composites accept optional `podSecurityContext`, `securityContext`, `nodeSelector`, `tolerations`, `affinity`, and `strategy` fields.
|
|
203
|
+
|
|
204
|
+
## Built-in Objects
|
|
205
|
+
|
|
206
|
+
Access Helm's built-in objects in templates:
|
|
207
|
+
|
|
208
|
+
```typescript
|
|
209
|
+
import { Release, ChartRef, Capabilities } from "@intentius/chant-lexicon-helm";
|
|
210
|
+
|
|
211
|
+
Release.Name // {{ .Release.Name }}
|
|
212
|
+
Release.Namespace // {{ .Release.Namespace }}
|
|
213
|
+
Release.IsUpgrade // {{ .Release.IsUpgrade }}
|
|
214
|
+
ChartRef.Name // {{ .Chart.Name }}
|
|
215
|
+
ChartRef.Version // {{ .Chart.Version }}
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
## Template Functions
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { include, printf, toYaml, quote, required, helmDefault } from "@intentius/chant-lexicon-helm";
|
|
222
|
+
|
|
223
|
+
include("my-app.fullname") // {{ include "my-app.fullname" . }}
|
|
224
|
+
printf("%s-%s", Release.Name, "worker") // {{ printf "%s-%s" .Release.Name "worker" }}
|
|
225
|
+
toYaml(values.resources, 12) // {{ toYaml .Values.resources | nindent 12 }}
|
|
226
|
+
quote(values.annotations) // {{ quote .Values.annotations }}
|
|
227
|
+
required("image.tag is required", values.image.tag) // {{ required "..." .Values.image.tag }}
|
|
228
|
+
helmDefault(values.replicas, 1) // {{ default 1 .Values.replicas }}
|
|
229
|
+
```
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
---
|
|
2
|
+
skill: chant-helm-security
|
|
3
|
+
description: Security best practices for Helm charts built with chant
|
|
4
|
+
user-invocable: true
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Helm Chart Security Patterns
|
|
8
|
+
|
|
9
|
+
## Pod Security Context
|
|
10
|
+
|
|
11
|
+
Always set pod-level security constraints. The `podSecurityContext` field applies to all containers in the pod.
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import { HelmWebApp } from "@intentius/chant-lexicon-helm";
|
|
15
|
+
|
|
16
|
+
const app = HelmWebApp({
|
|
17
|
+
name: "secure-app",
|
|
18
|
+
port: 3000,
|
|
19
|
+
podSecurityContext: {
|
|
20
|
+
runAsNonRoot: true,
|
|
21
|
+
runAsUser: 1000,
|
|
22
|
+
runAsGroup: 1000,
|
|
23
|
+
fsGroup: 1000,
|
|
24
|
+
seccompProfile: { type: "RuntimeDefault" },
|
|
25
|
+
},
|
|
26
|
+
});
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Lint rule WHM402** fires when `runAsNonRoot` is not set on any pod spec.
|
|
30
|
+
|
|
31
|
+
## Container Security Context
|
|
32
|
+
|
|
33
|
+
Set per-container restrictions to minimize attack surface:
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
const app = HelmWebApp({
|
|
37
|
+
name: "hardened-app",
|
|
38
|
+
port: 8080,
|
|
39
|
+
securityContext: {
|
|
40
|
+
readOnlyRootFilesystem: true,
|
|
41
|
+
allowPrivilegeEscalation: false,
|
|
42
|
+
capabilities: { drop: ["ALL"] },
|
|
43
|
+
runAsUser: 1000,
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
- **WHM403**: `readOnlyRootFilesystem` not set
|
|
49
|
+
- **WHM404**: `privileged: true` detected
|
|
50
|
+
|
|
51
|
+
If the application needs to write temporary files, mount a writable `emptyDir` volume at the specific path rather than disabling `readOnlyRootFilesystem`.
|
|
52
|
+
|
|
53
|
+
## Image Tagging
|
|
54
|
+
|
|
55
|
+
Never use `:latest` or omit the tag entirely. Use semver tags or digests.
|
|
56
|
+
|
|
57
|
+
```typescript
|
|
58
|
+
// Bad — WHM401 fires
|
|
59
|
+
HelmWebApp({ name: "app", imageTag: "latest", port: 3000 });
|
|
60
|
+
|
|
61
|
+
// Good — pinned semver
|
|
62
|
+
HelmWebApp({ name: "app", imageTag: "v2.4.1", port: 3000 });
|
|
63
|
+
|
|
64
|
+
// Best — pinned digest
|
|
65
|
+
HelmWebApp({ name: "app", imageTag: "sha256:abc123...", port: 3000 });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
**Lint rule WHM401** fires when the image uses `:latest` or has no tag at all.
|
|
69
|
+
|
|
70
|
+
## Secret Management
|
|
71
|
+
|
|
72
|
+
Never inline secrets in `values.yaml`. Use External Secrets Operator or Sealed Secrets.
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
import { HelmExternalSecret } from "@intentius/chant-lexicon-helm";
|
|
76
|
+
|
|
77
|
+
// External Secrets Operator — fetches secrets from a remote store at runtime
|
|
78
|
+
const secrets = HelmExternalSecret({
|
|
79
|
+
name: "app-secrets",
|
|
80
|
+
secretStoreName: "aws-secretsmanager",
|
|
81
|
+
data: {
|
|
82
|
+
DB_PASSWORD: "prod/db-password",
|
|
83
|
+
API_KEY: "prod/api-key",
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Lint rule WHM407** fires when a Secret resource contains inline `data` or `stringData` values. The fix is always to use an external secret provider.
|
|
89
|
+
|
|
90
|
+
### What NOT to do
|
|
91
|
+
|
|
92
|
+
```typescript
|
|
93
|
+
// Bad — WHM407 fires, secret value is in source control
|
|
94
|
+
new Secret({
|
|
95
|
+
metadata: { name: "db-creds" },
|
|
96
|
+
stringData: { password: "hunter2" },
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
## RBAC and ServiceAccount
|
|
101
|
+
|
|
102
|
+
Every workload should run under a dedicated ServiceAccount with minimal RBAC:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
const app = HelmMicroservice({
|
|
106
|
+
name: "payment-api",
|
|
107
|
+
port: 8080,
|
|
108
|
+
serviceAccount: true, // creates a dedicated SA
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
When the workload needs API access, bind only the required verbs and resources:
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
import { Role, RoleBinding } from "@intentius/chant-lexicon-k8s";
|
|
116
|
+
|
|
117
|
+
export const role = new Role({
|
|
118
|
+
metadata: { name: include("payment-api.fullname") },
|
|
119
|
+
rules: [{
|
|
120
|
+
apiGroups: [""],
|
|
121
|
+
resources: ["configmaps"],
|
|
122
|
+
verbs: ["get", "watch"],
|
|
123
|
+
}],
|
|
124
|
+
});
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Avoid `ClusterRole` with wildcard resources or verbs. Prefer namespace-scoped `Role` bindings.
|
|
128
|
+
|
|
129
|
+
## Network Policies
|
|
130
|
+
|
|
131
|
+
Restrict pod-to-pod traffic to only what is required:
|
|
132
|
+
|
|
133
|
+
```typescript
|
|
134
|
+
import { NetworkPolicy } from "@intentius/chant-lexicon-k8s";
|
|
135
|
+
|
|
136
|
+
export const netpol = new NetworkPolicy({
|
|
137
|
+
metadata: { name: "allow-frontend-to-api" },
|
|
138
|
+
spec: {
|
|
139
|
+
podSelector: { matchLabels: { app: "payment-api" } },
|
|
140
|
+
policyTypes: ["Ingress"],
|
|
141
|
+
ingress: [{
|
|
142
|
+
from: [{ podSelector: { matchLabels: { app: "frontend" } } }],
|
|
143
|
+
ports: [{ protocol: "TCP", port: 8080 }],
|
|
144
|
+
}],
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Start with a default-deny policy per namespace, then add allow rules for known traffic patterns.
|
|
150
|
+
|
|
151
|
+
## Resource Limits and Requests
|
|
152
|
+
|
|
153
|
+
Always set both requests and limits. Without them, a single pod can starve others.
|
|
154
|
+
|
|
155
|
+
```typescript
|
|
156
|
+
const app = HelmMicroservice({
|
|
157
|
+
name: "api",
|
|
158
|
+
port: 8080,
|
|
159
|
+
// HelmMicroservice sets sensible defaults:
|
|
160
|
+
// requests: { cpu: "250m", memory: "128Mi" }
|
|
161
|
+
// limits: { cpu: "500m", memory: "256Mi" }
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
**Lint rule WHM405** fires when a container spec is missing `cpu` or `memory` in resources. **WHM302** fires when resource limits are not set at all.
|
|
166
|
+
|
|
167
|
+
## Security Lint Rules Reference
|
|
168
|
+
|
|
169
|
+
| Rule | What it checks | Severity |
|
|
170
|
+
|--------|---------------------------------------------|----------|
|
|
171
|
+
| WHM401 | Image uses `:latest` tag or no tag | Warning |
|
|
172
|
+
| WHM402 | `runAsNonRoot` not set on pod | Warning |
|
|
173
|
+
| WHM403 | `readOnlyRootFilesystem` not set | Warning |
|
|
174
|
+
| WHM404 | `privileged: true` on a container | Error |
|
|
175
|
+
| WHM405 | Missing cpu/memory in resource spec | Warning |
|
|
176
|
+
| WHM406 | CRD lifecycle limitation (no auto-upgrade) | Info |
|
|
177
|
+
| WHM407 | Secret with inline data in source | Error |
|
|
178
|
+
|
|
179
|
+
## Checklist
|
|
180
|
+
|
|
181
|
+
When reviewing or building a Helm chart, verify:
|
|
182
|
+
|
|
183
|
+
1. Pod runs as non-root with a numeric UID (`runAsNonRoot: true`, `runAsUser: 1000`)
|
|
184
|
+
2. Containers drop all capabilities (`capabilities: { drop: ["ALL"] }`)
|
|
185
|
+
3. Root filesystem is read-only (`readOnlyRootFilesystem: true`)
|
|
186
|
+
4. Privilege escalation is blocked (`allowPrivilegeEscalation: false`)
|
|
187
|
+
5. Images use pinned semver or digest tags, never `:latest`
|
|
188
|
+
6. Secrets come from an external provider, never inline
|
|
189
|
+
7. Each workload has a dedicated ServiceAccount
|
|
190
|
+
8. RBAC is namespace-scoped with minimal verbs
|
|
191
|
+
9. Network policies restrict ingress/egress to known peers
|
|
192
|
+
10. CPU and memory requests and limits are set on every container
|