@intentius/chant-lexicon-helm 0.46.0 → 0.50.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 +19 -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 +25 -13
- 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/op/builders.d.ts +47 -0
- package/dist/op/builders.d.ts.map +1 -0
- 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 +106 -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/op/builders.test.ts +70 -0
- package/src/op/builders.ts +66 -0
- 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 +97 -12
- 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,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Release-scoped observation plumbing, shared by the helm lexicon's
|
|
3
|
+
* `describeResources` (#1246) and `observeResourcesDeep` (#1247).
|
|
4
|
+
*
|
|
5
|
+
* The helm lexicon's runtime unit is the release: the chant project declares
|
|
6
|
+
* a `Helm::Chart`, `helm upgrade --install` turns it into a release, and the
|
|
7
|
+
* release stores the rendered manifests it applied. Observation therefore
|
|
8
|
+
* resolves each declared chart to its release (via `helm list`, the same
|
|
9
|
+
* read `listArtifacts` has always used) and reads what the release holds.
|
|
10
|
+
*
|
|
11
|
+
* Two channels, both mandatory (#1246): `helm get manifest` reports the
|
|
12
|
+
* non-hook documents and `helm get hooks` reports the hook resources —
|
|
13
|
+
* hooks are excluded from the manifest channel, so reading one channel
|
|
14
|
+
* would report every hook resource as drift.
|
|
15
|
+
*
|
|
16
|
+
* Failure discipline is the observation contract (#1089): a missing helm
|
|
17
|
+
* binary or an unreachable cluster is NOT-OBSERVED with a total reason,
|
|
18
|
+
* never a clean empty result that classifies as N creates. Only a release
|
|
19
|
+
* that `helm list` was asked about and did not report is an absence.
|
|
20
|
+
*
|
|
21
|
+
* Cluster selection follows #1488, same as `list-artifacts.ts` and
|
|
22
|
+
* `describe-stack-status.ts`: the environment's declared k8s binding
|
|
23
|
+
* (`k8s.profiles.<env>.context`) rides as `--kube-context` when present;
|
|
24
|
+
* ambient otherwise.
|
|
25
|
+
*/
|
|
26
|
+
import { exec } from "node:child_process";
|
|
27
|
+
import { promisify } from "node:util";
|
|
28
|
+
import { loadAll } from "js-yaml";
|
|
29
|
+
import type { UnobservedEntity, UnobservedReason } from "@intentius/chant/lexicon";
|
|
30
|
+
import { loadChantConfigUpward } from "@intentius/chant/config";
|
|
31
|
+
import { resolveClusterTarget } from "@intentius/chant/kubectl-context";
|
|
32
|
+
import { gvkToTypeName } from "@intentius/chant-lexicon-k8s/spec/parse";
|
|
33
|
+
|
|
34
|
+
// `helm get manifest` for a large chart is several megabytes, past node's
|
|
35
|
+
// default 1MiB exec buffer — the same truncation `helm-upgrade.ts` hit live.
|
|
36
|
+
const execP = promisify(exec);
|
|
37
|
+
const defaultRunner = (command: string): Promise<{ stdout: string }> =>
|
|
38
|
+
execP(command, { maxBuffer: 64 * 1024 * 1024 });
|
|
39
|
+
|
|
40
|
+
/** Injectable command runner, so tests drive every branch without helm or a cluster. */
|
|
41
|
+
export type HelmRunner = (command: string) => Promise<{ stdout: string }>;
|
|
42
|
+
|
|
43
|
+
export { defaultRunner as defaultHelmRunner };
|
|
44
|
+
|
|
45
|
+
export const HELM_CHART_ENTITY_TYPE = "Helm::Chart";
|
|
46
|
+
export const HELM_RELEASE_TYPE = "Helm::Release";
|
|
47
|
+
|
|
48
|
+
/** Shell-quote one argv element, same convention as `helm-upgrade.ts`. */
|
|
49
|
+
export function q(arg: string): string {
|
|
50
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The environment's kube context (#1488): the declared binding when present,
|
|
55
|
+
* ambient otherwise. Unresolvable config is ambient, chant's own fallback —
|
|
56
|
+
* identical to `list-artifacts.ts`.
|
|
57
|
+
*/
|
|
58
|
+
export async function resolveHelmContext(environment: string): Promise<string | undefined> {
|
|
59
|
+
try {
|
|
60
|
+
const { config } = await loadChantConfigUpward(process.cwd());
|
|
61
|
+
return (await resolveClusterTarget(config as Record<string, unknown>, environment, "helm")).context;
|
|
62
|
+
} catch {
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Total verdict for a failed helm invocation (#1089/#1246). A missing binary
|
|
69
|
+
* and a missing/unusable kubeconfig are both `no-credentials` — the read had
|
|
70
|
+
* no usable way to reach the target. Anything else the CLI refused is
|
|
71
|
+
* `read-failed`.
|
|
72
|
+
*/
|
|
73
|
+
export function classifyHelmFailure(err: unknown): { reason: UnobservedReason; detail: string } {
|
|
74
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
75
|
+
const code = (err as { code?: string | number } | null)?.code;
|
|
76
|
+
if (
|
|
77
|
+
code === "ENOENT" ||
|
|
78
|
+
code === 127 ||
|
|
79
|
+
/command not found|not recognized|ENOENT/i.test(message)
|
|
80
|
+
) {
|
|
81
|
+
return { reason: "no-credentials", detail: "helm binary not found on PATH" };
|
|
82
|
+
}
|
|
83
|
+
if (/kubeconfig|kubernetes cluster unreachable|unauthorized|forbidden|no configuration has been provided|connection refused/i.test(message)) {
|
|
84
|
+
return { reason: "no-credentials", detail: firstLine(message) };
|
|
85
|
+
}
|
|
86
|
+
return { reason: "read-failed", detail: firstLine(message) };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function firstLine(message: string): string {
|
|
90
|
+
const line = message.split("\n").find((l) => l.trim().length > 0);
|
|
91
|
+
return (line ?? message).trim();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface HelmListEntry {
|
|
95
|
+
name?: string;
|
|
96
|
+
namespace?: string;
|
|
97
|
+
revision?: string;
|
|
98
|
+
updated?: string;
|
|
99
|
+
status?: string;
|
|
100
|
+
chart?: string;
|
|
101
|
+
app_version?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Kinds that are cluster-scoped, so a document without `metadata.namespace`
|
|
106
|
+
* must not inherit the release namespace. Well-known built-ins only; a
|
|
107
|
+
* cluster-scoped CRD instance this set does not name defaults to the release
|
|
108
|
+
* namespace, which affects the row's address, never its verdict.
|
|
109
|
+
*/
|
|
110
|
+
export const CLUSTER_SCOPED_KINDS = new Set([
|
|
111
|
+
"Namespace",
|
|
112
|
+
"CustomResourceDefinition",
|
|
113
|
+
"ClusterRole",
|
|
114
|
+
"ClusterRoleBinding",
|
|
115
|
+
"StorageClass",
|
|
116
|
+
"PriorityClass",
|
|
117
|
+
"IngressClass",
|
|
118
|
+
"PersistentVolume",
|
|
119
|
+
"ValidatingWebhookConfiguration",
|
|
120
|
+
"MutatingWebhookConfiguration",
|
|
121
|
+
"APIService",
|
|
122
|
+
"RuntimeClass",
|
|
123
|
+
"CSIDriver",
|
|
124
|
+
"CSINode",
|
|
125
|
+
]);
|
|
126
|
+
|
|
127
|
+
/** One rendered document a release holds, from either channel. */
|
|
128
|
+
export interface ParsedReleaseResource {
|
|
129
|
+
/**
|
|
130
|
+
* Row key, following the k8s runtime-sweep convention
|
|
131
|
+
* (`<Kind>/<namespace>/<name>`, `cluster:<Kind>/<name>`) so a helm row and
|
|
132
|
+
* a k8s row for the same object spell the same identity.
|
|
133
|
+
*/
|
|
134
|
+
key: string;
|
|
135
|
+
apiVersion: string;
|
|
136
|
+
kind: string;
|
|
137
|
+
name: string;
|
|
138
|
+
namespace?: string;
|
|
139
|
+
/** Which read reported it — hooks are invisible to the manifest channel. */
|
|
140
|
+
channel: "manifest" | "hooks";
|
|
141
|
+
/** chant's k8s entity type for the document's GVK (`K8s::Apps::Deployment`). */
|
|
142
|
+
entityType: string;
|
|
143
|
+
/** The document itself, minus nothing — the release's stored declaration. */
|
|
144
|
+
doc: Record<string, unknown>;
|
|
145
|
+
/** `helm.sh/hook*` annotations, for hook-channel rows. */
|
|
146
|
+
hook?: { hook: string; weight?: string; deletePolicy?: string };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** One declared chart resolved to a live release, with what the release holds. */
|
|
150
|
+
export interface ObservedRelease {
|
|
151
|
+
entityName: string;
|
|
152
|
+
release: string;
|
|
153
|
+
namespace: string;
|
|
154
|
+
status?: string;
|
|
155
|
+
revision?: string;
|
|
156
|
+
chart?: string;
|
|
157
|
+
appVersion?: string;
|
|
158
|
+
updated?: string;
|
|
159
|
+
resources: ParsedReleaseResource[];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ReleaseObservation {
|
|
163
|
+
/** Charts whose release was found and fully read. */
|
|
164
|
+
releases: ObservedRelease[];
|
|
165
|
+
/** Chart entities whose release `helm list` did not report — a real absence. */
|
|
166
|
+
absent: string[];
|
|
167
|
+
/** Chart entities that could not be read, with a total reason (#1089). */
|
|
168
|
+
unobserved: Record<string, UnobservedEntity>;
|
|
169
|
+
/** The helm command issued per chart entity (#1620). */
|
|
170
|
+
queried: Record<string, string>;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ReleaseObserveOptions {
|
|
174
|
+
environment: string;
|
|
175
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>;
|
|
176
|
+
/** Deploy unit, `<namespace>/<release>` or `<release>` (see `describeStackStatus`). */
|
|
177
|
+
stack?: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** The declared chart entities, with the release name each one deploys as. */
|
|
181
|
+
export function chartEntities(
|
|
182
|
+
entities: Map<string, { entityType: string; props: Record<string, unknown> }>,
|
|
183
|
+
): Array<{ entityName: string; release: string }> {
|
|
184
|
+
const charts: Array<{ entityName: string; release: string }> = [];
|
|
185
|
+
for (const [entityName, entity] of entities) {
|
|
186
|
+
if (entity.entityType !== HELM_CHART_ENTITY_TYPE) continue;
|
|
187
|
+
const name = entity.props?.name;
|
|
188
|
+
charts.push({ entityName, release: typeof name === "string" && name ? name : entityName });
|
|
189
|
+
}
|
|
190
|
+
return charts;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function parseStack(stack: string): { namespace?: string; release: string } {
|
|
194
|
+
const slash = stack.indexOf("/");
|
|
195
|
+
return slash > 0
|
|
196
|
+
? { namespace: stack.slice(0, slash), release: stack.slice(slash + 1) }
|
|
197
|
+
: { release: stack };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Find the list entry for one declared chart. Release identity resolves in
|
|
202
|
+
* order: the deploy unit (`stack`), a release named after the chart, then a
|
|
203
|
+
* unique release whose `chart` field is `<chartName>-<version>` — the release
|
|
204
|
+
* installed from this chart under another name. Ambiguity resolves to
|
|
205
|
+
* nothing: two candidate releases are not an identity.
|
|
206
|
+
*/
|
|
207
|
+
function findRelease(
|
|
208
|
+
entries: HelmListEntry[],
|
|
209
|
+
release: string,
|
|
210
|
+
wantNamespace: string | undefined,
|
|
211
|
+
chartName: string,
|
|
212
|
+
): { entry: HelmListEntry } | { ambiguous: string } | undefined {
|
|
213
|
+
const named = entries.filter(
|
|
214
|
+
(e) => e.name === release && (wantNamespace === undefined || e.namespace === wantNamespace),
|
|
215
|
+
);
|
|
216
|
+
if (named.length === 1) return { entry: named[0] };
|
|
217
|
+
if (named.length > 1) {
|
|
218
|
+
// The same release name in several namespaces is not an identity — and
|
|
219
|
+
// not an absence either. Say which namespaces so the caller can scope
|
|
220
|
+
// the read with a deploy unit (`<namespace>/<release>`).
|
|
221
|
+
return { ambiguous: named.map((e) => e.namespace).join(", ") };
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const byChart = entries.filter(
|
|
225
|
+
(e) => typeof e.chart === "string" && new RegExp(`^${escapeRegExp(chartName)}-\\d`).test(e.chart),
|
|
226
|
+
);
|
|
227
|
+
return byChart.length === 1 ? { entry: byChart[0] } : undefined;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function escapeRegExp(s: string): string {
|
|
231
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** Parse one channel's multi-document YAML stream into per-resource rows. */
|
|
235
|
+
export function parseReleaseDocuments(
|
|
236
|
+
text: string,
|
|
237
|
+
channel: "manifest" | "hooks",
|
|
238
|
+
releaseNamespace: string,
|
|
239
|
+
): ParsedReleaseResource[] {
|
|
240
|
+
const rows: ParsedReleaseResource[] = [];
|
|
241
|
+
let docs: unknown[];
|
|
242
|
+
try {
|
|
243
|
+
docs = loadAll(text);
|
|
244
|
+
} catch {
|
|
245
|
+
return rows;
|
|
246
|
+
}
|
|
247
|
+
for (const raw of docs) {
|
|
248
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) continue;
|
|
249
|
+
const doc = raw as Record<string, unknown>;
|
|
250
|
+
const apiVersion = typeof doc.apiVersion === "string" ? doc.apiVersion : undefined;
|
|
251
|
+
const kind = typeof doc.kind === "string" ? doc.kind : undefined;
|
|
252
|
+
const metadata = doc.metadata as { name?: unknown; namespace?: unknown; annotations?: Record<string, unknown> } | undefined;
|
|
253
|
+
const name = typeof metadata?.name === "string" ? metadata.name : undefined;
|
|
254
|
+
if (!apiVersion || !kind || !name) continue;
|
|
255
|
+
|
|
256
|
+
const clusterScoped = CLUSTER_SCOPED_KINDS.has(kind) && typeof metadata?.namespace !== "string";
|
|
257
|
+
const namespace = clusterScoped
|
|
258
|
+
? undefined
|
|
259
|
+
: typeof metadata?.namespace === "string"
|
|
260
|
+
? metadata.namespace
|
|
261
|
+
: releaseNamespace; // helm applies the release namespace to namespace-silent documents
|
|
262
|
+
|
|
263
|
+
const slash = apiVersion.indexOf("/");
|
|
264
|
+
const entityType = gvkToTypeName({
|
|
265
|
+
group: slash > 0 ? apiVersion.slice(0, slash) : "",
|
|
266
|
+
version: slash > 0 ? apiVersion.slice(slash + 1) : apiVersion,
|
|
267
|
+
kind,
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const annotations = metadata?.annotations ?? {};
|
|
271
|
+
const hookAnnotation = annotations["helm.sh/hook"];
|
|
272
|
+
const hook =
|
|
273
|
+
channel === "hooks" && typeof hookAnnotation === "string"
|
|
274
|
+
? {
|
|
275
|
+
hook: hookAnnotation,
|
|
276
|
+
...(typeof annotations["helm.sh/hook-weight"] === "string"
|
|
277
|
+
? { weight: annotations["helm.sh/hook-weight"] as string }
|
|
278
|
+
: {}),
|
|
279
|
+
...(typeof annotations["helm.sh/hook-delete-policy"] === "string"
|
|
280
|
+
? { deletePolicy: annotations["helm.sh/hook-delete-policy"] as string }
|
|
281
|
+
: {}),
|
|
282
|
+
}
|
|
283
|
+
: undefined;
|
|
284
|
+
|
|
285
|
+
rows.push({
|
|
286
|
+
key: namespace ? `${kind}/${namespace}/${name}` : `cluster:${kind}/${name}`,
|
|
287
|
+
apiVersion,
|
|
288
|
+
kind,
|
|
289
|
+
name,
|
|
290
|
+
...(namespace ? { namespace } : {}),
|
|
291
|
+
channel,
|
|
292
|
+
entityType,
|
|
293
|
+
doc,
|
|
294
|
+
...(hook ? { hook } : {}),
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
return rows;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Resolve every declared chart to its release and read both channels.
|
|
302
|
+
* Never throws for a per-release failure; a whole-read failure (the `helm
|
|
303
|
+
* list` itself) lands every chart entity in `unobserved` with one reason.
|
|
304
|
+
*/
|
|
305
|
+
export async function observeReleases(
|
|
306
|
+
options: ReleaseObserveOptions,
|
|
307
|
+
run: HelmRunner = defaultRunner,
|
|
308
|
+
): Promise<ReleaseObservation> {
|
|
309
|
+
const charts = chartEntities(options.entities);
|
|
310
|
+
const result: ReleaseObservation = { releases: [], absent: [], unobserved: {}, queried: {} };
|
|
311
|
+
if (charts.length === 0) return result;
|
|
312
|
+
|
|
313
|
+
const stack = options.stack ? parseStack(options.stack) : undefined;
|
|
314
|
+
// The deploy unit names the release when the project has one chart — the
|
|
315
|
+
// component flow deploys each chart under the step's `release`, which need
|
|
316
|
+
// not equal the chart name.
|
|
317
|
+
if (stack && charts.length === 1) charts[0].release = stack.release;
|
|
318
|
+
|
|
319
|
+
const context = await resolveHelmContext(options.environment);
|
|
320
|
+
const ctxFlag = context ? ` --kube-context ${q(context)}` : "";
|
|
321
|
+
|
|
322
|
+
const listCommand = `helm list -A -o json${ctxFlag}`;
|
|
323
|
+
let entries: HelmListEntry[];
|
|
324
|
+
try {
|
|
325
|
+
const { stdout } = await run(listCommand);
|
|
326
|
+
const parsed: unknown = JSON.parse(stdout);
|
|
327
|
+
if (!Array.isArray(parsed)) throw new Error("helm list returned non-array JSON");
|
|
328
|
+
entries = parsed as HelmListEntry[];
|
|
329
|
+
} catch (err) {
|
|
330
|
+
const { reason, detail } = classifyHelmFailure(err);
|
|
331
|
+
for (const { entityName } of charts) {
|
|
332
|
+
result.unobserved[entityName] = { type: HELM_CHART_ENTITY_TYPE, reason, detail };
|
|
333
|
+
result.queried[entityName] = listCommand;
|
|
334
|
+
}
|
|
335
|
+
return result;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
for (const { entityName, release } of charts) {
|
|
339
|
+
const found = findRelease(entries, release, stack?.namespace, release);
|
|
340
|
+
if (found && "ambiguous" in found) {
|
|
341
|
+
result.unobserved[entityName] = {
|
|
342
|
+
type: HELM_CHART_ENTITY_TYPE,
|
|
343
|
+
reason: "read-failed",
|
|
344
|
+
detail: `release "${release}" exists in several namespaces (${found.ambiguous}) — scope the read with a <namespace>/<release> deploy unit`,
|
|
345
|
+
};
|
|
346
|
+
result.queried[entityName] = listCommand;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const entry = found?.entry;
|
|
350
|
+
if (!entry || !entry.name || !entry.namespace) {
|
|
351
|
+
result.absent.push(entityName);
|
|
352
|
+
result.queried[entityName] = listCommand;
|
|
353
|
+
continue;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const base = `-n ${q(entry.namespace)}${ctxFlag}`;
|
|
357
|
+
const manifestCommand = `helm get manifest ${q(entry.name)} ${base}`;
|
|
358
|
+
const hooksCommand = `helm get hooks ${q(entry.name)} ${base}`;
|
|
359
|
+
result.queried[entityName] = manifestCommand;
|
|
360
|
+
|
|
361
|
+
let manifestText: string;
|
|
362
|
+
let hooksText: string;
|
|
363
|
+
try {
|
|
364
|
+
({ stdout: manifestText } = await run(manifestCommand));
|
|
365
|
+
({ stdout: hooksText } = await run(hooksCommand));
|
|
366
|
+
} catch (err) {
|
|
367
|
+
// The release exists but what it holds could not be read — an
|
|
368
|
+
// unreachable release is NOT-OBSERVED with a reason, never absent and
|
|
369
|
+
// never a clean row that claims nothing drifted (#1246).
|
|
370
|
+
const { reason, detail } = classifyHelmFailure(err);
|
|
371
|
+
result.unobserved[entityName] = { type: HELM_CHART_ENTITY_TYPE, reason, detail };
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
result.releases.push({
|
|
376
|
+
entityName,
|
|
377
|
+
release: entry.name,
|
|
378
|
+
namespace: entry.namespace,
|
|
379
|
+
...(entry.status ? { status: entry.status } : {}),
|
|
380
|
+
...(entry.revision ? { revision: entry.revision } : {}),
|
|
381
|
+
...(entry.chart ? { chart: entry.chart } : {}),
|
|
382
|
+
...(entry.app_version ? { appVersion: entry.app_version } : {}),
|
|
383
|
+
...(entry.updated ? { updated: entry.updated } : {}),
|
|
384
|
+
resources: [
|
|
385
|
+
...parseReleaseDocuments(manifestText, "manifest", entry.namespace),
|
|
386
|
+
...parseReleaseDocuments(hooksText, "hooks", entry.namespace),
|
|
387
|
+
],
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return result;
|
|
392
|
+
}
|