@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.
Files changed (110) hide show
  1. package/README.md +22 -0
  2. package/dist/integrity.json +36 -0
  3. package/dist/manifest.json +37 -0
  4. package/dist/meta.json +208 -0
  5. package/dist/rules/chart-metadata.ts +64 -0
  6. package/dist/rules/helm-helpers.ts +64 -0
  7. package/dist/rules/no-hardcoded-image.ts +62 -0
  8. package/dist/rules/values-no-secrets.ts +82 -0
  9. package/dist/rules/whm101.ts +46 -0
  10. package/dist/rules/whm102.ts +33 -0
  11. package/dist/rules/whm103.ts +59 -0
  12. package/dist/rules/whm104.ts +35 -0
  13. package/dist/rules/whm105.ts +30 -0
  14. package/dist/rules/whm201.ts +36 -0
  15. package/dist/rules/whm202.ts +50 -0
  16. package/dist/rules/whm203.ts +39 -0
  17. package/dist/rules/whm204.ts +60 -0
  18. package/dist/rules/whm301.ts +41 -0
  19. package/dist/rules/whm302.ts +40 -0
  20. package/dist/rules/whm401.ts +57 -0
  21. package/dist/rules/whm402.ts +45 -0
  22. package/dist/rules/whm403.ts +45 -0
  23. package/dist/rules/whm404.ts +36 -0
  24. package/dist/rules/whm405.ts +53 -0
  25. package/dist/rules/whm406.ts +34 -0
  26. package/dist/rules/whm407.ts +83 -0
  27. package/dist/rules/whm501.ts +103 -0
  28. package/dist/rules/whm502.ts +94 -0
  29. package/dist/skills/chant-helm-chart-patterns.md +229 -0
  30. package/dist/skills/chant-helm-chart-security-patterns.md +192 -0
  31. package/dist/skills/chant-helm-create-chart.md +211 -0
  32. package/dist/types/index.d.ts +132 -0
  33. package/package.json +34 -0
  34. package/src/codegen/docs-cli.ts +4 -0
  35. package/src/codegen/docs.ts +483 -0
  36. package/src/codegen/generate-cli.ts +28 -0
  37. package/src/codegen/generate.ts +249 -0
  38. package/src/codegen/naming.ts +38 -0
  39. package/src/codegen/package.ts +64 -0
  40. package/src/composites/composites.test.ts +1050 -0
  41. package/src/composites/helm-batch-job.ts +209 -0
  42. package/src/composites/helm-crd-lifecycle.ts +184 -0
  43. package/src/composites/helm-cron-job.ts +177 -0
  44. package/src/composites/helm-daemon-set.ts +169 -0
  45. package/src/composites/helm-external-secret.ts +93 -0
  46. package/src/composites/helm-library.ts +51 -0
  47. package/src/composites/helm-microservice.ts +331 -0
  48. package/src/composites/helm-monitored-service.ts +252 -0
  49. package/src/composites/helm-namespace-env.ts +154 -0
  50. package/src/composites/helm-secure-ingress.ts +114 -0
  51. package/src/composites/helm-stateful-service.ts +213 -0
  52. package/src/composites/helm-web-app.ts +264 -0
  53. package/src/composites/helm-worker.ts +207 -0
  54. package/src/composites/index.ts +38 -0
  55. package/src/coverage.test.ts +21 -0
  56. package/src/coverage.ts +50 -0
  57. package/src/generated/index.d.ts +132 -0
  58. package/src/generated/index.ts +13 -0
  59. package/src/generated/lexicon-helm.json +208 -0
  60. package/src/helpers.test.ts +51 -0
  61. package/src/helpers.ts +100 -0
  62. package/src/import/generator.ts +285 -0
  63. package/src/import/import.test.ts +224 -0
  64. package/src/import/parser.ts +160 -0
  65. package/src/import/template-stripper.ts +123 -0
  66. package/src/index.ts +108 -0
  67. package/src/intrinsics.test.ts +380 -0
  68. package/src/intrinsics.ts +484 -0
  69. package/src/lint/post-synth/helm-helpers.ts +64 -0
  70. package/src/lint/post-synth/post-synth.test.ts +504 -0
  71. package/src/lint/post-synth/whm101.ts +46 -0
  72. package/src/lint/post-synth/whm102.ts +33 -0
  73. package/src/lint/post-synth/whm103.ts +59 -0
  74. package/src/lint/post-synth/whm104.ts +35 -0
  75. package/src/lint/post-synth/whm105.ts +30 -0
  76. package/src/lint/post-synth/whm201.ts +36 -0
  77. package/src/lint/post-synth/whm202.ts +50 -0
  78. package/src/lint/post-synth/whm203.ts +39 -0
  79. package/src/lint/post-synth/whm204.ts +60 -0
  80. package/src/lint/post-synth/whm301.ts +41 -0
  81. package/src/lint/post-synth/whm302.ts +40 -0
  82. package/src/lint/post-synth/whm401.ts +57 -0
  83. package/src/lint/post-synth/whm402.ts +45 -0
  84. package/src/lint/post-synth/whm403.ts +45 -0
  85. package/src/lint/post-synth/whm404.ts +36 -0
  86. package/src/lint/post-synth/whm405.ts +53 -0
  87. package/src/lint/post-synth/whm406.ts +34 -0
  88. package/src/lint/post-synth/whm407.ts +83 -0
  89. package/src/lint/post-synth/whm501.ts +103 -0
  90. package/src/lint/post-synth/whm502.ts +94 -0
  91. package/src/lint/rules/chart-metadata.ts +64 -0
  92. package/src/lint/rules/lint-rules.test.ts +97 -0
  93. package/src/lint/rules/no-hardcoded-image.ts +62 -0
  94. package/src/lint/rules/values-no-secrets.ts +82 -0
  95. package/src/lsp/completions.test.ts +72 -0
  96. package/src/lsp/completions.ts +20 -0
  97. package/src/lsp/hover.test.ts +46 -0
  98. package/src/lsp/hover.ts +46 -0
  99. package/src/package-cli.ts +28 -0
  100. package/src/plugin.test.ts +71 -0
  101. package/src/plugin.ts +206 -0
  102. package/src/resources.ts +77 -0
  103. package/src/serializer.test.ts +930 -0
  104. package/src/serializer.ts +835 -0
  105. package/src/skills/chart-patterns.md +229 -0
  106. package/src/skills/chart-security-patterns.md +192 -0
  107. package/src/skills/create-chart.md +211 -0
  108. package/src/validate-cli.ts +21 -0
  109. package/src/validate.test.ts +37 -0
  110. 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