@intentius/chant-lexicon-helm 0.124.30 → 0.124.32
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/archive-render.js +76 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +113 -0
- package/dist/codegen/generate-cli.js +19 -0
- package/dist/codegen/generate.js +210 -0
- package/dist/codegen/naming.js +29 -0
- package/dist/codegen/package.js +44 -0
- package/dist/codegen/values-type.js +87 -0
- package/dist/commands.js +413 -0
- package/dist/components/builders.js +9 -0
- package/dist/components/capability-plugin.js +17 -0
- package/dist/components/helm-upgrade.js +109 -0
- package/dist/components/index.js +11 -0
- package/dist/composites/catalog.js +1119 -0
- package/dist/composites/helm-batch-job.js +148 -0
- package/dist/composites/helm-crd-lifecycle.js +145 -0
- package/dist/composites/helm-cron-job.js +130 -0
- package/dist/composites/helm-daemon-set.js +125 -0
- package/dist/composites/helm-external-secret.js +63 -0
- package/dist/composites/helm-library.js +24 -0
- package/dist/composites/helm-microservice.js +260 -0
- package/dist/composites/helm-monitored-service.js +188 -0
- package/dist/composites/helm-namespace-env.js +116 -0
- package/dist/composites/helm-secure-ingress.js +86 -0
- package/dist/composites/helm-stateful-service.js +160 -0
- package/dist/composites/helm-web-app.js +201 -0
- package/dist/composites/helm-worker.js +164 -0
- package/dist/composites/index.js +13 -0
- package/dist/config.js +161 -0
- package/dist/coverage.js +37 -0
- package/dist/deep-observe-hooks.js +19 -0
- package/dist/deep-observe.js +55 -0
- package/dist/describe-resources.js +119 -0
- package/dist/describe-stack-status.js +66 -0
- package/dist/detect.js +11 -0
- package/dist/generated/index.js +11 -0
- package/dist/helpers.js +84 -0
- package/dist/import/generator.js +292 -0
- package/dist/import/parser.js +141 -0
- package/dist/import/template-stripper.js +86 -0
- package/dist/index.js +55 -0
- package/dist/integrity.json +2 -2
- package/dist/intrinsics.js +482 -0
- package/dist/lint/audit-catalog.js +33 -0
- package/dist/lint/audit-lineage.js +53 -0
- package/dist/lint/post-synth/helm-helpers.js +60 -0
- package/dist/lint/post-synth/index.js +48 -0
- package/dist/lint/post-synth/whm005-no-empty-wrapper.js +47 -0
- package/dist/lint/post-synth/whm101.js +38 -0
- package/dist/lint/post-synth/whm102.js +26 -0
- package/dist/lint/post-synth/whm103.js +51 -0
- package/dist/lint/post-synth/whm104.js +29 -0
- package/dist/lint/post-synth/whm105.js +23 -0
- package/dist/lint/post-synth/whm201.js +29 -0
- package/dist/lint/post-synth/whm202.js +42 -0
- package/dist/lint/post-synth/whm203.js +32 -0
- package/dist/lint/post-synth/whm204.js +51 -0
- package/dist/lint/post-synth/whm301.js +31 -0
- package/dist/lint/post-synth/whm302.js +33 -0
- package/dist/lint/post-synth/whm401.js +52 -0
- package/dist/lint/post-synth/whm402.js +38 -0
- package/dist/lint/post-synth/whm403.js +38 -0
- package/dist/lint/post-synth/whm404.js +30 -0
- package/dist/lint/post-synth/whm405.js +47 -0
- package/dist/lint/post-synth/whm406.js +27 -0
- package/dist/lint/post-synth/whm407.js +72 -0
- package/dist/lint/post-synth/whm501.js +89 -0
- package/dist/lint/post-synth/whm502.js +85 -0
- package/dist/lint/post-synth/whm503.js +82 -0
- package/dist/lint/post-synth/whm504.js +47 -0
- package/dist/lint/rules/chart-metadata.js +53 -0
- package/dist/lint/rules/no-hardcoded-image.js +50 -0
- package/dist/lint/rules/values-no-helm-tpl.js +78 -0
- package/dist/lint/rules/values-no-secrets.js +65 -0
- package/dist/list-artifacts.js +134 -0
- package/dist/lsp/completions.d.ts.map +1 -1
- package/dist/lsp/completions.js +18 -0
- package/dist/lsp/hover.d.ts.map +1 -1
- package/dist/lsp/hover.js +40 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/cluster-probe.js +116 -0
- package/dist/op/activities/helm.js +449 -0
- package/dist/op/activities/index.js +10 -0
- package/dist/op/builders.js +45 -0
- package/dist/package-cli.js +21 -0
- package/dist/pinnability/actions.js +207 -0
- package/dist/pinnability/classify.js +416 -0
- package/dist/pinnability/conditions.js +0 -0
- package/dist/pinnability/index.js +16 -0
- package/dist/pinnability/localize.js +518 -0
- package/dist/pinnability/render-stream.js +53 -0
- package/dist/pinnability/values.js +151 -0
- package/dist/plugin.js +458 -0
- package/dist/project-codegen.js +215 -0
- package/dist/release-observe.js +287 -0
- package/dist/render-diff.js +279 -0
- package/dist/render-digest.js +0 -0
- package/dist/render-records.js +29 -0
- package/dist/render-store.js +401 -0
- package/dist/render-wrapper.js +164 -0
- package/dist/render.js +394 -0
- package/dist/resources.js +109 -0
- package/dist/serializer.js +903 -0
- package/dist/validate-cli.js +17 -0
- package/dist/validate.js +30 -0
- package/dist/values-probe.js +504 -0
- package/dist/wrapper-chart.js +95 -0
- package/package.json +11 -11
- package/src/codegen/package.ts +2 -2
- package/src/lsp/completions.ts +2 -1
- package/src/lsp/hover.ts +2 -1
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chart model and value coalescing for the pinnability classifier (#1234,
|
|
3
|
+
* epic #1228).
|
|
4
|
+
*
|
|
5
|
+
* Pinnability is a property of `(chart, values)`, not of the chart alone —
|
|
6
|
+
* so the classifier needs each subchart instance's EFFECTIVE values to ask
|
|
7
|
+
* whether a gate is open. This module walks `Chart.yaml` dependencies
|
|
8
|
+
* (aliases and `condition:` included — finding 12: dependency conditions are
|
|
9
|
+
* values-driven and therefore closed inputs) and approximates helm's
|
|
10
|
+
* coalescing: child defaults, overridden by the parent's `<alias-or-name>`
|
|
11
|
+
* subtree, with `global` propagated down.
|
|
12
|
+
*
|
|
13
|
+
* The approximation is deliberate and bounded. `import-values`, `tags`, and
|
|
14
|
+
* `tpl`-computed values are not modeled; a gate they would decide resolves
|
|
15
|
+
* as UNKNOWN and the classifier refuses rather than pins (see
|
|
16
|
+
* conditions.ts). The build-time values probe (#1251) replaces this with
|
|
17
|
+
* helm's own coalescing.
|
|
18
|
+
*/
|
|
19
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import yaml from "js-yaml";
|
|
22
|
+
export function readChartMeta(chartDir) {
|
|
23
|
+
const raw = readYamlFile(join(chartDir, "Chart.yaml"));
|
|
24
|
+
const deps = [];
|
|
25
|
+
if (Array.isArray(raw?.dependencies)) {
|
|
26
|
+
for (const d of raw.dependencies) {
|
|
27
|
+
if (d === null || typeof d !== "object")
|
|
28
|
+
continue;
|
|
29
|
+
const dep = d;
|
|
30
|
+
if (typeof dep.name !== "string")
|
|
31
|
+
continue;
|
|
32
|
+
deps.push({
|
|
33
|
+
name: dep.name,
|
|
34
|
+
alias: typeof dep.alias === "string" ? dep.alias : undefined,
|
|
35
|
+
condition: typeof dep.condition === "string" ? dep.condition : undefined,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
name: typeof raw?.name === "string" ? raw.name : "",
|
|
41
|
+
dependencies: deps,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function readYamlFile(path) {
|
|
45
|
+
if (!existsSync(path))
|
|
46
|
+
return undefined;
|
|
47
|
+
return yaml.load(readFileSync(path, "utf8"));
|
|
48
|
+
}
|
|
49
|
+
function isPlainObject(v) {
|
|
50
|
+
return v !== null && typeof v === "object" && !Array.isArray(v);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Helm-style deep merge: `override` wins, maps merge recursively, a `null`
|
|
54
|
+
* override deletes the key, arrays replace wholesale.
|
|
55
|
+
*/
|
|
56
|
+
export function mergeValues(base, override) {
|
|
57
|
+
if (override === undefined)
|
|
58
|
+
return base;
|
|
59
|
+
if (!isPlainObject(base) || !isPlainObject(override))
|
|
60
|
+
return override;
|
|
61
|
+
const out = { ...base };
|
|
62
|
+
for (const [k, v] of Object.entries(override)) {
|
|
63
|
+
if (v === null) {
|
|
64
|
+
delete out[k];
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
out[k] = k in out ? mergeValues(out[k], v) : v;
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
/** Resolve a dot path against a values tree; undefined when absent. */
|
|
72
|
+
export function valueAtPath(values, path) {
|
|
73
|
+
let v = values;
|
|
74
|
+
for (const seg of path) {
|
|
75
|
+
if (!isPlainObject(v))
|
|
76
|
+
return undefined;
|
|
77
|
+
v = v[seg];
|
|
78
|
+
}
|
|
79
|
+
return v;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build the instance tree for a chart directory and the values the render
|
|
83
|
+
* was given. `suppliedValues` merges over the root chart's `values.yaml`
|
|
84
|
+
* exactly like `helm template --values`.
|
|
85
|
+
*/
|
|
86
|
+
export function buildChartInstances(chartDir, suppliedValues) {
|
|
87
|
+
const warnings = [];
|
|
88
|
+
const rootDefaults = readYamlFile(join(chartDir, "values.yaml")) ?? {};
|
|
89
|
+
const rootValues = mergeValues(rootDefaults, suppliedValues);
|
|
90
|
+
const instances = [];
|
|
91
|
+
const visit = (dir, relDir, scope, values, disabledBy) => {
|
|
92
|
+
instances.push({ dir, relDir, scope, values, disabledBy });
|
|
93
|
+
const meta = readChartMeta(dir);
|
|
94
|
+
const chartsDir = join(dir, "charts");
|
|
95
|
+
const childDirs = existsSync(chartsDir)
|
|
96
|
+
? readdirSync(chartsDir).filter((e) => {
|
|
97
|
+
const p = join(chartsDir, e);
|
|
98
|
+
return statSync(p).isDirectory() && existsSync(join(p, "Chart.yaml"));
|
|
99
|
+
})
|
|
100
|
+
: [];
|
|
101
|
+
const declared = new Set();
|
|
102
|
+
const visitChild = (dep) => {
|
|
103
|
+
const key = dep.alias ?? dep.name;
|
|
104
|
+
const childDir = join(chartsDir, dep.name);
|
|
105
|
+
if (!existsSync(join(childDir, "Chart.yaml"))) {
|
|
106
|
+
warnings.push(`dependency ${dep.name} of ${relDir === "" ? "the chart" : relDir} is not vendored under charts/ — not scanned`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
declared.add(dep.name);
|
|
110
|
+
// Condition paths are resolved against the TOP parent's values
|
|
111
|
+
// (helm's documented semantics); first present path wins, absent
|
|
112
|
+
// conditions leave the dependency enabled.
|
|
113
|
+
let childDisabledBy = disabledBy;
|
|
114
|
+
if (childDisabledBy === undefined && dep.condition !== undefined) {
|
|
115
|
+
for (const rawPath of dep.condition.split(",")) {
|
|
116
|
+
const path = rawPath.trim();
|
|
117
|
+
if (path === "")
|
|
118
|
+
continue;
|
|
119
|
+
const v = valueAtPath(rootValues, path.split("."));
|
|
120
|
+
if (v === undefined)
|
|
121
|
+
continue;
|
|
122
|
+
if (v === false)
|
|
123
|
+
childDisabledBy = path;
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
const childDefaults = readYamlFile(join(childDir, "values.yaml")) ?? {};
|
|
128
|
+
const parentSlice = isPlainObject(values) ? values[key] : undefined;
|
|
129
|
+
let childValues = mergeValues(childDefaults, parentSlice);
|
|
130
|
+
// Globals propagate into every subchart tree.
|
|
131
|
+
const parentGlobal = isPlainObject(values) ? values.global : undefined;
|
|
132
|
+
if (parentGlobal !== undefined && isPlainObject(childValues)) {
|
|
133
|
+
childValues = {
|
|
134
|
+
...childValues,
|
|
135
|
+
global: mergeValues(childValues.global, parentGlobal),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
visit(childDir, relDir === "" ? `charts/${dep.name}` : `${relDir}/charts/${dep.name}`, [...scope, key], childValues, childDisabledBy);
|
|
139
|
+
};
|
|
140
|
+
for (const dep of meta.dependencies)
|
|
141
|
+
visitChild(dep);
|
|
142
|
+
// charts/ directories not declared in Chart.yaml are implicit,
|
|
143
|
+
// always-enabled dependencies.
|
|
144
|
+
for (const name of childDirs) {
|
|
145
|
+
if (!declared.has(name))
|
|
146
|
+
visitChild({ name });
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
visit(chartDir, "", [], rootValues, undefined);
|
|
150
|
+
return { instances, warnings };
|
|
151
|
+
}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,458 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Helm lexicon plugin.
|
|
3
|
+
*
|
|
4
|
+
* Implements the LexiconPlugin interface with the Helm chart serializer,
|
|
5
|
+
* Helm-specific intrinsics, lint rules, and post-synth checks.
|
|
6
|
+
*/
|
|
7
|
+
import { compositeCatalog } from "./composites/catalog.js";
|
|
8
|
+
import { helmCommandGroup } from "./commands.js";
|
|
9
|
+
import { detectTemplate } from "./detect.js";
|
|
10
|
+
import { discoverLintRules } from "@intentius/chant/lint/discover";
|
|
11
|
+
import { postSynthChecks as postSynthCheckList } from "./lint/post-synth/index.js";
|
|
12
|
+
import { helmAuditCatalog } from "./lint/audit-catalog.js";
|
|
13
|
+
import { createSkillsLoader, createDiffTool } from "@intentius/chant/lexicon-plugin-helpers";
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
|
+
import { fileURLToPath } from "url";
|
|
16
|
+
import { helmSerializer } from "./serializer.js";
|
|
17
|
+
import { helmCompletions } from "./lsp/completions.js";
|
|
18
|
+
import { helmHover } from "./lsp/hover.js";
|
|
19
|
+
import { helmConfigSchema } from "./config.js";
|
|
20
|
+
import { helmProjectCodegen } from "./project-codegen.js";
|
|
21
|
+
import { helmDeepNormalizationHooks } from "./deep-observe-hooks.js";
|
|
22
|
+
import { LABEL_OWNERSHIP_KEYS } from "@intentius/chant/ownership";
|
|
23
|
+
export const helmPlugin = {
|
|
24
|
+
name: "helm",
|
|
25
|
+
// The `helm` namespace in chant.config.ts — capability profiles (#1235).
|
|
26
|
+
// Declaring the schema makes core validate the namespace at load, so a
|
|
27
|
+
// typo'd profile field fails the build instead of silently unpinning.
|
|
28
|
+
configSchema: helmConfigSchema,
|
|
29
|
+
// Typed values and a render factory per chart in `helm.charts`, written by
|
|
30
|
+
// `chant generate` and checked for drift by every build. See
|
|
31
|
+
// ./project-codegen.ts.
|
|
32
|
+
projectCodegen: () => helmProjectCodegen(),
|
|
33
|
+
// #1246 — helm resolves real ownership verdicts on the thin read (every row
|
|
34
|
+
// is release-scoped, so helm-managed = owned via release identity), and the
|
|
35
|
+
// deep read (#1247) delegates to the k8s reader, which resolves the shared
|
|
36
|
+
// label channel (`app.kubernetes.io/managed-by` + `chant.intentius.io/*`)
|
|
37
|
+
// on the live objects.
|
|
38
|
+
ownershipChannel: { keys: LABEL_OWNERSHIP_KEYS, reads: ["describeResources", "observeResourcesDeep"] },
|
|
39
|
+
auditCatalog: () => helmAuditCatalog,
|
|
40
|
+
serializer: helmSerializer,
|
|
41
|
+
// #1248 (epic #1228 Phase 6, via the #1078 seam) — `chant helm` mounts the
|
|
42
|
+
// pinned-render surface: the pinnability gate (#1234), the double-render
|
|
43
|
+
// localizer (#1236), and the recorded-render listing with digests (#1237).
|
|
44
|
+
// See ./commands.ts for the verb list; this plugin only mounts it. The
|
|
45
|
+
// diff verbs (#1249 render-to-render, #1250 render-to-live, both mounted on
|
|
46
|
+
// "diff") land there.
|
|
47
|
+
commands() {
|
|
48
|
+
return helmCommandGroup();
|
|
49
|
+
},
|
|
50
|
+
lintRules() {
|
|
51
|
+
const rulesDir = join(dirname(fileURLToPath(import.meta.url)), "lint", "rules");
|
|
52
|
+
return discoverLintRules(rulesDir, import.meta.url);
|
|
53
|
+
},
|
|
54
|
+
postSynthChecks() {
|
|
55
|
+
return postSynthCheckList;
|
|
56
|
+
},
|
|
57
|
+
// The providers themselves have lived in src/lsp/ with tests since the
|
|
58
|
+
// lexicon was written; they were never registered here, so `chant serve lsp`
|
|
59
|
+
// never reached them (#1342).
|
|
60
|
+
completionProvider: helmCompletions,
|
|
61
|
+
hoverProvider: helmHover,
|
|
62
|
+
// chant #1174 — `foldsAsCall: true` opts an intrinsic's PLAIN-CALL form
|
|
63
|
+
// into `chant build --fold` (chant #1044's mechanism, previously unused by
|
|
64
|
+
// this lexicon: every helm intrinsic is a plain call — helm has no tagged
|
|
65
|
+
// templates at all — so before this PR NONE of them folded, and a single
|
|
66
|
+
// `include(...)`/`printf(...)` anywhere in a chart's resources forced the
|
|
67
|
+
// whole file back to the run path).
|
|
68
|
+
//
|
|
69
|
+
// Audited one at a time against `IntrinsicDef.foldsAsCall`'s criterion
|
|
70
|
+
// (../../../packages/core/src/lexicon.ts): "calling the intrinsic is a
|
|
71
|
+
// pure function of its arguments that builds a deterministic data
|
|
72
|
+
// envelope". Every function below (`include`/`required`/`helmDefault`/
|
|
73
|
+
// `toYaml`/`quote`/`printf`/`tpl`/`lookup`/`filesGet`/`filesGlob`/
|
|
74
|
+
// `filesAsConfig`/`filesAsSecrets`, ./intrinsics.ts) does exactly that: it
|
|
75
|
+
// builds a `HelmTpl` (or, for `lookup`, a string) by interpolating its
|
|
76
|
+
// arguments into a Go template expression string — no I/O, no mutation, no
|
|
77
|
+
// dependency on anything but the arguments. `lookup`'s own side effect
|
|
78
|
+
// (a live cluster read) happens when HELM renders the resulting template
|
|
79
|
+
// at deploy time, not when this JS function runs at `chant build` time, so
|
|
80
|
+
// it is just as pure here as the rest.
|
|
81
|
+
//
|
|
82
|
+
// Left OUT, each for a specific reason rather than an oversight:
|
|
83
|
+
// - `values`/`Release`/`ChartRef`/`Capabilities`/`Template` are proxy
|
|
84
|
+
// OBJECTS, never called (`values(...)` isn't valid source) — nothing
|
|
85
|
+
// to opt in.
|
|
86
|
+
// - `withOrder`/`argoWave` return a PLAIN record meant to be spread
|
|
87
|
+
// (`{...withOrder(1)}`), not a `HelmTpl`. `fold()`'s object-spread
|
|
88
|
+
// handling (`Object.assign(obj, src)`, ../fold/fold.ts) runs
|
|
89
|
+
// SYNCHRONOUSLY on the immediately-folded value, before the async
|
|
90
|
+
// revival that would turn a folded call into its real result — opting
|
|
91
|
+
// these in would spread the folded `{__intrinsic, args}` envelope's
|
|
92
|
+
// own keys into the object instead of the real record. A real
|
|
93
|
+
// soundness hazard, not a "not yet audited."
|
|
94
|
+
// - `If`/`ElseIf`/`Range`/`With` take an arbitrary `body`/`elseBody`
|
|
95
|
+
// that is often an arrow function (`Range(list, (item) => ({...}))`)
|
|
96
|
+
// or a nested Declarable — `fold()` already rejects a function used as
|
|
97
|
+
// a value on its own terms, so opting these in buys nothing measured
|
|
98
|
+
// on this corpus and is left for a future PR to size against real
|
|
99
|
+
// usage rather than widened on speculation.
|
|
100
|
+
intrinsics() {
|
|
101
|
+
return [
|
|
102
|
+
{ name: "values", description: "Proxy accessor for {{ .Values.x }} references", isTag: false },
|
|
103
|
+
{ name: "Release", description: "Built-in Release object (Name, Namespace, etc.)", isTag: false },
|
|
104
|
+
{ name: "ChartRef", description: "Built-in Chart object (Name, Version, AppVersion)", isTag: false },
|
|
105
|
+
{ name: "include", description: 'Include a named template: {{ include "name" . }}', isTag: false, foldsAsCall: true },
|
|
106
|
+
{ name: "required", description: 'Require a value: {{ required "msg" .Values.x }}', isTag: false, foldsAsCall: true },
|
|
107
|
+
{ name: "helmDefault", description: 'Default value: {{ default "def" .Values.x }}', isTag: false, foldsAsCall: true },
|
|
108
|
+
{ name: "toYaml", description: "Convert to YAML: {{ toYaml .Values.x | nindent N }}", isTag: false, foldsAsCall: true },
|
|
109
|
+
{ name: "quote", description: "Quote a value: {{ .Values.x | quote }}", isTag: false, foldsAsCall: true },
|
|
110
|
+
{ name: "printf", description: 'Format string: {{ printf "%s" .Values.x }}', isTag: false, foldsAsCall: true },
|
|
111
|
+
{ name: "tpl", description: "Evaluate template: {{ tpl .Values.x . }}", isTag: false, foldsAsCall: true },
|
|
112
|
+
{ name: "lookup", description: 'Lookup resource: {{ lookup "v1" "Secret" "ns" "name" }}', isTag: false, foldsAsCall: true },
|
|
113
|
+
{ name: "Capabilities", description: "Built-in Capabilities object (KubeVersion, APIVersions, HelmVersion)", isTag: false },
|
|
114
|
+
{ name: "Template", description: "Built-in Template object (Name, BasePath)", isTag: false },
|
|
115
|
+
{ name: "filesGet", description: '.Files.Get: {{ .Files.Get "path" }}', isTag: false, foldsAsCall: true },
|
|
116
|
+
{ name: "filesGlob", description: '.Files.Glob: {{ .Files.Glob "pattern" }}', isTag: false, foldsAsCall: true },
|
|
117
|
+
{ name: "filesAsConfig", description: ".Files.Glob.AsConfig for ConfigMap data", isTag: false, foldsAsCall: true },
|
|
118
|
+
{ name: "filesAsSecrets", description: ".Files.Glob.AsSecrets for Secret data", isTag: false, foldsAsCall: true },
|
|
119
|
+
{ name: "ElseIf", description: "Else-if chaining: {{- else if .Values.x }}...{{- end }}", isTag: false },
|
|
120
|
+
{ name: "If", description: "Conditional: {{- if .Values.x }}...{{- end }}", isTag: false },
|
|
121
|
+
{ name: "Range", description: "Range loop: {{- range .Values.x }}...{{- end }}", isTag: false },
|
|
122
|
+
{ name: "With", description: "With scope: {{- with .Values.x }}...{{- end }}", isTag: false },
|
|
123
|
+
{ name: "withOrder", description: "Helm hook annotations for resource ordering (pre-install/pre-upgrade)", isTag: false },
|
|
124
|
+
{ name: "argoWave", description: "Argo CD sync wave annotation", isTag: false },
|
|
125
|
+
];
|
|
126
|
+
},
|
|
127
|
+
detectTemplate,
|
|
128
|
+
composites() {
|
|
129
|
+
return compositeCatalog;
|
|
130
|
+
},
|
|
131
|
+
mcpTools() {
|
|
132
|
+
return [createDiffTool(helmSerializer, "Compare current Helm chart build output against previous output", "helm")];
|
|
133
|
+
},
|
|
134
|
+
mcpResources() {
|
|
135
|
+
return [
|
|
136
|
+
{
|
|
137
|
+
uri: "helm:resource-catalog",
|
|
138
|
+
name: "Helm Chart Resource Catalog",
|
|
139
|
+
description: "JSON list of all supported Helm chart resource types",
|
|
140
|
+
mimeType: "application/json",
|
|
141
|
+
async handler() {
|
|
142
|
+
const { readFileSync } = await import("fs");
|
|
143
|
+
const { join, dirname } = await import("path");
|
|
144
|
+
const { fileURLToPath } = await import("url");
|
|
145
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
146
|
+
try {
|
|
147
|
+
const lexicon = JSON.parse(readFileSync(join(pkgDir, "dist", "meta.json"), "utf-8"));
|
|
148
|
+
const entries = Object.entries(lexicon).map(([className, entry]) => ({
|
|
149
|
+
className,
|
|
150
|
+
resourceType: entry.resourceType,
|
|
151
|
+
kind: entry.kind,
|
|
152
|
+
}));
|
|
153
|
+
return JSON.stringify(entries);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return JSON.stringify([{ className: "Chart", kind: "resource" }, { className: "Values", kind: "resource" }, { className: "HelmNotes", kind: "resource" }]);
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
uri: "examples/web-app",
|
|
162
|
+
name: "Web App Helm Chart Example",
|
|
163
|
+
description: "A basic web application Helm chart with Deployment, Service, and Ingress",
|
|
164
|
+
mimeType: "text/typescript",
|
|
165
|
+
async handler() {
|
|
166
|
+
return `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
|
|
167
|
+
import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
|
|
168
|
+
import { Deployment, Service, Ingress } from "@intentius/chant-lexicon-k8s";
|
|
169
|
+
|
|
170
|
+
export const chart = new Chart({
|
|
171
|
+
apiVersion: "v2",
|
|
172
|
+
name: "web-app",
|
|
173
|
+
version: "0.1.0",
|
|
174
|
+
appVersion: "1.0.0",
|
|
175
|
+
type: "application",
|
|
176
|
+
description: "A web application chart",
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
export const valuesSchema = new Values({
|
|
180
|
+
replicaCount: 2,
|
|
181
|
+
image: { repository: "nginx", tag: "latest", pullPolicy: "IfNotPresent" },
|
|
182
|
+
service: { type: "ClusterIP", port: 80 },
|
|
183
|
+
ingress: { enabled: false, host: "example.com" },
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
export const deployment = new Deployment({
|
|
187
|
+
metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
|
|
188
|
+
spec: {
|
|
189
|
+
replicas: values.replicaCount,
|
|
190
|
+
selector: { matchLabels: include("web-app.selectorLabels") },
|
|
191
|
+
template: {
|
|
192
|
+
metadata: { labels: include("web-app.selectorLabels") },
|
|
193
|
+
spec: {
|
|
194
|
+
containers: [{
|
|
195
|
+
name: "web-app",
|
|
196
|
+
image: printf("%s:%s", values.image.repository, values.image.tag),
|
|
197
|
+
ports: [{ containerPort: values.service.port, name: "http" }],
|
|
198
|
+
}],
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
export const service = new Service({
|
|
205
|
+
metadata: { name: include("web-app.fullname"), labels: include("web-app.labels") },
|
|
206
|
+
spec: {
|
|
207
|
+
type: values.service.type,
|
|
208
|
+
ports: [{ port: values.service.port, targetPort: "http", protocol: "TCP", name: "http" }],
|
|
209
|
+
selector: include("web-app.selectorLabels"),
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
`;
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
];
|
|
216
|
+
},
|
|
217
|
+
initTemplates(_template) {
|
|
218
|
+
if (_template === "stateful-service") {
|
|
219
|
+
return {
|
|
220
|
+
src: {
|
|
221
|
+
"chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
|
|
222
|
+
import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
|
|
223
|
+
import { StatefulSet, Service, PersistentVolumeClaim, ConfigMap } from "@intentius/chant-lexicon-k8s";
|
|
224
|
+
|
|
225
|
+
export const chart = new Chart({
|
|
226
|
+
apiVersion: "v2",
|
|
227
|
+
name: "my-stateful-app",
|
|
228
|
+
version: "0.1.0",
|
|
229
|
+
appVersion: "1.0.0",
|
|
230
|
+
type: "application",
|
|
231
|
+
description: "A Helm chart for a stateful application",
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
export const valuesSchema = new Values({
|
|
235
|
+
replicaCount: 3,
|
|
236
|
+
image: {
|
|
237
|
+
repository: "postgres",
|
|
238
|
+
tag: "16",
|
|
239
|
+
pullPolicy: "IfNotPresent",
|
|
240
|
+
},
|
|
241
|
+
service: {
|
|
242
|
+
port: 5432,
|
|
243
|
+
},
|
|
244
|
+
storage: {
|
|
245
|
+
size: "10Gi",
|
|
246
|
+
storageClass: "",
|
|
247
|
+
},
|
|
248
|
+
config: {
|
|
249
|
+
maxConnections: "100",
|
|
250
|
+
sharedBuffers: "256MB",
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
export const configMap = new ConfigMap({
|
|
255
|
+
metadata: {
|
|
256
|
+
name: include("my-stateful-app.fullname"),
|
|
257
|
+
labels: include("my-stateful-app.labels"),
|
|
258
|
+
},
|
|
259
|
+
data: {
|
|
260
|
+
"max_connections": values.config.maxConnections,
|
|
261
|
+
"shared_buffers": values.config.sharedBuffers,
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
export const headlessService = new Service({
|
|
266
|
+
metadata: {
|
|
267
|
+
name: printf("%s-headless", include("my-stateful-app.fullname")),
|
|
268
|
+
labels: include("my-stateful-app.labels"),
|
|
269
|
+
},
|
|
270
|
+
spec: {
|
|
271
|
+
type: "ClusterIP",
|
|
272
|
+
clusterIP: "None",
|
|
273
|
+
ports: [{ port: values.service.port, targetPort: "db", protocol: "TCP", name: "db" }],
|
|
274
|
+
selector: include("my-stateful-app.selectorLabels"),
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
export const statefulSet = new StatefulSet({
|
|
279
|
+
metadata: {
|
|
280
|
+
name: include("my-stateful-app.fullname"),
|
|
281
|
+
labels: include("my-stateful-app.labels"),
|
|
282
|
+
},
|
|
283
|
+
spec: {
|
|
284
|
+
serviceName: printf("%s-headless", include("my-stateful-app.fullname")),
|
|
285
|
+
replicas: values.replicaCount,
|
|
286
|
+
selector: { matchLabels: include("my-stateful-app.selectorLabels") },
|
|
287
|
+
template: {
|
|
288
|
+
metadata: { labels: include("my-stateful-app.selectorLabels") },
|
|
289
|
+
spec: {
|
|
290
|
+
containers: [{
|
|
291
|
+
name: "db",
|
|
292
|
+
image: printf("%s:%s", values.image.repository, values.image.tag),
|
|
293
|
+
ports: [{ containerPort: values.service.port, name: "db" }],
|
|
294
|
+
volumeMounts: [{ name: "data", mountPath: "/var/lib/postgresql/data" }],
|
|
295
|
+
}],
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
volumeClaimTemplates: [
|
|
299
|
+
new PersistentVolumeClaim({
|
|
300
|
+
metadata: { name: "data" },
|
|
301
|
+
spec: {
|
|
302
|
+
accessModes: ["ReadWriteOnce"],
|
|
303
|
+
resources: { requests: { storage: values.storage.size } },
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
],
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
`,
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
return {
|
|
314
|
+
src: {
|
|
315
|
+
"chart.ts": `import { Chart, Values, HelmNotes } from "@intentius/chant-lexicon-helm";
|
|
316
|
+
import { values, Release, include, toYaml, printf } from "@intentius/chant-lexicon-helm/intrinsics";
|
|
317
|
+
import { Deployment, Service } from "@intentius/chant-lexicon-k8s";
|
|
318
|
+
|
|
319
|
+
export const chart = new Chart({
|
|
320
|
+
apiVersion: "v2",
|
|
321
|
+
name: "my-app",
|
|
322
|
+
version: "0.1.0",
|
|
323
|
+
appVersion: "1.0.0",
|
|
324
|
+
type: "application",
|
|
325
|
+
description: "A Helm chart for my application",
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
export const valuesSchema = new Values({
|
|
329
|
+
replicaCount: 1,
|
|
330
|
+
image: {
|
|
331
|
+
repository: "nginx",
|
|
332
|
+
tag: "",
|
|
333
|
+
pullPolicy: "IfNotPresent",
|
|
334
|
+
},
|
|
335
|
+
service: {
|
|
336
|
+
type: "ClusterIP",
|
|
337
|
+
port: 80,
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
export const deployment = new Deployment({
|
|
342
|
+
metadata: {
|
|
343
|
+
name: include("my-app.fullname"),
|
|
344
|
+
labels: include("my-app.labels"),
|
|
345
|
+
},
|
|
346
|
+
spec: {
|
|
347
|
+
replicas: values.replicaCount,
|
|
348
|
+
selector: {
|
|
349
|
+
matchLabels: include("my-app.selectorLabels"),
|
|
350
|
+
},
|
|
351
|
+
template: {
|
|
352
|
+
metadata: {
|
|
353
|
+
labels: include("my-app.selectorLabels"),
|
|
354
|
+
},
|
|
355
|
+
spec: {
|
|
356
|
+
containers: [{
|
|
357
|
+
name: "my-app",
|
|
358
|
+
image: printf("%s:%s", values.image.repository, values.image.tag),
|
|
359
|
+
ports: [{ containerPort: values.service.port, name: "http" }],
|
|
360
|
+
}],
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
export const service = new Service({
|
|
367
|
+
metadata: {
|
|
368
|
+
name: include("my-app.fullname"),
|
|
369
|
+
labels: include("my-app.labels"),
|
|
370
|
+
},
|
|
371
|
+
spec: {
|
|
372
|
+
type: values.service.type,
|
|
373
|
+
ports: [{
|
|
374
|
+
port: values.service.port,
|
|
375
|
+
targetPort: "http",
|
|
376
|
+
protocol: "TCP",
|
|
377
|
+
name: "http",
|
|
378
|
+
}],
|
|
379
|
+
selector: include("my-app.selectorLabels"),
|
|
380
|
+
},
|
|
381
|
+
});
|
|
382
|
+
`,
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
},
|
|
386
|
+
skills: createSkillsLoader(import.meta.url, [
|
|
387
|
+
{ file: "chant-helm.md", name: "chant-helm", description: "Build, validate, and package Helm charts from a chant project" },
|
|
388
|
+
{ file: "chant-helm-patterns.md", name: "chant-helm-patterns", description: "Common Helm chart patterns and best practices using chant" },
|
|
389
|
+
{ file: "chant-helm-security.md", name: "chant-helm-security", description: "Security best practices for Helm charts built with chant" },
|
|
390
|
+
]),
|
|
391
|
+
async docs(options) {
|
|
392
|
+
const { generateDocs } = await import("./codegen/docs.js");
|
|
393
|
+
await generateDocs({ verbose: options?.verbose });
|
|
394
|
+
},
|
|
395
|
+
async generate(options) {
|
|
396
|
+
const { generate, writeGeneratedFiles } = await import("./codegen/generate.js");
|
|
397
|
+
const { dirname } = await import("path");
|
|
398
|
+
const { fileURLToPath } = await import("url");
|
|
399
|
+
const result = await generate({ verbose: options?.verbose ?? true });
|
|
400
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
401
|
+
writeGeneratedFiles(result, pkgDir);
|
|
402
|
+
console.error(`Generated ${result.resources} resources, ${result.properties} property types`);
|
|
403
|
+
},
|
|
404
|
+
async validate(options) {
|
|
405
|
+
const { validate } = await import("./validate.js");
|
|
406
|
+
const { printValidationResult } = await import("@intentius/chant/codegen/validate");
|
|
407
|
+
const result = await validate();
|
|
408
|
+
printValidationResult(result);
|
|
409
|
+
},
|
|
410
|
+
async coverage(options) {
|
|
411
|
+
const { analyzeHelmCoverage } = await import("./coverage.js");
|
|
412
|
+
await analyzeHelmCoverage({
|
|
413
|
+
verbose: options?.verbose,
|
|
414
|
+
minOverall: options?.minOverall,
|
|
415
|
+
});
|
|
416
|
+
},
|
|
417
|
+
async package(options) {
|
|
418
|
+
const { packageLexicon } = await import("./codegen/package.js");
|
|
419
|
+
const { writeBundleSpec } = await import("@intentius/chant/codegen/package");
|
|
420
|
+
const { join, dirname } = await import("path");
|
|
421
|
+
const { fileURLToPath } = await import("url");
|
|
422
|
+
const { spec, stats } = await packageLexicon({ verbose: options?.verbose, force: options?.force });
|
|
423
|
+
const pkgDir = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
424
|
+
const distDir = join(pkgDir, "dist");
|
|
425
|
+
writeBundleSpec(spec, distDir);
|
|
426
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
427
|
+
},
|
|
428
|
+
async listArtifacts(options) {
|
|
429
|
+
const { listArtifacts } = await import("./list-artifacts.js");
|
|
430
|
+
return listArtifacts(options);
|
|
431
|
+
},
|
|
432
|
+
// #1246 — per-resource observation of what each declared chart's release
|
|
433
|
+
// holds (`helm get manifest` + `helm get hooks`, both channels), with total
|
|
434
|
+
// tri-state verdicts. This is what gives `lifecycle diff --live` helm rows.
|
|
435
|
+
async describeResources(options) {
|
|
436
|
+
const { describeResources } = await import("./describe-resources.js");
|
|
437
|
+
return describeResources(options);
|
|
438
|
+
},
|
|
439
|
+
// #1247 — deep property-level trees for the release's resources, read live
|
|
440
|
+
// through the k8s lexicon's typed client and normalized by its hooks.
|
|
441
|
+
// Dynamic import for the same reason k8s's is: the reader reaches
|
|
442
|
+
// `@intentius/chant-k8s-client`, which must stay off the build path.
|
|
443
|
+
async observeResourcesDeep(options) {
|
|
444
|
+
const { observeResourcesDeepHelm } = await import("./deep-observe.js");
|
|
445
|
+
return observeResourcesDeepHelm(options);
|
|
446
|
+
},
|
|
447
|
+
// The k8s lexicon's hooks by reference, not a copy (#1247) — the objects a
|
|
448
|
+
// release deploys are Kubernetes objects, and core normalizes the declared
|
|
449
|
+
// tree with the same rules the reader used.
|
|
450
|
+
deepNormalizationHooks: helmDeepNormalizationHooks,
|
|
451
|
+
// #1495 piece 4 — the deploy-unit observer for helm-upgrade units: a
|
|
452
|
+
// release's presence and native status, read back by the same name the
|
|
453
|
+
// capability deploys under. Dynamic import mirrors listArtifacts.
|
|
454
|
+
async describeStackStatus(options) {
|
|
455
|
+
const { describeStackStatus } = await import("./describe-stack-status.js");
|
|
456
|
+
return describeStackStatus(options);
|
|
457
|
+
},
|
|
458
|
+
};
|