@intentius/chant-lexicon-helm 0.45.0 → 0.49.0
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.d.ts +94 -0
- package/dist/archive-render.d.ts.map +1 -0
- package/dist/commands.d.ts +54 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/config.d.ts +135 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/deep-observe-hooks.d.ts +2 -0
- package/dist/deep-observe-hooks.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +54 -0
- package/dist/deep-observe.d.ts.map +1 -0
- package/dist/describe-resources.d.ts +17 -0
- package/dist/describe-resources.d.ts.map +1 -0
- package/dist/index.d.ts +18 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +5 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/whm503.d.ts +34 -0
- package/dist/lint/post-synth/whm503.d.ts.map +1 -0
- package/dist/lint/post-synth/whm504.d.ts +20 -0
- package/dist/lint/post-synth/whm504.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +2 -0
- package/dist/okf/rules/WHM503.md +15 -0
- package/dist/okf/rules/WHM504.md +11 -0
- package/dist/okf/types/Values.md +1 -0
- package/dist/op/activities/cluster-probe.d.ts +44 -0
- package/dist/op/activities/cluster-probe.d.ts.map +1 -0
- package/dist/op/activities/helm.d.ts +179 -5
- package/dist/op/activities/helm.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +4 -2
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/pinnability/actions.d.ts +76 -0
- package/dist/pinnability/actions.d.ts.map +1 -0
- package/dist/pinnability/classify.d.ts +129 -0
- package/dist/pinnability/classify.d.ts.map +1 -0
- package/dist/pinnability/conditions.d.ts +109 -0
- package/dist/pinnability/conditions.d.ts.map +1 -0
- package/dist/pinnability/index.d.ts +17 -0
- package/dist/pinnability/index.d.ts.map +1 -0
- package/dist/pinnability/localize.d.ts +124 -0
- package/dist/pinnability/localize.d.ts.map +1 -0
- package/dist/pinnability/render-stream.d.ts +27 -0
- package/dist/pinnability/render-stream.d.ts.map +1 -0
- package/dist/pinnability/values.d.ts +69 -0
- package/dist/pinnability/values.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- package/dist/release-observe.d.ts +109 -0
- package/dist/release-observe.d.ts.map +1 -0
- package/dist/render-diff.d.ts +192 -0
- package/dist/render-diff.d.ts.map +1 -0
- package/dist/render-digest.d.ts +135 -0
- package/dist/render-digest.d.ts.map +1 -0
- package/dist/render-store.d.ts +310 -0
- package/dist/render-store.d.ts.map +1 -0
- package/dist/render-wrapper.d.ts +140 -0
- package/dist/render-wrapper.d.ts.map +1 -0
- package/dist/render.d.ts +81 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/rules/whm503.ts +101 -0
- package/dist/rules/whm504.ts +52 -0
- package/dist/skills/chant-helm.md +7 -0
- package/dist/values-probe.d.ts +177 -0
- package/dist/values-probe.d.ts.map +1 -0
- package/dist/wrapper-chart.d.ts +61 -0
- package/dist/wrapper-chart.d.ts.map +1 -0
- package/package.json +10 -4
- package/src/archive-render.test.ts +201 -0
- package/src/archive-render.ts +119 -0
- package/src/commands.test.ts +487 -0
- package/src/commands.ts +445 -0
- package/src/config.test.ts +130 -0
- package/src/config.ts +220 -0
- package/src/deep-observe-hooks.ts +20 -0
- package/src/deep-observe.test.ts +165 -0
- package/src/deep-observe.ts +121 -0
- package/src/describe-resources.test.ts +255 -0
- package/src/describe-resources.ts +198 -0
- package/src/index.ts +98 -2
- package/src/lifecycle-integration.test.ts +289 -0
- package/src/lint/audit-catalog.ts +2 -0
- package/src/lint/post-synth/index.ts +4 -0
- package/src/lint/post-synth/whm503.test.ts +134 -0
- package/src/lint/post-synth/whm503.ts +101 -0
- package/src/lint/post-synth/whm504.test.ts +105 -0
- package/src/lint/post-synth/whm504.ts +52 -0
- package/src/op/activities/cluster-probe.test.ts +66 -0
- package/src/op/activities/cluster-probe.ts +148 -0
- package/src/op/activities/helm.test.ts +824 -0
- package/src/op/activities/helm.ts +628 -5
- package/src/op/activities/index.ts +17 -2
- package/src/pinnability/actions.ts +263 -0
- package/src/pinnability/classify.ts +579 -0
- package/src/pinnability/conditions.ts +0 -0
- package/src/pinnability/index.ts +77 -0
- package/src/pinnability/localize.test.ts +234 -0
- package/src/pinnability/localize.ts +644 -0
- package/src/pinnability/pinnability.test.ts +560 -0
- package/src/pinnability/render-stream.ts +59 -0
- package/src/pinnability/values.ts +205 -0
- package/src/plugin.test.ts +3 -1
- package/src/plugin.ts +47 -0
- package/src/release-observe.ts +392 -0
- package/src/render-diff.test.ts +366 -0
- package/src/render-diff.ts +407 -0
- package/src/render-digest.test.ts +186 -0
- package/src/render-digest.ts +207 -0
- package/src/render-store.test.ts +412 -0
- package/src/render-store.ts +621 -0
- package/src/render-wrapper.test.ts +259 -0
- package/src/render-wrapper.ts +274 -0
- package/src/render.test.ts +383 -4
- package/src/render.ts +319 -8
- package/src/skills/chant-helm.md +7 -0
- package/src/values-probe.test.ts +400 -0
- package/src/values-probe.ts +627 -0
- package/src/wrapper-chart.test.ts +110 -0
- package/src/wrapper-chart.ts +124 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHM504: Dead value assignment (#1252, epic #1228 Phase 7).
|
|
3
|
+
*
|
|
4
|
+
* A supplied value that never survives coalescing is a lie in the config:
|
|
5
|
+
* it reads as if it configures something, and nothing ever sees it. The
|
|
6
|
+
* coalesced-values probe (#1251, ../../values-probe.ts) knows three ways a
|
|
7
|
+
* supplied value dies — shadowed by a later supplied layer, targeting a
|
|
8
|
+
* dependency a `condition:` disabled, or a values map under a top-level key
|
|
9
|
+
* that names no dependency at all (the silently-ignored subchart typo).
|
|
10
|
+
*
|
|
11
|
+
* The check reports over the probe records of the current process
|
|
12
|
+
* (`getValuesProbeRecords`): whatever ran the probe — the pinned-render
|
|
13
|
+
* build path, a test, a direct call — this check turns its dead assignments
|
|
14
|
+
* into diagnostics. No probe run, nothing to report; the probe itself needs
|
|
15
|
+
* the helm binary and the chart source, which a post-synth check must not
|
|
16
|
+
* require on its own.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
20
|
+
import { getValuesProbeRecords, type DeadAssignment } from "../../values-probe";
|
|
21
|
+
|
|
22
|
+
function describe(dead: DeadAssignment): string {
|
|
23
|
+
switch (dead.reason) {
|
|
24
|
+
case "shadowed":
|
|
25
|
+
return `supplied value "${dead.path}" (${dead.origin}) never survives coalescing — shadowed by ${dead.shadowedBy}`;
|
|
26
|
+
case "disabled-subchart":
|
|
27
|
+
return `supplied value "${dead.path}" (${dead.origin}) targets a disabled subchart — ${dead.shadowedBy}`;
|
|
28
|
+
case "unknown-subchart":
|
|
29
|
+
return `supplied values under "${dead.path}" (${dead.origin}) target no subchart — ${dead.shadowedBy}`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export const whm504: PostSynthCheck = {
|
|
34
|
+
id: "WHM504",
|
|
35
|
+
description: "Detect supplied values that never survive coalescing (shadowed, disabled subchart, unknown subchart)",
|
|
36
|
+
|
|
37
|
+
check(_ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
38
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
39
|
+
for (const record of getValuesProbeRecords()) {
|
|
40
|
+
for (const dead of record.probe.deadAssignments) {
|
|
41
|
+
diagnostics.push({
|
|
42
|
+
checkId: "WHM504",
|
|
43
|
+
severity: "warning",
|
|
44
|
+
message: describe(dead),
|
|
45
|
+
entity: record.name,
|
|
46
|
+
lexicon: "helm",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return diagnostics;
|
|
51
|
+
},
|
|
52
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure comparison/parsing halves of the deploy-time capability probe
|
|
3
|
+
* (#1244). The exec-driven probe itself is exercised through `helmInstall`
|
|
4
|
+
* in helm.test.ts, against the scripted kubectl double.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, test, expect } from "vitest";
|
|
7
|
+
import { compareCapabilityProfile, majorMinorOf, type LiveClusterCapabilities } from "./cluster-probe";
|
|
8
|
+
|
|
9
|
+
const live: LiveClusterCapabilities = {
|
|
10
|
+
kubeVersion: "v1.33.6+k3s1",
|
|
11
|
+
majorMinor: "1.33",
|
|
12
|
+
apiVersions: ["v1", "apps/v1", "batch/v1", "monitoring.coreos.com/v1"],
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
describe("majorMinorOf", () => {
|
|
16
|
+
test("accepts every shape KUBE_VERSION_PATTERN admits, plus live gitVersions", () => {
|
|
17
|
+
expect(majorMinorOf("1.33")).toBe("1.33");
|
|
18
|
+
expect(majorMinorOf("1.33.6")).toBe("1.33");
|
|
19
|
+
expect(majorMinorOf("v1.33.6")).toBe("1.33");
|
|
20
|
+
expect(majorMinorOf("v1.33.6+k3s1")).toBe("1.33");
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("rejects the unparseable", () => {
|
|
24
|
+
expect(majorMinorOf("latest")).toBeUndefined();
|
|
25
|
+
expect(majorMinorOf("")).toBeUndefined();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("compareCapabilityProfile", () => {
|
|
30
|
+
test("matching profile has no divergences — patch skew tolerated, extra live apiVersions ignored", () => {
|
|
31
|
+
expect(
|
|
32
|
+
compareCapabilityProfile(
|
|
33
|
+
{ kubeVersion: "1.33.4", apiVersions: ["apps/v1", "monitoring.coreos.com/v1"] },
|
|
34
|
+
live,
|
|
35
|
+
),
|
|
36
|
+
).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("a kubeVersion divergence names both the declared and the live version", () => {
|
|
40
|
+
expect(compareCapabilityProfile({ kubeVersion: "1.31.4" }, live)).toEqual([
|
|
41
|
+
"kubeVersion: profile declares 1.31.4 (1.31), cluster runs v1.33.6+k3s1 (1.33)",
|
|
42
|
+
]);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("every missing apiVersion is named, one divergence each", () => {
|
|
46
|
+
expect(
|
|
47
|
+
compareCapabilityProfile(
|
|
48
|
+
{ apiVersions: ["missing.io/v1", "apps/v1", "alsomissing.io/v1beta1"] },
|
|
49
|
+
live,
|
|
50
|
+
),
|
|
51
|
+
).toEqual([
|
|
52
|
+
"apiVersion missing.io/v1: declared by the profile, not served by the cluster",
|
|
53
|
+
"apiVersion alsomissing.io/v1beta1: declared by the profile, not served by the cluster",
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("kubeVersion and apiVersion divergences accumulate", () => {
|
|
58
|
+
expect(
|
|
59
|
+
compareCapabilityProfile({ kubeVersion: "1.31", apiVersions: ["missing.io/v1"] }, live),
|
|
60
|
+
).toHaveLength(2);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test("a profile declaring nothing diverges from nothing", () => {
|
|
64
|
+
expect(compareCapabilityProfile({}, live)).toEqual([]);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live-cluster capability probe for the deploy-time profile assertion
|
|
3
|
+
* (#1244, epic #1228 phase 4).
|
|
4
|
+
*
|
|
5
|
+
* A render pinned for one capability profile and deployed to a cluster with
|
|
6
|
+
* another is silently skewed: the bytes were computed against a declared
|
|
7
|
+
* `kubeVersion` and `apiVersions` set, and nothing at deploy time checked
|
|
8
|
+
* the target actually matches. This module reads the target cluster's real
|
|
9
|
+
* capabilities — `kubectl version -o json` for the server version,
|
|
10
|
+
* `kubectl api-versions` for the served API set — through the same
|
|
11
|
+
* exec-and-ambient-kubeconfig convention `helmInstall` already uses for the
|
|
12
|
+
* helm binary, and names every divergence from a declared profile.
|
|
13
|
+
*
|
|
14
|
+
* The comparison tolerance is deliberate: kubeVersion matches on
|
|
15
|
+
* major.minor. A profile declaring `1.33.6` matches a live `v1.33.4+k3s1` —
|
|
16
|
+
* the capability surface helm renders against (`.Capabilities.KubeVersion`)
|
|
17
|
+
* is a major.minor fact, and patch skew within a minor changes no API
|
|
18
|
+
* surface a chart can probe. Declared apiVersions are exact-match: each one
|
|
19
|
+
* the profile names must be served by the cluster.
|
|
20
|
+
*/
|
|
21
|
+
import { exec } from "node:child_process";
|
|
22
|
+
import { promisify } from "node:util";
|
|
23
|
+
|
|
24
|
+
const execAsync = promisify(exec);
|
|
25
|
+
|
|
26
|
+
/** What the probe read off the live cluster. */
|
|
27
|
+
export interface LiveClusterCapabilities {
|
|
28
|
+
/** The server's full version string (`serverVersion.gitVersion`), e.g. `v1.33.6+k3s1`. */
|
|
29
|
+
kubeVersion: string;
|
|
30
|
+
/** The server's `major.minor`, e.g. `1.33`, parsed from `serverVersion.major`/`minor` (or `gitVersion` when those are absent). */
|
|
31
|
+
majorMinor: string;
|
|
32
|
+
/** Every API version the cluster serves, one `group/version` (or `v1`) per entry, from `kubectl api-versions`. */
|
|
33
|
+
apiVersions: string[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Thrown when the cluster's capabilities cannot be read at all — kubectl
|
|
38
|
+
* missing, cluster unreachable, or unparseable output. A deploy that
|
|
39
|
+
* declared a profile cannot verify its guarantee through this error, so the
|
|
40
|
+
* caller must refuse to proceed rather than deploy blind.
|
|
41
|
+
*/
|
|
42
|
+
export class ClusterProbeError extends Error {
|
|
43
|
+
constructor(message: string) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.name = "ClusterProbeError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `major.minor` from a declared or live version string (`1.33`, `1.33.6`, `v1.33.6+k3s1`). Undefined when the string has no parseable major.minor. */
|
|
50
|
+
export function majorMinorOf(version: string): string | undefined {
|
|
51
|
+
const match = /^v?(\d+)\.(\d+)/.exec(version.trim());
|
|
52
|
+
return match ? `${match[1]}.${match[2]}` : undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseServerVersion(raw: string): { kubeVersion: string; majorMinor: string } {
|
|
56
|
+
let parsed: unknown;
|
|
57
|
+
try {
|
|
58
|
+
parsed = JSON.parse(raw);
|
|
59
|
+
} catch {
|
|
60
|
+
throw new ClusterProbeError(`kubectl version -o json produced unparseable output: ${raw.slice(0, 200)}`);
|
|
61
|
+
}
|
|
62
|
+
const server = (parsed as { serverVersion?: Record<string, unknown> }).serverVersion;
|
|
63
|
+
if (!server) {
|
|
64
|
+
throw new ClusterProbeError(
|
|
65
|
+
"kubectl version -o json reported no serverVersion — the cluster did not answer",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
const gitVersion = typeof server.gitVersion === "string" ? server.gitVersion : "";
|
|
69
|
+
// major/minor are the authoritative fields; minor can carry a vendor
|
|
70
|
+
// suffix (GKE reports "33+"), so digits are extracted rather than trusted.
|
|
71
|
+
const major = typeof server.major === "string" ? /\d+/.exec(server.major)?.[0] : undefined;
|
|
72
|
+
const minor = typeof server.minor === "string" ? /\d+/.exec(server.minor)?.[0] : undefined;
|
|
73
|
+
const majorMinor = major && minor ? `${major}.${minor}` : gitVersion ? majorMinorOf(gitVersion) : undefined;
|
|
74
|
+
if (!majorMinor) {
|
|
75
|
+
throw new ClusterProbeError(
|
|
76
|
+
`kubectl version -o json reported no usable server version (gitVersion: ${gitVersion || "<absent>"})`,
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return { kubeVersion: gitVersion || majorMinor, majorMinor };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Read the target cluster's capabilities. Uses whatever kubeconfig/context
|
|
84
|
+
* the process environment provides — the same ambient target `helm upgrade`
|
|
85
|
+
* itself will act on, so the probe and the deploy see one cluster.
|
|
86
|
+
*/
|
|
87
|
+
export async function probeClusterCapabilities(signal?: AbortSignal): Promise<LiveClusterCapabilities> {
|
|
88
|
+
let versionRaw: string;
|
|
89
|
+
let apiVersionsRaw: string;
|
|
90
|
+
try {
|
|
91
|
+
const [versionResult, apiResult] = await Promise.all([
|
|
92
|
+
execAsync("kubectl version -o json", { signal }),
|
|
93
|
+
execAsync("kubectl api-versions", { signal }),
|
|
94
|
+
]);
|
|
95
|
+
versionRaw = versionResult.stdout;
|
|
96
|
+
apiVersionsRaw = apiResult.stdout;
|
|
97
|
+
} catch (err) {
|
|
98
|
+
throw new ClusterProbeError(
|
|
99
|
+
`could not read the target cluster's capabilities: ${err instanceof Error ? err.message : String(err)}`,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
const { kubeVersion, majorMinor } = parseServerVersion(versionRaw);
|
|
103
|
+
const apiVersions = apiVersionsRaw
|
|
104
|
+
.split("\n")
|
|
105
|
+
.map((line) => line.trim())
|
|
106
|
+
.filter((line) => line.length > 0);
|
|
107
|
+
return { kubeVersion, majorMinor, apiVersions };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** The declared facts a profile asserts about a cluster — the activity-side profile shape. */
|
|
111
|
+
export interface DeclaredCapabilityProfile {
|
|
112
|
+
name?: string;
|
|
113
|
+
kubeVersion?: string;
|
|
114
|
+
apiVersions?: string[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Every way `live` diverges from what `profile` declares, one message per
|
|
119
|
+
* divergence, each naming the declared and the live value. Empty when the
|
|
120
|
+
* cluster matches the profile.
|
|
121
|
+
*
|
|
122
|
+
* kubeVersion compares on major.minor (see module doc). apiVersions is a
|
|
123
|
+
* subset check: everything the profile declares must be served; extra API
|
|
124
|
+
* versions on the cluster are not a divergence, since a render only ever
|
|
125
|
+
* probed the declared set.
|
|
126
|
+
*/
|
|
127
|
+
export function compareCapabilityProfile(
|
|
128
|
+
profile: DeclaredCapabilityProfile,
|
|
129
|
+
live: LiveClusterCapabilities,
|
|
130
|
+
): string[] {
|
|
131
|
+
const divergences: string[] = [];
|
|
132
|
+
if (profile.kubeVersion) {
|
|
133
|
+
const declared = majorMinorOf(profile.kubeVersion);
|
|
134
|
+
if (declared !== live.majorMinor) {
|
|
135
|
+
divergences.push(
|
|
136
|
+
`kubeVersion: profile declares ${profile.kubeVersion} (${declared ?? "unparseable"}), ` +
|
|
137
|
+
`cluster runs ${live.kubeVersion} (${live.majorMinor})`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const served = new Set(live.apiVersions);
|
|
142
|
+
for (const apiVersion of profile.apiVersions ?? []) {
|
|
143
|
+
if (!served.has(apiVersion)) {
|
|
144
|
+
divergences.push(`apiVersion ${apiVersion}: declared by the profile, not served by the cluster`);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
return divergences;
|
|
148
|
+
}
|