@intentius/chant-lexicon-helm 0.0.18 → 0.0.22

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 (33) hide show
  1. package/dist/integrity.json +3 -3
  2. package/dist/manifest.json +1 -1
  3. package/dist/skills/chant-helm-create-chart.md +1 -1
  4. package/package.json +21 -3
  5. package/src/codegen/docs.test.ts +14 -0
  6. package/src/codegen/generate.test.ts +71 -0
  7. package/src/codegen/package.test.ts +36 -0
  8. package/src/import/roundtrip.test.ts +144 -0
  9. package/src/lint/post-synth/whm101.test.ts +69 -0
  10. package/src/lint/post-synth/whm102.test.ts +57 -0
  11. package/src/lint/post-synth/whm103.test.ts +58 -0
  12. package/src/lint/post-synth/whm104.test.ts +57 -0
  13. package/src/lint/post-synth/whm105.test.ts +41 -0
  14. package/src/lint/post-synth/whm201.test.ts +59 -0
  15. package/src/lint/post-synth/whm202.test.ts +62 -0
  16. package/src/lint/post-synth/whm203.test.ts +58 -0
  17. package/src/lint/post-synth/whm204.test.ts +56 -0
  18. package/src/lint/post-synth/whm301.test.ts +49 -0
  19. package/src/lint/post-synth/whm302.test.ts +58 -0
  20. package/src/lint/post-synth/whm401.test.ts +59 -0
  21. package/src/lint/post-synth/whm402.test.ts +58 -0
  22. package/src/lint/post-synth/whm403.test.ts +50 -0
  23. package/src/lint/post-synth/whm404.test.ts +50 -0
  24. package/src/lint/post-synth/whm405.test.ts +60 -0
  25. package/src/lint/post-synth/whm406.test.ts +43 -0
  26. package/src/lint/post-synth/whm407.test.ts +60 -0
  27. package/src/lint/post-synth/whm501.test.ts +70 -0
  28. package/src/lint/post-synth/whm502.test.ts +72 -0
  29. package/src/lint/rules/chart-metadata.test.ts +45 -0
  30. package/src/lint/rules/no-hardcoded-image.test.ts +41 -0
  31. package/src/lint/rules/values-no-secrets.test.ts +48 -0
  32. package/src/plugin.ts +205 -0
  33. package/src/skills/create-chart.md +1 -1
@@ -0,0 +1,60 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import type { SerializerResult } from "@intentius/chant/serializer";
4
+ import { whm407 } from "./whm407";
5
+
6
+ function makeCtx(files: Record<string, string>): PostSynthContext {
7
+ const result: SerializerResult = { primary: files["Chart.yaml"] ?? "", files };
8
+ const outputs = new Map<string, string | SerializerResult>();
9
+ outputs.set("helm", result);
10
+ return {
11
+ outputs,
12
+ entities: new Map(),
13
+ buildResult: {
14
+ outputs,
15
+ entities: new Map(),
16
+ warnings: [],
17
+ errors: [],
18
+ sourceFileCount: 1,
19
+ },
20
+ };
21
+ }
22
+
23
+ describe("WHM407: inline secrets", () => {
24
+ test("warns on Secret with inline data", () => {
25
+ const ctx = makeCtx({
26
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
27
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: c2VjcmV0\n",
28
+ });
29
+ const diags = whm407.check(ctx);
30
+ expect(diags).toHaveLength(1);
31
+ expect(diags[0].checkId).toBe("WHM407");
32
+ expect(diags[0].severity).toBe("warning");
33
+ });
34
+
35
+ test("passes when ExternalSecret is used in chart", () => {
36
+ const ctx = makeCtx({
37
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
38
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: c2VjcmV0\n",
39
+ "templates/external-secret.yaml": "apiVersion: external-secrets.io/v1beta1\nkind: ExternalSecret\n",
40
+ });
41
+ expect(whm407.check(ctx)).toHaveLength(0);
42
+ });
43
+
44
+ test("passes when data uses template values", () => {
45
+ const ctx = makeCtx({
46
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
47
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: {{ .Values.secret.password }}\n",
48
+ });
49
+ expect(whm407.check(ctx)).toHaveLength(0);
50
+ });
51
+
52
+ test("passes when SealedSecret is present", () => {
53
+ const ctx = makeCtx({
54
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
55
+ "templates/secret.yaml": "apiVersion: v1\nkind: Secret\nmetadata:\n name: my-secret\ndata:\n password: c2VjcmV0\n",
56
+ "templates/sealed.yaml": "kind: SealedSecret\n",
57
+ });
58
+ expect(whm407.check(ctx)).toHaveLength(0);
59
+ });
60
+ });
@@ -0,0 +1,70 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import type { SerializerResult } from "@intentius/chant/serializer";
4
+ import { whm501 } from "./whm501";
5
+
6
+ function makeCtx(files: Record<string, string>): PostSynthContext {
7
+ const result: SerializerResult = { primary: files["Chart.yaml"] ?? "", files };
8
+ const outputs = new Map<string, string | SerializerResult>();
9
+ outputs.set("helm", result);
10
+ return {
11
+ outputs,
12
+ entities: new Map(),
13
+ buildResult: {
14
+ outputs,
15
+ entities: new Map(),
16
+ warnings: [],
17
+ errors: [],
18
+ sourceFileCount: 1,
19
+ },
20
+ };
21
+ }
22
+
23
+ describe("WHM501: unused values", () => {
24
+ test("info on values key not referenced in templates", () => {
25
+ const ctx = makeCtx({
26
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
27
+ "values.yaml": "replicaCount: 1\nunusedKey: hello\n",
28
+ "templates/deploy.yaml": "replicas: {{ .Values.replicaCount }}\n",
29
+ });
30
+ const diags = whm501.check(ctx);
31
+ expect(diags.some((d) => d.message.includes("unusedKey"))).toBe(true);
32
+ expect(diags[0].checkId).toBe("WHM501");
33
+ expect(diags[0].severity).toBe("info");
34
+ });
35
+
36
+ test("passes when all values are referenced", () => {
37
+ const ctx = makeCtx({
38
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
39
+ "values.yaml": "replicaCount: 1\n",
40
+ "templates/deploy.yaml": "replicas: {{ .Values.replicaCount }}\n",
41
+ });
42
+ expect(whm501.check(ctx)).toHaveLength(0);
43
+ });
44
+
45
+ test("excludes nameOverride and fullnameOverride", () => {
46
+ const ctx = makeCtx({
47
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
48
+ "values.yaml": 'nameOverride: ""\nfullnameOverride: ""\n',
49
+ "templates/deploy.yaml": "kind: Deployment\n",
50
+ });
51
+ expect(whm501.check(ctx)).toHaveLength(0);
52
+ });
53
+
54
+ test("parent key is not unused when child is referenced", () => {
55
+ const ctx = makeCtx({
56
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
57
+ "values.yaml": "image:\n repository: nginx\n tag: latest\n",
58
+ "templates/deploy.yaml": "image: {{ .Values.image.repository }}:{{ .Values.image.tag }}\n",
59
+ });
60
+ expect(whm501.check(ctx)).toHaveLength(0);
61
+ });
62
+
63
+ test("passes with empty values", () => {
64
+ const ctx = makeCtx({
65
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
66
+ "values.yaml": "{}\n",
67
+ });
68
+ expect(whm501.check(ctx)).toHaveLength(0);
69
+ });
70
+ });
@@ -0,0 +1,72 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
3
+ import type { SerializerResult } from "@intentius/chant/serializer";
4
+ import { whm502 } from "./whm502";
5
+
6
+ function makeCtx(files: Record<string, string>): PostSynthContext {
7
+ const result: SerializerResult = { primary: files["Chart.yaml"] ?? "", files };
8
+ const outputs = new Map<string, string | SerializerResult>();
9
+ outputs.set("helm", result);
10
+ return {
11
+ outputs,
12
+ entities: new Map(),
13
+ buildResult: {
14
+ outputs,
15
+ entities: new Map(),
16
+ warnings: [],
17
+ errors: [],
18
+ sourceFileCount: 1,
19
+ },
20
+ };
21
+ }
22
+
23
+ describe("WHM502: deprecated API versions", () => {
24
+ test("warns on extensions/v1beta1 Ingress", () => {
25
+ const ctx = makeCtx({
26
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
27
+ "templates/ingress.yaml": "apiVersion: extensions/v1beta1\nkind: Ingress\n",
28
+ });
29
+ const diags = whm502.check(ctx);
30
+ expect(diags).toHaveLength(1);
31
+ expect(diags[0].checkId).toBe("WHM502");
32
+ expect(diags[0].severity).toBe("warning");
33
+ expect(diags[0].message).toContain("networking.k8s.io/v1");
34
+ });
35
+
36
+ test("warns on apps/v1beta2", () => {
37
+ const ctx = makeCtx({
38
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
39
+ "templates/deploy.yaml": "apiVersion: apps/v1beta2\nkind: Deployment\n",
40
+ });
41
+ const diags = whm502.check(ctx);
42
+ expect(diags).toHaveLength(1);
43
+ expect(diags[0].message).toContain("apps/v1");
44
+ });
45
+
46
+ test("passes with current API versions", () => {
47
+ const ctx = makeCtx({
48
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
49
+ "templates/deploy.yaml": "apiVersion: apps/v1\nkind: Deployment\n",
50
+ "templates/ingress.yaml": "apiVersion: networking.k8s.io/v1\nkind: Ingress\n",
51
+ });
52
+ expect(whm502.check(ctx)).toHaveLength(0);
53
+ });
54
+
55
+ test("skips template expression apiVersions", () => {
56
+ const ctx = makeCtx({
57
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
58
+ "templates/deploy.yaml": "apiVersion: {{ .Capabilities.APIVersions }}\nkind: Deployment\n",
59
+ });
60
+ expect(whm502.check(ctx)).toHaveLength(0);
61
+ });
62
+
63
+ test("warns on batch/v1beta1 CronJob", () => {
64
+ const ctx = makeCtx({
65
+ "Chart.yaml": "apiVersion: v2\nname: test\nversion: 0.1.0\n",
66
+ "templates/cron.yaml": "apiVersion: batch/v1beta1\nkind: CronJob\n",
67
+ });
68
+ const diags = whm502.check(ctx);
69
+ expect(diags).toHaveLength(1);
70
+ expect(diags[0].message).toContain("batch/v1");
71
+ });
72
+ });
@@ -0,0 +1,45 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import type { LintContext } from "@intentius/chant/lint/rule";
4
+ import { chartMetadataRule } from "./chart-metadata";
5
+
6
+ function makeContext(code: string): LintContext {
7
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath: "test.ts" };
9
+ }
10
+
11
+ describe("WHM001: chartMetadataRule", () => {
12
+ test("passes when all required fields present", () => {
13
+ const ctx = makeContext(`new Chart({ apiVersion: "v2", name: "my-app", version: "0.1.0" });`);
14
+ expect(chartMetadataRule.check(ctx)).toHaveLength(0);
15
+ });
16
+
17
+ test("fails when name is missing", () => {
18
+ const ctx = makeContext(`new Chart({ apiVersion: "v2", version: "0.1.0" });`);
19
+ const diags = chartMetadataRule.check(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].ruleId).toBe("WHM001");
22
+ expect(diags[0].message).toContain("name");
23
+ });
24
+
25
+ test("fails when all fields are missing", () => {
26
+ const ctx = makeContext(`new Chart({});`);
27
+ const diags = chartMetadataRule.check(ctx);
28
+ expect(diags).toHaveLength(1);
29
+ expect(diags[0].message).toContain("name");
30
+ expect(diags[0].message).toContain("version");
31
+ expect(diags[0].message).toContain("apiVersion");
32
+ });
33
+
34
+ test("ignores non-Chart constructors", () => {
35
+ const ctx = makeContext(`new Deployment({ name: "test" });`);
36
+ expect(chartMetadataRule.check(ctx)).toHaveLength(0);
37
+ });
38
+
39
+ test("fails when version is missing", () => {
40
+ const ctx = makeContext(`new Chart({ apiVersion: "v2", name: "my-app" });`);
41
+ const diags = chartMetadataRule.check(ctx);
42
+ expect(diags).toHaveLength(1);
43
+ expect(diags[0].message).toContain("version");
44
+ });
45
+ });
@@ -0,0 +1,41 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import type { LintContext } from "@intentius/chant/lint/rule";
4
+ import { noHardcodedImageRule } from "./no-hardcoded-image";
5
+
6
+ function makeContext(code: string): LintContext {
7
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath: "test.ts" };
9
+ }
10
+
11
+ describe("WHM003: noHardcodedImageRule", () => {
12
+ test("warns on hardcoded image with tag", () => {
13
+ const ctx = makeContext(`new Deployment({ spec: { containers: [{ image: "nginx:1.19" }] } });`);
14
+ const diags = noHardcodedImageRule.check(ctx);
15
+ expect(diags).toHaveLength(1);
16
+ expect(diags[0].ruleId).toBe("WHM003");
17
+ expect(diags[0].message).toContain("nginx:1.19");
18
+ });
19
+
20
+ test("warns on registry/image:tag format", () => {
21
+ const ctx = makeContext(`({ image: "registry.io/app:latest" })`);
22
+ const diags = noHardcodedImageRule.check(ctx);
23
+ expect(diags).toHaveLength(1);
24
+ });
25
+
26
+ test("passes when image is not a string literal (intrinsic)", () => {
27
+ const ctx = makeContext(`({ image: printf("%s:%s", values.image.repo, values.image.tag) })`);
28
+ expect(noHardcodedImageRule.check(ctx)).toHaveLength(0);
29
+ });
30
+
31
+ test("passes for image key without colon-tag pattern", () => {
32
+ const ctx = makeContext(`({ image: "just-a-name" })`);
33
+ expect(noHardcodedImageRule.check(ctx)).toHaveLength(0);
34
+ });
35
+
36
+ test("warns on multi-segment registry path", () => {
37
+ const ctx = makeContext(`({ image: "gcr.io/my-project/my-app:v1.0" })`);
38
+ const diags = noHardcodedImageRule.check(ctx);
39
+ expect(diags).toHaveLength(1);
40
+ });
41
+ });
@@ -0,0 +1,48 @@
1
+ import { describe, test, expect } from "bun:test";
2
+ import * as ts from "typescript";
3
+ import type { LintContext } from "@intentius/chant/lint/rule";
4
+ import { valuesNoSecretsRule } from "./values-no-secrets";
5
+
6
+ function makeContext(code: string): LintContext {
7
+ const sourceFile = ts.createSourceFile("test.ts", code, ts.ScriptTarget.Latest, true);
8
+ return { sourceFile, entities: [], filePath: "test.ts" };
9
+ }
10
+
11
+ describe("WHM002: valuesNoSecretsRule", () => {
12
+ test("passes with empty values", () => {
13
+ const ctx = makeContext(`new Values({});`);
14
+ expect(valuesNoSecretsRule.check(ctx)).toHaveLength(0);
15
+ });
16
+
17
+ test("warns on hardcoded password", () => {
18
+ const ctx = makeContext(`new Values({ password: "hunter2" });`);
19
+ const diags = valuesNoSecretsRule.check(ctx);
20
+ expect(diags).toHaveLength(1);
21
+ expect(diags[0].ruleId).toBe("WHM002");
22
+ expect(diags[0].message).toContain("password");
23
+ });
24
+
25
+ test("warns on hardcoded secret in nested object", () => {
26
+ const ctx = makeContext(`new Values({ db: { secret: "s3cret" } });`);
27
+ const diags = valuesNoSecretsRule.check(ctx);
28
+ expect(diags).toHaveLength(1);
29
+ expect(diags[0].message).toContain("secret");
30
+ });
31
+
32
+ test("passes when secret value is empty", () => {
33
+ const ctx = makeContext(`new Values({ password: "" });`);
34
+ expect(valuesNoSecretsRule.check(ctx)).toHaveLength(0);
35
+ });
36
+
37
+ test("passes for non-sensitive keys", () => {
38
+ const ctx = makeContext(`new Values({ replicaCount: 3, name: "test" });`);
39
+ expect(valuesNoSecretsRule.check(ctx)).toHaveLength(0);
40
+ });
41
+
42
+ test("warns on hardcoded token", () => {
43
+ const ctx = makeContext(`new Values({ token: "abc123" });`);
44
+ const diags = valuesNoSecretsRule.check(ctx);
45
+ expect(diags).toHaveLength(1);
46
+ expect(diags[0].message).toContain("token");
47
+ });
48
+ });
package/src/plugin.ts CHANGED
@@ -64,7 +64,212 @@ export const helmPlugin: LexiconPlugin = {
64
64
  return false;
65
65
  },
66
66
 
67
+ mcpTools() {
68
+ return [
69
+ {
70
+ name: "diff",
71
+ description: "Compare current Helm chart build output against previous output",
72
+ inputSchema: {
73
+ type: "object" as const,
74
+ properties: {
75
+ path: {
76
+ type: "string",
77
+ description: "Path to the infrastructure project directory",
78
+ },
79
+ },
80
+ },
81
+ async handler(params: Record<string, unknown>): Promise<unknown> {
82
+ const { diffCommand } = await import("@intentius/chant/cli/commands/diff");
83
+ const result = await diffCommand({
84
+ path: (params.path as string) ?? ".",
85
+ serializers: [helmSerializer],
86
+ });
87
+ return result;
88
+ },
89
+ },
90
+ ];
91
+ },
92
+
93
+ mcpResources() {
94
+ return [
95
+ {
96
+ uri: "resource-catalog",
97
+ name: "Helm Chart Resource Catalog",
98
+ description: "JSON list of all supported Helm chart resource types",
99
+ mimeType: "application/json",
100
+ async handler(): Promise<string> {
101
+ const { readFileSync } = await import("fs");
102
+ const { join, dirname } = await import("path");
103
+ const { fileURLToPath } = await import("url");
104
+ const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
105
+ try {
106
+ const lexicon = JSON.parse(readFileSync(join(pkgDir, "dist", "meta.json"), "utf-8")) as Record<string, { resourceType: string; kind: string }>;
107
+ const entries = Object.entries(lexicon).map(([className, entry]) => ({
108
+ className,
109
+ resourceType: entry.resourceType,
110
+ kind: entry.kind,
111
+ }));
112
+ return JSON.stringify(entries);
113
+ } catch {
114
+ return JSON.stringify([{ className: "Chart", kind: "resource" }, { className: "Values", kind: "resource" }, { className: "HelmNotes", kind: "resource" }]);
115
+ }
116
+ },
117
+ },
118
+ {
119
+ uri: "examples/web-app",
120
+ name: "Web App Helm Chart Example",
121
+ description: "A basic web application Helm chart with Deployment, Service, and Ingress",
122
+ mimeType: "text/typescript",
123
+ async handler(): Promise<string> {
124
+ return `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
125
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
126
+ import { Deployment, Service, Ingress } from "@intentius/chant-lexicon-k8s";
127
+
128
+ export const chart = new Chart({
129
+ apiVersion: "v2",
130
+ name: "web-app",
131
+ version: "0.1.0",
132
+ appVersion: "1.0.0",
133
+ type: "application",
134
+ description: "A web application chart",
135
+ });
136
+
137
+ export const valuesSchema = new Values({
138
+ replicaCount: 2,
139
+ image: { repository: "nginx", tag: "latest", pullPolicy: "IfNotPresent" },
140
+ service: { type: "ClusterIP", port: 80 },
141
+ ingress: { enabled: false, host: "example.com" },
142
+ });
143
+
144
+ export const deployment = new Deployment({
145
+ metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
146
+ spec: {
147
+ replicas: values.replicaCount,
148
+ selector: { matchLabels: include("web-app.selectorLabels") },
149
+ template: {
150
+ metadata: { labels: include("web-app.selectorLabels") },
151
+ spec: {
152
+ containers: [{
153
+ name: "web-app",
154
+ image: printf("%s:%s", values.image.repository, values.image.tag),
155
+ ports: [{ containerPort: values.service.port, name: "http" }],
156
+ }],
157
+ },
158
+ },
159
+ },
160
+ });
161
+
162
+ export const service = new Service({
163
+ metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
164
+ spec: {
165
+ type: values.service.type,
166
+ ports: [{ port: values.service.port, targetPort: "http", protocol: "TCP", name: "http" }],
167
+ selector: include("web-app.selectorLabels"),
168
+ },
169
+ });
170
+ `;
171
+ },
172
+ },
173
+ ];
174
+ },
175
+
67
176
  initTemplates(_template?: string): InitTemplateSet {
177
+ if (_template === "stateful-service") {
178
+ return {
179
+ src: {
180
+ "chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
181
+ import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
182
+ import { StatefulSet, Service, PersistentVolumeClaim, ConfigMap } from "@intentius/chant-lexicon-k8s";
183
+
184
+ export const chart = new Chart({
185
+ apiVersion: "v2",
186
+ name: "my-stateful-app",
187
+ version: "0.1.0",
188
+ appVersion: "1.0.0",
189
+ type: "application",
190
+ description: "A Helm chart for a stateful application",
191
+ });
192
+
193
+ export const valuesSchema = new Values({
194
+ replicaCount: 3,
195
+ image: {
196
+ repository: "postgres",
197
+ tag: "16",
198
+ pullPolicy: "IfNotPresent",
199
+ },
200
+ service: {
201
+ port: 5432,
202
+ },
203
+ storage: {
204
+ size: "10Gi",
205
+ storageClass: "",
206
+ },
207
+ config: {
208
+ maxConnections: "100",
209
+ sharedBuffers: "256MB",
210
+ },
211
+ });
212
+
213
+ export const configMap = new ConfigMap({
214
+ metadata: {
215
+ name: include("my-stateful-app.fullname"),
216
+ labels: include("my-stateful-app.labels"),
217
+ },
218
+ data: {
219
+ "max_connections": values.config.maxConnections,
220
+ "shared_buffers": values.config.sharedBuffers,
221
+ },
222
+ });
223
+
224
+ export const headlessService = new Service({
225
+ metadata: {
226
+ name: printf("%s-headless", include("my-stateful-app.fullname")),
227
+ labels: include("my-stateful-app.labels"),
228
+ },
229
+ spec: {
230
+ type: "ClusterIP",
231
+ clusterIP: "None",
232
+ ports: [{ port: values.service.port, targetPort: "db", protocol: "TCP", name: "db" }],
233
+ selector: include("my-stateful-app.selectorLabels"),
234
+ },
235
+ });
236
+
237
+ export const statefulSet = new StatefulSet({
238
+ metadata: {
239
+ name: include("my-stateful-app.fullname"),
240
+ labels: include("my-stateful-app.labels"),
241
+ },
242
+ spec: {
243
+ serviceName: printf("%s-headless", include("my-stateful-app.fullname")),
244
+ replicas: values.replicaCount,
245
+ selector: { matchLabels: include("my-stateful-app.selectorLabels") },
246
+ template: {
247
+ metadata: { labels: include("my-stateful-app.selectorLabels") },
248
+ spec: {
249
+ containers: [{
250
+ name: "db",
251
+ image: printf("%s:%s", values.image.repository, values.image.tag),
252
+ ports: [{ containerPort: values.service.port, name: "db" }],
253
+ volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }],
254
+ }],
255
+ },
256
+ },
257
+ volumeClaimTemplates: [
258
+ new PersistentVolumeClaim({
259
+ metadata: { name: "data" },
260
+ spec: {
261
+ accessModes: ["ReadWriteOnce"],
262
+ resources: { requests: { storage: values.storage.size } },
263
+ },
264
+ }),
265
+ ],
266
+ },
267
+ });
268
+ `,
269
+ },
270
+ };
271
+ }
272
+
68
273
  return {
69
274
  src: {
70
275
  "chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
@@ -8,7 +8,7 @@ user-invocable: true
8
8
 
9
9
  ## How chant and Helm relate
10
10
 
11
- chant is a **synthesis-only** tool — it compiles TypeScript source files into a complete Helm chart directory (Chart.yaml, values.yaml, templates/, etc.). chant does NOT call Helm CLI. Your job as an agent is to bridge the two:
11
+ chant is a **synthesis compiler** — it compiles TypeScript source files into a complete Helm chart directory (Chart.yaml, values.yaml, templates/, etc.). `chant build` does not call the Helm CLI; synthesis is pure and deterministic. Your job as an agent is to bridge synthesis and deployment:
12
12
 
13
13
  - Use **chant** for: build, lint, diff (local chart comparison)
14
14
  - Use **helm** for: install, upgrade, rollback, test, dependency update