@konfig.ts/core 0.0.1
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/LICENSE +21 -0
- package/README.md +181 -0
- package/dist/Bundle.d.ts +125 -0
- package/dist/Compose.d.ts +68 -0
- package/dist/Helm.d.ts +19 -0
- package/dist/Manifest.d.ts +42 -0
- package/dist/Module.d.ts +146 -0
- package/dist/RenderContext.d.ts +38 -0
- package/dist/RenderError.d.ts +63 -0
- package/dist/_cast.d.ts +22 -0
- package/dist/_unstable.d.ts +2 -0
- package/dist/boundary.d.ts +7 -0
- package/dist/deps.d.ts +126 -0
- package/dist/diff.d.ts +96 -0
- package/dist/images.d.ts +44 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.mjs +1182 -0
- package/dist/index.mjs.map +1 -0
- package/dist/konfigConfig.d.ts +108 -0
- package/dist/render.d.ts +34 -0
- package/dist/renderManifest.d.ts +9 -0
- package/dist/subprocess.d.ts +38 -0
- package/dist/yaml/index.d.ts +1 -0
- package/dist/yaml/serialize.d.ts +30 -0
- package/package.json +75 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,1182 @@
|
|
|
1
|
+
import { Config, Context, Data, Effect, Layer, Schema, Stream } from "effect";
|
|
2
|
+
import * as YAML from "yaml";
|
|
3
|
+
import { FileSystem } from "effect/FileSystem";
|
|
4
|
+
import { Path } from "effect/Path";
|
|
5
|
+
import { ChildProcess } from "effect/unstable/process";
|
|
6
|
+
import { ChildProcessSpawner } from "effect/unstable/process/ChildProcessSpawner";
|
|
7
|
+
import { NodeRuntime, NodeServices } from "@effect/platform-node";
|
|
8
|
+
//#region \0rolldown/runtime.js
|
|
9
|
+
var __defProp = Object.defineProperty;
|
|
10
|
+
var __exportAll = (all, no_symbols) => {
|
|
11
|
+
let target = {};
|
|
12
|
+
for (var name in all) __defProp(target, name, {
|
|
13
|
+
get: all[name],
|
|
14
|
+
enumerable: true
|
|
15
|
+
});
|
|
16
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
17
|
+
return target;
|
|
18
|
+
};
|
|
19
|
+
//#endregion
|
|
20
|
+
//#region src/_cast.ts
|
|
21
|
+
/**
|
|
22
|
+
* Nominal-typing primitive — attach a phantom brand to a string value.
|
|
23
|
+
* Used by `Dep.*Ref` constructors and the like. Safe by construction:
|
|
24
|
+
* the brand is a type-only label that the caller stamps on themselves.
|
|
25
|
+
*/
|
|
26
|
+
const brand = (value) => value;
|
|
27
|
+
/**
|
|
28
|
+
* Unsafe escape hatch — claim a value has type `T` without runtime
|
|
29
|
+
* proof. Every call site MUST pass a one-line `reason` explaining why
|
|
30
|
+
* the cast is sound (e.g. "variance erasure", "runtime narrowed via
|
|
31
|
+
* _tag check", "Object.keys is string[]"). Audit by grepping for
|
|
32
|
+
* `unsafeCoerce(` and reading the reasons.
|
|
33
|
+
*
|
|
34
|
+
* For values crossing a trust boundary (external command output, file
|
|
35
|
+
* contents, network payloads), prefer `boundary` from
|
|
36
|
+
* `@konfig.ts/core/boundary` — it produces a `BoundaryDecodeError`
|
|
37
|
+
* instead of silently accepting the claim.
|
|
38
|
+
*
|
|
39
|
+
* The `reason` parameter is intentionally not used at runtime; it
|
|
40
|
+
* documents intent for readers and audits.
|
|
41
|
+
*/
|
|
42
|
+
const unsafeCoerce = (value, _reason) => value;
|
|
43
|
+
//#endregion
|
|
44
|
+
//#region src/RenderError.ts
|
|
45
|
+
var RenderError = class extends Data.TaggedError("RenderError") {};
|
|
46
|
+
var EmbedYamlReadError = class extends Data.TaggedError("EmbedYamlReadError") {};
|
|
47
|
+
var BoundaryDecodeError = class extends Data.TaggedError("BoundaryDecodeError") {};
|
|
48
|
+
var HelmVersionTooLow = class extends Data.TaggedError("HelmVersionTooLow") {
|
|
49
|
+
get message() {
|
|
50
|
+
return `Helm CLI too old: requires >= ${this.required}, found ${this.found}`;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
var HelmRenderError = class extends Data.TaggedError("HelmRenderError") {};
|
|
54
|
+
var HelmDigestMismatch = class extends Data.TaggedError("HelmDigestMismatch") {
|
|
55
|
+
get message() {
|
|
56
|
+
return `Helm chart ${this.chart}@${this.version} digest mismatch: expected ${this.expected}, got ${this.actual}`;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
var CrdExtractError = class extends Data.TaggedError("CrdExtractError") {};
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/boundary.ts
|
|
62
|
+
const boundary = (input) => (value) => Schema.decodeUnknownEffect(input.schema)(value).pipe(Effect.mapError((cause) => new BoundaryDecodeError({
|
|
63
|
+
schema: input.label ?? "boundary",
|
|
64
|
+
cause
|
|
65
|
+
})));
|
|
66
|
+
//#endregion
|
|
67
|
+
//#region src/Compose.ts
|
|
68
|
+
var Compose_exports = /* @__PURE__ */ __exportAll({
|
|
69
|
+
composeLayers: () => composeLayers,
|
|
70
|
+
makeResidualEntrypoint: () => makeResidualEntrypoint
|
|
71
|
+
});
|
|
72
|
+
/**
|
|
73
|
+
* Runtime layer composition — `reduce(Layer.provideMerge)`. Each
|
|
74
|
+
* successive module receives every prior module's Out as available
|
|
75
|
+
* services. The runtime type collapses to a bottom Layer; the per-module
|
|
76
|
+
* Out/In is tracked statically by `ResidualIn`.
|
|
77
|
+
*/
|
|
78
|
+
const composeLayers = (modules) => {
|
|
79
|
+
return modules.reduce((acc, mod) => unsafeCoerce(Layer.provideMerge(unsafeCoerce(mod.layer, "handle.layer carries its narrow type at the call site; the fold collapses to AnyLayer here"), acc), "Layer.provideMerge's return type is per-call; the fold accumulator stays AnyLayer"), unsafeCoerce(Layer.empty, "Layer.empty has type Layer<never, never, never>; widening to AnyLayer is a no-op at runtime"));
|
|
80
|
+
};
|
|
81
|
+
/**
|
|
82
|
+
* Build an `entrypoint` function bound to a specific API name. The
|
|
83
|
+
* returned function accepts only programs whose `R` channel reduces to
|
|
84
|
+
* `Manifest.RenderServices`; otherwise the call fails at the program
|
|
85
|
+
* argument with a `_konfig_unsatisfied` hint that names the missing
|
|
86
|
+
* provider and the calling API.
|
|
87
|
+
*/
|
|
88
|
+
const makeResidualEntrypoint = (_api) => (program) => unsafeCoerce(program, "ResidualHintCheck is a phantom intersection; once the call typechecks, the runtime value is the original Effect");
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/deps.ts
|
|
91
|
+
var deps_exports = /* @__PURE__ */ __exportAll({
|
|
92
|
+
App: () => App,
|
|
93
|
+
Application: () => Application,
|
|
94
|
+
BuiltImageRef: () => BuiltImageRef,
|
|
95
|
+
ConfigMap: () => ConfigMap,
|
|
96
|
+
Image: () => Image,
|
|
97
|
+
Namespace: () => Namespace,
|
|
98
|
+
Pvc: () => Pvc,
|
|
99
|
+
Secret: () => Secret,
|
|
100
|
+
SecretValues: () => SecretValues,
|
|
101
|
+
ServiceAccount: () => ServiceAccount,
|
|
102
|
+
provideApplication: () => provideApplication,
|
|
103
|
+
provideConfigMap: () => provideConfigMap,
|
|
104
|
+
provideImage: () => provideImage,
|
|
105
|
+
provideNamespace: () => provideNamespace,
|
|
106
|
+
providePvc: () => providePvc,
|
|
107
|
+
provideSecret: () => provideSecret,
|
|
108
|
+
provideServiceAccount: () => provideServiceAccount
|
|
109
|
+
});
|
|
110
|
+
const Secret = (name) => Context.Service(`Secret:${name}`);
|
|
111
|
+
const SecretValues = (name) => Context.Service(`SecretValues:${name}`);
|
|
112
|
+
const ConfigMap = (name) => Context.Service(`ConfigMap:${name}`);
|
|
113
|
+
const Namespace = (name) => Context.Service(`Namespace:${name}`);
|
|
114
|
+
const ServiceAccount = (name) => Context.Service(`ServiceAccount:${name}`);
|
|
115
|
+
const Application = (name) => Context.Service(`Application:${name}`);
|
|
116
|
+
const Pvc = (name) => Context.Service(`Pvc:${name}`);
|
|
117
|
+
const App = (name) => Context.Service(`App:${name}`);
|
|
118
|
+
/**
|
|
119
|
+
* Context.Service tag for a `BuiltImageRef<App>`. Modules that build an
|
|
120
|
+
* image emit a Layer providing this service; modules that deploy a
|
|
121
|
+
* container yield `Dep.Image(app)` to receive the typed ref. The
|
|
122
|
+
* dep-graph residual at `AppOfApps.entrypoint` catches a missing
|
|
123
|
+
* provider exactly like for `Dep.Secret`.
|
|
124
|
+
*/
|
|
125
|
+
const Image = (name) => Context.Service(`Image:${name}`);
|
|
126
|
+
const _fullRef = (input) => brand(`${input.registry}/${input.app}:${input.tag}`);
|
|
127
|
+
/**
|
|
128
|
+
* `BuiltImageRef` value namespace.
|
|
129
|
+
*
|
|
130
|
+
* const apiImage = BuiltImageRef.of({
|
|
131
|
+
* app: "api",
|
|
132
|
+
* registry: "ghcr.io/example",
|
|
133
|
+
* tag: "1.0.0",
|
|
134
|
+
* });
|
|
135
|
+
*
|
|
136
|
+
* `BuiltImageRef.of` constructs the brand from registry + app + tag.
|
|
137
|
+
* The literal `app` is captured in the brand so a workload's
|
|
138
|
+
* `Dep.Need<"Image", App>` matches only the build module that
|
|
139
|
+
* provides this exact app.
|
|
140
|
+
*/
|
|
141
|
+
const BuiltImageRef = { of: (input) => _fullRef(input) };
|
|
142
|
+
/**
|
|
143
|
+
* Layer providing `Dep.Image(App)` for downstream consumers. Combine
|
|
144
|
+
* with `Application.define` / `Module.fixedNs`'s `provides` slot to
|
|
145
|
+
* have a build module surface its image to sibling workload modules
|
|
146
|
+
* in the composition.
|
|
147
|
+
*/
|
|
148
|
+
const provideImage = (input) => Layer.succeed(Image(input.app))(_fullRef(input));
|
|
149
|
+
const provideSecret = (name) => Layer.succeed(Secret(name))(brand(name));
|
|
150
|
+
const provideConfigMap = (name) => Layer.succeed(ConfigMap(name))(brand(name));
|
|
151
|
+
const provideNamespace = (name) => Layer.succeed(Namespace(name))(name);
|
|
152
|
+
const provideServiceAccount = (name) => Layer.succeed(ServiceAccount(name))(brand(name));
|
|
153
|
+
const provideApplication = (name) => Layer.succeed(Application(name))(name);
|
|
154
|
+
const providePvc = (name) => Layer.succeed(Pvc(name))(brand(name));
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/Bundle.ts
|
|
157
|
+
var Bundle_exports = /* @__PURE__ */ __exportAll({
|
|
158
|
+
define: () => define,
|
|
159
|
+
entrypoint: () => entrypoint,
|
|
160
|
+
fromModules: () => fromModules,
|
|
161
|
+
make: () => make$1,
|
|
162
|
+
makeSet: () => makeSet,
|
|
163
|
+
target: () => target
|
|
164
|
+
});
|
|
165
|
+
/**
|
|
166
|
+
* Mutate-attach a `.layer` field to an Effect Context.Tag — same pattern
|
|
167
|
+
* `argocd/Application.ts:_attachLayerToTag` uses. A single unsafe cast
|
|
168
|
+
* in the dep-graph machinery; lives here so the rest of Bundle stays
|
|
169
|
+
* cast-free.
|
|
170
|
+
*/
|
|
171
|
+
const _attachLayerToTag = (tag, layer) => unsafeCoerce(Object.assign(tag, { layer }), "Effect Context.Tag is callable + extensible; Object.assign mutates in place and the cast widens the public type");
|
|
172
|
+
const make$1 = (opts) => ({
|
|
173
|
+
name: opts.name,
|
|
174
|
+
manifests: opts.manifests,
|
|
175
|
+
...opts.namespace !== void 0 ? { namespace: opts.namespace } : {}
|
|
176
|
+
});
|
|
177
|
+
/**
|
|
178
|
+
* Build a typed handle for a manifest bundle — a name + optional
|
|
179
|
+
* namespace + a set of manifests, plus dep-graph wiring. Same
|
|
180
|
+
* compile-time guarantees as argocd's `Application.define` minus
|
|
181
|
+
* `source: ArgoSource` / `syncPolicy` / sync-wave annotations:
|
|
182
|
+
* - the literal `Name` flows into `Dep.Provide<"App", Name>`,
|
|
183
|
+
* - the optional literal namespace flows into `Dep.Provide<"Namespace", Ns>`,
|
|
184
|
+
* - the build callback's `R` channel becomes the handle's `In` after
|
|
185
|
+
* subtracting what this bundle provides itself.
|
|
186
|
+
*
|
|
187
|
+
* Pair with `Bundle.fromModules` to compose multiple bundles
|
|
188
|
+
* and have the dep-graph residual checked at `Bundle.entrypoint`.
|
|
189
|
+
*
|
|
190
|
+
* For `Module.fixedNs` / `Module.dynamicNs` use, see `Bundle.target`
|
|
191
|
+
* — it adapts this `define` so namespace is required (Module wrappers
|
|
192
|
+
* always thread a namespace through), which lets the dep-graph drop
|
|
193
|
+
* the `Provide<"Namespace", never>` cell the optional shape needs.
|
|
194
|
+
*/
|
|
195
|
+
const define = (opts) => {
|
|
196
|
+
const name = unsafeCoerce(opts.name, "LiteralName<Name> resolves to Name itself once the call typechecks");
|
|
197
|
+
const namespace = opts.namespace === void 0 ? void 0 : unsafeCoerce(opts.namespace, "LiteralName<Ns> resolves to Ns itself once the call typechecks");
|
|
198
|
+
const tag = App(name);
|
|
199
|
+
const nsLayer = namespace === void 0 ? Layer.empty : Layer.succeed(Namespace(namespace))(namespace);
|
|
200
|
+
const internalLayer = opts.provides !== void 0 ? Layer.mergeAll(nsLayer, opts.provides) : nsLayer;
|
|
201
|
+
const buildEffect = Effect.isEffect(opts.build) ? opts.build : Effect.sync(opts.build);
|
|
202
|
+
const bundleLayer = Layer.effect(tag, buildEffect.pipe(Effect.map((manifests) => make$1({
|
|
203
|
+
name,
|
|
204
|
+
...namespace !== void 0 ? { namespace } : {},
|
|
205
|
+
manifests
|
|
206
|
+
})))).pipe(Layer.provide(internalLayer));
|
|
207
|
+
return unsafeCoerce(_attachLayerToTag(tag, opts.provides !== void 0 ? Layer.mergeAll(bundleLayer, nsLayer, opts.provides) : Layer.mergeAll(bundleLayer, nsLayer)), "narrow generic BundleHandle from the attachLayerToTag helper's loose Tag arg");
|
|
208
|
+
};
|
|
209
|
+
/**
|
|
210
|
+
* `Module.Target` adapter for `Bundle.define`. `Module.fixedNs` /
|
|
211
|
+
* `Module.dynamicNs` always pass a namespace through, so this adapter
|
|
212
|
+
* requires `namespace` (whereas `Bundle.define` itself accepts it as
|
|
213
|
+
* optional) — pinning `Ns` to a real literal lets the dep-graph emit
|
|
214
|
+
* a concrete `Provide<"Namespace", Ns>` cell on the resulting handle.
|
|
215
|
+
*
|
|
216
|
+
* ```ts
|
|
217
|
+
* const defineCache = Module.fixedNs({
|
|
218
|
+
* target: Bundle.target,
|
|
219
|
+
* namespace: "cache",
|
|
220
|
+
* build: ({ name, namespace }) => [ ... ],
|
|
221
|
+
* });
|
|
222
|
+
* ```
|
|
223
|
+
*/
|
|
224
|
+
const target = { define: (args) => unsafeCoerce(define(args), "target requires namespace so Ns is always a string literal; under that constraint Bundle.define's _NsProvides<Ns> reduces to Provide<\"Namespace\", Ns>, matching HandleKind") };
|
|
225
|
+
const makeSet = (opts) => ({
|
|
226
|
+
name: opts.name ?? "bundles",
|
|
227
|
+
bundles: opts.bundles
|
|
228
|
+
});
|
|
229
|
+
/**
|
|
230
|
+
* Phantom check that rejects a `Bundle.fromModules` program whose `R`
|
|
231
|
+
* channel still carries unmet dep-graph Needs. Bound to the
|
|
232
|
+
* "Bundle.fromModules" API label so the `_konfig_unsatisfied` hint
|
|
233
|
+
* guides the user to the right call site.
|
|
234
|
+
*/
|
|
235
|
+
const entrypoint = makeResidualEntrypoint("Bundle.fromModules");
|
|
236
|
+
/**
|
|
237
|
+
* One-list composition for a backend-agnostic bundle set. Yields each
|
|
238
|
+
* module's `Bundle` in tuple order, then wires the merged provider layer
|
|
239
|
+
* with `Compose.composeLayers`. The returned Effect's R channel is the
|
|
240
|
+
* residual unmet Needs after the fold (`Compose.ResidualIn<Ms>`),
|
|
241
|
+
* which `entrypoint` rejects unless empty.
|
|
242
|
+
*
|
|
243
|
+
* **Order matters.** List providers before their consumers. A consumer
|
|
244
|
+
* placed before its provider leaves an unmet `Need` in the residual,
|
|
245
|
+
* which surfaces at `entrypoint` as a `_konfig_unsatisfied` hint.
|
|
246
|
+
*/
|
|
247
|
+
const fromModules = (opts) => {
|
|
248
|
+
const program = Effect.gen(function* () {
|
|
249
|
+
const bundles = [];
|
|
250
|
+
for (const mod of opts.modules) {
|
|
251
|
+
const b = yield* mod;
|
|
252
|
+
bundles.push(b);
|
|
253
|
+
}
|
|
254
|
+
return makeSet({
|
|
255
|
+
name: opts.name,
|
|
256
|
+
bundles
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
const wired = composeLayers(opts.modules);
|
|
260
|
+
return unsafeCoerce(Effect.provide(program, wired), "the runtime Effect is the same; only the static R channel is narrowed to ResidualIn<Ms> by the fold-as-type");
|
|
261
|
+
};
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/diff.ts
|
|
264
|
+
const IGNORED_LABEL_KEYS = new Set(["helm.sh/chart"]);
|
|
265
|
+
const IGNORED_ANNOTATION_KEYS = new Set(["meta.helm.sh/release-name", "meta.helm.sh/release-namespace"]);
|
|
266
|
+
const MANAGED_BY_HELM_LABEL = "app.kubernetes.io/managed-by";
|
|
267
|
+
const _redactLabelMap = (labels) => {
|
|
268
|
+
const out = {};
|
|
269
|
+
for (const [k, v] of Object.entries(labels)) {
|
|
270
|
+
if (IGNORED_LABEL_KEYS.has(k)) continue;
|
|
271
|
+
if (k === MANAGED_BY_HELM_LABEL && v === "Helm") continue;
|
|
272
|
+
out[k] = v;
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
};
|
|
276
|
+
const _redactAnnotationMap = (annotations) => {
|
|
277
|
+
const out = {};
|
|
278
|
+
for (const [k, v] of Object.entries(annotations)) {
|
|
279
|
+
if (IGNORED_ANNOTATION_KEYS.has(k)) continue;
|
|
280
|
+
out[k] = v;
|
|
281
|
+
}
|
|
282
|
+
return out;
|
|
283
|
+
};
|
|
284
|
+
const _isNumericString = (s) => /^-?\d+(\.\d+)?(e[-+]?\d+)?$/i.test(s);
|
|
285
|
+
/**
|
|
286
|
+
* Canonicalize a parsed YAML/JSON value for structural comparison,
|
|
287
|
+
* applying two normalizations:
|
|
288
|
+
*
|
|
289
|
+
* 1. **Null/undefined keys are dropped.** Any object key whose value is
|
|
290
|
+
* `null` or `undefined` is omitted from the redacted output. This is
|
|
291
|
+
* deliberate: it makes an *explicit* `field: null` compare equal to
|
|
292
|
+
* an *absent* `field`, which is what we want when diffing manifests
|
|
293
|
+
* where one side spells out a null default the other side simply
|
|
294
|
+
* omits. The trade-off is that a genuine "field was set to null vs.
|
|
295
|
+
* field removed" distinction is intentionally invisible to the diff.
|
|
296
|
+
* 2. **Numeric normalization** (opt-in via
|
|
297
|
+
* {@link RedactOptions.normalizeNumerics}) — see that field.
|
|
298
|
+
*
|
|
299
|
+
* Helm metadata redaction (dropping chart-churn labels/annotations) is
|
|
300
|
+
* layered on top when a `labels`/`annotations` map appears under
|
|
301
|
+
* `metadata`.
|
|
302
|
+
*/
|
|
303
|
+
const redact = (input) => {
|
|
304
|
+
const value = input.value;
|
|
305
|
+
const parentKey = input.parentKey ?? null;
|
|
306
|
+
const options = input.options ?? {};
|
|
307
|
+
if (Array.isArray(value)) return value.map((v) => redact({
|
|
308
|
+
value: v,
|
|
309
|
+
parentKey: null,
|
|
310
|
+
options
|
|
311
|
+
}));
|
|
312
|
+
if (value !== null && typeof value === "object") {
|
|
313
|
+
const obj = unsafeCoerce(value, "typeof === object && !Array.isArray && !== null narrowed above");
|
|
314
|
+
const out = {};
|
|
315
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
316
|
+
if (v === null || v === void 0) continue;
|
|
317
|
+
if (k === "labels" && parentKey === "metadata" && v !== null && typeof v === "object") {
|
|
318
|
+
out[k] = _redactLabelMap(unsafeCoerce(v, "metadata.labels is Record<string, string>"));
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
if (k === "annotations" && parentKey === "metadata" && v !== null && typeof v === "object") {
|
|
322
|
+
out[k] = _redactAnnotationMap(unsafeCoerce(v, "metadata.annotations is Record<string, string>"));
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
out[k] = redact({
|
|
326
|
+
value: v,
|
|
327
|
+
parentKey: k,
|
|
328
|
+
options
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return out;
|
|
332
|
+
}
|
|
333
|
+
if (options.normalizeNumerics === true) {
|
|
334
|
+
if (typeof value === "string" && _isNumericString(value)) return Number(value);
|
|
335
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
336
|
+
}
|
|
337
|
+
return value;
|
|
338
|
+
};
|
|
339
|
+
const deepEqual = (input) => {
|
|
340
|
+
const { a, b } = input;
|
|
341
|
+
if (a === b) return true;
|
|
342
|
+
if (a === null || b === null) return false;
|
|
343
|
+
if (typeof a !== typeof b) return false;
|
|
344
|
+
if (Array.isArray(a)) {
|
|
345
|
+
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
346
|
+
for (let i = 0; i < a.length; i++) if (!deepEqual({
|
|
347
|
+
a: a[i],
|
|
348
|
+
b: b[i]
|
|
349
|
+
})) return false;
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
if (typeof a === "object" && typeof b === "object") {
|
|
353
|
+
const ka = Object.keys(unsafeCoerce(a, "typeof === object branch")).sort();
|
|
354
|
+
const kb = Object.keys(unsafeCoerce(b, "typeof === object branch")).sort();
|
|
355
|
+
if (ka.length !== kb.length) return false;
|
|
356
|
+
for (let i = 0; i < ka.length; i++) if (ka[i] !== kb[i]) return false;
|
|
357
|
+
for (const k of ka) {
|
|
358
|
+
const av = unsafeCoerce(a, "typeof === object branch")[k];
|
|
359
|
+
const bv = unsafeCoerce(b, "typeof === object branch")[k];
|
|
360
|
+
if (!deepEqual({
|
|
361
|
+
a: av,
|
|
362
|
+
b: bv
|
|
363
|
+
})) return false;
|
|
364
|
+
}
|
|
365
|
+
return true;
|
|
366
|
+
}
|
|
367
|
+
return false;
|
|
368
|
+
};
|
|
369
|
+
const parseYaml = (text) => YAML.parse(text);
|
|
370
|
+
/**
|
|
371
|
+
* Parse a multi-doc YAML file into an ordered list of documents. Each
|
|
372
|
+
* document's parsed structure is preserved as-is. Empty / whitespace-only
|
|
373
|
+
* segments are dropped, but their position is *not* preserved — only
|
|
374
|
+
* present documents are returned. Use the returned index to identify
|
|
375
|
+
* documents inside one file (alongside the filename) when reporting.
|
|
376
|
+
*/
|
|
377
|
+
const parseYamlAll = (text) => {
|
|
378
|
+
const docs = YAML.parseAllDocuments(text);
|
|
379
|
+
const out = [];
|
|
380
|
+
for (const d of docs) {
|
|
381
|
+
const value = d.toJS();
|
|
382
|
+
if (value === null || value === void 0) continue;
|
|
383
|
+
out.push(value);
|
|
384
|
+
}
|
|
385
|
+
return out;
|
|
386
|
+
};
|
|
387
|
+
/**
|
|
388
|
+
* Identifier for a document inside a multi-doc YAML file. We key by
|
|
389
|
+
* (kind, name, namespace) when those fields are present so a
|
|
390
|
+
* label-only edit on a Service inside a 12-doc file diffs against the
|
|
391
|
+
* same-kind/name Service in the other side, regardless of position
|
|
392
|
+
* shuffle.
|
|
393
|
+
*/
|
|
394
|
+
const _docKey = (value, fallbackIdx) => {
|
|
395
|
+
if (value === null || typeof value !== "object") return `:doc:${fallbackIdx}`;
|
|
396
|
+
const v = unsafeCoerce(value, "typeof === object && !== null branch above narrowed value; every field access below is guarded by a typeof check");
|
|
397
|
+
const kind = typeof v.kind === "string" ? v.kind : "";
|
|
398
|
+
const name = typeof v.metadata?.name === "string" ? v.metadata.name : "";
|
|
399
|
+
const ns = typeof v.metadata?.namespace === "string" ? v.metadata.namespace : "";
|
|
400
|
+
if (kind || name) return `${kind}|${ns}|${name}`;
|
|
401
|
+
return `:doc:${fallbackIdx}`;
|
|
402
|
+
};
|
|
403
|
+
const _diffOne = (file, leftText, rightText, options) => {
|
|
404
|
+
const lDocs = parseYamlAll(leftText).map((v, i) => [_docKey(v, i), redact({
|
|
405
|
+
value: v,
|
|
406
|
+
options
|
|
407
|
+
})]);
|
|
408
|
+
const rDocs = parseYamlAll(rightText).map((v, i) => [_docKey(v, i), redact({
|
|
409
|
+
value: v,
|
|
410
|
+
options
|
|
411
|
+
})]);
|
|
412
|
+
if (lDocs.length <= 1 && rDocs.length <= 1) {
|
|
413
|
+
const l = lDocs[0]?.[1];
|
|
414
|
+
const r = rDocs[0]?.[1];
|
|
415
|
+
if (deepEqual({
|
|
416
|
+
a: l,
|
|
417
|
+
b: r
|
|
418
|
+
})) return {
|
|
419
|
+
_tag: "Same",
|
|
420
|
+
file
|
|
421
|
+
};
|
|
422
|
+
return {
|
|
423
|
+
_tag: "Changed",
|
|
424
|
+
file,
|
|
425
|
+
left: l,
|
|
426
|
+
right: r
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const lByKey = new Map(lDocs);
|
|
430
|
+
const rByKey = new Map(rDocs);
|
|
431
|
+
const keys = Array.from(new Set([...lByKey.keys(), ...rByKey.keys()])).sort();
|
|
432
|
+
const docs = [];
|
|
433
|
+
let anyChange = false;
|
|
434
|
+
for (const key of keys) {
|
|
435
|
+
const l = lByKey.get(key);
|
|
436
|
+
const r = rByKey.get(key);
|
|
437
|
+
if (l === void 0) {
|
|
438
|
+
docs.push({
|
|
439
|
+
_tag: "MissingLeft",
|
|
440
|
+
key,
|
|
441
|
+
right: r
|
|
442
|
+
});
|
|
443
|
+
anyChange = true;
|
|
444
|
+
} else if (r === void 0) {
|
|
445
|
+
docs.push({
|
|
446
|
+
_tag: "MissingRight",
|
|
447
|
+
key,
|
|
448
|
+
left: l
|
|
449
|
+
});
|
|
450
|
+
anyChange = true;
|
|
451
|
+
} else if (deepEqual({
|
|
452
|
+
a: l,
|
|
453
|
+
b: r
|
|
454
|
+
})) docs.push({
|
|
455
|
+
_tag: "Same",
|
|
456
|
+
key
|
|
457
|
+
});
|
|
458
|
+
else {
|
|
459
|
+
docs.push({
|
|
460
|
+
_tag: "Changed",
|
|
461
|
+
key,
|
|
462
|
+
left: l,
|
|
463
|
+
right: r
|
|
464
|
+
});
|
|
465
|
+
anyChange = true;
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if (!anyChange) return {
|
|
469
|
+
_tag: "Same",
|
|
470
|
+
file
|
|
471
|
+
};
|
|
472
|
+
return {
|
|
473
|
+
_tag: "Changed",
|
|
474
|
+
file,
|
|
475
|
+
left: lDocs.map((d) => d[1]),
|
|
476
|
+
right: rDocs.map((d) => d[1]),
|
|
477
|
+
docs
|
|
478
|
+
};
|
|
479
|
+
};
|
|
480
|
+
const diffFiles = (input) => {
|
|
481
|
+
const { left, right } = input;
|
|
482
|
+
const options = input.options ?? {};
|
|
483
|
+
const files = Array.from(new Set([...Object.keys(left), ...Object.keys(right)])).sort();
|
|
484
|
+
const entries = [];
|
|
485
|
+
for (const file of files) {
|
|
486
|
+
const hasL = Object.hasOwn(left, file);
|
|
487
|
+
const hasR = Object.hasOwn(right, file);
|
|
488
|
+
if (!hasL) {
|
|
489
|
+
entries.push({
|
|
490
|
+
_tag: "MissingLeft",
|
|
491
|
+
file
|
|
492
|
+
});
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
if (!hasR) {
|
|
496
|
+
entries.push({
|
|
497
|
+
_tag: "MissingRight",
|
|
498
|
+
file
|
|
499
|
+
});
|
|
500
|
+
continue;
|
|
501
|
+
}
|
|
502
|
+
entries.push(_diffOne(file, left[file] ?? "", right[file] ?? "", options));
|
|
503
|
+
}
|
|
504
|
+
return { entries };
|
|
505
|
+
};
|
|
506
|
+
const hasDifferences = (result) => result.entries.some((e) => e._tag !== "Same");
|
|
507
|
+
const formatDiff = (input) => {
|
|
508
|
+
const { result } = input;
|
|
509
|
+
const format = input.format ?? "summary";
|
|
510
|
+
if (format === "json") return JSON.stringify(result, null, 2);
|
|
511
|
+
const changes = result.entries.filter((e) => e._tag !== "Same");
|
|
512
|
+
if (changes.length === 0) return "";
|
|
513
|
+
const lines = [];
|
|
514
|
+
for (const e of changes) {
|
|
515
|
+
if (e._tag === "MissingLeft") lines.push(`+ ${e.file}`);
|
|
516
|
+
else if (e._tag === "MissingRight") lines.push(`- ${e.file}`);
|
|
517
|
+
else lines.push(`~ ${e.file}`);
|
|
518
|
+
if (format === "detail" && e._tag === "Changed") if (e.docs && e.docs.length > 0) for (const d of e.docs) {
|
|
519
|
+
if (d._tag === "Same") continue;
|
|
520
|
+
lines.push(` [doc ${d.key}] ${d._tag}`);
|
|
521
|
+
if (d._tag === "Changed") {
|
|
522
|
+
lines.push(" left:");
|
|
523
|
+
lines.push(...YAML.stringify(d.left, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
|
|
524
|
+
lines.push(" right:");
|
|
525
|
+
lines.push(...YAML.stringify(d.right, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
lines.push(" left:");
|
|
530
|
+
lines.push(...YAML.stringify(e.left, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
|
|
531
|
+
lines.push(" right:");
|
|
532
|
+
lines.push(...YAML.stringify(e.right, { lineWidth: 0 }).split("\n").map((l) => ` ${l}`));
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
return lines.join("\n");
|
|
536
|
+
};
|
|
537
|
+
//#endregion
|
|
538
|
+
//#region src/Manifest.ts
|
|
539
|
+
var Manifest_exports = /* @__PURE__ */ __exportAll({
|
|
540
|
+
ManifestTypeId: () => ManifestTypeId,
|
|
541
|
+
combine: () => combine,
|
|
542
|
+
concat: () => concat,
|
|
543
|
+
embedYaml: () => embedYaml,
|
|
544
|
+
make: () => make,
|
|
545
|
+
whenever: () => whenever
|
|
546
|
+
});
|
|
547
|
+
const ManifestTypeId = Symbol.for("@konfig.ts/core/Manifest");
|
|
548
|
+
const variance = { _A: (_) => _ };
|
|
549
|
+
const make = (run) => ({
|
|
550
|
+
[ManifestTypeId]: unsafeCoerce(variance, "phantom variance witness — A only appears in covariant position"),
|
|
551
|
+
render: (ctx) => {
|
|
552
|
+
const result = run(ctx);
|
|
553
|
+
return Effect.isEffect(result) ? unsafeCoerce(result, "Effect.isEffect narrowed `result` to an Effect; TS's narrowing doesn't carry the Effect's full type parameters") : Effect.succeed(result);
|
|
554
|
+
}
|
|
555
|
+
});
|
|
556
|
+
const combine = (input) => make((ctx) => Effect.all([input.a.render(ctx), input.b.render(ctx)], { concurrency: "unbounded" }));
|
|
557
|
+
const concat = (...manifests) => make((ctx) => Effect.all(manifests.map((m) => m.render(ctx)), { concurrency: "unbounded" }).pipe(Effect.map((results) => results.flatMap((r) => Array.isArray(r) ? unsafeCoerce(r, "Array.isArray narrowed; element type is A by render contract") : [unsafeCoerce(r, "non-array branch carries a single A")]))));
|
|
558
|
+
const whenever = (input) => make((ctx) => input.cond ? input.thunk().render(ctx) : unsafeCoerce(Effect.succeed(void 0), "undefined branch — A is the type seen by the consumer when cond=false"));
|
|
559
|
+
const embedYaml = (source) => make((_ctx) => {
|
|
560
|
+
if ("literal" in source) return Effect.succeed({
|
|
561
|
+
_tag: "RawYaml",
|
|
562
|
+
content: source.literal
|
|
563
|
+
});
|
|
564
|
+
const path = source.path;
|
|
565
|
+
return Effect.gen(function* () {
|
|
566
|
+
return {
|
|
567
|
+
_tag: "RawYaml",
|
|
568
|
+
content: yield* (yield* FileSystem).readFileString(path),
|
|
569
|
+
origin: path
|
|
570
|
+
};
|
|
571
|
+
}).pipe(Effect.mapError((cause) => new EmbedYamlReadError({
|
|
572
|
+
path,
|
|
573
|
+
cause
|
|
574
|
+
})));
|
|
575
|
+
});
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/subprocess.ts
|
|
578
|
+
/**
|
|
579
|
+
* Exit code stamped on a `ProcessError` when the process never produced
|
|
580
|
+
* one — i.e. the spawn itself failed (binary not found, EACCES, a stream
|
|
581
|
+
* read error). Distinct from any real OS exit code.
|
|
582
|
+
*/
|
|
583
|
+
const SPAWN_FAILED_EXIT = -1;
|
|
584
|
+
/**
|
|
585
|
+
* Upper bound (in UTF-16 code units, a close proxy for bytes here) on the
|
|
586
|
+
* stderr tail retained by `ProcessError` — roughly the last 2KB. Keeps a
|
|
587
|
+
* failing command's diagnostics readable without pinning a runaway log in
|
|
588
|
+
* memory or in the serialized error.
|
|
589
|
+
*/
|
|
590
|
+
const STDERR_TAIL_LIMIT = 2048;
|
|
591
|
+
const _tail = (text) => text.length > STDERR_TAIL_LIMIT ? text.slice(text.length - STDERR_TAIL_LIMIT) : text;
|
|
592
|
+
const _commandLabel = (command) => ChildProcess.isStandardCommand(command) ? [command.command, ...command.args].join(" ") : "<pipeline>";
|
|
593
|
+
/**
|
|
594
|
+
* Failure of a spawned subprocess: a non-zero exit, a spawn that never
|
|
595
|
+
* started, or (for `runProcessString`) an empty stdout when a payload was
|
|
596
|
+
* required. Carries the exit code and a bounded tail of stderr so callers
|
|
597
|
+
* can surface a diagnostic without re-running the command.
|
|
598
|
+
*
|
|
599
|
+
* `command` is the program plus its already-parsed arguments; it must
|
|
600
|
+
* never be interpolated with secret material by callers.
|
|
601
|
+
*/
|
|
602
|
+
var ProcessError = class extends Data.TaggedError("ProcessError") {
|
|
603
|
+
get message() {
|
|
604
|
+
const tail = this.stderrTail.trim();
|
|
605
|
+
const suffix = tail.length > 0 ? `: ${tail}` : "";
|
|
606
|
+
return `command \`${this.command}\` failed (exit ${this.exitCode})${suffix}`;
|
|
607
|
+
}
|
|
608
|
+
};
|
|
609
|
+
const _collect = (stream) => Stream.mkString(Stream.decodeText(stream));
|
|
610
|
+
/**
|
|
611
|
+
* Spawn `command` exactly once and, concurrently, drain stdout and stderr
|
|
612
|
+
* to strings while awaiting the exit code. Draining both pipes alongside
|
|
613
|
+
* `exitCode` avoids the classic pipe-buffer deadlock. A spawn/stream
|
|
614
|
+
* failure (PlatformError) is folded into a `ProcessError` so callers see a
|
|
615
|
+
* single error channel.
|
|
616
|
+
*/
|
|
617
|
+
const _spawnCollect = (command) => Effect.scoped(Effect.gen(function* () {
|
|
618
|
+
const handle = yield* (yield* ChildProcessSpawner).spawn(command);
|
|
619
|
+
const [exitCode, stdout, stderr] = yield* Effect.all([
|
|
620
|
+
handle.exitCode,
|
|
621
|
+
_collect(handle.stdout),
|
|
622
|
+
_collect(handle.stderr)
|
|
623
|
+
], { concurrency: "unbounded" });
|
|
624
|
+
return {
|
|
625
|
+
exitCode,
|
|
626
|
+
stdout,
|
|
627
|
+
stderr
|
|
628
|
+
};
|
|
629
|
+
})).pipe(Effect.mapError((cause) => new ProcessError({
|
|
630
|
+
command: _commandLabel(command),
|
|
631
|
+
exitCode: SPAWN_FAILED_EXIT,
|
|
632
|
+
stderrTail: _tail(String(cause))
|
|
633
|
+
})));
|
|
634
|
+
/**
|
|
635
|
+
* Run `command` and return its stdout, checking the exit code — the guard
|
|
636
|
+
* that `spawner.string` omits. Fails with `ProcessError` (carrying the
|
|
637
|
+
* stderr tail) on any non-zero exit. Unless `allowEmptyStdout` is `true`,
|
|
638
|
+
* also fails when the trimmed stdout is empty, so a silently-failing
|
|
639
|
+
* command can never masquerade as an empty-but-successful result.
|
|
640
|
+
*/
|
|
641
|
+
const runProcessString = (command, options) => Effect.gen(function* () {
|
|
642
|
+
const result = yield* _spawnCollect(command);
|
|
643
|
+
if (result.exitCode !== 0) return yield* Effect.fail(new ProcessError({
|
|
644
|
+
command: _commandLabel(command),
|
|
645
|
+
exitCode: result.exitCode,
|
|
646
|
+
stderrTail: _tail(result.stderr)
|
|
647
|
+
}));
|
|
648
|
+
if (options?.allowEmptyStdout !== true && result.stdout.trim().length === 0) return yield* Effect.fail(new ProcessError({
|
|
649
|
+
command: _commandLabel(command),
|
|
650
|
+
exitCode: result.exitCode,
|
|
651
|
+
stderrTail: _tail(result.stderr)
|
|
652
|
+
}));
|
|
653
|
+
return result.stdout;
|
|
654
|
+
});
|
|
655
|
+
/**
|
|
656
|
+
* Run `command` for its success/failure only — stdout is drained (to avoid
|
|
657
|
+
* a pipe deadlock) but discarded. Fails with `ProcessError` on any
|
|
658
|
+
* non-zero exit, attaching the stderr tail when present.
|
|
659
|
+
*/
|
|
660
|
+
const runProcessExit = (command) => Effect.gen(function* () {
|
|
661
|
+
const result = yield* _spawnCollect(command);
|
|
662
|
+
if (result.exitCode !== 0) return yield* Effect.fail(new ProcessError({
|
|
663
|
+
command: _commandLabel(command),
|
|
664
|
+
exitCode: result.exitCode,
|
|
665
|
+
stderrTail: _tail(result.stderr)
|
|
666
|
+
}));
|
|
667
|
+
});
|
|
668
|
+
//#endregion
|
|
669
|
+
//#region src/Helm.ts
|
|
670
|
+
var Helm_exports = /* @__PURE__ */ __exportAll({ release: () => release });
|
|
671
|
+
const CLUSTER_SCOPED_KINDS = new Set([
|
|
672
|
+
"APIService",
|
|
673
|
+
"ClusterRole",
|
|
674
|
+
"ClusterRoleBinding",
|
|
675
|
+
"ComponentStatus",
|
|
676
|
+
"CSIDriver",
|
|
677
|
+
"CSINode",
|
|
678
|
+
"CustomResourceDefinition",
|
|
679
|
+
"FlowSchema",
|
|
680
|
+
"IngressClass",
|
|
681
|
+
"MutatingWebhookConfiguration",
|
|
682
|
+
"Namespace",
|
|
683
|
+
"Node",
|
|
684
|
+
"PersistentVolume",
|
|
685
|
+
"PodSecurityPolicy",
|
|
686
|
+
"PriorityClass",
|
|
687
|
+
"PriorityLevelConfiguration",
|
|
688
|
+
"RuntimeClass",
|
|
689
|
+
"StorageClass",
|
|
690
|
+
"ValidatingAdmissionPolicy",
|
|
691
|
+
"ValidatingAdmissionPolicyBinding",
|
|
692
|
+
"ValidatingWebhookConfiguration",
|
|
693
|
+
"VolumeAttachment"
|
|
694
|
+
]);
|
|
695
|
+
const _asDocShape = (value) => value !== null && typeof value === "object" ? unsafeCoerce(value, "parseYamlAll returned a parsed document object; the kind/metadata reads below are each typeof-guarded") : null;
|
|
696
|
+
/**
|
|
697
|
+
* Turn `helm template` stdout into individual `RawYaml` docs.
|
|
698
|
+
*
|
|
699
|
+
* Document boundaries come from the shared `parseYamlAll`
|
|
700
|
+
* (`YAML.parseAllDocuments`) helper rather than a naive `/^---$/m` split,
|
|
701
|
+
* so a `---` appearing inside a block scalar can't spuriously split one
|
|
702
|
+
* manifest into two. `parseYamlAll` also drops empty / comment-only
|
|
703
|
+
* segments for us.
|
|
704
|
+
*
|
|
705
|
+
* When a `namespace` is supplied, each namespaced-kind document that
|
|
706
|
+
* lacks an explicit namespace is patched to carry it (the re-parse
|
|
707
|
+
* branch behavior). Cluster-scoped kinds and documents that already pin
|
|
708
|
+
* a namespace are left untouched.
|
|
709
|
+
*/
|
|
710
|
+
const _parseHelmOutput = (input) => Effect.sync(() => {
|
|
711
|
+
const { output, chart, version, namespace } = input;
|
|
712
|
+
const origin = `helm:${chart}@${version}`;
|
|
713
|
+
const results = [];
|
|
714
|
+
for (const parsed of parseYamlAll(output)) {
|
|
715
|
+
let value = parsed;
|
|
716
|
+
if (namespace !== void 0) {
|
|
717
|
+
const shape = _asDocShape(parsed);
|
|
718
|
+
if (shape !== null && typeof shape.kind === "string" && !CLUSTER_SCOPED_KINDS.has(shape.kind) && (shape.metadata?.namespace === void 0 || shape.metadata.namespace === "")) value = {
|
|
719
|
+
...shape,
|
|
720
|
+
metadata: {
|
|
721
|
+
...shape.metadata,
|
|
722
|
+
namespace
|
|
723
|
+
}
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
let content = `---\n${YAML.stringify(value, { lineWidth: 0 })}`;
|
|
727
|
+
if (!content.endsWith("\n")) content += "\n";
|
|
728
|
+
results.push({
|
|
729
|
+
_tag: "RawYaml",
|
|
730
|
+
content,
|
|
731
|
+
origin
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
return results;
|
|
735
|
+
});
|
|
736
|
+
const _HELM_VERSION_RE = /v?(\d+)\.(\d+)\.(\d+)/;
|
|
737
|
+
/** Extract a `major.minor.patch` triple from a version string, or `null`. */
|
|
738
|
+
const _parseVersionTriple = (text) => {
|
|
739
|
+
const m = _HELM_VERSION_RE.exec(text.trim());
|
|
740
|
+
if (m === null) return null;
|
|
741
|
+
return [
|
|
742
|
+
Number(m[1]),
|
|
743
|
+
Number(m[2]),
|
|
744
|
+
Number(m[3])
|
|
745
|
+
];
|
|
746
|
+
};
|
|
747
|
+
/** True when `found` is strictly older than `min` (release triple compare). */
|
|
748
|
+
const _isBelow = (found, min) => {
|
|
749
|
+
for (let i = 0; i < 3; i++) {
|
|
750
|
+
const f = found[i] ?? 0;
|
|
751
|
+
const m = min[i] ?? 0;
|
|
752
|
+
if (f < m) return true;
|
|
753
|
+
if (f > m) return false;
|
|
754
|
+
}
|
|
755
|
+
return false;
|
|
756
|
+
};
|
|
757
|
+
/**
|
|
758
|
+
* One-shot `helm version --short` preflight. Fails `HelmVersionTooLow`
|
|
759
|
+
* when helm is absent/unparseable or older than `minVersion`. Pre-release
|
|
760
|
+
* / build metadata on the installed version is ignored — only the release
|
|
761
|
+
* triple gates the check.
|
|
762
|
+
*/
|
|
763
|
+
const _assertHelmMinVersion = (minVersion) => Effect.gen(function* () {
|
|
764
|
+
const stdout = yield* runProcessString(ChildProcess.make("helm", ["version", "--short"]), { allowEmptyStdout: false }).pipe(Effect.mapError(() => new HelmVersionTooLow({
|
|
765
|
+
required: minVersion,
|
|
766
|
+
found: "not found"
|
|
767
|
+
})));
|
|
768
|
+
const found = _parseVersionTriple(stdout);
|
|
769
|
+
const min = _parseVersionTriple(minVersion);
|
|
770
|
+
if (found === null || min !== null && _isBelow(found, min)) return yield* Effect.fail(new HelmVersionTooLow({
|
|
771
|
+
required: minVersion,
|
|
772
|
+
found: stdout.trim()
|
|
773
|
+
}));
|
|
774
|
+
});
|
|
775
|
+
const _normalizeDigest = (digest) => digest.startsWith("sha256:") ? digest : `sha256:${digest}`;
|
|
776
|
+
const _toHex = (buf) => {
|
|
777
|
+
const view = new Uint8Array(buf);
|
|
778
|
+
let hex = "";
|
|
779
|
+
for (let i = 0; i < view.length; i++) hex += (view[i] ?? 0).toString(16).padStart(2, "0");
|
|
780
|
+
return hex;
|
|
781
|
+
};
|
|
782
|
+
/**
|
|
783
|
+
* SHA-256 the file at `filePath` via Web Crypto API (`crypto.subtle.digest`).
|
|
784
|
+
* `crypto.subtle` is a runtime global on Node ≥ 20 and on Bun; no
|
|
785
|
+
* `node:crypto` import is needed, which keeps the file portable across
|
|
786
|
+
* the runtimes Effect targets.
|
|
787
|
+
*/
|
|
788
|
+
const _hashFile = (filePath) => Effect.gen(function* () {
|
|
789
|
+
const bytes = yield* (yield* FileSystem).readFile(filePath);
|
|
790
|
+
const subtle = unsafeCoerce(globalThis, "globalThis.crypto is provided by the runtime (Node ≥ 20, Bun) — typed via local _CryptoGlobal interface").crypto.subtle;
|
|
791
|
+
return `sha256:${_toHex(yield* Effect.promise(() => subtle.digest("SHA-256", bytes)))}`;
|
|
792
|
+
});
|
|
793
|
+
const _verifyDigest = (input) => Effect.gen(function* () {
|
|
794
|
+
const fs = yield* FileSystem;
|
|
795
|
+
const expected = _normalizeDigest(input.opts.digest);
|
|
796
|
+
const actual = yield* _hashFile(input.cachedTgz);
|
|
797
|
+
if (expected !== actual) {
|
|
798
|
+
yield* fs.remove(input.cachedTgz).pipe(Effect.ignore);
|
|
799
|
+
return yield* Effect.fail(new HelmDigestMismatch({
|
|
800
|
+
chart: input.opts.chart,
|
|
801
|
+
version: input.opts.version,
|
|
802
|
+
expected,
|
|
803
|
+
actual
|
|
804
|
+
}));
|
|
805
|
+
}
|
|
806
|
+
});
|
|
807
|
+
const _ensureCachedTarball = (input) => Effect.gen(function* () {
|
|
808
|
+
const { opts, cacheDir, cachedTgz } = input;
|
|
809
|
+
const fs = yield* FileSystem;
|
|
810
|
+
const path = yield* Path;
|
|
811
|
+
if (yield* fs.exists(cachedTgz)) {
|
|
812
|
+
yield* _verifyDigest({
|
|
813
|
+
opts,
|
|
814
|
+
cachedTgz
|
|
815
|
+
});
|
|
816
|
+
return;
|
|
817
|
+
}
|
|
818
|
+
const beforeFiles = new Set(yield* fs.readDirectory(cacheDir).pipe(Effect.orElseSucceed(() => [])));
|
|
819
|
+
yield* runProcessExit(ChildProcess.make("helm", [
|
|
820
|
+
"pull",
|
|
821
|
+
"--repo",
|
|
822
|
+
opts.repo,
|
|
823
|
+
opts.chart,
|
|
824
|
+
"--version",
|
|
825
|
+
opts.version,
|
|
826
|
+
"--destination",
|
|
827
|
+
cacheDir
|
|
828
|
+
]));
|
|
829
|
+
const candidates = (yield* fs.readDirectory(cacheDir)).filter((f) => f.endsWith(".tgz") && f.startsWith(opts.chart) && !beforeFiles.has(f) && path.join(cacheDir, f) !== cachedTgz);
|
|
830
|
+
if (candidates.length > 0) yield* fs.rename(path.join(cacheDir, candidates[0] ?? ""), cachedTgz);
|
|
831
|
+
yield* _verifyDigest({
|
|
832
|
+
opts,
|
|
833
|
+
cachedTgz
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
const release = (opts) => {
|
|
837
|
+
const extraOpts = opts.extraOpts ?? [];
|
|
838
|
+
return make(() => Effect.gen(function* () {
|
|
839
|
+
const fs = yield* FileSystem;
|
|
840
|
+
const path = yield* Path;
|
|
841
|
+
if (opts.minVersion !== void 0) yield* _assertHelmMinVersion(opts.minVersion);
|
|
842
|
+
const cacheDir = yield* Config.string("KONFIG_HELM_CACHE").pipe(Config.withDefault(path.resolve(".konfig", "helm-cache")));
|
|
843
|
+
yield* fs.makeDirectory(cacheDir, { recursive: true });
|
|
844
|
+
const digestSuffix = opts.digest.replace(/^sha256:/, "").slice(0, 12);
|
|
845
|
+
const cachedTgz = path.join(cacheDir, `${opts.chart}-${opts.version}-${digestSuffix}.tgz`);
|
|
846
|
+
yield* _ensureCachedTarball({
|
|
847
|
+
opts,
|
|
848
|
+
cacheDir,
|
|
849
|
+
cachedTgz
|
|
850
|
+
});
|
|
851
|
+
const tmpDir = yield* fs.makeTempDirectoryScoped({ prefix: "konfig-helm-" });
|
|
852
|
+
const valuesFile = path.join(tmpDir, "values.yaml");
|
|
853
|
+
yield* fs.writeFileString(valuesFile, YAML.stringify(opts.values, { lineWidth: 0 }));
|
|
854
|
+
const releaseName = opts.releaseName ?? opts.chart;
|
|
855
|
+
return yield* _parseHelmOutput({
|
|
856
|
+
output: yield* runProcessString(ChildProcess.make("helm", [
|
|
857
|
+
"template",
|
|
858
|
+
releaseName,
|
|
859
|
+
cachedTgz,
|
|
860
|
+
"--values",
|
|
861
|
+
valuesFile,
|
|
862
|
+
...opts.namespace !== void 0 ? ["--namespace", opts.namespace] : [],
|
|
863
|
+
...extraOpts
|
|
864
|
+
]), { allowEmptyStdout: false }),
|
|
865
|
+
chart: opts.chart,
|
|
866
|
+
version: opts.version,
|
|
867
|
+
namespace: opts.namespace
|
|
868
|
+
});
|
|
869
|
+
}).pipe(Effect.scoped, Effect.mapError((cause) => cause instanceof HelmVersionTooLow ? cause : new HelmRenderError({
|
|
870
|
+
chart: opts.chart,
|
|
871
|
+
version: opts.version,
|
|
872
|
+
cause
|
|
873
|
+
}))));
|
|
874
|
+
};
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/images.ts
|
|
877
|
+
const EnvImages = Schema.Record(Schema.String, Schema.String);
|
|
878
|
+
const ImagesConfig = Schema.Struct({ envs: Schema.Record(Schema.String, EnvImages) });
|
|
879
|
+
const decodeEff$1 = Schema.decodeUnknownEffect(ImagesConfig);
|
|
880
|
+
const strict$1 = { onExcessProperty: "error" };
|
|
881
|
+
const decodeImagesSync = (input) => Effect.runSync(decodeEff$1(input, strict$1));
|
|
882
|
+
const decodeImagesEffect = (input) => decodeEff$1(input, strict$1);
|
|
883
|
+
var ImagesEnvMissing = class extends Data.TaggedError("ImagesEnvMissing") {};
|
|
884
|
+
const lookupEnv = (input) => input.cfg.envs[input.env];
|
|
885
|
+
const lookupEnvEffect = (input) => {
|
|
886
|
+
const e = input.cfg.envs[input.env];
|
|
887
|
+
return e === void 0 ? Effect.fail(new ImagesEnvMissing({ env: input.env })) : Effect.succeed(e);
|
|
888
|
+
};
|
|
889
|
+
const imagesFor = (input) => {
|
|
890
|
+
const e = input.cfg.envs[input.env];
|
|
891
|
+
if (e === void 0) throw new ImagesEnvMissing({ env: input.env });
|
|
892
|
+
return e;
|
|
893
|
+
};
|
|
894
|
+
var ImagesAppMissing = class extends Data.TaggedError("ImagesAppMissing") {};
|
|
895
|
+
const requireImage = (input) => {
|
|
896
|
+
const v = input.e[input.app];
|
|
897
|
+
if (v === void 0) throw new ImagesAppMissing({
|
|
898
|
+
env: input.envName,
|
|
899
|
+
app: input.app
|
|
900
|
+
});
|
|
901
|
+
return v;
|
|
902
|
+
};
|
|
903
|
+
//#endregion
|
|
904
|
+
//#region src/konfigConfig.ts
|
|
905
|
+
const _stringWithKeyDefault = (def) => Schema.String.pipe(Schema.optionalKey, Schema.withDecodingDefaultKey(Effect.succeed(def)));
|
|
906
|
+
const _stringArrayWithKeyDefault = (def) => Schema.Array(Schema.String).pipe(Schema.optionalKey, Schema.withDecodingDefaultKey(Effect.succeed(def)));
|
|
907
|
+
const EnvEntry = Schema.Struct({ entry: Schema.String });
|
|
908
|
+
const OutDir = Schema.Struct({ manifests: Schema.String });
|
|
909
|
+
const CrdConfig = Schema.Struct({ outDir: _stringWithKeyDefault(".generated/crd") });
|
|
910
|
+
const HelmConfig = Schema.Struct({
|
|
911
|
+
cacheDir: _stringWithKeyDefault(".konfig/helm-cache"),
|
|
912
|
+
minVersion: _stringWithKeyDefault("3.16.0")
|
|
913
|
+
});
|
|
914
|
+
const ClusterSpec = Schema.Struct({
|
|
915
|
+
registry: Schema.optionalKey(Schema.String),
|
|
916
|
+
ingressClass: Schema.optionalKey(Schema.String),
|
|
917
|
+
storageClass: Schema.optionalKey(Schema.String),
|
|
918
|
+
repositoryUrl: Schema.optionalKey(Schema.String)
|
|
919
|
+
});
|
|
920
|
+
const DiffConfig = Schema.Struct({ baseline: Schema.String });
|
|
921
|
+
const ServicesConfig = Schema.Struct({
|
|
922
|
+
outFile: Schema.optionalKey(Schema.String),
|
|
923
|
+
globalPaths: _stringArrayWithKeyDefault([])
|
|
924
|
+
});
|
|
925
|
+
const KonfigConfig = Schema.Struct({
|
|
926
|
+
root: Schema.String,
|
|
927
|
+
cluster: _stringWithKeyDefault("cluster.ts"),
|
|
928
|
+
modules: _stringWithKeyDefault("modules"),
|
|
929
|
+
charts: _stringWithKeyDefault("charts"),
|
|
930
|
+
envs: Schema.Record(Schema.String, EnvEntry),
|
|
931
|
+
outDir: OutDir,
|
|
932
|
+
crd: Schema.optionalKey(CrdConfig).pipe(Schema.withDecodingDefaultKey(Effect.succeed({ outDir: ".generated/crd" }))),
|
|
933
|
+
helm: Schema.optionalKey(HelmConfig).pipe(Schema.withDecodingDefaultKey(Effect.succeed({
|
|
934
|
+
cacheDir: ".konfig/helm-cache",
|
|
935
|
+
minVersion: "3.16.0"
|
|
936
|
+
}))),
|
|
937
|
+
diff: Schema.optionalKey(DiffConfig),
|
|
938
|
+
services: Schema.optionalKey(ServicesConfig),
|
|
939
|
+
clusters: Schema.optionalKey(Schema.Record(Schema.String, ClusterSpec))
|
|
940
|
+
});
|
|
941
|
+
const strict = { onExcessProperty: "error" };
|
|
942
|
+
const decodeEff = Schema.decodeUnknownEffect(KonfigConfig);
|
|
943
|
+
const decodeKonfigConfigSync = (input) => Effect.runSync(decodeEff(input, strict));
|
|
944
|
+
const decodeKonfigConfigEffect = (input) => decodeEff(input, strict);
|
|
945
|
+
//#endregion
|
|
946
|
+
//#region src/Module.ts
|
|
947
|
+
var Module_exports = /* @__PURE__ */ __exportAll({
|
|
948
|
+
dynamicNs: () => dynamicNs,
|
|
949
|
+
fixedNs: () => fixedNs
|
|
950
|
+
});
|
|
951
|
+
const _liftBuild = (result) => Effect.isEffect(result) ? result : Effect.succeed(result);
|
|
952
|
+
/**
|
|
953
|
+
* Build a typed wrapper for a module whose namespace is part of its
|
|
954
|
+
* identity (e.g. `cert-manager` always installs into `cert-manager`).
|
|
955
|
+
*
|
|
956
|
+
* `target` is the backend adapter — typically `Application.target`
|
|
957
|
+
* (argocd) or `Bundle.target` (k8s). The wrapper's call signature
|
|
958
|
+
* merges the backend's per-instance fields (`source` for argocd,
|
|
959
|
+
* none for bundle) with the user-defined `Opts`.
|
|
960
|
+
*
|
|
961
|
+
* ```ts
|
|
962
|
+
* const defineSops = Module.fixedNs({
|
|
963
|
+
* target: Application.target,
|
|
964
|
+
* namespace: "sops",
|
|
965
|
+
* annotations: Sync.wave(-1),
|
|
966
|
+
* build: ({ namespace }, _opts: Record<never, never>) => [
|
|
967
|
+
* Namespace.make({ name: namespace }),
|
|
968
|
+
* Helm.release({ ... }),
|
|
969
|
+
* ],
|
|
970
|
+
* });
|
|
971
|
+
*
|
|
972
|
+
* const sops = defineSops({ name: "sops-operator", source: src("sops") });
|
|
973
|
+
* ```
|
|
974
|
+
*/
|
|
975
|
+
const fixedNs = (config) => {
|
|
976
|
+
const { target, namespace, provides, build, ...extraConfig } = unsafeCoerce(config, "Record spread shape mirrors the FixedNsConfig & ExtraConfig intersection");
|
|
977
|
+
const adapter = unsafeCoerce(target, "target was destructured from config without preserving its typed shape; reattach the constraint");
|
|
978
|
+
return (args) => {
|
|
979
|
+
const { name, ...rest } = unsafeCoerce(args, "destructuring the wrapper args; rest carries ExtraCallArgs & Opts as a flat record");
|
|
980
|
+
const buildResult = build({
|
|
981
|
+
name: unsafeCoerce(name, "LiteralName<Name> resolves to Name itself once the wrapper call typechecks"),
|
|
982
|
+
namespace
|
|
983
|
+
}, unsafeCoerce(rest, "rest carries Opts fields; ExtraCallArgs flow to target.define below"));
|
|
984
|
+
return adapter.define(unsafeCoerce({
|
|
985
|
+
...extraConfig,
|
|
986
|
+
...rest,
|
|
987
|
+
name,
|
|
988
|
+
namespace: unsafeCoerce(namespace, "Ns is a const string literal; LiteralName<Ns> resolves to Ns itself"),
|
|
989
|
+
build: _liftBuild(buildResult),
|
|
990
|
+
...provides !== void 0 ? { provides } : {}
|
|
991
|
+
}, "the assembled object structurally matches the target's define-args; spread layout matches the intersection"));
|
|
992
|
+
};
|
|
993
|
+
};
|
|
994
|
+
/**
|
|
995
|
+
* Build a typed wrapper for a module whose namespace is chosen per
|
|
996
|
+
* instance (e.g. an `api` module deployed into different namespaces
|
|
997
|
+
* per env).
|
|
998
|
+
*
|
|
999
|
+
* ```ts
|
|
1000
|
+
* const defineApi = Module.dynamicNs({
|
|
1001
|
+
* target: Application.target,
|
|
1002
|
+
* annotations: Sync.wave(1),
|
|
1003
|
+
* build: ({ name, namespace }, opts: ApiOpts) => [ ... ],
|
|
1004
|
+
* });
|
|
1005
|
+
*
|
|
1006
|
+
* const api = defineApi({
|
|
1007
|
+
* name: "api",
|
|
1008
|
+
* namespace: "prod",
|
|
1009
|
+
* source: src("api"),
|
|
1010
|
+
* replicas: 2,
|
|
1011
|
+
* });
|
|
1012
|
+
* ```
|
|
1013
|
+
*/
|
|
1014
|
+
const dynamicNs = (config) => {
|
|
1015
|
+
const { target, provides, build, ...extraConfig } = unsafeCoerce(config, "Record spread shape mirrors the DynamicNsConfig & ExtraConfig intersection");
|
|
1016
|
+
const adapter = unsafeCoerce(target, "target was destructured from config without preserving its typed shape; reattach the constraint");
|
|
1017
|
+
return (args) => {
|
|
1018
|
+
const { name, namespace, ...rest } = unsafeCoerce(args, "destructuring the wrapper args; rest carries ExtraCallArgs & Opts as a flat record");
|
|
1019
|
+
const buildResult = build({
|
|
1020
|
+
name: unsafeCoerce(name, "LiteralName<Name> resolves to Name itself once the wrapper call typechecks"),
|
|
1021
|
+
namespace: unsafeCoerce(namespace, "LiteralName<Ns> resolves to Ns itself once the wrapper call typechecks")
|
|
1022
|
+
}, unsafeCoerce(rest, "rest carries Opts fields; ExtraCallArgs flow to target.define below"));
|
|
1023
|
+
return adapter.define(unsafeCoerce({
|
|
1024
|
+
...extraConfig,
|
|
1025
|
+
...rest,
|
|
1026
|
+
name,
|
|
1027
|
+
namespace,
|
|
1028
|
+
build: _liftBuild(buildResult),
|
|
1029
|
+
...provides !== void 0 ? { provides } : {}
|
|
1030
|
+
}, "the assembled object structurally matches the target's define-args; spread layout matches the intersection"));
|
|
1031
|
+
};
|
|
1032
|
+
};
|
|
1033
|
+
//#endregion
|
|
1034
|
+
//#region src/RenderContext.ts
|
|
1035
|
+
const RenderContext = {
|
|
1036
|
+
make: (env) => ({ env }),
|
|
1037
|
+
makeFull: (input) => ({
|
|
1038
|
+
env: input.env,
|
|
1039
|
+
...input.cluster !== void 0 ? { cluster: input.cluster } : {},
|
|
1040
|
+
...input.k8sVersion !== void 0 ? { k8sVersion: input.k8sVersion } : {},
|
|
1041
|
+
...input.flags !== void 0 ? { flags: input.flags } : {}
|
|
1042
|
+
})
|
|
1043
|
+
};
|
|
1044
|
+
//#endregion
|
|
1045
|
+
//#region src/render.ts
|
|
1046
|
+
/** @internal */
|
|
1047
|
+
const _resolveEnv = (env) => env ?? "prod";
|
|
1048
|
+
/** @internal */
|
|
1049
|
+
const _buildLayers = (extra) => extra === void 0 ? NodeServices.layer : Layer.mergeAll(NodeServices.layer, extra);
|
|
1050
|
+
/**
|
|
1051
|
+
* @internal
|
|
1052
|
+
* Compose the ctx + layers wiring `render` hands to `NodeRuntime.runMain`
|
|
1053
|
+
* into a single runnable Effect (context fully provided). Extracted so
|
|
1054
|
+
* tests can exercise the composition with `Effect.runPromise` instead of
|
|
1055
|
+
* `runMain`, which would exit the process.
|
|
1056
|
+
*/
|
|
1057
|
+
const _compose = (program, options = {}) => {
|
|
1058
|
+
const ctx = RenderContext.make(_resolveEnv(options.env));
|
|
1059
|
+
const layers = _buildLayers(options.layers);
|
|
1060
|
+
return program(ctx).pipe(Effect.scoped, Effect.provide(layers));
|
|
1061
|
+
};
|
|
1062
|
+
/**
|
|
1063
|
+
* Run a render program against `NodeRuntime`.
|
|
1064
|
+
*
|
|
1065
|
+
* The callback receives a `RenderContext` keyed on `options.env`
|
|
1066
|
+
* (default `"prod"`) and returns an Effect whose only required
|
|
1067
|
+
* services are `NodeServices` and whatever the caller supplies via
|
|
1068
|
+
* `options.layers`. `render` provides both, wraps in `Effect.scoped`,
|
|
1069
|
+
* and hands off to `NodeRuntime.runMain`.
|
|
1070
|
+
*
|
|
1071
|
+
* Replaces the per-file `NodeRuntime.runMain(program.pipe(...))`
|
|
1072
|
+
* boilerplate every example used to repeat.
|
|
1073
|
+
*/
|
|
1074
|
+
const render = (program, options = {}) => {
|
|
1075
|
+
NodeRuntime.runMain(_compose(program, options));
|
|
1076
|
+
};
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/renderManifest.ts
|
|
1079
|
+
const renderManifest = (input) => input.manifest.render(input.ctx);
|
|
1080
|
+
//#endregion
|
|
1081
|
+
//#region src/yaml/serialize.ts
|
|
1082
|
+
const ORDER_ROOT = [
|
|
1083
|
+
"apiVersion",
|
|
1084
|
+
"kind",
|
|
1085
|
+
"metadata",
|
|
1086
|
+
"spec",
|
|
1087
|
+
"status"
|
|
1088
|
+
];
|
|
1089
|
+
const ORDER_METADATA = [
|
|
1090
|
+
"name",
|
|
1091
|
+
"namespace",
|
|
1092
|
+
"labels",
|
|
1093
|
+
"annotations"
|
|
1094
|
+
];
|
|
1095
|
+
const _reorderObject = (input) => {
|
|
1096
|
+
const { obj, depth, parentKey } = input;
|
|
1097
|
+
const keys = Object.keys(obj).filter((k) => obj[k] !== null && obj[k] !== void 0);
|
|
1098
|
+
const order = depth === 0 ? ORDER_ROOT : depth === 1 && parentKey === "metadata" ? ORDER_METADATA : null;
|
|
1099
|
+
let sortedKeys;
|
|
1100
|
+
if (order !== null) {
|
|
1101
|
+
const known = order.filter((k) => keys.includes(k));
|
|
1102
|
+
const rest = keys.filter((k) => !order.includes(k)).sort();
|
|
1103
|
+
sortedKeys = [...known, ...rest];
|
|
1104
|
+
} else sortedKeys = keys.slice().sort();
|
|
1105
|
+
const out = {};
|
|
1106
|
+
for (const k of sortedKeys) out[k] = _normalize({
|
|
1107
|
+
value: obj[k],
|
|
1108
|
+
depth: depth + 1,
|
|
1109
|
+
parentKey: k
|
|
1110
|
+
});
|
|
1111
|
+
return out;
|
|
1112
|
+
};
|
|
1113
|
+
const _normalize = (input) => {
|
|
1114
|
+
const { value, depth, parentKey } = input;
|
|
1115
|
+
if (value === null || value === void 0) return value;
|
|
1116
|
+
if (Array.isArray(value)) return value.map((v) => _normalize({
|
|
1117
|
+
value: v,
|
|
1118
|
+
depth: depth + 1,
|
|
1119
|
+
parentKey: null
|
|
1120
|
+
}));
|
|
1121
|
+
if (typeof value === "object") return _reorderObject({
|
|
1122
|
+
obj: unsafeCoerce(value, "typeof === object branch (value !== null, !Array.isArray above) — treated as a keyed record for reordering"),
|
|
1123
|
+
depth,
|
|
1124
|
+
parentKey
|
|
1125
|
+
});
|
|
1126
|
+
return value;
|
|
1127
|
+
};
|
|
1128
|
+
const serialize = (input) => {
|
|
1129
|
+
const normalized = _normalize({
|
|
1130
|
+
value: input.value,
|
|
1131
|
+
depth: 0,
|
|
1132
|
+
parentKey: null
|
|
1133
|
+
});
|
|
1134
|
+
const lf = YAML.stringify(normalized, {
|
|
1135
|
+
indent: 2,
|
|
1136
|
+
indentSeq: true,
|
|
1137
|
+
lineWidth: 0,
|
|
1138
|
+
minContentWidth: 0,
|
|
1139
|
+
defaultStringType: "PLAIN",
|
|
1140
|
+
defaultKeyType: "PLAIN",
|
|
1141
|
+
nullStr: "null",
|
|
1142
|
+
version: "1.1"
|
|
1143
|
+
}).replace(/\r\n/g, "\n").replace(/\s+$/g, "");
|
|
1144
|
+
return input.trailingNewline ?? true ? `${lf}\n` : lf;
|
|
1145
|
+
};
|
|
1146
|
+
/**
|
|
1147
|
+
* Derive the on-disk filename (`<Kind>-<name>.yaml`) for a rendered
|
|
1148
|
+
* resource.
|
|
1149
|
+
*
|
|
1150
|
+
* **Precondition (caller invariant):** `resource.kind` and
|
|
1151
|
+
* `resource.metadata.name` MUST both be non-empty strings. A rendered
|
|
1152
|
+
* Kubernetes object always satisfies this, so callers are expected to
|
|
1153
|
+
* validate/decode the object *before* reaching this point (the CLI keys
|
|
1154
|
+
* files by kind+name only for objects it has already confirmed carry
|
|
1155
|
+
* both fields).
|
|
1156
|
+
*
|
|
1157
|
+
* Because the precondition is a caller invariant rather than runtime
|
|
1158
|
+
* input, a violation is a programming error: this throws synchronously
|
|
1159
|
+
* (a defect) instead of returning a typed error. It is deliberately kept
|
|
1160
|
+
* out of the `AnyRenderError` channel — do not route untrusted resources
|
|
1161
|
+
* through it; narrow them first.
|
|
1162
|
+
*
|
|
1163
|
+
* @throws {Error} if `kind` or `metadata.name` is missing or empty.
|
|
1164
|
+
*/
|
|
1165
|
+
const filenameFor = (resource) => {
|
|
1166
|
+
const kind = resource.kind;
|
|
1167
|
+
const name = resource.metadata?.name;
|
|
1168
|
+
if (typeof kind !== "string" || kind.length === 0) throw new Error(`filenameFor: resource has no string kind`);
|
|
1169
|
+
if (typeof name !== "string" || name.length === 0) throw new Error(`filenameFor: resource '${kind}' has no metadata.name`);
|
|
1170
|
+
return `${kind}-${_sanitizeNameForFilename(name)}.yaml`;
|
|
1171
|
+
};
|
|
1172
|
+
const _sanitizeNameForFilename = (name) => name.replace(/[./]/g, "-");
|
|
1173
|
+
//#endregion
|
|
1174
|
+
//#region src/yaml/index.ts
|
|
1175
|
+
var yaml_exports = /* @__PURE__ */ __exportAll({
|
|
1176
|
+
filenameFor: () => filenameFor,
|
|
1177
|
+
serialize: () => serialize
|
|
1178
|
+
});
|
|
1179
|
+
//#endregion
|
|
1180
|
+
export { BoundaryDecodeError, BuiltImageRef, Bundle_exports as Bundle, ClusterSpec, Compose_exports as Compose, CrdConfig, CrdExtractError, deps_exports as Dep, DiffConfig, EmbedYamlReadError, EnvEntry, EnvImages, Helm_exports as Helm, HelmConfig, HelmDigestMismatch, HelmRenderError, HelmVersionTooLow, ImagesAppMissing, ImagesConfig, ImagesEnvMissing, KonfigConfig, Manifest_exports as Manifest, Module_exports as Module, OutDir, ProcessError, RenderContext, RenderError, ServicesConfig, yaml_exports as Yaml, boundary, brand, decodeImagesEffect, decodeImagesSync, decodeKonfigConfigEffect, decodeKonfigConfigSync, deepEqual, diffFiles, formatDiff, hasDifferences, imagesFor, lookupEnv, lookupEnvEffect, parseYaml, parseYamlAll, redact, render, renderManifest, requireImage, runProcessExit, runProcessString, unsafeCoerce };
|
|
1181
|
+
|
|
1182
|
+
//# sourceMappingURL=index.mjs.map
|