@intentius/chant-lexicon-helm 0.46.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
|
@@ -1,28 +1,432 @@
|
|
|
1
1
|
import { exec } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
2
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { load } from "js-yaml";
|
|
8
|
+
import { helmInputDigest } from "../../render-digest";
|
|
9
|
+
import { loadRenderContent, loadRenderManifest, renderStoreRoot, type RenderManifest } from "../../render-store";
|
|
10
|
+
import { routeRender } from "../../render-wrapper";
|
|
11
|
+
import { materializeWrapperChart } from "../../wrapper-chart";
|
|
12
|
+
import {
|
|
13
|
+
ClusterProbeError,
|
|
14
|
+
compareCapabilityProfile,
|
|
15
|
+
probeClusterCapabilities,
|
|
16
|
+
} from "./cluster-probe";
|
|
3
17
|
import { safeHeartbeat } from "@intentius/chant/op";
|
|
18
|
+
import {
|
|
19
|
+
maybeRecordAutoRelease,
|
|
20
|
+
type AutoReleaseResult,
|
|
21
|
+
} from "@intentius/chant/components/auto-release";
|
|
4
22
|
|
|
5
23
|
const execAsync = promisify(exec);
|
|
6
24
|
|
|
25
|
+
/**
|
|
26
|
+
* The cluster capability profile a deploy is declared against (epic #1228).
|
|
27
|
+
* Declared, never sniffed — the epic's finding 1 shows the default kube
|
|
28
|
+
* version is a property of the helm binary, so an undeclared profile cannot
|
|
29
|
+
* be part of a stable identity. When declared it joins the input digest.
|
|
30
|
+
*/
|
|
31
|
+
export interface HelmCapabilityProfile {
|
|
32
|
+
/** The profile's declared name (its `helm.capabilityProfiles` key), used in assertion messages. Not part of the digest — only the declared facts are. */
|
|
33
|
+
name?: string;
|
|
34
|
+
/** Kubernetes version the render targets (`--kube-version`). */
|
|
35
|
+
kubeVersion?: string;
|
|
36
|
+
/** Extra API versions the render assumes (`--api-versions`). Order-insensitive for the digest. */
|
|
37
|
+
apiVersions?: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
7
40
|
export interface HelmInstallArgs {
|
|
8
41
|
/** Helm release name. */
|
|
9
42
|
name: string;
|
|
10
|
-
/**
|
|
11
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Chart reference (local path or `repo/chart`). Required for an unpinned
|
|
45
|
+
* deploy; refused alongside `contentDigest` — a pinned deploy's bytes come
|
|
46
|
+
* from the render store, never from a deploy-time render.
|
|
47
|
+
*/
|
|
48
|
+
chart?: string;
|
|
49
|
+
/**
|
|
50
|
+
* `sha256:` content digest of a stored pinned render (#1242). When set,
|
|
51
|
+
* the deploy takes the pinned path: the recorded render is loaded from the
|
|
52
|
+
* render store, its digest verified, and those exact bytes installed as a
|
|
53
|
+
* structure-preserving wrapper chart — no deploy-time render. The chart /
|
|
54
|
+
* values / set / chartVersion render inputs are refused in this mode.
|
|
55
|
+
*/
|
|
56
|
+
contentDigest?: string;
|
|
57
|
+
/** Render store root override for the pinned path (tests, non-default stores). Defaults to `CHANT_HELM_RENDER_ROOT` / `~/.chant/helm-renders`. */
|
|
58
|
+
renderStoreRoot?: string;
|
|
59
|
+
/** Chart version to pin (`--version`). Part of the input digest. */
|
|
60
|
+
chartVersion?: string;
|
|
12
61
|
/** Path to a values file. */
|
|
13
62
|
values?: string;
|
|
14
|
-
/** Kubernetes namespace. */
|
|
63
|
+
/** Kubernetes namespace. Defaults to the stored render's namespace on the pinned path. */
|
|
15
64
|
namespace?: string;
|
|
16
65
|
/** Additional --set arguments. */
|
|
17
66
|
set?: Record<string, string>;
|
|
67
|
+
/** Capability profile the deploy is declared against, if any. Part of the input digest when declared, and asserted against the live target cluster before any helm mutation (#1244). */
|
|
68
|
+
capabilityProfile?: HelmCapabilityProfile;
|
|
69
|
+
/**
|
|
70
|
+
* Deliberate escape hatch for the capability-profile assertion (#1244).
|
|
71
|
+
* When the live cluster diverges from the declared profile — or cannot be
|
|
72
|
+
* probed at all — the deploy normally refuses before any helm mutation.
|
|
73
|
+
* Set `true` to deploy anyway; the bypassed divergences are warned about
|
|
74
|
+
* and recorded in the release record (`profileOverride`), so the ledger
|
|
75
|
+
* shows the release knowingly skewed from its declared profile. Has no
|
|
76
|
+
* effect when the cluster matches, and none when no profile is declared.
|
|
77
|
+
*/
|
|
78
|
+
overrideProfileAssertion?: boolean;
|
|
79
|
+
/** Component name for the release record. Defaults to the release `name` — a chart's release is its deploy unit. */
|
|
80
|
+
component?: string;
|
|
81
|
+
/** Environment for the release record. Defaults to `"local"`, the same default `chant run --components` applies. */
|
|
82
|
+
env?: string;
|
|
83
|
+
/** Run id for the release record. Defaults to `$GITHUB_RUN_ID`/`$CI_PIPELINE_ID`, then a locally generated id — the same fallback chain `chant components release` uses. */
|
|
84
|
+
runId?: string;
|
|
85
|
+
/** Set `false` to skip release-record emission, mirroring `--no-release-record`. Default: emission is ON. */
|
|
86
|
+
recordRelease?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** What `helmInstall` returns: the input digest and the release-record outcome, so a workflow can carry both. */
|
|
90
|
+
export interface HelmInstallResult {
|
|
91
|
+
/**
|
|
92
|
+
* `sha256:` digest over the deploy's *inputs* — chart, chart version,
|
|
93
|
+
* resolved values, capability profile if declared (#1243). This is the
|
|
94
|
+
* input-side join key: two environments deploying the same chart at the
|
|
95
|
+
* same version with the same values share it even when their clusters
|
|
96
|
+
* legitimately differ. It is deliberately not a rendered-output digest —
|
|
97
|
+
* that is the pinned-render work of later #1228 phases. Absent only when
|
|
98
|
+
* the digest could not be computed (see `release.reason`).
|
|
99
|
+
*/
|
|
100
|
+
inputDigest?: string;
|
|
101
|
+
/** Outcome of the ledger append — `recorded: true` carries the `ReleaseRecord` written. */
|
|
102
|
+
release: AutoReleaseResult;
|
|
103
|
+
/**
|
|
104
|
+
* Outcome of the deploy-time capability-profile assertion (#1244). Present
|
|
105
|
+
* exactly when the deploy declared a profile with assertable facts (on the
|
|
106
|
+
* pinned path the stored render's profile always is one). A mismatch
|
|
107
|
+
* without the override never reaches a result — the activity throws before
|
|
108
|
+
* any helm mutation — so a present outcome is either a verified match or a
|
|
109
|
+
* deliberate, recorded override.
|
|
110
|
+
*/
|
|
111
|
+
profileAssertion?: HelmProfileAssertionOutcome;
|
|
112
|
+
/** Helm release name the deploy targeted (`args.name`). */
|
|
113
|
+
releaseName: string;
|
|
114
|
+
/** Namespace the deploy targeted, or `null` when helm's default applied. */
|
|
115
|
+
namespace: string | null;
|
|
116
|
+
/**
|
|
117
|
+
* True when this deploy took the pinned path (#1242): recorded bytes
|
|
118
|
+
* loaded from the render store, digest-verified, installed as a wrapper
|
|
119
|
+
* chart with no deploy-time render.
|
|
120
|
+
*/
|
|
121
|
+
pinned: boolean;
|
|
122
|
+
/**
|
|
123
|
+
* `sha256:` content digest of the installed render — the artifact identity
|
|
124
|
+
* (the epic's `renderDigest`, named for the #1237 split). Pinned path only:
|
|
125
|
+
* an unpinned deploy has no rendered-output identity to report.
|
|
126
|
+
*/
|
|
127
|
+
contentDigest?: string;
|
|
128
|
+
/**
|
|
129
|
+
* Release revision after the deploy, from `helm get metadata -o json`.
|
|
130
|
+
* Pinned path only, and absent when the metadata read failed after a
|
|
131
|
+
* successful install (best-effort, like the ledger append).
|
|
132
|
+
*/
|
|
133
|
+
revision?: number;
|
|
134
|
+
/** Chart version installed — `helm get metadata`'s `version`, falling back to the stored render's. Pinned path only. */
|
|
135
|
+
chartVersion?: string | null;
|
|
136
|
+
/** Number of CRD documents shipped in the wrapper's `crds/` (uninstall-safe; epic finding 4). Pinned path only. */
|
|
137
|
+
crdsApplied?: number;
|
|
138
|
+
/** Number of non-CRD documents installed through the wrapper's templates (hook documents included). Pinned path only. */
|
|
139
|
+
docsApplied?: number;
|
|
140
|
+
/** Number of hook documents among `docsApplied` — helm registers and runs them from their recorded annotations (epic finding 5). Pinned path only. */
|
|
141
|
+
hooksRun?: number;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Thrown by the pinned path (#1242) when `contentDigest` resolves to nothing in the render store. */
|
|
145
|
+
export class PinnedRenderNotFoundError extends Error {
|
|
146
|
+
constructor(contentDigest: string, root: string) {
|
|
147
|
+
super(
|
|
148
|
+
`pinned install refused: render ${contentDigest} is not in the render store at ${root}. ` +
|
|
149
|
+
`Nothing was deployed. A pinned deploy installs recorded bytes; render and persist the chart ` +
|
|
150
|
+
`first (HelmRender with a capabilityProfile), or check the digest against \`listRenderManifests\`.`,
|
|
151
|
+
);
|
|
152
|
+
this.name = "PinnedRenderNotFoundError";
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Thrown by the pinned path (#1242) when the stored bytes no longer hash to their digest — store corruption, caught before any mutation. */
|
|
157
|
+
export class PinnedRenderIntegrityError extends Error {
|
|
158
|
+
constructor(contentDigest: string, actual: string) {
|
|
159
|
+
super(
|
|
160
|
+
`pinned install refused: stored bytes for ${contentDigest} hash to ${actual} — the render store ` +
|
|
161
|
+
`entry is corrupt. Nothing was deployed. Re-render to repopulate the store; a digest that does ` +
|
|
162
|
+
`not match its bytes must never reach a cluster.`,
|
|
163
|
+
);
|
|
164
|
+
this.name = "PinnedRenderIntegrityError";
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Thrown by the pinned path (#1242) when the call mixes pinned and unpinned inputs, or targets a release name the bytes were not rendered for. */
|
|
169
|
+
export class PinnedInstallInputError extends Error {
|
|
170
|
+
constructor(message: string) {
|
|
171
|
+
super(message);
|
|
172
|
+
this.name = "PinnedInstallInputError";
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Thrown by the pinned path (#1242) when the deploy's declared capability
|
|
178
|
+
* profile disagrees with the profile recorded in the stored render's
|
|
179
|
+
* manifest. Distinct from `CapabilityProfileMismatchError` (#1244, declared
|
|
180
|
+
* vs *live cluster*): this is declared vs *recorded*, checked first and
|
|
181
|
+
* entirely offline.
|
|
182
|
+
*/
|
|
183
|
+
export class PinnedProfileMismatchError extends Error {
|
|
184
|
+
constructor(release: string, contentDigest: string, divergences: string[]) {
|
|
185
|
+
super(
|
|
186
|
+
`pinned install refused: release "${release}" declares a capability profile that does not match ` +
|
|
187
|
+
`the one render ${contentDigest} was pinned against:\n` +
|
|
188
|
+
divergences.map((d) => ` - ${d}`).join("\n") +
|
|
189
|
+
`\nThe stored bytes are only accurate for the profile they were rendered for. Deploy them ` +
|
|
190
|
+
`against that profile, or re-render for this one. Nothing was deployed.`,
|
|
191
|
+
);
|
|
192
|
+
this.name = "PinnedProfileMismatchError";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** How the deploy-time profile assertion (#1244) concluded, when a profile was declared. */
|
|
197
|
+
export type HelmProfileAssertionOutcome =
|
|
198
|
+
| { matched: true }
|
|
199
|
+
| { matched: false; divergences: string[]; overridden: true };
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Thrown before any helm mutation when the target cluster does not match the
|
|
203
|
+
* deploy's declared capability profile (#1244). The message names every
|
|
204
|
+
* divergence — declared vs live — so the refusal is specific, never a shrug.
|
|
205
|
+
*/
|
|
206
|
+
export class CapabilityProfileMismatchError extends Error {
|
|
207
|
+
constructor(
|
|
208
|
+
release: string,
|
|
209
|
+
profileName: string | undefined,
|
|
210
|
+
public readonly divergences: string[],
|
|
211
|
+
) {
|
|
212
|
+
const profile = profileName ? ` "${profileName}"` : "";
|
|
213
|
+
super(
|
|
214
|
+
`helm release "${release}": target cluster does not match the declared capability profile${profile}:\n` +
|
|
215
|
+
divergences.map((d) => ` - ${d}`).join("\n") +
|
|
216
|
+
`\nThe deploy's identity was pinned against the declared profile; deploying to a diverging cluster ` +
|
|
217
|
+
`breaks the per-cluster guarantee that the same rendered bytes reach every cluster sharing a profile. ` +
|
|
218
|
+
`Fix the profile or retarget the deploy — or pass overrideProfileAssertion: true for a deliberate ` +
|
|
219
|
+
`override, which is recorded in the release record. Nothing was deployed.`,
|
|
220
|
+
);
|
|
221
|
+
this.name = "CapabilityProfileMismatchError";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
226
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Merge one `--set key=value` entry into `values` at its dotted path,
|
|
231
|
+
* helm's precedence (set wins over the file). Helm's backslash escaping for
|
|
232
|
+
* literal dots in keys is not interpreted — the digest needs a deterministic
|
|
233
|
+
* shape, not a re-implementation of helm's parser, and an escaped key still
|
|
234
|
+
* digests deterministically, just under its split form.
|
|
235
|
+
*/
|
|
236
|
+
function applySetEntry(values: Record<string, unknown>, path: string, value: string): void {
|
|
237
|
+
const segments = path.split(".");
|
|
238
|
+
let node = values;
|
|
239
|
+
for (const segment of segments.slice(0, -1)) {
|
|
240
|
+
const next = node[segment];
|
|
241
|
+
if (isRecord(next)) {
|
|
242
|
+
node = next;
|
|
243
|
+
} else {
|
|
244
|
+
const created: Record<string, unknown> = {};
|
|
245
|
+
node[segment] = created;
|
|
246
|
+
node = created;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
node[segments[segments.length - 1]] = value;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* The resolved values a deploy will hand to helm: the parsed values file
|
|
254
|
+
* (when given) with `--set` entries applied on top. A values file that does
|
|
255
|
+
* not parse to a mapping contributes nothing — helm itself rejects such a
|
|
256
|
+
* file at deploy time, so the digest never sees it succeed.
|
|
257
|
+
*/
|
|
258
|
+
function resolveValues(args: HelmInstallArgs): Record<string, unknown> {
|
|
259
|
+
const parsed = args.values ? load(readFileSync(args.values, "utf8")) : undefined;
|
|
260
|
+
const values: Record<string, unknown> = isRecord(parsed) ? parsed : {};
|
|
261
|
+
for (const [path, value] of Object.entries(args.set ?? {})) applySetEntry(values, path, value);
|
|
262
|
+
return values;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The input-side identity of a helm deploy (#1243): a `sha256:` digest over
|
|
267
|
+
* the canonical JSON of `{ chart, chartVersion, values, capabilityProfile? }`.
|
|
268
|
+
* Canonical JSON (core's `canonicalJson`, RFC 8785 shape) sorts object keys,
|
|
269
|
+
* so the digest is stable across value key order; `apiVersions` is sorted
|
|
270
|
+
* for the same reason. The digest changes when any actual input changes —
|
|
271
|
+
* the chart, its version, a value, the declared profile.
|
|
272
|
+
*
|
|
273
|
+
* Delegates to `helmInputDigest` (#1237) — the same helper `HelmRender`
|
|
274
|
+
* records as a pinned render's `inputDigest` — so a deploy and a render of
|
|
275
|
+
* the same inputs share their identity by construction.
|
|
276
|
+
*/
|
|
277
|
+
export function helmInstallInputDigest(args: HelmInstallArgs): string {
|
|
278
|
+
if (!args.chart) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`cannot compute an input digest for release "${args.name}": no chart reference. ` +
|
|
281
|
+
`A pinned deploy carries its digests in the stored RenderManifest instead.`,
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
return helmInputDigest({
|
|
285
|
+
chart: args.chart,
|
|
286
|
+
chartVersion: args.chartVersion,
|
|
287
|
+
values: resolveValues(args),
|
|
288
|
+
capabilityProfile: args.capabilityProfile,
|
|
289
|
+
});
|
|
18
290
|
}
|
|
19
291
|
|
|
20
292
|
/**
|
|
21
|
-
*
|
|
293
|
+
* Assert the target cluster matches the deploy's declared capability profile
|
|
294
|
+
* (#1244), before any helm mutation. Returns the assertion outcome, or
|
|
295
|
+
* `undefined` when there was nothing to assert (no profile, or a profile
|
|
296
|
+
* declaring no facts) — that path never touches kubectl, keeping the
|
|
297
|
+
* no-profile deploy exactly what it was.
|
|
298
|
+
*
|
|
299
|
+
* A mismatch throws `CapabilityProfileMismatchError` naming every divergence.
|
|
300
|
+
* An unprobeable cluster throws too — a deploy that cannot verify its
|
|
301
|
+
* guarantee must not silently proceed. `overrideProfileAssertion: true`
|
|
302
|
+
* turns both refusals into a warned, recorded override.
|
|
303
|
+
*/
|
|
304
|
+
async function assertCapabilityProfile(
|
|
305
|
+
args: HelmInstallArgs,
|
|
306
|
+
signal?: AbortSignal,
|
|
307
|
+
): Promise<HelmProfileAssertionOutcome | undefined> {
|
|
308
|
+
const profile = args.capabilityProfile;
|
|
309
|
+
if (!profile || (!profile.kubeVersion && !(profile.apiVersions?.length))) return undefined;
|
|
310
|
+
|
|
311
|
+
let divergences: string[];
|
|
312
|
+
try {
|
|
313
|
+
const live = await probeClusterCapabilities(signal);
|
|
314
|
+
divergences = compareCapabilityProfile(profile, live);
|
|
315
|
+
} catch (err) {
|
|
316
|
+
const why = err instanceof Error ? err.message : String(err);
|
|
317
|
+
if (!args.overrideProfileAssertion) {
|
|
318
|
+
throw new ClusterProbeError(
|
|
319
|
+
`helm release "${args.name}": the deploy declares capability profile` +
|
|
320
|
+
`${profile.name ? ` "${profile.name}"` : ""} but the target cluster's capabilities could not ` +
|
|
321
|
+
`be verified — ${why}. A deploy that cannot verify its declared profile does not proceed. ` +
|
|
322
|
+
`Nothing was deployed. Pass overrideProfileAssertion: true only for a deliberate override.`,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
divergences = [`capability probe failed: ${why}`];
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (divergences.length === 0) return { matched: true };
|
|
329
|
+
if (!args.overrideProfileAssertion) {
|
|
330
|
+
throw new CapabilityProfileMismatchError(args.name, profile.name, divergences);
|
|
331
|
+
}
|
|
332
|
+
console.error(
|
|
333
|
+
`warning: helm release "${args.name}" deploying despite capability profile` +
|
|
334
|
+
`${profile.name ? ` "${profile.name}"` : ""} divergence (overrideProfileAssertion):\n` +
|
|
335
|
+
divergences.map((d) => ` - ${d}`).join("\n") +
|
|
336
|
+
`\nThe override is recorded in the release record.`,
|
|
337
|
+
);
|
|
338
|
+
return { matched: false, divergences, overridden: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Append the deploy's release record, best-effort (#1243). Called only after
|
|
343
|
+
* `helm upgrade --install` succeeded — a failed deploy writes nothing, by
|
|
344
|
+
* construction, because this function is never reached.
|
|
345
|
+
*
|
|
346
|
+
* Reuses `maybeRecordAutoRelease` — the same field resolution the
|
|
347
|
+
* `chant run --components` post-run step applies (actor from
|
|
348
|
+
* `GITHUB_ACTOR`/`GITLAB_USER_LOGIN`/`USER`, git sha from `HEAD`, timestamp
|
|
349
|
+
* taken at record time, append + push to the `chant/lifecycle` orphan
|
|
350
|
+
* branch). A field that cannot be resolved here (e.g. no git repo in a
|
|
351
|
+
* worker container) is never faked: the append is skipped and the skip is
|
|
352
|
+
* warned about, exactly as the convention's error path behaves.
|
|
353
|
+
*/
|
|
354
|
+
async function recordHelmRelease(
|
|
355
|
+
args: HelmInstallArgs,
|
|
356
|
+
digest: string | undefined,
|
|
357
|
+
digestError: string | undefined,
|
|
358
|
+
profileOverride: string | undefined,
|
|
359
|
+
inputDigest?: string,
|
|
360
|
+
): Promise<AutoReleaseResult> {
|
|
361
|
+
if (args.recordRelease === false) return { recorded: false, reason: "opted-out" };
|
|
362
|
+
if (!digest) {
|
|
363
|
+
return { recorded: false, reason: "error", error: digestError ?? "input digest unavailable" };
|
|
364
|
+
}
|
|
365
|
+
const runId =
|
|
366
|
+
args.runId ?? process.env.GITHUB_RUN_ID ?? process.env.CI_PIPELINE_ID ?? `local-${Date.now()}`;
|
|
367
|
+
return maybeRecordAutoRelease({
|
|
368
|
+
component: args.component ?? args.name,
|
|
369
|
+
env: args.env ?? "local",
|
|
370
|
+
success: true,
|
|
371
|
+
digest,
|
|
372
|
+
runId,
|
|
373
|
+
profileOverride,
|
|
374
|
+
inputDigest,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Deploy a helm release and append a `ReleaseRecord` (#1243).
|
|
380
|
+
*
|
|
381
|
+
* Two paths, selected by `contentDigest`:
|
|
382
|
+
*
|
|
383
|
+
* - **Unpinned** (no `contentDigest`): `helm upgrade --install <name>
|
|
384
|
+
* <chart>`, exactly as before #1242 — helm renders at deploy time. The
|
|
385
|
+
* ledger record is keyed by the input digest.
|
|
386
|
+
* - **Pinned** (`contentDigest` set, #1242): the recorded render is loaded
|
|
387
|
+
* from the render store, its digest verified, and those exact bytes
|
|
388
|
+
* installed as a structure-preserving wrapper chart — no deploy-time
|
|
389
|
+
* render. The ledger record is keyed by the content digest and carries
|
|
390
|
+
* the stored render's input digest alongside it.
|
|
391
|
+
*
|
|
392
|
+
* A ledger-append failure after a successful deploy warns and never throws:
|
|
393
|
+
* the deploy already happened, so a ledger-write hiccup must not turn a
|
|
394
|
+
* finished deploy into a failed activity — the record is observability, not
|
|
395
|
+
* part of the deploy itself.
|
|
396
|
+
*
|
|
22
397
|
* Uses longInfra profile — 20m timeout, heartbeat every 15s.
|
|
23
398
|
*/
|
|
24
|
-
export async function helmInstall(
|
|
399
|
+
export async function helmInstall(
|
|
400
|
+
args: HelmInstallArgs,
|
|
401
|
+
signal?: AbortSignal,
|
|
402
|
+
): Promise<HelmInstallResult> {
|
|
403
|
+
if (args.contentDigest) return pinnedHelmInstall(args, args.contentDigest, signal);
|
|
404
|
+
if (!args.chart) {
|
|
405
|
+
throw new PinnedInstallInputError(
|
|
406
|
+
`helm release "${args.name}": no chart reference and no contentDigest — pass \`chart\` for an ` +
|
|
407
|
+
`unpinned deploy, or \`contentDigest\` to install a recorded pinned render. Nothing was deployed.`,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// The digest is computed over the same bytes the deploy reads, before the
|
|
412
|
+
// deploy runs. A compute failure (unreadable values file, unrepresentable
|
|
413
|
+
// value) must not block the deploy — helm gives the authoritative error —
|
|
414
|
+
// so it is carried into the record outcome instead of thrown here.
|
|
415
|
+
let inputDigest: string | undefined;
|
|
416
|
+
let digestError: string | undefined;
|
|
417
|
+
try {
|
|
418
|
+
inputDigest = helmInstallInputDigest(args);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
digestError = `could not compute input digest: ${err instanceof Error ? err.message : String(err)}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// Deploy-time profile assertion (#1244): when a profile is declared, the
|
|
424
|
+
// live cluster must match it before helm mutates anything. A refusal (or
|
|
425
|
+
// an unprobeable cluster) throws here, so no helm command has run yet.
|
|
426
|
+
const profileAssertion = await assertCapabilityProfile(args, signal);
|
|
427
|
+
|
|
25
428
|
const parts = ["helm", "upgrade", "--install", "--wait", args.name, args.chart];
|
|
429
|
+
if (args.chartVersion) parts.push("--version", args.chartVersion);
|
|
26
430
|
if (args.namespace) parts.push("--namespace", args.namespace, "--create-namespace");
|
|
27
431
|
if (args.values) parts.push("-f", args.values);
|
|
28
432
|
for (const [k, v] of Object.entries(args.set ?? {})) parts.push("--set", `${k}=${v}`);
|
|
@@ -38,4 +442,223 @@ export async function helmInstall(args: HelmInstallArgs, signal?: AbortSignal):
|
|
|
38
442
|
} finally {
|
|
39
443
|
clearInterval(heartbeatInterval);
|
|
40
444
|
}
|
|
445
|
+
|
|
446
|
+
const profileOverride =
|
|
447
|
+
profileAssertion && !profileAssertion.matched ? profileAssertion.divergences.join("; ") : undefined;
|
|
448
|
+
const release = await recordHelmRelease(args, inputDigest, digestError, profileOverride);
|
|
449
|
+
if (!release.recorded && release.reason !== "opted-out") {
|
|
450
|
+
const why =
|
|
451
|
+
release.reason === "error"
|
|
452
|
+
? release.error
|
|
453
|
+
: `${release.reason}${release.detail ? ` — ${release.detail}` : ""}`;
|
|
454
|
+
console.error(
|
|
455
|
+
`warning: helm release "${args.name}" deployed, but no release record was appended (${why}). ` +
|
|
456
|
+
`The deploy succeeded; the ledger write is best-effort observability and never fails a finished deploy.`,
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
inputDigest,
|
|
461
|
+
release,
|
|
462
|
+
...(profileAssertion ? { profileAssertion } : {}),
|
|
463
|
+
releaseName: args.name,
|
|
464
|
+
namespace: args.namespace ?? null,
|
|
465
|
+
pinned: false,
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* The pinned install path (#1242): deploy a recorded render's exact bytes.
|
|
471
|
+
*
|
|
472
|
+
* Refusals, all before any helm mutation, each with its own error type:
|
|
473
|
+
* mixed pinned/unpinned inputs (`PinnedInstallInputError`), digest not in
|
|
474
|
+
* the store (`PinnedRenderNotFoundError`), stored bytes not hashing to
|
|
475
|
+
* their digest (`PinnedRenderIntegrityError`), a declared profile
|
|
476
|
+
* disagreeing with the recorded one (`PinnedProfileMismatchError`), a
|
|
477
|
+
* release name the bytes were not rendered for
|
|
478
|
+
* (`PinnedInstallInputError` — `.Release.Name` is baked into the bytes),
|
|
479
|
+
* and the #1244 live-cluster assertion, which on this path always runs
|
|
480
|
+
* against the *manifest's* profile — a stored render is pinned by
|
|
481
|
+
* construction, so the deploy always has a profile to verify.
|
|
482
|
+
*
|
|
483
|
+
* The install input is a structure-preserving wrapper chart materialized
|
|
484
|
+
* from the routed render (`routeRender` over the stored bytes — the #1239
|
|
485
|
+
* seam): CRD-origin documents in `crds/` (uninstall-safe, epic finding 4),
|
|
486
|
+
* everything else shipped verbatim through `.Files.Get` shims so no byte is
|
|
487
|
+
* re-templated. `helm history` / `helm rollback` keep working — the wrapper
|
|
488
|
+
* inherits the source chart's name and version, and the release is a normal
|
|
489
|
+
* helm release.
|
|
490
|
+
*/
|
|
491
|
+
async function pinnedHelmInstall(
|
|
492
|
+
args: HelmInstallArgs,
|
|
493
|
+
contentDigest: string,
|
|
494
|
+
signal?: AbortSignal,
|
|
495
|
+
): Promise<HelmInstallResult> {
|
|
496
|
+
const renderInputs: string[] = (["chart", "chartVersion", "values"] as const).filter(
|
|
497
|
+
(k) => args[k] !== undefined,
|
|
498
|
+
);
|
|
499
|
+
if (args.set && Object.keys(args.set).length > 0) renderInputs.push("set");
|
|
500
|
+
if (renderInputs.length > 0) {
|
|
501
|
+
throw new PinnedInstallInputError(
|
|
502
|
+
`pinned install refused: release "${args.name}" passes contentDigest together with ` +
|
|
503
|
+
`deploy-time render input(s) ${renderInputs.map((k) => `\`${k}\``).join(", ")}. A pinned deploy ` +
|
|
504
|
+
`installs the recorded bytes of ${contentDigest} — chart, values, set and chartVersion were ` +
|
|
505
|
+
`closed at render time and cannot be re-opened here. Drop them, or drop contentDigest for an ` +
|
|
506
|
+
`unpinned deploy. Nothing was deployed.`,
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
const storeOpts = args.renderStoreRoot ? { root: args.renderStoreRoot } : undefined;
|
|
511
|
+
const manifest = loadRenderManifest(contentDigest, storeOpts);
|
|
512
|
+
const content = manifest ? loadRenderContent(contentDigest, storeOpts) : undefined;
|
|
513
|
+
if (!manifest || content === undefined) {
|
|
514
|
+
throw new PinnedRenderNotFoundError(contentDigest, args.renderStoreRoot ?? renderStoreRoot());
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Verify before any mutation: the stored bytes are the digest's preimage
|
|
518
|
+
// (the store writes canonical bytes under their own hash), so a mismatch
|
|
519
|
+
// means the entry was corrupted or tampered with after it was written.
|
|
520
|
+
const actual = `sha256:${createHash("sha256").update(content, "utf8").digest("hex")}`;
|
|
521
|
+
if (actual !== contentDigest) throw new PinnedRenderIntegrityError(contentDigest, actual);
|
|
522
|
+
|
|
523
|
+
if (args.name !== manifest.releaseName) {
|
|
524
|
+
throw new PinnedInstallInputError(
|
|
525
|
+
`pinned install refused: render ${contentDigest} was rendered for release ` +
|
|
526
|
+
`"${manifest.releaseName}" but the deploy targets release "${args.name}". \`.Release.Name\` is ` +
|
|
527
|
+
`baked into the recorded bytes (labels, resource names), so installing them under another name ` +
|
|
528
|
+
`deploys bytes that claim a different release. Deploy under "${manifest.releaseName}", or ` +
|
|
529
|
+
`render for "${args.name}". Nothing was deployed.`,
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
assertDeclaredProfileMatchesManifest(args, contentDigest, manifest);
|
|
534
|
+
|
|
535
|
+
// The #1244 live-cluster assertion, against the profile the bytes were
|
|
536
|
+
// actually rendered for — the RenderManifest's, not the caller's (the two
|
|
537
|
+
// were just proven to agree where the caller declared one).
|
|
538
|
+
const profileAssertion = await assertCapabilityProfile(
|
|
539
|
+
{
|
|
540
|
+
...args,
|
|
541
|
+
capabilityProfile: {
|
|
542
|
+
name: manifest.capabilityProfile.cluster,
|
|
543
|
+
kubeVersion: manifest.capabilityProfile.kubeVersion,
|
|
544
|
+
apiVersions: manifest.capabilityProfile.apiVersions,
|
|
545
|
+
},
|
|
546
|
+
},
|
|
547
|
+
signal,
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
const namespace = args.namespace ?? manifest.namespace ?? undefined;
|
|
551
|
+
if (args.namespace && manifest.namespace && args.namespace !== manifest.namespace) {
|
|
552
|
+
console.error(
|
|
553
|
+
`warning: pinned release "${args.name}" deploys to namespace "${args.namespace}" but was ` +
|
|
554
|
+
`rendered for "${manifest.namespace}" — documents that bake in \`.Release.Namespace\` will ` +
|
|
555
|
+
`carry the rendered value.`,
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const routed = routeRender(content, { chart: manifest.chart, chartVersion: manifest.chartVersion });
|
|
560
|
+
for (const warning of routed.warnings) {
|
|
561
|
+
console.error(`warning: pinned release "${args.name}": ${warning.message}`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const wrapperDir = mkdtempSync(join(tmpdir(), "chant-helm-pinned-"));
|
|
565
|
+
const heartbeatInterval = setInterval(() => {
|
|
566
|
+
safeHeartbeat({ step: "helm install (pinned)", release: args.name });
|
|
567
|
+
}, 15_000);
|
|
568
|
+
try {
|
|
569
|
+
materializeWrapperChart(routed, wrapperDir);
|
|
570
|
+
const parts = ["helm", "upgrade", "--install", "--wait", args.name, wrapperDir];
|
|
571
|
+
if (namespace) parts.push("--namespace", namespace, "--create-namespace");
|
|
572
|
+
const { stdout, stderr } = await execAsync(parts.join(" "), { signal });
|
|
573
|
+
if (stdout) console.log(stdout);
|
|
574
|
+
if (stderr) console.error(stderr);
|
|
575
|
+
} finally {
|
|
576
|
+
clearInterval(heartbeatInterval);
|
|
577
|
+
rmSync(wrapperDir, { recursive: true, force: true });
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Revision from helm's own bookkeeping (`helm get metadata -o json`) —
|
|
581
|
+
// best-effort after a successful install, like the ledger append: a
|
|
582
|
+
// metadata read hiccup must not fail a finished deploy.
|
|
583
|
+
let revision: number | undefined;
|
|
584
|
+
let chartVersion: string | null = manifest.chartVersion;
|
|
585
|
+
try {
|
|
586
|
+
const metaParts = ["helm", "get", "metadata", args.name, "-o", "json"];
|
|
587
|
+
if (namespace) metaParts.push("--namespace", namespace);
|
|
588
|
+
const { stdout } = await execAsync(metaParts.join(" "), { signal });
|
|
589
|
+
const meta = JSON.parse(stdout) as { revision?: unknown; version?: unknown };
|
|
590
|
+
const parsed = Number(meta.revision);
|
|
591
|
+
if (Number.isFinite(parsed)) revision = parsed;
|
|
592
|
+
if (typeof meta.version === "string" && meta.version.length > 0) chartVersion = meta.version;
|
|
593
|
+
} catch (err) {
|
|
594
|
+
console.error(
|
|
595
|
+
`warning: pinned release "${args.name}" deployed, but \`helm get metadata\` failed ` +
|
|
596
|
+
`(${err instanceof Error ? err.message : String(err)}); the result carries no revision.`,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const profileOverride =
|
|
601
|
+
profileAssertion && !profileAssertion.matched ? profileAssertion.divergences.join("; ") : undefined;
|
|
602
|
+
// Pinned deploys record the content digest — what this cluster actually
|
|
603
|
+
// received — with the stored render's input digest alongside as the
|
|
604
|
+
// cross-environment join key (epic Decisions: profiles are per cluster,
|
|
605
|
+
// so bytes legitimately differ across environments; inputs do not).
|
|
606
|
+
const release = await recordHelmRelease(args, contentDigest, undefined, profileOverride, manifest.inputDigest);
|
|
607
|
+
if (!release.recorded && release.reason !== "opted-out") {
|
|
608
|
+
const why =
|
|
609
|
+
release.reason === "error"
|
|
610
|
+
? release.error
|
|
611
|
+
: `${release.reason}${release.detail ? ` — ${release.detail}` : ""}`;
|
|
612
|
+
console.error(
|
|
613
|
+
`warning: helm release "${args.name}" deployed, but no release record was appended (${why}). ` +
|
|
614
|
+
`The deploy succeeded; the ledger write is best-effort observability and never fails a finished deploy.`,
|
|
615
|
+
);
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
return {
|
|
619
|
+
inputDigest: manifest.inputDigest,
|
|
620
|
+
release,
|
|
621
|
+
...(profileAssertion ? { profileAssertion } : {}),
|
|
622
|
+
releaseName: args.name,
|
|
623
|
+
namespace: namespace ?? null,
|
|
624
|
+
pinned: true,
|
|
625
|
+
contentDigest,
|
|
626
|
+
revision,
|
|
627
|
+
chartVersion,
|
|
628
|
+
crdsApplied: routed.crds.length,
|
|
629
|
+
docsApplied: routed.main.length + routed.hooks.length,
|
|
630
|
+
hooksRun: routed.hooks.length,
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Offline half of the pinned profile check: when the deploy itself declares
|
|
636
|
+
* a capability profile, its facts must agree with the profile recorded in
|
|
637
|
+
* the stored render's manifest — same kubeVersion, declared apiVersions all
|
|
638
|
+
* among the recorded set. A caller that declares nothing defers entirely to
|
|
639
|
+
* the manifest.
|
|
640
|
+
*/
|
|
641
|
+
function assertDeclaredProfileMatchesManifest(
|
|
642
|
+
args: HelmInstallArgs,
|
|
643
|
+
contentDigest: string,
|
|
644
|
+
manifest: RenderManifest,
|
|
645
|
+
): void {
|
|
646
|
+
const declared = args.capabilityProfile;
|
|
647
|
+
if (!declared) return;
|
|
648
|
+
const recorded = manifest.capabilityProfile;
|
|
649
|
+
const divergences: string[] = [];
|
|
650
|
+
if (declared.kubeVersion && declared.kubeVersion !== recorded.kubeVersion) {
|
|
651
|
+
divergences.push(
|
|
652
|
+
`kubeVersion: deploy declares ${declared.kubeVersion}, render was pinned against ${recorded.kubeVersion}`,
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
const recordedApis = new Set(recorded.apiVersions);
|
|
656
|
+
for (const apiVersion of declared.apiVersions ?? []) {
|
|
657
|
+
if (!recordedApis.has(apiVersion)) {
|
|
658
|
+
divergences.push(`apiVersion ${apiVersion}: declared by the deploy, absent from the render's recorded profile`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
if (divergences.length > 0) {
|
|
662
|
+
throw new PinnedProfileMismatchError(args.name, contentDigest, divergences);
|
|
663
|
+
}
|
|
41
664
|
}
|
|
@@ -6,5 +6,20 @@
|
|
|
6
6
|
* The `helmInstall` step builder stays in core (@intentius/chant/op), re-exported
|
|
7
7
|
* from the temporal Op-authoring barrel like the other core builders.
|
|
8
8
|
*/
|
|
9
|
-
export {
|
|
10
|
-
|
|
9
|
+
export {
|
|
10
|
+
helmInstall,
|
|
11
|
+
helmInstallInputDigest,
|
|
12
|
+
CapabilityProfileMismatchError,
|
|
13
|
+
PinnedRenderNotFoundError,
|
|
14
|
+
PinnedRenderIntegrityError,
|
|
15
|
+
PinnedInstallInputError,
|
|
16
|
+
PinnedProfileMismatchError,
|
|
17
|
+
} from "./helm";
|
|
18
|
+
export type {
|
|
19
|
+
HelmInstallArgs,
|
|
20
|
+
HelmInstallResult,
|
|
21
|
+
HelmCapabilityProfile,
|
|
22
|
+
HelmProfileAssertionOutcome,
|
|
23
|
+
} from "./helm";
|
|
24
|
+
export { probeClusterCapabilities, compareCapabilityProfile, ClusterProbeError } from "./cluster-probe";
|
|
25
|
+
export type { LiveClusterCapabilities, DeclaredCapabilityProfile } from "./cluster-probe";
|