@intentius/chant-lexicon-helm 0.1.4 → 0.1.9
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 +34 -34
- package/dist/manifest.json +1 -1
- package/package.json +8 -8
- package/src/codegen/docs-cli.ts +1 -1
- package/src/codegen/docs.test.ts +2 -2
- package/src/codegen/docs.ts +1 -1
- package/src/codegen/generate-cli.ts +1 -1
- package/src/codegen/generate.test.ts +3 -3
- package/src/codegen/package.test.ts +2 -2
- package/src/codegen/package.ts +1 -3
- package/src/composites/composites.test.ts +1 -1
- package/src/composites/helm-monitored-service.ts +2 -2
- package/src/composites/helm-namespace-env.ts +3 -3
- package/src/composites/helm-secure-ingress.ts +2 -2
- package/src/coverage.test.ts +1 -1
- package/src/helpers.test.ts +1 -1
- package/src/import/import.test.ts +1 -1
- package/src/import/roundtrip.test.ts +1 -1
- package/src/index.ts +4 -0
- package/src/intrinsics.test.ts +1 -1
- package/src/intrinsics.ts +3 -1
- package/src/lint/post-synth/post-synth.test.ts +1 -1
- package/src/lint/post-synth/whm101.test.ts +1 -1
- package/src/lint/post-synth/whm102.test.ts +1 -1
- package/src/lint/post-synth/whm103.test.ts +1 -1
- package/src/lint/post-synth/whm104.test.ts +1 -1
- package/src/lint/post-synth/whm105.test.ts +1 -1
- package/src/lint/post-synth/whm201.test.ts +1 -1
- package/src/lint/post-synth/whm202.test.ts +1 -1
- package/src/lint/post-synth/whm203.test.ts +1 -1
- package/src/lint/post-synth/whm204.test.ts +1 -1
- package/src/lint/post-synth/whm301.test.ts +1 -1
- package/src/lint/post-synth/whm302.test.ts +1 -1
- package/src/lint/post-synth/whm401.test.ts +1 -1
- package/src/lint/post-synth/whm402.test.ts +1 -1
- package/src/lint/post-synth/whm403.test.ts +1 -1
- package/src/lint/post-synth/whm404.test.ts +1 -1
- package/src/lint/post-synth/whm405.test.ts +1 -1
- package/src/lint/post-synth/whm406.test.ts +1 -1
- package/src/lint/post-synth/whm407.test.ts +1 -1
- package/src/lint/post-synth/whm501.test.ts +1 -1
- package/src/lint/post-synth/whm502.test.ts +1 -1
- package/src/lint/rules/chart-metadata.test.ts +1 -1
- package/src/lint/rules/lint-rules.test.ts +1 -1
- package/src/lint/rules/no-hardcoded-image.test.ts +1 -1
- package/src/lint/rules/values-no-secrets.test.ts +1 -1
- package/src/list-artifacts.test.ts +94 -0
- package/src/list-artifacts.ts +79 -0
- package/src/lsp/completions.test.ts +1 -1
- package/src/lsp/hover.test.ts +1 -1
- package/src/package-cli.ts +1 -1
- package/src/plugin.test.ts +1 -1
- package/src/plugin.ts +7 -2
- package/src/render.test.ts +173 -0
- package/src/render.ts +195 -0
- package/src/serializer.test.ts +4 -5
- package/src/serializer.ts +1 -1
- package/src/validate-cli.ts +1 -1
- package/src/validate.test.ts +1 -1
package/dist/integrity.json
CHANGED
|
@@ -1,38 +1,38 @@
|
|
|
1
1
|
{
|
|
2
|
-
"algorithm": "
|
|
2
|
+
"algorithm": "sha256",
|
|
3
3
|
"artifacts": {
|
|
4
|
-
"manifest.json": "
|
|
5
|
-
"meta.json": "
|
|
6
|
-
"types/index.d.ts": "
|
|
7
|
-
"rules/
|
|
8
|
-
"rules/
|
|
9
|
-
"rules/values-no-
|
|
10
|
-
"rules/no-
|
|
11
|
-
"rules/
|
|
12
|
-
"rules/
|
|
13
|
-
"rules/
|
|
14
|
-
"rules/
|
|
15
|
-
"rules/
|
|
16
|
-
"rules/
|
|
17
|
-
"rules/whm105.ts": "
|
|
18
|
-
"rules/
|
|
19
|
-
"rules/
|
|
20
|
-
"rules/
|
|
21
|
-
"rules/
|
|
22
|
-
"rules/
|
|
23
|
-
"rules/
|
|
24
|
-
"rules/
|
|
25
|
-
"rules/
|
|
26
|
-
"rules/
|
|
27
|
-
"rules/
|
|
28
|
-
"rules/
|
|
29
|
-
"rules/
|
|
30
|
-
"rules/
|
|
31
|
-
"rules/
|
|
32
|
-
"rules/
|
|
33
|
-
"skills/chant-helm.md": "
|
|
34
|
-
"skills/chant-helm-patterns.md": "
|
|
35
|
-
"skills/chant-helm-security.md": "
|
|
4
|
+
"manifest.json": "9a813a15786c5b5691cfc1ad544f2a2ba1148c0ef7a13a66ca72aeaf9b94a87b",
|
|
5
|
+
"meta.json": "14243c5730a07c6a6edc35ddd351438547d58df5cf345f2233a355b0c7611ccc",
|
|
6
|
+
"types/index.d.ts": "5377696ca8698cd2999e4680feb8e8e4b54a7b49fb603a87b2f27356114d1794",
|
|
7
|
+
"rules/chart-metadata.ts": "8f3377e893d5e2828460b7fe5924fca098334245a9a2fdb90f6b67e490eaf091",
|
|
8
|
+
"rules/no-hardcoded-image.ts": "b00433bd5f4e963ffd56d8a623c00fbfaa40dc37b6d72609d26c91a309d75a90",
|
|
9
|
+
"rules/values-no-helm-tpl.ts": "bd30478da004bafaed6ed6231636f1e2867f5255deb6313c8f6e3505c5c91b11",
|
|
10
|
+
"rules/values-no-secrets.ts": "ec9193fb39764e65e44022e39fda124147350e4d2b4e2e34deaa5db82116f76c",
|
|
11
|
+
"rules/helm-helpers.ts": "c2631bbf261573c553e475cfc2f1b007fc5feb4b1f986b5ba660f3290f4d3eaf",
|
|
12
|
+
"rules/whm005-no-empty-wrapper.ts": "bfecf05449106c639b317eecdcd969ad2a851c4be9ee029380f99278e2a19630",
|
|
13
|
+
"rules/whm101.ts": "6fb49f184cec898640118a3ce14edc5d62c05c694491b0eab4e53dbc10921e8d",
|
|
14
|
+
"rules/whm102.ts": "99ea2a695b49fe39e20b8dccdec5bcf5b189e08ac026318ce633fbf9a226132d",
|
|
15
|
+
"rules/whm103.ts": "80f533af6d9bae924a3cc3ceedf158f13d4138d6ef3115a0e3093f5a0464592d",
|
|
16
|
+
"rules/whm104.ts": "1752338ade55c87dafee23dc09e0150c4ae55ac90eb8b8e9010c89b7ad73503d",
|
|
17
|
+
"rules/whm105.ts": "d710b04267c558839ec99001e0b4604017ef6484e7083585fe160192a5920d69",
|
|
18
|
+
"rules/whm201.ts": "0de1a8cad242feadbb4e9c4e0eebfb94df1f2c2bf535802ebf535067f7859619",
|
|
19
|
+
"rules/whm202.ts": "6a2fc424d8b753113cd13de565ce93dfd1cdf0e606c56977b9e17e92147abf54",
|
|
20
|
+
"rules/whm203.ts": "d98176fd2a2c0d6ddc2d3b37180c5e7be618b47b6c12821df9b51cfe7950d938",
|
|
21
|
+
"rules/whm204.ts": "b5e37aea05662e16e02843e20e4642de82b666026d58e1ebf18c337768fdaff8",
|
|
22
|
+
"rules/whm301.ts": "0ed09a07a8f3d59fb3c2bc330e7b92f7c2eb31655b6a301b06b10b485fe12edc",
|
|
23
|
+
"rules/whm302.ts": "d4cb8cdf776e4bdf0551fc8ab28c584f66efbe6648aef0b325acb07e415da73b",
|
|
24
|
+
"rules/whm401.ts": "a49b41ff9837f2c9ada011d4ee31fffe8e68b278d50badbab32459ea05bc4a01",
|
|
25
|
+
"rules/whm402.ts": "917f85fbc4d31c9b7eec5eda684aa24b847d33bd6f0507bdc435c1796bd66476",
|
|
26
|
+
"rules/whm403.ts": "716cc86eb1a4ba345ef875ac50cef033d017edf50964695ec9399b593d3bcce2",
|
|
27
|
+
"rules/whm404.ts": "e4c7d6877fb3f658ce3a109722fa65b508d7160e186fdf5a63d8ddfc6262e218",
|
|
28
|
+
"rules/whm405.ts": "6d7d230b94e694c3b94dd8fbfe719856c1fbf171ef6dd0552843f56f6c512c03",
|
|
29
|
+
"rules/whm406.ts": "79ce3b318ce53ee74bbf814ae31e0873fe882096dcb5c5369495bed022dbf57d",
|
|
30
|
+
"rules/whm407.ts": "200190f5de2d86d2bcc5aee0b5ce4807919ec727b1fc14576ea17b9b3073838b",
|
|
31
|
+
"rules/whm501.ts": "e6afa7c0eface9820380bf189fe8be58f4a6997dd5b74f76335022650616d080",
|
|
32
|
+
"rules/whm502.ts": "c3ed7b4b5215d96e46eba758aca2171928d8f8fb1eb20452ea6b4393672755b0",
|
|
33
|
+
"skills/chant-helm.md": "94528606c7a972f478d3c716eb7fe5fd79a6520532ccc4f9a3c99e9b0d691025",
|
|
34
|
+
"skills/chant-helm-patterns.md": "9e79e6a46391da46709d8aa57e2825a7cd9eb981cd923f02ad60836c49b2561e",
|
|
35
|
+
"skills/chant-helm-security.md": "bfc367eabceed2e84f1cf94501b407df78aeed963cec104f24a321d0962063c9"
|
|
36
36
|
},
|
|
37
|
-
"composite": "
|
|
37
|
+
"composite": "50e28442c88c8b3fddc2f5c33e9a5ddca3fd44e129c2147d500d91b9f45de1b5"
|
|
38
38
|
}
|
package/dist/manifest.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@intentius/chant-lexicon-helm",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
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",
|
|
@@ -36,17 +36,17 @@
|
|
|
36
36
|
"./types": "./dist/types/index.d.ts"
|
|
37
37
|
},
|
|
38
38
|
"scripts": {
|
|
39
|
-
"generate": "
|
|
40
|
-
"bundle": "
|
|
41
|
-
"validate": "
|
|
42
|
-
"docs": "
|
|
43
|
-
"prepack": "
|
|
39
|
+
"generate": "tsx src/codegen/generate-cli.ts",
|
|
40
|
+
"bundle": "tsx src/package-cli.ts",
|
|
41
|
+
"validate": "tsx src/validate-cli.ts",
|
|
42
|
+
"docs": "tsx src/codegen/docs-cli.ts",
|
|
43
|
+
"prepack": "npm run generate && npm run bundle && npm run validate"
|
|
44
44
|
},
|
|
45
45
|
"dependencies": {
|
|
46
|
-
"@intentius/chant-lexicon-k8s": "
|
|
46
|
+
"@intentius/chant-lexicon-k8s": "*"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@intentius/chant": "
|
|
49
|
+
"@intentius/chant": "*",
|
|
50
50
|
"typescript": "^5.9.3"
|
|
51
51
|
},
|
|
52
52
|
"peerDependencies": {
|
package/src/codegen/docs-cli.ts
CHANGED
package/src/codegen/docs.test.ts
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
|
|
3
3
|
describe("Helm docs generation", () => {
|
|
4
4
|
test("docs module is importable", async () => {
|
|
5
5
|
const mod = await import("./docs");
|
|
6
|
-
expect(mod.generateDocs).
|
|
6
|
+
expect(typeof mod.generateDocs).toBe("function");
|
|
7
7
|
});
|
|
8
8
|
|
|
9
9
|
test("generateDocs function signature accepts options", async () => {
|
package/src/codegen/docs.ts
CHANGED
|
@@ -479,6 +479,6 @@ When you scaffold a new project with \`chant init --lexicon helm\`, the skill is
|
|
|
479
479
|
writeDocsSite(config, result);
|
|
480
480
|
|
|
481
481
|
if (opts?.verbose) {
|
|
482
|
-
console.error(`Generated ${result.pages.
|
|
482
|
+
console.error(`Generated ${result.pages.size} documentation pages`);
|
|
483
483
|
}
|
|
484
484
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { generate } from "./generate";
|
|
3
3
|
|
|
4
4
|
describe("Helm generate pipeline", () => {
|
|
5
5
|
test("generate module is importable", async () => {
|
|
6
6
|
const mod = await import("./generate");
|
|
7
|
-
expect(mod.generate).
|
|
8
|
-
expect(mod.writeGeneratedFiles).
|
|
7
|
+
expect(typeof mod.generate).toBe("function");
|
|
8
|
+
expect(typeof mod.writeGeneratedFiles).toBe("function");
|
|
9
9
|
});
|
|
10
10
|
|
|
11
11
|
test("generates lexicon JSON, types, and index", async () => {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { packageLexicon } from "./package";
|
|
3
3
|
|
|
4
4
|
describe("Helm package pipeline", () => {
|
|
5
5
|
test("packageLexicon is importable", async () => {
|
|
6
6
|
const mod = await import("./package");
|
|
7
|
-
expect(mod.packageLexicon).
|
|
7
|
+
expect(typeof mod.packageLexicon).toBe("function");
|
|
8
8
|
});
|
|
9
9
|
|
|
10
10
|
test("packageLexicon returns a valid result", async () => {
|
package/src/codegen/package.ts
CHANGED
|
@@ -3,11 +3,10 @@
|
|
|
3
3
|
* with Helm-specific manifest building and skill collection.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { createRequire } from "module";
|
|
7
6
|
import { readFileSync } from "fs";
|
|
8
|
-
const require = createRequire(import.meta.url);
|
|
9
7
|
import { join, dirname } from "path";
|
|
10
8
|
import { fileURLToPath } from "url";
|
|
9
|
+
import { helmPlugin } from "../plugin";
|
|
11
10
|
import {
|
|
12
11
|
packagePipeline,
|
|
13
12
|
collectSkills,
|
|
@@ -52,7 +51,6 @@ export async function packageLexicon(opts: PackageOptions = {}): Promise<Package
|
|
|
52
51
|
srcDir: pkgDir,
|
|
53
52
|
|
|
54
53
|
collectSkills: () => {
|
|
55
|
-
const { helmPlugin } = require("../plugin");
|
|
56
54
|
const skillDefs = helmPlugin.skills?.() ?? [];
|
|
57
55
|
return collectSkills(skillDefs);
|
|
58
56
|
},
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
|
|
3
3
|
import { HelmWebApp } from "./helm-web-app";
|
|
4
4
|
import { HelmStatefulService } from "./helm-stateful-service";
|
|
@@ -228,7 +228,7 @@ export const HelmMonitoredService = Composite<HelmMonitoredServiceProps>((props)
|
|
|
228
228
|
interval: values.monitoring.scrapeInterval,
|
|
229
229
|
}],
|
|
230
230
|
},
|
|
231
|
-
}) as Record<string, unknown>,
|
|
231
|
+
}) as unknown as Record<string, unknown>,
|
|
232
232
|
defs?.serviceMonitor,
|
|
233
233
|
));
|
|
234
234
|
|
|
@@ -267,7 +267,7 @@ export const HelmMonitoredService = Composite<HelmMonitoredServiceProps>((props)
|
|
|
267
267
|
rules: toYaml(values.alerting.rules),
|
|
268
268
|
}],
|
|
269
269
|
},
|
|
270
|
-
}) as Record<string, unknown>,
|
|
270
|
+
}) as unknown as Record<string, unknown>,
|
|
271
271
|
defs?.prometheusRule,
|
|
272
272
|
));
|
|
273
273
|
}
|
|
@@ -134,7 +134,7 @@ export const HelmNamespaceEnv = Composite<HelmNamespaceEnvProps>((props) => {
|
|
|
134
134
|
spec: {
|
|
135
135
|
hard: toYaml(values.resourceQuota.hard),
|
|
136
136
|
},
|
|
137
|
-
}) as Record<string, unknown>,
|
|
137
|
+
}) as unknown as Record<string, unknown>,
|
|
138
138
|
defs?.resourceQuota,
|
|
139
139
|
));
|
|
140
140
|
}
|
|
@@ -155,7 +155,7 @@ export const HelmNamespaceEnv = Composite<HelmNamespaceEnvProps>((props) => {
|
|
|
155
155
|
defaultRequest: toYaml(values.limitRange.defaultRequest),
|
|
156
156
|
}],
|
|
157
157
|
},
|
|
158
|
-
}) as Record<string, unknown>,
|
|
158
|
+
}) as unknown as Record<string, unknown>,
|
|
159
159
|
defs?.limitRange,
|
|
160
160
|
));
|
|
161
161
|
}
|
|
@@ -173,7 +173,7 @@ export const HelmNamespaceEnv = Composite<HelmNamespaceEnvProps>((props) => {
|
|
|
173
173
|
podSelector: {},
|
|
174
174
|
policyTypes: ["Ingress", "Egress"],
|
|
175
175
|
},
|
|
176
|
-
}) as Record<string, unknown>,
|
|
176
|
+
}) as unknown as Record<string, unknown>,
|
|
177
177
|
defs?.networkPolicy,
|
|
178
178
|
));
|
|
179
179
|
}
|
|
@@ -96,7 +96,7 @@ export const HelmSecureIngress = Composite<HelmSecureIngressProps>((props) => {
|
|
|
96
96
|
},
|
|
97
97
|
}),
|
|
98
98
|
},
|
|
99
|
-
}) as Record<string, unknown>,
|
|
99
|
+
}) as unknown as Record<string, unknown>,
|
|
100
100
|
defs?.ingress,
|
|
101
101
|
));
|
|
102
102
|
|
|
@@ -117,7 +117,7 @@ export const HelmSecureIngress = Composite<HelmSecureIngressProps>((props) => {
|
|
|
117
117
|
},
|
|
118
118
|
dnsNames: values.ingress.hosts,
|
|
119
119
|
},
|
|
120
|
-
}) as Record<string, unknown>,
|
|
120
|
+
}) as unknown as Record<string, unknown>,
|
|
121
121
|
defs?.certificate,
|
|
122
122
|
));
|
|
123
123
|
|
package/src/coverage.test.ts
CHANGED
package/src/helpers.test.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { stripTemplateExpressions, classifyExpression } from "./template-stripper";
|
|
3
3
|
import { HelmParser } from "./parser";
|
|
4
4
|
import { HelmGenerator } from "./generator";
|
package/src/index.ts
CHANGED
|
@@ -7,6 +7,10 @@ export { helmPlugin } from "./plugin";
|
|
|
7
7
|
// Resources
|
|
8
8
|
export { Chart, Values, ValuesOverride, HelmTest, HelmNotes, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
|
|
9
9
|
|
|
10
|
+
// HelmRender — render an upstream chart at chant build time
|
|
11
|
+
export { HelmRender } from "./render";
|
|
12
|
+
export type { HelmRenderProps } from "./render";
|
|
13
|
+
|
|
10
14
|
// Intrinsics
|
|
11
15
|
export {
|
|
12
16
|
HelmTpl,
|
package/src/intrinsics.test.ts
CHANGED
package/src/intrinsics.ts
CHANGED
|
@@ -308,7 +308,9 @@ export function quote(val: HelmTpl): HelmTpl {
|
|
|
308
308
|
* `printf("%s:%s", values.image.repository, values.image.tag)`
|
|
309
309
|
* → `{{ printf "%s:%s" .Values.image.repository .Values.image.tag }}`
|
|
310
310
|
*/
|
|
311
|
-
export function printf(fmt: string, ...args: HelmTpl[]): HelmTpl {
|
|
311
|
+
export function printf(fmt: string, ...args: (HelmTpl | string)[]): HelmTpl {
|
|
312
|
+
// extractExpr already accepts both HelmTpl and string; widening here is
|
|
313
|
+
// purely additive (existing callers passing HelmTpl[] keep working).
|
|
312
314
|
const argExprs = args.map(extractExpr).join(" ");
|
|
313
315
|
return new HelmTpl(`{{ printf "${fmt}" ${argExprs} }}`);
|
|
314
316
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm101 } from "./whm101";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm101 } from "./whm101";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm102 } from "./whm102";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm103 } from "./whm103";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm104 } from "./whm104";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm105 } from "./whm105";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm201 } from "./whm201";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm202 } from "./whm202";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm203 } from "./whm203";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm204 } from "./whm204";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm301 } from "./whm301";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm302 } from "./whm302";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm401 } from "./whm401";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm402 } from "./whm402";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm403 } from "./whm403";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm404 } from "./whm404";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm405 } from "./whm405";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm406 } from "./whm406";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm407 } from "./whm407";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm501 } from "./whm501";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
3
3
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
4
4
|
import { whm502 } from "./whm502";
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { describe, test, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
const execMock = vi.fn();
|
|
4
|
+
vi.mock("node:child_process", async () => {
|
|
5
|
+
const actual = await vi.importActual<typeof import("node:child_process")>("node:child_process");
|
|
6
|
+
return { ...actual, exec: (cmd: string, cb: (err: Error | null, out: { stdout: string; stderr: string }) => void) => {
|
|
7
|
+
Promise.resolve(execMock(cmd)).then(
|
|
8
|
+
(out) => cb(null, out),
|
|
9
|
+
(err) => cb(err as Error, { stdout: "", stderr: "" }),
|
|
10
|
+
);
|
|
11
|
+
} };
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
const { listArtifacts } = await import("./list-artifacts");
|
|
15
|
+
|
|
16
|
+
describe("helm listArtifacts", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
execMock.mockReset();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("queries `helm list -A -o json` and maps releases to artifacts", async () => {
|
|
22
|
+
let receivedCmd = "";
|
|
23
|
+
execMock.mockImplementation((cmd: string) => {
|
|
24
|
+
receivedCmd = cmd;
|
|
25
|
+
return {
|
|
26
|
+
stdout: JSON.stringify([
|
|
27
|
+
{
|
|
28
|
+
name: "web", namespace: "default", revision: "1",
|
|
29
|
+
updated: "2026-05-09 10:00:00.000000 +0000 UTC",
|
|
30
|
+
status: "deployed", chart: "web-1.0.0", app_version: "1.0",
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "redis", namespace: "infra", revision: "3",
|
|
34
|
+
updated: "2026-05-09 09:00:00.000000 +0000 UTC",
|
|
35
|
+
status: "deployed", chart: "redis-7.4.0", app_version: "7.4",
|
|
36
|
+
},
|
|
37
|
+
]),
|
|
38
|
+
stderr: "",
|
|
39
|
+
};
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
43
|
+
|
|
44
|
+
expect(receivedCmd).toBe("helm list -A -o json");
|
|
45
|
+
expect(Object.keys(result).sort()).toEqual(["release/default/web", "release/infra/redis"]);
|
|
46
|
+
expect(result["release/default/web"]).toEqual({
|
|
47
|
+
type: "Helm::Release",
|
|
48
|
+
physicalId: "default/web",
|
|
49
|
+
status: "deployed",
|
|
50
|
+
lastUpdated: "2026-05-09 10:00:00.000000 +0000 UTC",
|
|
51
|
+
attributes: { chart: "web-1.0.0", revision: "1", appVersion: "1.0", namespace: "default" },
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("helm binary not installed → returns {} cleanly", async () => {
|
|
56
|
+
execMock.mockImplementation(() => { throw new Error("helm: command not found"); });
|
|
57
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
58
|
+
expect(result).toEqual({});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("empty cluster (no releases) → returns {}", async () => {
|
|
62
|
+
execMock.mockResolvedValue({ stdout: "[]", stderr: "" });
|
|
63
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
64
|
+
expect(result).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("status mapping for non-deployed states surfaces correctly", async () => {
|
|
68
|
+
execMock.mockResolvedValue({
|
|
69
|
+
stdout: JSON.stringify([
|
|
70
|
+
{ name: "broken", namespace: "default", revision: "2", status: "failed", chart: "x-1.0", app_version: "1" },
|
|
71
|
+
]),
|
|
72
|
+
stderr: "",
|
|
73
|
+
});
|
|
74
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
75
|
+
expect(result["release/default/broken"].status).toBe("failed");
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("revision attribute changes between releases (drift signal)", async () => {
|
|
79
|
+
execMock.mockResolvedValue({
|
|
80
|
+
stdout: JSON.stringify([
|
|
81
|
+
{ name: "web", namespace: "default", revision: "5", status: "deployed", chart: "web-2.0.0", app_version: "2.0" },
|
|
82
|
+
]),
|
|
83
|
+
stderr: "",
|
|
84
|
+
});
|
|
85
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
86
|
+
expect(result["release/default/web"].attributes).toMatchObject({ revision: "5", chart: "web-2.0.0" });
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("malformed JSON output → returns {} (don't fail the snapshot)", async () => {
|
|
90
|
+
execMock.mockResolvedValue({ stdout: "not json", stderr: "" });
|
|
91
|
+
const result = await listArtifacts({ environment: "prod", entities: new Map() });
|
|
92
|
+
expect(result).toEqual({});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live introspection of Helm releases via `helm list -A -o json`.
|
|
3
|
+
*
|
|
4
|
+
* The Helm lexicon's chant entities describe chart-authoring primitives
|
|
5
|
+
* (Chart.yaml, templates/, values.yaml). The runtime concept — a Helm
|
|
6
|
+
* release installed in a kubeconfig context — is created by `helm install`
|
|
7
|
+
* outside chant's entity model. This implementation reports those releases
|
|
8
|
+
* as artifacts so `state diff --live` / `WatchOp` can detect manual
|
|
9
|
+
* installs/upgrades/rollbacks that slip past CI.
|
|
10
|
+
*
|
|
11
|
+
* Helm-not-installed (binary missing) → returns `{}` cleanly so other
|
|
12
|
+
* lexicons' snapshots aren't blocked.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { exec } from "node:child_process";
|
|
16
|
+
import { promisify } from "node:util";
|
|
17
|
+
import type { ArtifactMetadata } from "@intentius/chant/lexicon";
|
|
18
|
+
|
|
19
|
+
const execAsync = promisify(exec);
|
|
20
|
+
|
|
21
|
+
interface HelmListEntry {
|
|
22
|
+
name?: string;
|
|
23
|
+
namespace?: string;
|
|
24
|
+
revision?: string;
|
|
25
|
+
updated?: string;
|
|
26
|
+
status?: string;
|
|
27
|
+
chart?: string;
|
|
28
|
+
app_version?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function pruneUndefined<T extends Record<string, unknown>>(obj: T): Record<string, unknown> {
|
|
32
|
+
const out: Record<string, unknown> = {};
|
|
33
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
34
|
+
if (v !== undefined) out[k] = v;
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function listArtifacts(_options: {
|
|
40
|
+
environment: string;
|
|
41
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
42
|
+
}): Promise<Record<string, ArtifactMetadata>> {
|
|
43
|
+
const result: Record<string, ArtifactMetadata> = {};
|
|
44
|
+
|
|
45
|
+
let stdout: string;
|
|
46
|
+
try {
|
|
47
|
+
({ stdout } = await execAsync("helm list -A -o json"));
|
|
48
|
+
} catch {
|
|
49
|
+
// Binary not installed, no kubeconfig, or some other error — return
|
|
50
|
+
// empty rather than blocking the whole snapshot.
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
let entries: HelmListEntry[];
|
|
55
|
+
try {
|
|
56
|
+
entries = JSON.parse(stdout);
|
|
57
|
+
} catch {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (!entry.name || !entry.namespace) continue;
|
|
63
|
+
const key = `release/${entry.namespace}/${entry.name}`;
|
|
64
|
+
result[key] = {
|
|
65
|
+
type: "Helm::Release",
|
|
66
|
+
physicalId: `${entry.namespace}/${entry.name}`,
|
|
67
|
+
status: entry.status ?? "unknown",
|
|
68
|
+
lastUpdated: entry.updated,
|
|
69
|
+
attributes: pruneUndefined({
|
|
70
|
+
chart: entry.chart,
|
|
71
|
+
revision: entry.revision,
|
|
72
|
+
appVersion: entry.app_version,
|
|
73
|
+
namespace: entry.namespace,
|
|
74
|
+
}),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return result;
|
|
79
|
+
}
|
package/src/lsp/hover.test.ts
CHANGED
package/src/package-cli.ts
CHANGED
package/src/plugin.test.ts
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -65,13 +65,13 @@ export const helmPlugin: LexiconPlugin = {
|
|
|
65
65
|
},
|
|
66
66
|
|
|
67
67
|
mcpTools() {
|
|
68
|
-
return [createDiffTool(helmSerializer, "Compare current Helm chart build output against previous output")];
|
|
68
|
+
return [createDiffTool(helmSerializer, "Compare current Helm chart build output against previous output", "helm")];
|
|
69
69
|
},
|
|
70
70
|
|
|
71
71
|
mcpResources() {
|
|
72
72
|
return [
|
|
73
73
|
{
|
|
74
|
-
uri: "resource-catalog",
|
|
74
|
+
uri: "helm:resource-catalog",
|
|
75
75
|
name: "Helm Chart Resource Catalog",
|
|
76
76
|
description: "JSON list of all supported Helm chart resource types",
|
|
77
77
|
mimeType: "application/json",
|
|
@@ -374,4 +374,9 @@ export const service = new Service({
|
|
|
374
374
|
|
|
375
375
|
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
376
376
|
},
|
|
377
|
+
|
|
378
|
+
async listArtifacts(options) {
|
|
379
|
+
const { listArtifacts } = await import("./list-artifacts");
|
|
380
|
+
return listArtifacts(options);
|
|
381
|
+
},
|
|
377
382
|
};
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { describe, test, expect, beforeAll } from "vitest";
|
|
2
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
|
|
7
|
+
import { HelmRender } from "./render";
|
|
8
|
+
|
|
9
|
+
const FIXTURE_DIR = join(tmpdir(), "chant-helm-render-fixture");
|
|
10
|
+
const CHART_DIR = join(FIXTURE_DIR, "tiny-chart");
|
|
11
|
+
const REPO_DIR = join(FIXTURE_DIR, "repo");
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Builds a tiny self-contained chart that emits one Deployment + one
|
|
15
|
+
* Service, packages it as a local chart repo, and serves it via file://.
|
|
16
|
+
* Avoids network access in tests.
|
|
17
|
+
*/
|
|
18
|
+
function maybeSetupFixture(): boolean {
|
|
19
|
+
// If helm isn't on PATH, the test will be skipped at the call site.
|
|
20
|
+
try {
|
|
21
|
+
execFileSync("helm", ["version"], { stdio: "ignore" });
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (existsSync(FIXTURE_DIR)) {
|
|
27
|
+
rmSync(FIXTURE_DIR, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
mkdirSync(join(CHART_DIR, "templates"), { recursive: true });
|
|
30
|
+
mkdirSync(REPO_DIR, { recursive: true });
|
|
31
|
+
|
|
32
|
+
writeFileSync(
|
|
33
|
+
join(CHART_DIR, "Chart.yaml"),
|
|
34
|
+
`apiVersion: v2
|
|
35
|
+
name: tiny-chart
|
|
36
|
+
description: Minimal chart for chant-lexicon-helm HelmRender tests
|
|
37
|
+
type: application
|
|
38
|
+
version: 0.1.0
|
|
39
|
+
appVersion: "1.0"
|
|
40
|
+
`,
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
writeFileSync(
|
|
44
|
+
join(CHART_DIR, "values.yaml"),
|
|
45
|
+
`replicaCount: 1
|
|
46
|
+
image:
|
|
47
|
+
repository: nginx
|
|
48
|
+
tag: "latest"
|
|
49
|
+
service:
|
|
50
|
+
port: 80
|
|
51
|
+
`,
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
writeFileSync(
|
|
55
|
+
join(CHART_DIR, "templates", "deployment.yaml"),
|
|
56
|
+
`apiVersion: apps/v1
|
|
57
|
+
kind: Deployment
|
|
58
|
+
metadata:
|
|
59
|
+
name: {{ .Release.Name }}-tiny
|
|
60
|
+
spec:
|
|
61
|
+
replicas: {{ .Values.replicaCount }}
|
|
62
|
+
selector:
|
|
63
|
+
matchLabels:
|
|
64
|
+
app: {{ .Release.Name }}
|
|
65
|
+
template:
|
|
66
|
+
metadata:
|
|
67
|
+
labels:
|
|
68
|
+
app: {{ .Release.Name }}
|
|
69
|
+
spec:
|
|
70
|
+
containers:
|
|
71
|
+
- name: app
|
|
72
|
+
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
|
73
|
+
`,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
writeFileSync(
|
|
77
|
+
join(CHART_DIR, "templates", "service.yaml"),
|
|
78
|
+
`apiVersion: v1
|
|
79
|
+
kind: Service
|
|
80
|
+
metadata:
|
|
81
|
+
name: {{ .Release.Name }}-tiny
|
|
82
|
+
spec:
|
|
83
|
+
selector:
|
|
84
|
+
app: {{ .Release.Name }}
|
|
85
|
+
ports:
|
|
86
|
+
- port: {{ .Values.service.port }}
|
|
87
|
+
`,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Package the chart into a .tgz and create a repo index.yaml.
|
|
91
|
+
execFileSync("helm", ["package", CHART_DIR, "-d", REPO_DIR], { stdio: "ignore" });
|
|
92
|
+
execFileSync("helm", ["repo", "index", REPO_DIR], { stdio: "ignore" });
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const fixtureAvailable = maybeSetupFixture();
|
|
97
|
+
|
|
98
|
+
describe.skipIf(!fixtureAvailable)("HelmRender", () => {
|
|
99
|
+
beforeAll(() => {
|
|
100
|
+
// Ensure fixture is fresh for the suite.
|
|
101
|
+
expect(fixtureAvailable).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("renders a local chart into K8s declarables (Deployment + Service)", () => {
|
|
105
|
+
const result = HelmRender({
|
|
106
|
+
name: "rel",
|
|
107
|
+
chart: CHART_DIR,
|
|
108
|
+
noCache: true,
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// Composite returns its members under .members; iterate keys.
|
|
112
|
+
const members = result.members as Record<string, unknown>;
|
|
113
|
+
const keys = Object.keys(members);
|
|
114
|
+
expect(keys.length).toBeGreaterThanOrEqual(2);
|
|
115
|
+
const deployment = keys.find((k) => k.startsWith("Deployment_"));
|
|
116
|
+
const service = keys.find((k) => k.startsWith("Service_"));
|
|
117
|
+
expect(deployment).toBeDefined();
|
|
118
|
+
expect(service).toBeDefined();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("createNamespace adds a Namespace declarable", () => {
|
|
122
|
+
const result = HelmRender({
|
|
123
|
+
name: "rel",
|
|
124
|
+
chart: CHART_DIR,
|
|
125
|
+
namespace: "myns",
|
|
126
|
+
createNamespace: true,
|
|
127
|
+
noCache: true,
|
|
128
|
+
});
|
|
129
|
+
const keys = Object.keys(result.members as Record<string, unknown>);
|
|
130
|
+
expect(keys).toContain("__namespace");
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("values overrides are applied (replicaCount: 3)", () => {
|
|
134
|
+
const result = HelmRender({
|
|
135
|
+
name: "rel",
|
|
136
|
+
chart: CHART_DIR,
|
|
137
|
+
values: { replicaCount: 3 },
|
|
138
|
+
noCache: true,
|
|
139
|
+
});
|
|
140
|
+
const members = result.members as Record<string, unknown>;
|
|
141
|
+
const deploymentKey = Object.keys(members).find((k) => k.startsWith("Deployment_"));
|
|
142
|
+
expect(deploymentKey).toBeDefined();
|
|
143
|
+
const dep = members[deploymentKey!] as {
|
|
144
|
+
props: { spec: { replicas: number } };
|
|
145
|
+
};
|
|
146
|
+
expect(dep.props.spec.replicas).toBe(3);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("cache reuse: second render with same args skips helm CLI", () => {
|
|
150
|
+
// First, render with cache enabled.
|
|
151
|
+
const first = HelmRender({
|
|
152
|
+
name: "rel",
|
|
153
|
+
chart: CHART_DIR,
|
|
154
|
+
});
|
|
155
|
+
expect(Object.keys(first.members as Record<string, unknown>).length).toBeGreaterThanOrEqual(2);
|
|
156
|
+
|
|
157
|
+
// Now sabotage `helm` by pointing PATH at an empty dir — if cache is used,
|
|
158
|
+
// the second call should still succeed.
|
|
159
|
+
const emptyDir = join(tmpdir(), "chant-helm-render-empty-path");
|
|
160
|
+
if (!existsSync(emptyDir)) mkdirSync(emptyDir);
|
|
161
|
+
const origPath = process.env.PATH;
|
|
162
|
+
process.env.PATH = emptyDir;
|
|
163
|
+
try {
|
|
164
|
+
const second = HelmRender({
|
|
165
|
+
name: "rel",
|
|
166
|
+
chart: CHART_DIR,
|
|
167
|
+
});
|
|
168
|
+
expect(Object.keys(second.members as Record<string, unknown>).length).toBeGreaterThanOrEqual(2);
|
|
169
|
+
} finally {
|
|
170
|
+
process.env.PATH = origPath;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
});
|
package/src/render.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HelmRender — render an upstream Helm chart at chant build time.
|
|
3
|
+
*
|
|
4
|
+
* Most chant projects that want to install third-party operators (ESO,
|
|
5
|
+
* cert-manager, ingress-nginx, etc.) ran `helm template` or `helm install`
|
|
6
|
+
* as a separate deploy phase. That meant the chant build output was
|
|
7
|
+
* incomplete — `kubectl apply -f dist/...yaml` didn't carry those operators.
|
|
8
|
+
*
|
|
9
|
+
* `HelmRender({ repo, chart, version, values })` resolves at synth time:
|
|
10
|
+
* 1. Shells out to `helm template` (requires the `helm` binary in PATH).
|
|
11
|
+
* 2. Parses the resulting multi-document YAML.
|
|
12
|
+
* 3. Emits each rendered K8s manifest as a Declarable in the build output.
|
|
13
|
+
* 4. Caches the rendered output under `~/.chant/helm-renders/<hash>/`
|
|
14
|
+
* keyed by (repo, chart, version, values) so subsequent builds skip
|
|
15
|
+
* network access.
|
|
16
|
+
*
|
|
17
|
+
* The lexicon must include both `helm` and `k8s` (since rendered manifests
|
|
18
|
+
* are k8s resources).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* import { HelmRender } from "@intentius/chant-lexicon-helm";
|
|
22
|
+
*
|
|
23
|
+
* export const eso = HelmRender({
|
|
24
|
+
* name: "external-secrets",
|
|
25
|
+
* repo: "https://charts.external-secrets.io",
|
|
26
|
+
* chart: "external-secrets",
|
|
27
|
+
* version: "0.10.4",
|
|
28
|
+
* namespace: "external-secrets",
|
|
29
|
+
* createNamespace: true,
|
|
30
|
+
* values: { installCRDs: true },
|
|
31
|
+
* });
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { execFileSync } from "node:child_process";
|
|
35
|
+
import { createHash } from "node:crypto";
|
|
36
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
37
|
+
import { homedir, tmpdir } from "node:os";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
|
|
40
|
+
import { Composite } from "@intentius/chant";
|
|
41
|
+
import { Deployment } from "@intentius/chant-lexicon-k8s/generated";
|
|
42
|
+
import yaml from "js-yaml";
|
|
43
|
+
|
|
44
|
+
export interface HelmRenderProps {
|
|
45
|
+
/** Logical name for the render (used in cache key + composite name). */
|
|
46
|
+
name: string;
|
|
47
|
+
/** Chart repo URL, e.g. https://charts.external-secrets.io */
|
|
48
|
+
repo: string;
|
|
49
|
+
/** Chart name, e.g. "external-secrets" */
|
|
50
|
+
chart: string;
|
|
51
|
+
/** Pinned chart version, e.g. "0.10.4" */
|
|
52
|
+
version: string;
|
|
53
|
+
/** Target namespace passed to `helm template --namespace`. */
|
|
54
|
+
namespace?: string;
|
|
55
|
+
/** Also emit a Namespace manifest. Default: false. */
|
|
56
|
+
createNamespace?: boolean;
|
|
57
|
+
/** Helm values overrides (written to a values.yaml then passed via -f). */
|
|
58
|
+
values?: Record<string, unknown>;
|
|
59
|
+
/**
|
|
60
|
+
* Skip the on-disk cache. Default: false. Tests pass `true` to force a
|
|
61
|
+
* fresh render.
|
|
62
|
+
*/
|
|
63
|
+
noCache?: boolean;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface RenderedDoc {
|
|
67
|
+
apiVersion?: string;
|
|
68
|
+
kind?: string;
|
|
69
|
+
metadata?: { name?: string; namespace?: string; [k: string]: unknown };
|
|
70
|
+
[k: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const CACHE_ROOT = join(homedir(), ".chant", "helm-renders");
|
|
74
|
+
|
|
75
|
+
function cacheKey(props: HelmRenderProps): string {
|
|
76
|
+
const stable = JSON.stringify({
|
|
77
|
+
repo: props.repo,
|
|
78
|
+
chart: props.chart,
|
|
79
|
+
version: props.version,
|
|
80
|
+
namespace: props.namespace ?? null,
|
|
81
|
+
values: props.values ?? null,
|
|
82
|
+
});
|
|
83
|
+
return createHash("sha256").update(stable).digest("hex").slice(0, 16);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function renderViaHelm(props: HelmRenderProps): string {
|
|
87
|
+
// Write values overrides to a tempfile if any.
|
|
88
|
+
let valuesArgs: string[] = [];
|
|
89
|
+
if (props.values && Object.keys(props.values).length > 0) {
|
|
90
|
+
const valuesPath = join(tmpdir(), `chant-helm-values-${cacheKey(props)}.yaml`);
|
|
91
|
+
writeFileSync(valuesPath, yaml.dump(props.values));
|
|
92
|
+
valuesArgs = ["--values", valuesPath];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// When `repo` is set, helm fetches the chart by name+version from the repo.
|
|
96
|
+
// When `repo` is absent, treat `chart` as a local path.
|
|
97
|
+
const fetchArgs: string[] = [];
|
|
98
|
+
if (props.repo) {
|
|
99
|
+
fetchArgs.push("--repo", props.repo);
|
|
100
|
+
if (props.version) fetchArgs.push("--version", props.version);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const args = [
|
|
104
|
+
"template",
|
|
105
|
+
props.name,
|
|
106
|
+
props.chart,
|
|
107
|
+
...fetchArgs,
|
|
108
|
+
...(props.namespace ? ["--namespace", props.namespace] : []),
|
|
109
|
+
...valuesArgs,
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const out = execFileSync("helm", args, {
|
|
114
|
+
encoding: "utf8",
|
|
115
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
116
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
117
|
+
});
|
|
118
|
+
return out;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const stderr =
|
|
121
|
+
err && typeof err === "object" && "stderr" in err
|
|
122
|
+
? String((err as { stderr: unknown }).stderr)
|
|
123
|
+
: String(err);
|
|
124
|
+
throw new Error(
|
|
125
|
+
`HelmRender failed for ${props.repo}/${props.chart}@${props.version}:\n${stderr}\n` +
|
|
126
|
+
`Hint: ensure the 'helm' CLI is on PATH (helm version) and the chart is reachable.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function loadOrRender(props: HelmRenderProps): string {
|
|
132
|
+
if (props.noCache) {
|
|
133
|
+
return renderViaHelm(props);
|
|
134
|
+
}
|
|
135
|
+
const cacheDir = join(CACHE_ROOT, cacheKey(props));
|
|
136
|
+
const cachePath = join(cacheDir, "manifests.yaml");
|
|
137
|
+
if (existsSync(cachePath)) {
|
|
138
|
+
return readFileSync(cachePath, "utf8");
|
|
139
|
+
}
|
|
140
|
+
const out = renderViaHelm(props);
|
|
141
|
+
try {
|
|
142
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
143
|
+
writeFileSync(cachePath, out);
|
|
144
|
+
} catch {
|
|
145
|
+
// Cache write failure is non-fatal — the render is still in memory.
|
|
146
|
+
}
|
|
147
|
+
return out;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function parseMultiDoc(text: string): RenderedDoc[] {
|
|
151
|
+
const docs = yaml.loadAll(text);
|
|
152
|
+
return docs
|
|
153
|
+
.filter((d): d is RenderedDoc => d !== null && typeof d === "object")
|
|
154
|
+
.filter((d) => d.kind && d.apiVersion);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Sanitize an arbitrary string into a valid TS/JS identifier suffix.
|
|
159
|
+
* Used to derive Composite Members keys from manifest kind+name pairs.
|
|
160
|
+
*/
|
|
161
|
+
function safeKey(input: string): string {
|
|
162
|
+
return input.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export const HelmRender = Composite<HelmRenderProps>((props) => {
|
|
166
|
+
const yamlText = loadOrRender(props);
|
|
167
|
+
const docs = parseMultiDoc(yamlText);
|
|
168
|
+
|
|
169
|
+
const out: Record<string, InstanceType<typeof Deployment>> = {};
|
|
170
|
+
|
|
171
|
+
if (props.createNamespace && props.namespace) {
|
|
172
|
+
out["__namespace"] = new Deployment({
|
|
173
|
+
apiVersion: "v1",
|
|
174
|
+
kind: "Namespace",
|
|
175
|
+
metadata: { name: props.namespace },
|
|
176
|
+
} as Record<string, unknown>);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const usedKeys = new Set<string>();
|
|
180
|
+
for (let i = 0; i < docs.length; i++) {
|
|
181
|
+
const doc = docs[i];
|
|
182
|
+
const kind = doc.kind ?? "Unknown";
|
|
183
|
+
const name = doc.metadata?.name ?? `doc${i}`;
|
|
184
|
+
let key = safeKey(`${kind}_${name}`);
|
|
185
|
+
// Disambiguate on collision (e.g. same kind+name across docs).
|
|
186
|
+
let collisionN = 2;
|
|
187
|
+
while (usedKeys.has(key)) {
|
|
188
|
+
key = `${safeKey(`${kind}_${name}`)}_${collisionN++}`;
|
|
189
|
+
}
|
|
190
|
+
usedKeys.add(key);
|
|
191
|
+
out[key] = new Deployment(doc as Record<string, unknown>);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return out;
|
|
195
|
+
}, "HelmRender");
|
package/src/serializer.test.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { describe, test, expect } from "
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
2
|
import { createResource, createProperty } from "@intentius/chant/runtime";
|
|
3
3
|
import type { Declarable } from "@intentius/chant/declarable";
|
|
4
4
|
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
5
5
|
import { helmSerializer } from "./serializer";
|
|
6
|
+
import { generateHelpers } from "./helpers";
|
|
6
7
|
import { Chart, Values, ValuesOverride, HelmNotes, HelmTest, HelmHook, HelmDependency, HelmMaintainer, HelmCRD } from "./resources";
|
|
7
8
|
import { values, include, printf, toYaml, quote, helmDefault, required, If, ElseIf, Range, With, Release, ChartRef, Capabilities, runtimeSlot } from "./intrinsics";
|
|
8
9
|
|
|
@@ -318,12 +319,12 @@ describe("resource-level If", () => {
|
|
|
318
319
|
const template = result.files!["templates/ingress.yaml"];
|
|
319
320
|
|
|
320
321
|
expect(template).toBeDefined();
|
|
321
|
-
expect(template
|
|
322
|
+
expect(template.startsWith("{{- if .Values.ingress.enabled }}\n")).toBe(true);
|
|
322
323
|
expect(template).toContain("apiVersion: networking.k8s.io/v1");
|
|
323
324
|
expect(template).toContain("kind: Ingress");
|
|
324
325
|
expect(template).toContain('{{ include "test.fullname" . }}');
|
|
325
326
|
expect(template).toContain("{{ .Values.ingress.host }}");
|
|
326
|
-
expect(template.trimEnd()
|
|
327
|
+
expect(template.trimEnd().endsWith("{{- end }}")).toBe(true);
|
|
327
328
|
});
|
|
328
329
|
|
|
329
330
|
test("does not wrap non-conditional resources", () => {
|
|
@@ -905,7 +906,6 @@ describe("ValuesOverride serialization", () => {
|
|
|
905
906
|
|
|
906
907
|
describe("helpers", () => {
|
|
907
908
|
test("generateHelpers includes all standard templates", () => {
|
|
908
|
-
const { generateHelpers } = require("./helpers");
|
|
909
909
|
const content = generateHelpers({ chartName: "my-chart" });
|
|
910
910
|
|
|
911
911
|
expect(content).toContain('define "my-chart.name"');
|
|
@@ -917,7 +917,6 @@ describe("helpers", () => {
|
|
|
917
917
|
});
|
|
918
918
|
|
|
919
919
|
test("generateHelpers respects includeServiceAccount=false", () => {
|
|
920
|
-
const { generateHelpers } = require("./helpers");
|
|
921
920
|
const content = generateHelpers({ chartName: "test", includeServiceAccount: false });
|
|
922
921
|
|
|
923
922
|
expect(content).toContain('define "test.name"');
|
package/src/serializer.ts
CHANGED
|
@@ -111,7 +111,7 @@ function emitHelmYAML(value: unknown, indent: number, valuesContext: boolean = f
|
|
|
111
111
|
|
|
112
112
|
// Detect HelmTpl / Intrinsic objects via INTRINSIC_MARKER before string check
|
|
113
113
|
if (typeof value === "object" && value !== null && INTRINSIC_MARKER in value) {
|
|
114
|
-
const tplObj = value as { toJSON(): unknown };
|
|
114
|
+
const tplObj = value as unknown as { toJSON(): unknown };
|
|
115
115
|
if (valuesContext) {
|
|
116
116
|
// In values.yaml context: emit empty placeholder — actual value comes from override files
|
|
117
117
|
return "''";
|
package/src/validate-cli.ts
CHANGED
package/src/validate.test.ts
CHANGED