@intentius/chant-lexicon-helm 0.124.48 → 0.124.50

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/config.d.ts CHANGED
@@ -98,6 +98,18 @@ export interface HelmChantConfig {
98
98
  * ```
99
99
  */
100
100
  charts?: Record<string, HelmProjectChart>;
101
+ /**
102
+ * One workspace member read into the graph (#3179). `chant workspace graph`
103
+ * writes this into the reader project it makes for a `helm-chart` member; it
104
+ * is not meant to be written by hand. `dir` is the chart directory, absolute;
105
+ * `label` names the entities, the member's name. The chart is rendered with
106
+ * `helm template`; with no `helm` binary the graph carries no objects for it
107
+ * and says why on `meta.helmMember`.
108
+ */
109
+ member?: {
110
+ dir: string;
111
+ label?: string;
112
+ };
101
113
  }
102
114
  /**
103
115
  * One `helm.charts` entry: a chart in a classic repository (`repo` + `chart`),
@@ -139,6 +151,10 @@ export declare const helmConfigSchema: z.ZodObject<{
139
151
  }, z.core.$strict>, z.ZodObject<{
140
152
  path: z.ZodString;
141
153
  }, z.core.$strict>]>>>;
154
+ member: z.ZodOptional<z.ZodObject<{
155
+ dir: z.ZodString;
156
+ label: z.ZodOptional<z.ZodString>;
157
+ }, z.core.$strict>>;
142
158
  }, z.core.$strict>;
143
159
  declare module "@intentius/chant/config" {
144
160
  interface ChantConfig {
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAyB,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,yFAAyF;IACzF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,gGAAgG;AAChG,MAAM,WAAW,qBAAsB,SAAQ,2BAA2B;IACxE,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,qBAAqB,CAAC;AAEtE,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IACjE;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;CAC3C;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,eAAO,MAAM,2BAA2B;;;kBAKtC,CAAC;AAEH,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,QAA6B,CAAC;AAE5D,eAAO,MAAM,sBAAsB;;;;;;;oBAQjC,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;kBAK3B,CAAC;AAEH,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,WAAW;QACnB,IAAI,CAAC,EAAE,eAAe,CAAC;KACxB;CACF;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnE;;;GAGG;AACH,KAAK,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,SAAS,eAAe,GAClF,eAAe,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,GACtD,IAAI,GACJ,KAAK,GACP,KAAK,CAAC;AACV,MAAM,MAAM,0BAA0B,GAAG,sBAAsB,CAAC;AAEhE;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAyBlF;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,wBAAwB,EAC7B,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,qBAAqB,CAyBvB"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyCG;AAGH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAI3D;;;;;GAKG;AACH,eAAO,MAAM,oBAAoB,QAAyB,CAAC;AAE3D;;;GAGG;AACH,MAAM,WAAW,2BAA2B;IAC1C,yFAAyF;IACzF,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED,gGAAgG;AAChG,MAAM,WAAW,qBAAsB,SAAQ,2BAA2B;IACxE,6FAA6F;IAC7F,IAAI,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,MAAM,MAAM,wBAAwB,GAAG,MAAM,GAAG,qBAAqB,CAAC;AAEtE,MAAM,WAAW,eAAe;IAC9B,oFAAoF;IACpF,kBAAkB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,2BAA2B,CAAC,CAAC;IACjE;;;;;;;;;;;;;;;;;;;OAmBG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAC1C;;;;;;;OAOG;IACH,MAAM,CAAC,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;CAC1C;AAED;;;;GAIG;AACH,MAAM,MAAM,gBAAgB,GACxB;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,GAClE;IAAE,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC;AAErB,eAAO,MAAM,2BAA2B;;;kBAKtC,CAAC;AAEH,uEAAuE;AACvE,eAAO,MAAM,iBAAiB,QAA6B,CAAC;AAE5D,eAAO,MAAM,sBAAsB;;;;;;;oBAQjC,CAAC;AAEH,eAAO,MAAM,gBAAgB;;;;;;;;;;;;;;;;;kBAM3B,CAAC;AAEH,OAAO,QAAQ,yBAAyB,CAAC;IACvC,UAAU,WAAW;QACnB,IAAI,CAAC,EAAE,eAAe,CAAC;KACxB;CACF;AAED;;;;;GAKG;AACH,MAAM,MAAM,mBAAmB,GAAG,WAAW,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC;AAEnE;;;GAGG;AACH,KAAK,sBAAsB,GAAG,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,SAAS,eAAe,GAClF,eAAe,SAAS,CAAC,CAAC,KAAK,CAAC,OAAO,gBAAgB,CAAC,GACtD,IAAI,GACJ,KAAK,GACP,KAAK,CAAC;AACV,MAAM,MAAM,0BAA0B,GAAG,sBAAsB,CAAC;AAEhE;;;;;;;;GAQG;AACH,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAyBlF;AAmBD;;;;;;;;;;;GAWG;AACH,wBAAgB,wBAAwB,CACtC,GAAG,EAAE,wBAAwB,EAC7B,IAAI,CAAC,EAAE;IAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;CAAE,GAC3B,qBAAqB,CAyBvB"}
package/dist/config.js CHANGED
@@ -73,6 +73,7 @@ export const helmConfigSchema = z.strictObject({
73
73
  charts: z
74
74
  .record(z.string().regex(CHART_KEY_PATTERN, "must start with a letter and hold only letters, digits, - and _"), helmProjectChartSchema)
75
75
  .optional(),
76
+ member: z.strictObject({ dir: z.string().min(1), label: z.string().optional() }).optional(),
76
77
  });
77
78
  /**
78
79
  * Validate a resolved profile's fields. Returns one message per problem,
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "algorithm": "sha256",
3
3
  "artifacts": {
4
- "manifest.json": "fea472989ef2a5c19660740b6ba32a98b8eb1e9531fbd209eb0f4801c9a2c3ef",
4
+ "manifest.json": "f807993cfc0f8ea40bde723a3934e777b82c7ac0914de9b17bddfd2fc9a0c1be",
5
5
  "meta.json": "14243c5730a07c6a6edc35ddd351438547d58df5cf345f2233a355b0c7611ccc",
6
6
  "types/index.d.ts": "5377696ca8698cd2999e4680feb8e8e4b54a7b49fb603a87b2f27356114d1794",
7
7
  "rules/chart-metadata.ts": "3ac9183a3bd42c657d1c4b7e45ddb302a75baf126d62284fa9d6f6226c2734cc",
@@ -36,5 +36,5 @@
36
36
  "skills/chant-helm-patterns.md": "9e79e6a46391da46709d8aa57e2825a7cd9eb981cd923f02ad60836c49b2561e",
37
37
  "skills/chant-helm-security.md": "bfc367eabceed2e84f1cf94501b407df78aeed963cec104f24a321d0962063c9"
38
38
  },
39
- "composite": "9354c1eeabaf9b11c7efbeef98f008967855a6e7cd849da17dbc1e961a12d7ca"
39
+ "composite": "86dc1a4c881ab01e2ebc60a50d21f9fed06118f6482a2c871adf56d6deaf7980"
40
40
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helm",
3
- "version": "0.124.48",
3
+ "version": "0.124.50",
4
4
  "chantVersion": ">=0.1.0",
5
5
  "namespace": "Helm",
6
6
  "intrinsics": [
@@ -1 +1 @@
1
- {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAkB7F,eAAO,MAAM,UAAU,EAAE,aA6cxB,CAAC"}
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,aAAa,EAAiC,MAAM,0BAA0B,CAAC;AAmB7F,eAAO,MAAM,UAAU,EAAE,aA4dxB,CAAC"}
package/dist/plugin.js CHANGED
@@ -17,6 +17,7 @@ import { helmSerializer } from "./serializer.js";
17
17
  import { helmCompletions } from "./lsp/completions.js";
18
18
  import { helmHover } from "./lsp/hover.js";
19
19
  import { helmConfigSchema } from "./config.js";
20
+ import { lastMemberReport, readHelmMember, setLastMemberReport } from "./workspace-member.js";
20
21
  import { helmProjectCodegen } from "./project-codegen.js";
21
22
  import { helmDeepNormalizationHooks } from "./deep-observe-hooks.js";
22
23
  import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
@@ -38,6 +39,21 @@ export const helmPlugin = {
38
39
  ownershipChannel: { keys: LABEL_OWNERSHIP_KEYS, reads: ["describeResources", "observeResourcesDeep"] },
39
40
  auditCatalog: () => helmAuditCatalog,
40
41
  serializer: helmSerializer,
42
+ // A helm-chart workspace member's reader project (#3179): `helm template`
43
+ // over the chart, the rendered objects as nodes. See ./workspace-member.ts.
44
+ async buildRoots(ctx) {
45
+ setLastMemberReport(undefined);
46
+ const member = ctx.config.helm?.member;
47
+ if (!member)
48
+ return { entities: new Map() };
49
+ const read = await readHelmMember({ member });
50
+ setLastMemberReport(read.report);
51
+ return { entities: read.entities, warnings: read.warnings };
52
+ },
53
+ graphMeta() {
54
+ const report = lastMemberReport();
55
+ return report ? { helmMember: report } : undefined;
56
+ },
41
57
  // #1248 (epic #1228 Phase 6, via the #1078 seam) — `chant helm` mounts the
42
58
  // pinned-render surface: the pinnability gate (#1234), the double-render
43
59
  // localizer (#1236), and the recorded-render listing with digests (#1237).
@@ -0,0 +1,37 @@
1
+ import type { Declarable } from "@intentius/chant/declarable";
2
+ export interface HelmMemberConfig {
3
+ /** The chart directory, absolute. */
4
+ dir: string;
5
+ /** Names the entities (`<label>/deploymentWeb`): the member's name. */
6
+ label?: string;
7
+ }
8
+ /** What the read found, carried on the graph's `meta.helmMember`. */
9
+ export interface HelmMemberReport {
10
+ rendered: boolean;
11
+ /** Why nothing was read; set with `rendered: false`. */
12
+ reason?: string;
13
+ objects: number;
14
+ }
15
+ /** Runs the helm binary with these arguments. Injectable so tests need no binary. */
16
+ export type HelmRunner = (args: string[]) => Promise<{
17
+ stdout: string;
18
+ }>;
19
+ export declare const defaultHelmRunner: HelmRunner;
20
+ /** The exact render argv: the release is named for the chart directory. */
21
+ export declare function helmTemplateArgs(dir: string): string[];
22
+ export declare function readHelmMember(opts: {
23
+ member: HelmMemberConfig;
24
+ run?: HelmRunner;
25
+ }): Promise<{
26
+ entities: Map<string, Declarable>;
27
+ warnings: string[];
28
+ report: HelmMemberReport;
29
+ }>;
30
+ /**
31
+ * The report of the read in this process. `graphMeta` receives only the
32
+ * entities, so the hook that read the member leaves its report here for the
33
+ * hook that publishes it; `chant graph` runs the two in order in one process.
34
+ */
35
+ export declare function setLastMemberReport(report: HelmMemberReport | undefined): void;
36
+ export declare function lastMemberReport(): HelmMemberReport | undefined;
37
+ //# sourceMappingURL=workspace-member.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workspace-member.d.ts","sourceRoot":"","sources":["../src/workspace-member.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAK9D,MAAM,WAAW,gBAAgB;IAC/B,qCAAqC;IACrC,GAAG,EAAE,MAAM,CAAC;IACZ,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,qEAAqE;AACrE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,OAAO,CAAC;IAClB,wDAAwD;IACxD,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qFAAqF;AACrF,MAAM,MAAM,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEzE,eAAO,MAAM,iBAAiB,EAAE,UAAmF,CAAC;AAEpH,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAEtD;AAMD,wBAAsB,cAAc,CAAC,IAAI,EAAE;IACzC,MAAM,EAAE,gBAAgB,CAAC;IACzB,GAAG,CAAC,EAAE,UAAU,CAAC;CAClB,GAAG,OAAO,CAAC;IAAE,QAAQ,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC;IAAC,QAAQ,EAAE,MAAM,EAAE,CAAC;IAAC,MAAM,EAAE,gBAAgB,CAAA;CAAE,CAAC,CA0B/F;AAID;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,gBAAgB,GAAG,SAAS,GAAG,IAAI,CAE9E;AAED,wBAAgB,gBAAgB,IAAI,gBAAgB,GAAG,SAAS,CAE/D"}
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Reading a helm-chart workspace member into the graph (#3179).
3
+ *
4
+ * `chant workspace graph` writes a reader project for a member of kind
5
+ * `helm-chart`, with `helm.member` in its config (see `workspace-kinds.json`).
6
+ * The plugin's `buildRoots` hook calls {@link readHelmMember}: `helm template`
7
+ * over the chart directory, and the rendered objects are the member's nodes.
8
+ *
9
+ * With no `helm` binary the member contributes no objects, and its report says
10
+ * `rendered: false` with the reason. A render the binary ran and refused (a
11
+ * template that doesn't parse, a dependency that isn't built) is not that case
12
+ * and throws, so a broken chart is never a quiet empty graph.
13
+ */
14
+ import { execFile } from "node:child_process";
15
+ import { basename } from "node:path";
16
+ import { promisify } from "node:util";
17
+ import { loadAll } from "js-yaml";
18
+ import { addRenderedEntity, renderedManifestEntity } from "@intentius/chant-lexicon-k8s/kustomize/rendered-entity";
19
+ const execFileAsync = promisify(execFile);
20
+ export const defaultHelmRunner = (args) => execFileAsync("helm", args, { maxBuffer: 64 * 1024 * 1024 });
21
+ /** The exact render argv: the release is named for the chart directory. */
22
+ export function helmTemplateArgs(dir) {
23
+ return ["template", basename(dir) || "release", dir];
24
+ }
25
+ function isMissingBinary(err) {
26
+ return err?.code === "ENOENT" || /ENOENT|command not found/.test(err instanceof Error ? err.message : String(err));
27
+ }
28
+ export async function readHelmMember(opts) {
29
+ const { member } = opts;
30
+ const label = member.label ?? basename(member.dir);
31
+ const entities = new Map();
32
+ const warnings = [];
33
+ let stdout;
34
+ try {
35
+ ({ stdout } = await (opts.run ?? defaultHelmRunner)(helmTemplateArgs(member.dir)));
36
+ }
37
+ catch (err) {
38
+ if (!isMissingBinary(err))
39
+ throw err;
40
+ const reason = `Cannot render chart "${member.dir}": the \`helm\` binary was not found on PATH. Install helm and retry.`;
41
+ warnings.push(`helm-chart member "${label}" was not rendered: ${reason}`);
42
+ return { entities, warnings, report: { rendered: false, reason, objects: 0 } };
43
+ }
44
+ for (const doc of loadAll(stdout)) {
45
+ if (!doc || typeof doc !== "object" || Array.isArray(doc))
46
+ continue;
47
+ const entity = renderedManifestEntity(doc, label);
48
+ if (!entity) {
49
+ warnings.push(`helm-chart member "${label}" rendered a document without apiVersion/kind; skipped`);
50
+ continue;
51
+ }
52
+ addRenderedEntity(entities, label, entity);
53
+ }
54
+ return { entities, warnings, report: { rendered: true, objects: entities.size } };
55
+ }
56
+ let lastReport;
57
+ /**
58
+ * The report of the read in this process. `graphMeta` receives only the
59
+ * entities, so the hook that read the member leaves its report here for the
60
+ * hook that publishes it; `chant graph` runs the two in order in one process.
61
+ */
62
+ export function setLastMemberReport(report) {
63
+ lastReport = report;
64
+ }
65
+ export function lastMemberReport() {
66
+ return lastReport;
67
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@intentius/chant-lexicon-helm",
3
- "version": "0.124.48",
3
+ "version": "0.124.50",
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",
@@ -23,12 +23,15 @@
23
23
  "type": "module",
24
24
  "files": [
25
25
  "src/",
26
- "dist/"
26
+ "dist/",
27
+ "schemas/",
28
+ "workspace-kinds.json"
27
29
  ],
28
30
  "publishConfig": {
29
31
  "access": "public"
30
32
  },
31
33
  "exports": {
34
+ "./workspace-kinds": "./workspace-kinds.json",
32
35
  ".": {
33
36
  "development": "./src/index.ts",
34
37
  "types": "./dist/index.d.ts",
@@ -77,7 +80,7 @@
77
80
  "build": "../../scripts/tsc7.sh -p tsconfig.build.json --sourceMap false && tsc-alias -p tsconfig.build.json"
78
81
  },
79
82
  "dependencies": {
80
- "@intentius/chant-lexicon-k8s": "^0.124.48",
83
+ "@intentius/chant-lexicon-k8s": "^0.124.50",
81
84
  "js-yaml": "^4.3.1"
82
85
  },
83
86
  "devDependencies": {
@@ -86,7 +89,7 @@
86
89
  "@types/js-yaml": "^4.0.9"
87
90
  },
88
91
  "peerDependencies": {
89
- "@intentius/chant": "^0.124.48",
92
+ "@intentius/chant": "^0.124.50",
90
93
  "zod": "^4.3.6"
91
94
  }
92
95
  }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "title": "Chart.yaml",
4
+ "type": "object",
5
+ "required": [
6
+ "apiVersion",
7
+ "name",
8
+ "version"
9
+ ],
10
+ "properties": {
11
+ "apiVersion": {
12
+ "enum": [
13
+ "v1",
14
+ "v2"
15
+ ]
16
+ },
17
+ "name": {
18
+ "type": "string",
19
+ "minLength": 1
20
+ },
21
+ "version": {
22
+ "type": "string",
23
+ "minLength": 1
24
+ },
25
+ "type": {
26
+ "enum": [
27
+ "application",
28
+ "library"
29
+ ]
30
+ }
31
+ }
32
+ }
package/src/config.ts CHANGED
@@ -107,6 +107,15 @@ export interface HelmChantConfig {
107
107
  * ```
108
108
  */
109
109
  charts?: Record<string, HelmProjectChart>;
110
+ /**
111
+ * One workspace member read into the graph (#3179). `chant workspace graph`
112
+ * writes this into the reader project it makes for a `helm-chart` member; it
113
+ * is not meant to be written by hand. `dir` is the chart directory, absolute;
114
+ * `label` names the entities, the member's name. The chart is rendered with
115
+ * `helm template`; with no `helm` binary the graph carries no objects for it
116
+ * and says why on `meta.helmMember`.
117
+ */
118
+ member?: { dir: string; label?: string };
110
119
  }
111
120
 
112
121
  /**
@@ -143,6 +152,7 @@ export const helmConfigSchema = z.strictObject({
143
152
  charts: z
144
153
  .record(z.string().regex(CHART_KEY_PATTERN, "must start with a letter and hold only letters, digits, - and _"), helmProjectChartSchema)
145
154
  .optional(),
155
+ member: z.strictObject({ dir: z.string().min(1), label: z.string().optional() }).optional(),
146
156
  });
147
157
 
148
158
  declare module "@intentius/chant/config" {
package/src/plugin.ts CHANGED
@@ -20,6 +20,7 @@ import { helmSerializer } from "./serializer";
20
20
  import { helmCompletions } from "./lsp/completions";
21
21
  import { helmHover } from "./lsp/hover";
22
22
  import { helmConfigSchema } from "./config";
23
+ import { lastMemberReport, readHelmMember, setLastMemberReport } from "./workspace-member";
23
24
  import { helmProjectCodegen } from "./project-codegen";
24
25
  import { helmDeepNormalizationHooks } from "./deep-observe-hooks";
25
26
  import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
@@ -43,6 +44,21 @@ export const helmPlugin: LexiconPlugin = {
43
44
  auditCatalog: () => helmAuditCatalog,
44
45
  serializer: helmSerializer,
45
46
 
47
+ // A helm-chart workspace member's reader project (#3179): `helm template`
48
+ // over the chart, the rendered objects as nodes. See ./workspace-member.ts.
49
+ async buildRoots(ctx) {
50
+ setLastMemberReport(undefined);
51
+ const member = (ctx.config as { helm?: { member?: { dir: string; label?: string } } }).helm?.member;
52
+ if (!member) return { entities: new Map() };
53
+ const read = await readHelmMember({ member });
54
+ setLastMemberReport(read.report);
55
+ return { entities: read.entities, warnings: read.warnings };
56
+ },
57
+ graphMeta() {
58
+ const report = lastMemberReport();
59
+ return report ? { helmMember: report } : undefined;
60
+ },
61
+
46
62
  // #1248 (epic #1228 Phase 6, via the #1078 seam) — `chant helm` mounts the
47
63
  // pinned-render surface: the pinnability gate (#1234), the double-render
48
64
  // localizer (#1236), and the recorded-render listing with digests (#1237).
@@ -0,0 +1,3 @@
1
+ apiVersion: v2
2
+ name: web
3
+ version: 0.1.0
@@ -0,0 +1,6 @@
1
+ apiVersion: apps/v1
2
+ kind: Deployment
3
+ metadata:
4
+ name: {{ .Release.Name }}
5
+ spec:
6
+ replicas: {{ .Values.replicas }}
@@ -0,0 +1 @@
1
+ replicas: 2
@@ -0,0 +1,37 @@
1
+ /**
2
+ * The helm-chart member kind this lexicon supplies to a chant workspace
3
+ * (#3179): data in `workspace-kinds.json`, exported at `./workspace-kinds`.
4
+ */
5
+ import { join } from "node:path";
6
+ import { describe, expect, it } from "vitest";
7
+ import { describeWorkspaceKindConformance } from "@intentius/chant-test-utils";
8
+ import { checkMemberFile, loadMemberFileSchema, readPackageKinds } from "../../../packages/core/src/workspace/kinds";
9
+
10
+ const packageDir = join(import.meta.dirname, "..");
11
+
12
+ /** What each kind's own file must do: pass for a good one, fail for a bad one (#3672). */
13
+ function memberFileChecks(kind: string, good: string, bad: string): void {
14
+ describe(`${kind} member file`, () => {
15
+ const memberFile = readPackageKinds(packageDir).kinds.find((k) => k.name === kind)!.memberFile!;
16
+ it("names a schema that loads", () => {
17
+ expect(loadMemberFileSchema(memberFile, packageDir)).toBeUndefined();
18
+ });
19
+ it("passes a valid file", () => {
20
+ expect(checkMemberFile(memberFile, memberFile.path, good)).toEqual([]);
21
+ });
22
+ it("fails an invalid one", () => {
23
+ expect(checkMemberFile(memberFile, memberFile.path, bad).length).toBeGreaterThan(0);
24
+ });
25
+ });
26
+ }
27
+
28
+ describeWorkspaceKindConformance({
29
+ packageDir,
30
+ scenarios: [
31
+ { name: "a chart", kind: "helm-chart", files: { "Chart.yaml": "apiVersion: v2\nname: web\nversion: 0.1.0\n", "values.yaml": "" }, claims: true },
32
+ { name: "a chart whose templates sit one level down", kind: "helm-chart", files: { "web/Chart.yaml": "apiVersion: v2\nname: web\nversion: 0.1.0\n" }, claims: false },
33
+ { name: "values without a Chart.yaml", kind: "helm-chart", files: { "values.yaml": "a: 1\n" }, claims: false },
34
+ ],
35
+ });
36
+
37
+ memberFileChecks("helm-chart", "apiVersion: v2\nname: web\nversion: 0.1.0\n", "apiVersion: v3\nname: web\n");
@@ -0,0 +1,140 @@
1
+ /**
2
+ * The helm-chart member kind's graph block (#3179). The test builds the reader
3
+ * project the way chant workspace graph does (the kind's graph config,
4
+ * placeholders substituted, written to a chant.config.json) and runs chant
5
+ * graph over it. The render is a canned runner, so no test needs helm; the last
6
+ * test uses the real binary when one is installed.
7
+ */
8
+ import { afterEach, describe, expect, test, vi } from "vitest";
9
+ import { execFileSync } from "node:child_process";
10
+ import { dirname, join } from "node:path";
11
+ import { fileURLToPath } from "node:url";
12
+ import { mkdtemp, rm, writeFile } from "node:fs/promises";
13
+ import { tmpdir } from "node:os";
14
+ import type { GraphIR } from "@intentius/chant/graph-ir";
15
+ import type { LexiconPlugin } from "@intentius/chant/lexicon";
16
+ import { runGraph } from "@intentius/chant/cli/handlers/graph";
17
+ import { readPackageKinds, substituteGraphConfig } from "../../../packages/core/src/workspace/kinds";
18
+ import { helmPlugin } from "./plugin";
19
+ import { helmTemplateArgs, readHelmMember, setLastMemberReport, type HelmMemberConfig, type HelmRunner } from "./workspace-member";
20
+
21
+ const here = dirname(fileURLToPath(import.meta.url));
22
+ const packageDir = join(here, "..");
23
+ const chart = join(here, "testdata", "web-chart");
24
+
25
+ const RENDERED = `---
26
+ # Source: web/templates/deployment.yaml
27
+ apiVersion: apps/v1
28
+ kind: Deployment
29
+ metadata:
30
+ name: web-chart
31
+ spec:
32
+ replicas: 2
33
+ ---
34
+ # Source: web/templates/service.yaml
35
+ apiVersion: v1
36
+ kind: Service
37
+ metadata:
38
+ name: web-chart
39
+ spec:
40
+ ports:
41
+ - port: 80
42
+ `;
43
+
44
+ const noBinary: HelmRunner = async () => {
45
+ throw Object.assign(new Error("spawn helm ENOENT"), { code: "ENOENT" });
46
+ };
47
+
48
+ function pluginWith(run?: HelmRunner): LexiconPlugin {
49
+ if (!run) return helmPlugin;
50
+ return {
51
+ ...helmPlugin,
52
+ buildRoots: async (ctx) => {
53
+ const member = (ctx.config as { helm?: { member?: HelmMemberConfig } }).helm?.member;
54
+ setLastMemberReport(undefined);
55
+ if (!member) return { entities: new Map() };
56
+ const read = await readHelmMember({ member, run });
57
+ setLastMemberReport(read.report);
58
+ return { entities: read.entities, warnings: read.warnings };
59
+ },
60
+ };
61
+ }
62
+
63
+ const scratch: string[] = [];
64
+ afterEach(async () => {
65
+ vi.restoreAllMocks();
66
+ await Promise.all(scratch.splice(0).map((d) => rm(d, { recursive: true, force: true })));
67
+ });
68
+
69
+ async function graphOf(plugin: LexiconPlugin): Promise<{ ir: GraphIR; err: string[]; exit: number }> {
70
+ const graph = readPackageKinds(packageDir).kinds.find((k) => k.name === "helm-chart")!.graph!;
71
+ expect(graph.lexicon).toBe("helm");
72
+ const config = substituteGraphConfig(graph.config, { member: "web", dir: chart, workspace: dirname(chart) });
73
+ const project = await mkdtemp(join(tmpdir(), "chant-helm-member-"));
74
+ scratch.push(project);
75
+ await writeFile(join(project, "chant.config.json"), JSON.stringify({ lexicons: ["helm"], helm: config, graph: { lint: "report" } }));
76
+ const out: string[] = [];
77
+ const err: string[] = [];
78
+ vi.spyOn(console, "log").mockImplementation((line: unknown) => void out.push(String(line)));
79
+ vi.spyOn(console, "error").mockImplementation((line: unknown) => void err.push(String(line)));
80
+ const cwd = process.cwd();
81
+ process.chdir(project);
82
+ try {
83
+ const exit = await runGraph({ args: { command: "graph", path: ".", format: "ir" } as never, plugins: [plugin], serializers: [] });
84
+ return { ir: exit === 0 ? (JSON.parse(out.join("\n")) as GraphIR) : { nodes: [], edges: [], groups: {} }, err, exit };
85
+ } finally {
86
+ process.chdir(cwd);
87
+ }
88
+ }
89
+
90
+ describe("helm-chart member graph block (#3179)", () => {
91
+ test("renders the chart and draws its objects", async () => {
92
+ let argv: string[] = [];
93
+ const { ir, err, exit } = await graphOf(
94
+ pluginWith(async (args) => {
95
+ argv = args;
96
+ return { stdout: RENDERED };
97
+ }),
98
+ );
99
+ expect(exit, err.join("\n")).toBe(0);
100
+ expect(argv).toEqual(helmTemplateArgs(chart));
101
+ expect(ir.nodes.map((n) => n.id).sort()).toEqual(["web/deploymentWebChart", "web/serviceWebChart"]);
102
+ expect(ir.meta?.helmMember).toEqual({ rendered: true, objects: 2 });
103
+ });
104
+
105
+ test("with no helm binary the member says rendered: false and why", async () => {
106
+ const { ir, err, exit } = await graphOf(pluginWith(noBinary));
107
+ expect(exit, err.join("\n")).toBe(0);
108
+ expect(ir.nodes).toEqual([]);
109
+ const report = ir.meta?.helmMember as { rendered: boolean; reason: string };
110
+ expect(report.rendered).toBe(false);
111
+ expect(report.reason).toMatch(/`helm` binary was not found/);
112
+ expect(err.join("\n")).toMatch(/was not rendered/);
113
+ });
114
+
115
+ test("a render helm refused still fails", async () => {
116
+ const { exit, err } = await graphOf(
117
+ pluginWith(async () => {
118
+ throw new Error("Error: parse error in templates/deployment.yaml");
119
+ }),
120
+ );
121
+ expect(exit).not.toBe(0);
122
+ expect(err.join("\n")).toMatch(/parse error/);
123
+ });
124
+
125
+ const haveHelm = (() => {
126
+ try {
127
+ execFileSync("helm", ["version"], { stdio: "ignore" });
128
+ return true;
129
+ } catch {
130
+ return false;
131
+ }
132
+ })();
133
+
134
+ test.skipIf(!haveHelm)("the real helm binary renders the chart", async () => {
135
+ const { ir, err, exit } = await graphOf(pluginWith());
136
+ expect(exit, err.join("\n")).toBe(0);
137
+ expect((ir.meta?.helmMember as { rendered: boolean }).rendered).toBe(true);
138
+ expect(ir.nodes.map((n) => n.id)).toEqual(["web/deploymentWebChart"]);
139
+ });
140
+ });
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Reading a helm-chart workspace member into the graph (#3179).
3
+ *
4
+ * `chant workspace graph` writes a reader project for a member of kind
5
+ * `helm-chart`, with `helm.member` in its config (see `workspace-kinds.json`).
6
+ * The plugin's `buildRoots` hook calls {@link readHelmMember}: `helm template`
7
+ * over the chart directory, and the rendered objects are the member's nodes.
8
+ *
9
+ * With no `helm` binary the member contributes no objects, and its report says
10
+ * `rendered: false` with the reason. A render the binary ran and refused (a
11
+ * template that doesn't parse, a dependency that isn't built) is not that case
12
+ * and throws, so a broken chart is never a quiet empty graph.
13
+ */
14
+ import { execFile } from "node:child_process";
15
+ import { basename } from "node:path";
16
+ import { promisify } from "node:util";
17
+ import { loadAll } from "js-yaml";
18
+ import type { Declarable } from "@intentius/chant/declarable";
19
+ import { addRenderedEntity, renderedManifestEntity } from "@intentius/chant-lexicon-k8s/kustomize/rendered-entity";
20
+
21
+ const execFileAsync = promisify(execFile);
22
+
23
+ export interface HelmMemberConfig {
24
+ /** The chart directory, absolute. */
25
+ dir: string;
26
+ /** Names the entities (`<label>/deploymentWeb`): the member's name. */
27
+ label?: string;
28
+ }
29
+
30
+ /** What the read found, carried on the graph's `meta.helmMember`. */
31
+ export interface HelmMemberReport {
32
+ rendered: boolean;
33
+ /** Why nothing was read; set with `rendered: false`. */
34
+ reason?: string;
35
+ objects: number;
36
+ }
37
+
38
+ /** Runs the helm binary with these arguments. Injectable so tests need no binary. */
39
+ export type HelmRunner = (args: string[]) => Promise<{ stdout: string }>;
40
+
41
+ export const defaultHelmRunner: HelmRunner = (args) => execFileAsync("helm", args, { maxBuffer: 64 * 1024 * 1024 });
42
+
43
+ /** The exact render argv: the release is named for the chart directory. */
44
+ export function helmTemplateArgs(dir: string): string[] {
45
+ return ["template", basename(dir) || "release", dir];
46
+ }
47
+
48
+ function isMissingBinary(err: unknown): boolean {
49
+ return (err as NodeJS.ErrnoException | undefined)?.code === "ENOENT" || /ENOENT|command not found/.test(err instanceof Error ? err.message : String(err));
50
+ }
51
+
52
+ export async function readHelmMember(opts: {
53
+ member: HelmMemberConfig;
54
+ run?: HelmRunner;
55
+ }): Promise<{ entities: Map<string, Declarable>; warnings: string[]; report: HelmMemberReport }> {
56
+ const { member } = opts;
57
+ const label = member.label ?? basename(member.dir);
58
+ const entities = new Map<string, Declarable>();
59
+ const warnings: string[] = [];
60
+
61
+ let stdout: string;
62
+ try {
63
+ ({ stdout } = await (opts.run ?? defaultHelmRunner)(helmTemplateArgs(member.dir)));
64
+ } catch (err) {
65
+ if (!isMissingBinary(err)) throw err;
66
+ const reason = `Cannot render chart "${member.dir}": the \`helm\` binary was not found on PATH. Install helm and retry.`;
67
+ warnings.push(`helm-chart member "${label}" was not rendered: ${reason}`);
68
+ return { entities, warnings, report: { rendered: false, reason, objects: 0 } };
69
+ }
70
+
71
+ for (const doc of loadAll(stdout)) {
72
+ if (!doc || typeof doc !== "object" || Array.isArray(doc)) continue;
73
+ const entity = renderedManifestEntity(doc as Record<string, unknown>, label);
74
+ if (!entity) {
75
+ warnings.push(`helm-chart member "${label}" rendered a document without apiVersion/kind; skipped`);
76
+ continue;
77
+ }
78
+ addRenderedEntity(entities, label, entity);
79
+ }
80
+ return { entities, warnings, report: { rendered: true, objects: entities.size } };
81
+ }
82
+
83
+ let lastReport: HelmMemberReport | undefined;
84
+
85
+ /**
86
+ * The report of the read in this process. `graphMeta` receives only the
87
+ * entities, so the hook that read the member leaves its report here for the
88
+ * hook that publishes it; `chant graph` runs the two in order in one process.
89
+ */
90
+ export function setLastMemberReport(report: HelmMemberReport | undefined): void {
91
+ lastReport = report;
92
+ }
93
+
94
+ export function lastMemberReport(): HelmMemberReport | undefined {
95
+ return lastReport;
96
+ }
@@ -0,0 +1,29 @@
1
+ {
2
+ "$comment": "Member kinds this lexicon supplies to a chant workspace (#3179). chant reads this file as data and never imports the lexicon. A Helm chart is a directory with a Chart.yaml, and Chart.yaml is the member's own file, checked against schemas/chart.schema.json. The graph block (#3179) reads a chart through helm template (src/workspace-member.ts); with no helm binary the member says rendered: false on meta.helmMember.",
3
+ "schema": 1,
4
+ "kinds": [
5
+ {
6
+ "name": "helm-chart",
7
+ "description": "a Helm chart, with a Chart.yaml in its directory",
8
+ "precedence": 330,
9
+ "probe": {
10
+ "anyFile": [
11
+ "Chart.yaml"
12
+ ]
13
+ },
14
+ "memberFile": {
15
+ "path": "Chart.yaml",
16
+ "schema": "schemas/chart.schema.json"
17
+ },
18
+ "graph": {
19
+ "lexicon": "helm",
20
+ "config": {
21
+ "member": {
22
+ "dir": "{dir}",
23
+ "label": "{member}"
24
+ }
25
+ }
26
+ }
27
+ }
28
+ ]
29
+ }