@konfig.ts/k8s 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/dist/index.mjs ADDED
@@ -0,0 +1,1008 @@
1
+ import { Dep, Manifest, RenderError, brand, unsafeCoerce } from "@konfig.ts/core";
2
+ import { Data, Effect, Layer, Match, Redacted } from "effect";
3
+ import { Environment as Environment$1, Secret as Secret$1, runtime } from "@konfig.ts/env";
4
+ import { createHash } from "node:crypto";
5
+ //#region \0rolldown/runtime.js
6
+ var __defProp = Object.defineProperty;
7
+ var __exportAll = (all, no_symbols) => {
8
+ let target = {};
9
+ for (var name in all) __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true
12
+ });
13
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
14
+ return target;
15
+ };
16
+ //#endregion
17
+ //#region src/.generated/k8s-types/index.ts
18
+ var k8s_types_exports = /* @__PURE__ */ __exportAll({});
19
+ //#endregion
20
+ //#region src/container.ts
21
+ /**
22
+ * `Container` value namespace.
23
+ *
24
+ * const apiContainer = Container.define({
25
+ * name: "api",
26
+ * image: apiImage,
27
+ * ports: [Port.make({ name: "http", containerPort: 8080 })],
28
+ * readinessProbe: { httpGet: { port: Port.ref("http") } },
29
+ * env: [...],
30
+ * });
31
+ *
32
+ * `Container.define` captures the union of named ports (from
33
+ * `Port.make`) as `Ports`, the union of mounted volume names (from
34
+ * `Volume.mountRef`) as `Mounts`, and validates that the `env` list
35
+ * has no duplicate env-var names. The first two phantoms travel on
36
+ * `ContainerSpec`; `Pod.define` checks the container's Mounts against
37
+ * the pod's declared volume names.
38
+ *
39
+ * Duplicate env names are caught at the call site via a template-literal
40
+ * error message — see `EnvDupCheck`. With no volumeMounts and no env,
41
+ * all phantoms collapse to `never` so the container slots into any pod.
42
+ */
43
+ const Container = { define: (input) => {
44
+ return {
45
+ ...input,
46
+ ports: unsafeCoerce(input.ports, "Ports tuple's element brands are the same PortName<N>; widening Ports → readonly ContainerPort<P>[] only changes the static shape, not the runtime values"),
47
+ readinessProbe: input.readinessProbe,
48
+ livenessProbe: input.livenessProbe,
49
+ startupProbe: input.startupProbe,
50
+ volumeMounts: unsafeCoerce(input.volumeMounts, "Mounts tuple's element brands are the same VolumeMount<N>; widening Mounts → readonly VolumeMount<M>[] preserves runtime shape"),
51
+ env: unsafeCoerce(input.env, "EnvDupCheck<Envs> intersection vanishes at runtime; the runtime value is the original EnvVar[]")
52
+ };
53
+ } };
54
+ /**
55
+ * `Pod` value namespace.
56
+ *
57
+ * const pod = Pod.define({
58
+ * volumes: [Volume.empty({ name: "config" })],
59
+ * containers: [Container.define({ ..., volumeMounts: [...] })],
60
+ * });
61
+ *
62
+ * imagePullSecrets: [Pod.imagePullSecret(ghcrRef)],
63
+ *
64
+ * - `Pod.define(input)` ties container `volumeMounts[i].name` to the
65
+ * pod's declared volume names via `NoInfer`.
66
+ * - `Pod.imagePullSecret(ref)` returns the `{ name: SecretRef }` entry
67
+ * consumed by Deployment / StatefulSet / Job / CronJob workloads.
68
+ */
69
+ const Pod = {
70
+ define: (input) => ({
71
+ volumes: unsafeCoerce(input.volumes, "V tuple's elements are Volume<N>; widening V → readonly Volume<VolumeNamesOf<V>>[] is a structural relaxation, runtime value unchanged"),
72
+ containers: input.containers,
73
+ initContainers: input.initContainers
74
+ }),
75
+ imagePullSecret: (ref) => ({ name: ref })
76
+ };
77
+ //#endregion
78
+ //#region src/ports.ts
79
+ const _portName = (name) => brand(name);
80
+ /**
81
+ * `Port` value namespace.
82
+ *
83
+ * ports: [Port.make({ name: "http", containerPort: 8080 })],
84
+ * readinessProbe: { httpGet: { port: Port.ref("http") } },
85
+ *
86
+ * - `Port.make(input)` constructs a named container port; the literal
87
+ * `name` is captured in the returned `ContainerPort<N>` brand so
88
+ * `Container` can infer the port-name union and constrain
89
+ * cross-references (probes, Service.targetPort).
90
+ * - `Port.ref(name)` returns the brand alone, for probe targets and
91
+ * `targetPort` references that need to name an existing declared port.
92
+ */
93
+ const Port = {
94
+ make: (input) => ({
95
+ containerPort: input.containerPort,
96
+ name: _portName(input.name),
97
+ protocol: input.protocol,
98
+ hostPort: input.hostPort,
99
+ hostIP: input.hostIP
100
+ }),
101
+ ref: (name) => _portName(name)
102
+ };
103
+ //#endregion
104
+ //#region src/env.ts
105
+ /**
106
+ * `EnvVar` value namespace.
107
+ *
108
+ * env: [
109
+ * EnvVar.value({ name: "PORT", value: "8080" }),
110
+ * EnvVar.fromSecret({ name: "DB_URL", ref: dbCreds.ref, key: "url" }),
111
+ * EnvVar.fromSecretForPod({
112
+ * name: "DB_URL_PRIMARY", ref: dbCreds.ref, key: "url",
113
+ * podNamespace: "app",
114
+ * }),
115
+ * EnvVar.fromConfigMap({ name: "NEW_UI", ref: flags.ref, key: "NEW_UI" }),
116
+ * EnvVar.raw({ name: "POD_NAME", valueFrom: { fieldRef: ... } }),
117
+ * ]
118
+ *
119
+ * Each constructor captures the literal env-var name (`N`) so
120
+ * `Container` can detect duplicate entries across the env list.
121
+ */
122
+ const EnvVar = {
123
+ value: (input) => ({
124
+ name: input.name,
125
+ value: input.value
126
+ }),
127
+ fromSecret: (input) => ({
128
+ name: input.name,
129
+ valueFrom: { secretKeyRef: {
130
+ name: input.ref,
131
+ key: input.key,
132
+ optional: input.optional
133
+ } }
134
+ }),
135
+ /**
136
+ * Namespace-checked variant. The ref's namespace slot (`Ns`) must
137
+ * match `podNamespace`. Catches "pod in namespace A references a
138
+ * Secret in namespace B" at compile time — kube-apiserver only
139
+ * resolves `valueFrom.secretKeyRef` against the pod's own namespace,
140
+ * so cross-namespace refs are runtime errors.
141
+ */
142
+ fromSecretForPod: (input) => ({
143
+ name: input.name,
144
+ valueFrom: { secretKeyRef: {
145
+ name: input.ref,
146
+ key: input.key,
147
+ optional: input.optional
148
+ } }
149
+ }),
150
+ fromConfigMap: (input) => ({
151
+ name: input.name,
152
+ valueFrom: { configMapKeyRef: {
153
+ name: input.ref,
154
+ key: input.key,
155
+ optional: input.optional
156
+ } }
157
+ }),
158
+ raw: (input) => input
159
+ };
160
+ //#endregion
161
+ //#region src/refs.ts
162
+ const SecretRef = {
163
+ of: (name) => brand(name),
164
+ /**
165
+ * Escape hatch — widen a typed ref's namespace slot to `any`, making
166
+ * it usable across any pod context. Use sparingly (legitimate
167
+ * cross-namespace cases: ExternalSecret reflection, in-cluster
168
+ * shared infra). The cast is grep-able so PR reviewers see opt-ins.
169
+ */
170
+ unsafeReNamespace: (ref) => ref
171
+ };
172
+ const ConfigMapRef = { of: (name) => brand(name) };
173
+ const ServiceAccountRef = { of: (name) => brand(name) };
174
+ const PvcRef = { of: (name) => brand(name) };
175
+ //#endregion
176
+ //#region src/identity.ts
177
+ const Namespace = { make: (input) => {
178
+ const resource = {
179
+ apiVersion: "v1",
180
+ kind: "Namespace",
181
+ metadata: {
182
+ name: input.name,
183
+ labels: input.labels,
184
+ annotations: input.annotations
185
+ }
186
+ };
187
+ const m = Manifest.make(() => Effect.succeed(resource));
188
+ return Object.assign(m, { ref: input.name });
189
+ } };
190
+ const ServiceAccount = { make: (input) => {
191
+ const resource = {
192
+ apiVersion: "v1",
193
+ kind: "ServiceAccount",
194
+ metadata: {
195
+ name: input.name,
196
+ namespace: input.namespace,
197
+ labels: input.labels,
198
+ annotations: input.annotations
199
+ },
200
+ automountServiceAccountToken: input.automountServiceAccountToken,
201
+ imagePullSecrets: input.imagePullSecrets?.map((s) => ({ name: s.name }))
202
+ };
203
+ const m = Manifest.make(() => Effect.succeed(resource));
204
+ return Object.assign(m, { ref: ServiceAccountRef.of(input.name) });
205
+ } };
206
+ const ConfigMap = { make: (input) => {
207
+ const resource = {
208
+ apiVersion: "v1",
209
+ kind: "ConfigMap",
210
+ metadata: {
211
+ name: input.name,
212
+ namespace: input.namespace,
213
+ labels: input.labels,
214
+ annotations: input.annotations
215
+ },
216
+ data: input.data,
217
+ binaryData: input.binaryData,
218
+ immutable: input.immutable
219
+ };
220
+ const m = Manifest.make(() => Effect.succeed(resource));
221
+ return Object.assign(m, { ref: ConfigMapRef.of(input.name) });
222
+ } };
223
+ const Secret$2 = { make: (input) => {
224
+ const resource = {
225
+ apiVersion: "v1",
226
+ kind: "Secret",
227
+ metadata: {
228
+ name: input.name,
229
+ namespace: input.namespace,
230
+ labels: input.labels,
231
+ annotations: input.annotations
232
+ },
233
+ type: input.type,
234
+ data: input.data,
235
+ stringData: input.stringData,
236
+ immutable: input.immutable
237
+ };
238
+ const m = Manifest.make(() => Effect.succeed(resource));
239
+ return Object.assign(m, { ref: SecretRef.of(input.name) });
240
+ } };
241
+ //#endregion
242
+ //#region src/secretBind.ts
243
+ const bindSecret = (input) => {
244
+ const { secret } = input;
245
+ const namespace = unsafeCoerce(input.namespace ?? secret.namespace, "Ns defaults to `string`; the override (if present) is `Ns`, the secret's own namespace is `string` — runtime value either way is a string");
246
+ const ref = SecretRef.of(secret.name);
247
+ const envVars = secret.keys.map((key) => EnvVar.fromSecret({
248
+ name: secret.env[key],
249
+ ref,
250
+ key
251
+ }));
252
+ const refLayer = Dep.provideSecret(secret.name);
253
+ const manifest = input.backend === void 0 ? void 0 : input.backend.emit({
254
+ name: secret.name,
255
+ namespace,
256
+ keys: secret.keys,
257
+ labels: input.labels,
258
+ annotations: input.annotations,
259
+ source: input.source
260
+ });
261
+ const out = {
262
+ ref,
263
+ name: secret.name,
264
+ namespace,
265
+ keys: secret.keys,
266
+ envVars,
267
+ manifest,
268
+ refLayer
269
+ };
270
+ if (input.source === void 0) return out;
271
+ const source = input.source;
272
+ const valuesTag = Dep.SecretValues(secret.name);
273
+ const layer = Layer.effect(valuesTag, source.resolve.pipe(Effect.mapError((cause) => new RenderError({
274
+ message: `SecretValues(${namespace}/${secret.name}): source failed for key "${cause.key}"`,
275
+ cause
276
+ }))));
277
+ return {
278
+ ...out,
279
+ values: valuesTag,
280
+ layer
281
+ };
282
+ };
283
+ //#endregion
284
+ //#region src/environmentBind.ts
285
+ const _bindLiteral = (input) => {
286
+ const hasOverride = input.override !== void 0;
287
+ const value = hasOverride ? input.override : input.entry.value;
288
+ const serialized = hasOverride ? input.entry.serialize(input.override) : input.entry.serialized;
289
+ return {
290
+ envName: input.entry.envName,
291
+ value,
292
+ envVar: {
293
+ name: input.entry.envName,
294
+ value: serialized
295
+ }
296
+ };
297
+ };
298
+ const _bindDownward = (input) => ({
299
+ envName: input.entry.envName,
300
+ fieldPath: input.entry.fieldPath,
301
+ envVar: {
302
+ name: input.entry.envName,
303
+ valueFrom: { fieldRef: { fieldPath: input.entry.fieldPath } }
304
+ }
305
+ });
306
+ const _handleSecret = (input) => {
307
+ const memberOpts = unsafeCoerce(input.secretsOpts?.[input.memberKey], "SecretMembersOpts<M> shape — runtime key lookup against the typed input");
308
+ const d = bindSecret({
309
+ secret: unsafeCoerce(input.entry, "Match.tag('Secret') narrowed entry to SecretEntry; the helper-level signature is the general SecretEntry shape"),
310
+ backend: memberOpts?.backend,
311
+ source: memberOpts?.source,
312
+ labels: memberOpts?.labels,
313
+ annotations: memberOpts?.annotations,
314
+ namespace: input.namespace
315
+ });
316
+ input.acc.declared[input.memberKey] = d;
317
+ input.acc.envVars.push(...d.envVars);
318
+ if (d.manifest !== void 0) input.acc.manifests.push(d.manifest);
319
+ if (d.layer !== void 0) input.acc.valuesLayers.push(unsafeCoerce(d.layer, "DeclaredSecret.layer is Layer<Provide<SecretValues, N>, ...>; widen to unknown for the heterogeneous Layer.mergeAll"));
320
+ };
321
+ const _handleLiteral = (input) => {
322
+ const d = _bindLiteral({
323
+ entry: unsafeCoerce(input.entry, "Match.tag('Literal') narrowed entry to LiteralEntry<string, unknown>"),
324
+ override: input.literalsOpts?.[input.memberKey]
325
+ });
326
+ input.acc.declared[input.memberKey] = d;
327
+ input.acc.envVars.push(d.envVar);
328
+ };
329
+ const _handleDownward = (input) => {
330
+ const d = _bindDownward({ entry: unsafeCoerce(input.entry, "Match.tag('Downward') narrowed entry to DownwardEntry<string>") });
331
+ input.acc.declared[input.memberKey] = d;
332
+ input.acc.envVars.push(d.envVar);
333
+ };
334
+ const _handleEnvironment = (input) => {
335
+ const sub = bindEnvironment({
336
+ env: unsafeCoerce(input.entry, "Match.tag('Environment') narrowed entry to a nested Environment"),
337
+ secrets: unsafeCoerce(input.secretsOpts?.[input.memberKey] ?? {}, "sub-record from SecretMembersOpts<M> — type is checked at the outer call site"),
338
+ literals: unsafeCoerce(input.literalsOpts?.[input.memberKey] ?? {}, "sub-record from LiteralMembersOpts<M> — type is checked at the outer call site"),
339
+ namespace: input.namespace
340
+ });
341
+ input.acc.declared[input.memberKey] = sub.members;
342
+ input.acc.envVars.push(...sub.envVars);
343
+ input.acc.manifests.push(...sub.manifests);
344
+ input.acc.valuesLayers.push(unsafeCoerce(sub.valuesLayer, "valuesLayer aggregate over the recursive merge"));
345
+ };
346
+ const _dispatch = (input) => Match.value(input.entry._kind).pipe(Match.when("Secret", () => _handleSecret(input)), Match.when("Literal", () => _handleLiteral(input)), Match.when("Downward", () => _handleDownward(input)), Match.when("Environment", () => _handleEnvironment(input)), Match.exhaustive);
347
+ const bindEnvironment = (input) => {
348
+ const { env } = input;
349
+ const acc = {
350
+ declared: {},
351
+ envVars: [],
352
+ manifests: [],
353
+ valuesLayers: []
354
+ };
355
+ const secretsOpts = unsafeCoerce(input.secrets, "discriminated union from BindEnvironmentInput; iterate keys at runtime");
356
+ const literalsOpts = unsafeCoerce(input.literals, "discriminated union from BindEnvironmentInput; iterate keys at runtime");
357
+ for (const memberKey of Object.keys(env.members)) _dispatch({
358
+ memberKey,
359
+ entry: unsafeCoerce(env.members[memberKey], "env.members values are EnvMember by construction"),
360
+ secretsOpts,
361
+ literalsOpts,
362
+ namespace: input.namespace,
363
+ acc
364
+ });
365
+ const valuesLayer = unsafeCoerce(acc.valuesLayers.length === 0 ? Layer.empty : Layer.mergeAll(acc.valuesLayers[0], ...acc.valuesLayers.slice(1)), "merged Layer over a heterogeneous list of per-secret value layers");
366
+ return {
367
+ envVars: acc.envVars,
368
+ manifests: acc.manifests,
369
+ members: unsafeCoerce(acc.declared, "declared populated by iterating env.members; each key maps to its DeclaredMember<M[K], Ns>"),
370
+ valuesLayer
371
+ };
372
+ };
373
+ //#endregion
374
+ //#region src/backend.ts
375
+ var BackendSourceMissing = class extends Data.TaggedError("BackendSourceMissing") {
376
+ get message() {
377
+ return `backend "${this.backend}" requires a source but none was provided for secret "${this.secret}"`;
378
+ }
379
+ };
380
+ //#endregion
381
+ //#region src/nativeSecret.ts
382
+ const _emit = (input) => Manifest.make((_ctx) => Effect.gen(function* () {
383
+ if (input.opts.silenceWarning !== true) yield* Effect.logWarning(`NativeSecret backend emits a plaintext Secret with stringData on disk for "${input.base.namespace}/${input.base.name}". Pass { silenceWarning: true } to suppress, or switch to ExternalSecrets / SealedSecrets / Sops for production.`);
384
+ const resolved = yield* input.source.resolve.pipe(Effect.mapError((cause) => new RenderError({
385
+ message: `NativeSecret(${input.base.namespace}/${input.base.name}): source failed for key "${cause.key}"`,
386
+ cause
387
+ })));
388
+ const stringData = {};
389
+ for (const key of input.base.keys) stringData[key] = Redacted.value(resolved[key]);
390
+ return {
391
+ apiVersion: "v1",
392
+ kind: "Secret",
393
+ metadata: {
394
+ name: input.base.name,
395
+ namespace: input.base.namespace,
396
+ labels: input.base.labels,
397
+ annotations: input.base.annotations
398
+ },
399
+ type: input.opts.type,
400
+ stringData,
401
+ immutable: input.opts.immutable
402
+ };
403
+ }));
404
+ const NativeSecret = { backend: (opts) => {
405
+ const resolvedOpts = opts ?? {};
406
+ return {
407
+ _tag: "NativeSecret",
408
+ requiresSource: true,
409
+ emit: (input) => {
410
+ if (input.source === void 0) throw new BackendSourceMissing({
411
+ backend: "NativeSecret",
412
+ secret: input.name
413
+ });
414
+ return _emit({
415
+ base: input,
416
+ opts: resolvedOpts,
417
+ source: input.source
418
+ });
419
+ }
420
+ };
421
+ } };
422
+ //#endregion
423
+ //#region src/podHash.ts
424
+ const _frame = (hasher, value) => {
425
+ const bytes = Buffer.from(value, "utf8");
426
+ hasher.update(`${bytes.length}:`);
427
+ hasher.update(bytes);
428
+ hasher.update(",");
429
+ };
430
+ const hashSecretValues = (input) => {
431
+ const hasher = createHash("sha256");
432
+ _frame(hasher, "konfig/secret-values-hash/v1");
433
+ _frame(hasher, input.salt);
434
+ const keys = Object.keys(input.values).sort();
435
+ for (const key of keys) {
436
+ _frame(hasher, key);
437
+ _frame(hasher, Redacted.value(input.values[key]));
438
+ }
439
+ return hasher.digest("hex");
440
+ };
441
+ //#endregion
442
+ //#region src/network.ts
443
+ const Service = {
444
+ make: (input) => {
445
+ const resource = {
446
+ apiVersion: "v1",
447
+ kind: "Service",
448
+ metadata: {
449
+ name: input.name,
450
+ namespace: input.namespace,
451
+ labels: input.labels,
452
+ annotations: input.annotations
453
+ },
454
+ spec: {
455
+ selector: input.selector,
456
+ type: input.type,
457
+ ports: unsafeCoerce(input.ports, "input.ports is the user-typed Service spec; K8s ServicePort allows the same fields"),
458
+ clusterIP: input.clusterIP,
459
+ sessionAffinity: input.sessionAffinity,
460
+ publishNotReadyAddresses: input.publishNotReadyAddresses,
461
+ externalTrafficPolicy: input.externalTrafficPolicy,
462
+ internalTrafficPolicy: input.internalTrafficPolicy
463
+ }
464
+ };
465
+ return Manifest.make(() => Effect.succeed(resource));
466
+ },
467
+ fromContainer: (input) => Service.make({
468
+ name: input.name,
469
+ namespace: input.namespace,
470
+ labels: input.labels,
471
+ annotations: input.annotations,
472
+ selector: input.selector,
473
+ ports: unsafeCoerce(input.ports, "ServicePortSpec<Ports> structurally matches K8sServicePort; targetPort's PortName<Ports> brand is a phantom — runtime value is the underlying string"),
474
+ type: input.type,
475
+ clusterIP: input.clusterIP,
476
+ sessionAffinity: input.sessionAffinity,
477
+ publishNotReadyAddresses: input.publishNotReadyAddresses,
478
+ externalTrafficPolicy: input.externalTrafficPolicy,
479
+ internalTrafficPolicy: input.internalTrafficPolicy
480
+ }),
481
+ fromPodSet: (input) => Service.make({
482
+ name: input.name,
483
+ namespace: input.namespace,
484
+ labels: input.labels,
485
+ annotations: input.annotations,
486
+ selector: input.podSet.labels,
487
+ ports: input.ports,
488
+ type: input.type,
489
+ clusterIP: input.clusterIP,
490
+ sessionAffinity: input.sessionAffinity,
491
+ publishNotReadyAddresses: input.publishNotReadyAddresses,
492
+ externalTrafficPolicy: input.externalTrafficPolicy,
493
+ internalTrafficPolicy: input.internalTrafficPolicy
494
+ })
495
+ };
496
+ const Ingress = {
497
+ make: (input) => {
498
+ const resource = {
499
+ apiVersion: "networking.k8s.io/v1",
500
+ kind: "Ingress",
501
+ metadata: {
502
+ name: input.name,
503
+ namespace: input.namespace,
504
+ labels: input.labels,
505
+ annotations: input.annotations
506
+ },
507
+ spec: {
508
+ ingressClassName: input.ingressClassName,
509
+ rules: unsafeCoerce(input.rules, "user-supplied Ingress rules; widening from our convenience type to the K8s type"),
510
+ tls: unsafeCoerce(input.tls, "Ingress.tls produces K8sIngressTLS with branded secretName"),
511
+ defaultBackend: unsafeCoerce(input.defaultBackend, "user-supplied IngressBackend; structural match to K8s type")
512
+ }
513
+ };
514
+ return Manifest.make(() => Effect.succeed(resource));
515
+ },
516
+ /**
517
+ * TLS entry constructor for `Ingress.make({ tls: [...] })`. Takes a
518
+ * branded `SecretRef` so the secret's name can't widen to a raw
519
+ * string at the call site.
520
+ */
521
+ tls: (input) => ({
522
+ secretName: input.secretName,
523
+ hosts: input.hosts
524
+ })
525
+ };
526
+ //#endregion
527
+ //#region src/policy.ts
528
+ const PersistentVolume = { make: (input) => Manifest.make(() => Effect.succeed({
529
+ apiVersion: "v1",
530
+ kind: "PersistentVolume",
531
+ metadata: {
532
+ name: input.name,
533
+ labels: input.labels,
534
+ annotations: input.annotations
535
+ },
536
+ spec: unsafeCoerce(input.spec, "user-supplied PV spec; structural match to the K8s type")
537
+ })) };
538
+ const PersistentVolumeClaim = { make: (input) => Manifest.make(() => Effect.succeed({
539
+ apiVersion: "v1",
540
+ kind: "PersistentVolumeClaim",
541
+ metadata: {
542
+ name: input.name,
543
+ namespace: input.namespace,
544
+ labels: input.labels,
545
+ annotations: input.annotations
546
+ },
547
+ spec: unsafeCoerce(input.spec, "user-supplied PVC spec; structural match to the K8s type")
548
+ })) };
549
+ const _lowerPeer = (peer) => ({
550
+ ...peer.podSet !== void 0 ? { podSelector: { matchLabels: peer.podSet.labels } } : {},
551
+ ...peer.namespaceSelector !== void 0 ? { namespaceSelector: peer.namespaceSelector } : {},
552
+ ...peer.ipBlock !== void 0 ? { ipBlock: peer.ipBlock } : {}
553
+ });
554
+ const NetworkPolicy = {
555
+ make: (input) => Manifest.make(() => Effect.succeed({
556
+ apiVersion: "networking.k8s.io/v1",
557
+ kind: "NetworkPolicy",
558
+ metadata: {
559
+ name: input.name,
560
+ namespace: input.namespace,
561
+ labels: input.labels,
562
+ annotations: input.annotations
563
+ },
564
+ spec: input.spec
565
+ })),
566
+ fromPodSet: (input) => {
567
+ const ingress = input.ingress?.map((rule) => ({
568
+ from: rule.from?.map(_lowerPeer),
569
+ ports: rule.ports
570
+ }));
571
+ const egress = input.egress?.map((rule) => ({
572
+ to: rule.to?.map(_lowerPeer),
573
+ ports: rule.ports
574
+ }));
575
+ return NetworkPolicy.make({
576
+ name: input.name,
577
+ namespace: input.namespace,
578
+ labels: input.labels,
579
+ annotations: input.annotations,
580
+ spec: unsafeCoerce({
581
+ podSelector: { matchLabels: input.podSet.labels },
582
+ policyTypes: input.policyTypes,
583
+ ingress,
584
+ egress
585
+ }, "konfig peers carry readonly arrays; upstream NetworkPolicySpec is mutable but the runtime shape matches")
586
+ });
587
+ }
588
+ };
589
+ const ClusterRole = { make: (input) => Manifest.make(() => Effect.succeed({
590
+ apiVersion: "rbac.authorization.k8s.io/v1",
591
+ kind: "ClusterRole",
592
+ metadata: {
593
+ name: input.name,
594
+ labels: input.labels,
595
+ annotations: input.annotations
596
+ },
597
+ rules: input.rules,
598
+ aggregationRule: input.aggregationRule
599
+ })) };
600
+ const ClusterRoleBinding = { make: (input) => Manifest.make(() => Effect.succeed({
601
+ apiVersion: "rbac.authorization.k8s.io/v1",
602
+ kind: "ClusterRoleBinding",
603
+ metadata: {
604
+ name: input.name,
605
+ labels: input.labels,
606
+ annotations: input.annotations
607
+ },
608
+ roleRef: input.roleRef,
609
+ subjects: input.subjects
610
+ })) };
611
+ const Role = { make: (input) => Manifest.make(() => Effect.succeed({
612
+ apiVersion: "rbac.authorization.k8s.io/v1",
613
+ kind: "Role",
614
+ metadata: {
615
+ name: input.name,
616
+ namespace: input.namespace,
617
+ labels: input.labels,
618
+ annotations: input.annotations
619
+ },
620
+ rules: input.rules
621
+ })) };
622
+ const RoleBinding = { make: (input) => Manifest.make(() => Effect.succeed({
623
+ apiVersion: "rbac.authorization.k8s.io/v1",
624
+ kind: "RoleBinding",
625
+ metadata: {
626
+ name: input.name,
627
+ namespace: input.namespace,
628
+ labels: input.labels,
629
+ annotations: input.annotations
630
+ },
631
+ roleRef: input.roleRef,
632
+ subjects: input.subjects
633
+ })) };
634
+ //#endregion
635
+ //#region src/volume.ts
636
+ const _volumeName = (name) => brand(name);
637
+ /**
638
+ * `Volume` value namespace.
639
+ *
640
+ * volumes: [
641
+ * Volume.empty({ name: "config" }),
642
+ * Volume.fromSecret({ name: "tls", ref: tlsSecret.ref }),
643
+ * Volume.fromConfigMap({ name: "settings", ref: cfg.ref }),
644
+ * Volume.fromPvc({ name: "data", claim: pvc.ref }),
645
+ * ],
646
+ * volumeMounts: [
647
+ * { name: Volume.mountRef("config"), mountPath: "/etc/conf" },
648
+ * ],
649
+ *
650
+ * All four constructors capture the literal `name` in the returned
651
+ * `Volume<N>` brand; `Pod` infers the union and constrains each
652
+ * container's `volumeMounts[i].name` to it.
653
+ */
654
+ const Volume = {
655
+ empty: (input) => ({
656
+ name: _volumeName(input.name),
657
+ emptyDir: {
658
+ medium: input.medium,
659
+ sizeLimit: input.sizeLimit
660
+ }
661
+ }),
662
+ fromSecret: (input) => ({
663
+ name: _volumeName(input.name),
664
+ secret: {
665
+ secretName: input.ref,
666
+ optional: input.optional,
667
+ defaultMode: input.defaultMode
668
+ }
669
+ }),
670
+ fromConfigMap: (input) => ({
671
+ name: _volumeName(input.name),
672
+ configMap: {
673
+ name: input.ref,
674
+ optional: input.optional,
675
+ defaultMode: input.defaultMode,
676
+ items: input.items
677
+ }
678
+ }),
679
+ fromPvc: (input) => ({
680
+ name: _volumeName(input.name),
681
+ persistentVolumeClaim: {
682
+ claimName: input.claim,
683
+ readOnly: input.readOnly
684
+ }
685
+ }),
686
+ mountRef: (name) => _volumeName(name)
687
+ };
688
+ //#endregion
689
+ //#region src/workload.ts
690
+ const Deployment = {
691
+ make: (input) => {
692
+ const resource = {
693
+ apiVersion: "apps/v1",
694
+ kind: "Deployment",
695
+ metadata: {
696
+ name: input.name,
697
+ namespace: input.namespace,
698
+ labels: input.labels,
699
+ annotations: input.annotations
700
+ },
701
+ spec: {
702
+ replicas: input.replicas,
703
+ selector: input.selector,
704
+ template: unsafeCoerce(input.template, "konfig PodSpecInput is structurally a K8s PodTemplateSpec body; brand-checked fields lower at construction"),
705
+ strategy: unsafeCoerce(input.strategy, "DeploymentStrategy is escape-hatch unknown; lift to K8s type for serialization"),
706
+ revisionHistoryLimit: input.revisionHistoryLimit,
707
+ progressDeadlineSeconds: input.progressDeadlineSeconds,
708
+ minReadySeconds: input.minReadySeconds
709
+ }
710
+ };
711
+ return Manifest.make(() => Effect.succeed(resource));
712
+ },
713
+ fromPodSet: (input) => Deployment.make({
714
+ name: input.name,
715
+ namespace: input.namespace,
716
+ labels: input.labels,
717
+ annotations: input.annotations,
718
+ replicas: input.replicas,
719
+ selector: { matchLabels: input.podSet.labels },
720
+ template: {
721
+ metadata: {
722
+ labels: {
723
+ ...input.template.metadata?.labels,
724
+ ...input.podSet.labels
725
+ },
726
+ annotations: input.template.metadata?.annotations
727
+ },
728
+ spec: input.template.spec
729
+ },
730
+ strategy: input.strategy,
731
+ revisionHistoryLimit: input.revisionHistoryLimit,
732
+ progressDeadlineSeconds: input.progressDeadlineSeconds,
733
+ minReadySeconds: input.minReadySeconds
734
+ })
735
+ };
736
+ const StatefulSet = { make: (input) => {
737
+ const resource = {
738
+ apiVersion: "apps/v1",
739
+ kind: "StatefulSet",
740
+ metadata: {
741
+ name: input.name,
742
+ namespace: input.namespace,
743
+ labels: input.labels,
744
+ annotations: input.annotations
745
+ },
746
+ spec: {
747
+ replicas: input.replicas,
748
+ selector: input.selector,
749
+ template: unsafeCoerce(input.template, "konfig PodSpecInput is structurally a K8s PodTemplateSpec body; brand-checked fields lower at construction"),
750
+ serviceName: input.serviceName,
751
+ volumeClaimTemplates: unsafeCoerce(input.volumeClaimTemplates, "StatefulSet volumeClaimTemplates is escape-hatch unknown; lift to K8s type"),
752
+ podManagementPolicy: input.podManagementPolicy,
753
+ updateStrategy: unsafeCoerce(input.updateStrategy, "StatefulSetUpdateStrategy is escape-hatch unknown")
754
+ }
755
+ };
756
+ return Manifest.make(() => Effect.succeed(resource));
757
+ } };
758
+ const Job = { make: (input) => {
759
+ const resource = {
760
+ apiVersion: "batch/v1",
761
+ kind: "Job",
762
+ metadata: {
763
+ name: input.name,
764
+ namespace: input.namespace,
765
+ labels: input.labels,
766
+ annotations: input.annotations
767
+ },
768
+ spec: {
769
+ parallelism: input.parallelism,
770
+ completions: input.completions,
771
+ backoffLimit: input.backoffLimit,
772
+ activeDeadlineSeconds: input.activeDeadlineSeconds,
773
+ ttlSecondsAfterFinished: input.ttlSecondsAfterFinished,
774
+ suspend: input.suspend,
775
+ template: unsafeCoerce(input.template, "konfig PodSpecInput is structurally a K8s PodTemplateSpec body; brand-checked fields lower at construction")
776
+ }
777
+ };
778
+ return Manifest.make(() => Effect.succeed(resource));
779
+ } };
780
+ const CronJob = { make: (input) => {
781
+ const resource = {
782
+ apiVersion: "batch/v1",
783
+ kind: "CronJob",
784
+ metadata: {
785
+ name: input.name,
786
+ namespace: input.namespace,
787
+ labels: input.labels,
788
+ annotations: input.annotations
789
+ },
790
+ spec: {
791
+ schedule: input.schedule,
792
+ concurrencyPolicy: input.concurrencyPolicy,
793
+ successfulJobsHistoryLimit: input.successfulJobsHistoryLimit,
794
+ failedJobsHistoryLimit: input.failedJobsHistoryLimit,
795
+ startingDeadlineSeconds: input.startingDeadlineSeconds,
796
+ suspend: input.suspend,
797
+ jobTemplate: unsafeCoerce(input.jobTemplate, "konfig CronJob jobTemplate is structurally a K8s JobTemplateSpec body")
798
+ }
799
+ };
800
+ return Manifest.make(() => Effect.succeed(resource));
801
+ } };
802
+ //#endregion
803
+ //#region src/workloadHelpers.ts
804
+ var workloadHelpers_exports = /* @__PURE__ */ __exportAll({
805
+ cron: () => cron,
806
+ web: () => web
807
+ });
808
+ const _reloaderAnnotations = (opt) => {
809
+ if (opt === void 0 || opt === "off") return {};
810
+ if (opt === "stakater") return { "reloader.stakater.com/auto": "true" };
811
+ if (opt === "stakater-strict") return {
812
+ "reloader.stakater.com/auto": "true",
813
+ "reloader.stakater.com/match": "true"
814
+ };
815
+ const out = {};
816
+ if (opt.secrets && opt.secrets.length > 0) out["secret.reloader.stakater.com/reload"] = opt.secrets.join(",");
817
+ if (opt.configMaps && opt.configMaps.length > 0) out["configmap.reloader.stakater.com/reload"] = opt.configMaps.join(",");
818
+ return out;
819
+ };
820
+ const web = (input) => {
821
+ const selectorLabels = { app: input.name };
822
+ const podLabels = {
823
+ ...input.deployment.podLabels,
824
+ ...selectorLabels
825
+ };
826
+ const reloaderAnns = _reloaderAnnotations(input.reloader);
827
+ const deployment = Deployment.make({
828
+ name: input.name,
829
+ namespace: input.namespace,
830
+ labels: {
831
+ ...selectorLabels,
832
+ ...input.labels
833
+ },
834
+ annotations: {
835
+ ...input.annotations,
836
+ ...reloaderAnns
837
+ },
838
+ replicas: input.deployment.replicas,
839
+ selector: { matchLabels: selectorLabels },
840
+ template: {
841
+ metadata: {
842
+ labels: podLabels,
843
+ annotations: input.deployment.podAnnotations
844
+ },
845
+ spec: {
846
+ containers: input.deployment.containers,
847
+ volumes: input.deployment.volumes,
848
+ imagePullSecrets: input.deployment.imagePullSecrets,
849
+ serviceAccountName: input.deployment.serviceAccountName
850
+ }
851
+ }
852
+ });
853
+ const service = Service.make({
854
+ name: input.name,
855
+ namespace: input.namespace,
856
+ labels: {
857
+ ...selectorLabels,
858
+ ...input.labels
859
+ },
860
+ annotations: input.annotations,
861
+ selector: selectorLabels,
862
+ type: input.service.type ?? "ClusterIP",
863
+ ports: unsafeCoerce(input.service.ports, "ServicePortSpec<Ports> structurally matches K8sServicePort; the PortName<Ports> brand on targetPort is a phantom whose runtime value is the underlying string")
864
+ });
865
+ if (input.ingress === void 0) return Manifest.make((ctx) => Effect.all([deployment.render(ctx), service.render(ctx)], { concurrency: "unbounded" }));
866
+ const ingress = Ingress.make({
867
+ name: input.name,
868
+ namespace: input.namespace,
869
+ labels: {
870
+ ...selectorLabels,
871
+ ...input.labels
872
+ },
873
+ annotations: {
874
+ ...input.annotations,
875
+ ...input.ingress.annotations
876
+ },
877
+ ingressClassName: input.ingress.ingressClassName,
878
+ rules: input.ingress.rules,
879
+ tls: input.ingress.tls
880
+ });
881
+ return Manifest.make((ctx) => Effect.all([
882
+ deployment.render(ctx),
883
+ service.render(ctx),
884
+ ingress.render(ctx)
885
+ ], { concurrency: "unbounded" }));
886
+ };
887
+ const cron = (input) => {
888
+ const selectorLabels = { app: input.name };
889
+ const sa = Manifest.make(() => Effect.succeed({
890
+ apiVersion: "v1",
891
+ kind: "ServiceAccount",
892
+ metadata: {
893
+ name: input.name,
894
+ namespace: input.namespace,
895
+ labels: {
896
+ ...selectorLabels,
897
+ ...input.labels
898
+ },
899
+ annotations: input.annotations
900
+ }
901
+ }));
902
+ const cronJob = CronJob.make({
903
+ name: input.name,
904
+ namespace: input.namespace,
905
+ labels: {
906
+ ...selectorLabels,
907
+ ...input.labels
908
+ },
909
+ annotations: input.annotations,
910
+ schedule: input.schedule,
911
+ concurrencyPolicy: input.concurrencyPolicy,
912
+ successfulJobsHistoryLimit: input.successfulJobsHistoryLimit,
913
+ failedJobsHistoryLimit: input.failedJobsHistoryLimit,
914
+ jobTemplate: { spec: { template: {
915
+ metadata: { labels: selectorLabels },
916
+ spec: {
917
+ containers: input.containers,
918
+ volumes: input.volumes,
919
+ imagePullSecrets: input.imagePullSecrets,
920
+ serviceAccountName: input.name,
921
+ restartPolicy: input.restartPolicy ?? "OnFailure"
922
+ }
923
+ } } }
924
+ });
925
+ return Manifest.make((ctx) => Effect.all([sa.render(ctx), cronJob.render(ctx)], { concurrency: "unbounded" }));
926
+ };
927
+ //#endregion
928
+ //#region src/selector.ts
929
+ /**
930
+ * `Selector` value namespace.
931
+ *
932
+ * const apiPods = Selector.make({ app: "api", tier: "web" });
933
+ */
934
+ const Selector = { make: (labels) => unsafeCoerce({ labels }, "SelectorBrand is a unique-symbol phantom — no runtime value; runtime shape is { labels }") };
935
+ //#endregion
936
+ //#region src/podSet.ts
937
+ /**
938
+ * `PodSet` value namespace.
939
+ *
940
+ * const trio = PodSet.define({
941
+ * podSet: redisCachePods,
942
+ * deployment: { ... },
943
+ * service: { ... },
944
+ * netPol: { ... },
945
+ * });
946
+ *
947
+ * Umbrella over `Deployment.fromPodSet`, `Service.fromPodSet`, and
948
+ * `NetworkPolicy.fromPodSet` — emits whichever subset of the trio the
949
+ * input specifies, all rooted at one `Selector`.
950
+ */
951
+ const PodSet = { define: (input) => {
952
+ const deployment = Deployment.fromPodSet({
953
+ podSet: input.podSet,
954
+ ...input.deployment
955
+ });
956
+ const service = input.service !== void 0 ? Service.fromPodSet({
957
+ podSet: input.podSet,
958
+ ...input.service
959
+ }) : void 0;
960
+ const netPol = input.netPol !== void 0 ? NetworkPolicy.fromPodSet({
961
+ podSet: input.podSet,
962
+ ...input.netPol
963
+ }) : void 0;
964
+ return Manifest.make((ctx) => Effect.gen(function* () {
965
+ const d = yield* deployment.render(ctx);
966
+ const s = service !== void 0 ? yield* service.render(ctx) : void 0;
967
+ const n = netPol !== void 0 ? yield* netPol.render(ctx) : void 0;
968
+ const reason = "tuple element types are statically known per branch; the array literal is widened by TS, the brand-free runtime shape matches the typed tuple";
969
+ if (s !== void 0 && n !== void 0) return unsafeCoerce([
970
+ d,
971
+ s,
972
+ n
973
+ ], reason);
974
+ if (s !== void 0) return unsafeCoerce([d, s], reason);
975
+ if (n !== void 0) return unsafeCoerce([d, n], reason);
976
+ return unsafeCoerce([d], reason);
977
+ }));
978
+ } };
979
+ //#endregion
980
+ //#region src/index.ts
981
+ /**
982
+ * `Secret` value namespace — merges the env-contracts side (`define`)
983
+ * with the K8s side (`make`, `bind`, identity helpers). Importing
984
+ * `Secret` from `@konfig.ts/k8s` gives you the full surface:
985
+ *
986
+ * Secret.define({ name, namespace, env }) — env contract (from @konfig.ts/env)
987
+ * Secret.make({ name, namespace, stringData }) — K8s Secret manifest
988
+ * Secret.bind({ secret, backend, source }) — env-to-manifest binder
989
+ */
990
+ const Secret = {
991
+ ...Secret$1,
992
+ ...Secret$2,
993
+ bind: bindSecret
994
+ };
995
+ /**
996
+ * `Environment` value namespace — merges `define` (env-contracts) with
997
+ * `bind` + `runtime` (K8s). The same `Environment` symbol carries the
998
+ * declaration, the manifest binder, and the runtime decoder.
999
+ */
1000
+ const Environment = {
1001
+ ...Environment$1,
1002
+ bind: bindEnvironment,
1003
+ runtime
1004
+ };
1005
+ //#endregion
1006
+ export { BackendSourceMissing, ClusterRole, ClusterRoleBinding, ConfigMap, ConfigMapRef, Container, CronJob, Deployment, EnvVar, Environment, Ingress, Job, k8s_types_exports as K8s, Namespace, NativeSecret, NetworkPolicy, PersistentVolume, PersistentVolumeClaim, Pod, PodSet, Port, PvcRef, Role, RoleBinding, Secret, SecretRef, Selector, Service, ServiceAccount, ServiceAccountRef, StatefulSet, Volume, workloadHelpers_exports as Workload, hashSecretValues };
1007
+
1008
+ //# sourceMappingURL=index.mjs.map