@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/LICENSE +21 -0
- package/README.md +154 -0
- package/dist/.generated/k8s-types/index.d.ts +6 -0
- package/dist/backend.d.ts +26 -0
- package/dist/container.d.ts +190 -0
- package/dist/env.d.ts +110 -0
- package/dist/environmentBind.d.ts +133 -0
- package/dist/identity.d.ts +58 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.mjs +1008 -0
- package/dist/index.mjs.map +1 -0
- package/dist/nativeSecret.d.ts +9 -0
- package/dist/network.d.ts +99 -0
- package/dist/podHash.d.ts +12 -0
- package/dist/podSet.d.ts +36 -0
- package/dist/policy.d.ts +150 -0
- package/dist/ports.d.ts +101 -0
- package/dist/refs.d.ts +26 -0
- package/dist/secretBind.d.ts +36 -0
- package/dist/selector.d.ts +30 -0
- package/dist/volume.d.ts +108 -0
- package/dist/workload.d.ts +117 -0
- package/dist/workloadHelpers.d.ts +97 -0
- package/package.json +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-present David Stahl-Gruber
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# @konfig.ts/k8s
|
|
2
|
+
|
|
3
|
+
Kubernetes resource constructors with branded references on top of
|
|
4
|
+
`@konfig.ts/core`'s `Manifest<A>` carrier. The brands are where the
|
|
5
|
+
package earns its keep: cross-resource invariants (env-var → Secret,
|
|
6
|
+
volume → ConfigMap, Ingress TLS → Secret) are checked at the type level,
|
|
7
|
+
and the `Dep.*` tracking propagates needs/provides through Effect
|
|
8
|
+
`Layer`s.
|
|
9
|
+
|
|
10
|
+
## Branded references
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { ConfigMapRef, SecretRef, ServiceAccountRef } from "@konfig.ts/k8s"
|
|
14
|
+
|
|
15
|
+
const apiCreds = SecretRef.of("api-creds") // SecretRef<"api-creds">
|
|
16
|
+
const cfg = ConfigMapRef.of("oauth-templates") // ConfigMapRef<"oauth-templates">
|
|
17
|
+
const sa = ServiceAccountRef.of("api") // ServiceAccountRef<"api">
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Each ref carries the resource _name_ in its type parameter. The
|
|
21
|
+
enforcement points (env var `secretKeyRef` / `configMapKeyRef`, volume
|
|
22
|
+
secret, volume configMap, `imagePullSecret`, Ingress TLS) all accept the
|
|
23
|
+
branded type and reject raw strings.
|
|
24
|
+
|
|
25
|
+
## Identity constructors expose `.ref`
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
const apiSecret = Secret.make({ name: "api-creds", namespace: "prod", stringData: {...} });
|
|
29
|
+
// apiSecret.ref is a SecretRef<"api-creds">
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The `.ref` accessor means consumers wire the brand through without
|
|
33
|
+
restating the name:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
const env = secretEnv("DATABASE_URL", { ref: apiSecret.ref, key: "url" })
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Env-var helpers
|
|
40
|
+
|
|
41
|
+
| Helper | Rejects raw string for |
|
|
42
|
+
| ------------------------------------------- | ---------------------- |
|
|
43
|
+
| `valueEnv(name, value)` | n/a |
|
|
44
|
+
| `secretEnv(name, {ref, key, optional?})` | `ref` |
|
|
45
|
+
| `configMapEnv(name, {ref, key, optional?})` | `ref` |
|
|
46
|
+
| `rawEnv({name, value?, valueFrom?})` | n/a (escape hatch) |
|
|
47
|
+
|
|
48
|
+
## Volume helpers
|
|
49
|
+
|
|
50
|
+
| Helper |
|
|
51
|
+
| ----------------------------------- |
|
|
52
|
+
| `volumeFromSecret(name, ref)` |
|
|
53
|
+
| `volumeFromConfigMap(name, ref)` |
|
|
54
|
+
| `emptyDirVolume(name, opts?)` |
|
|
55
|
+
| `pvcVolume(name, claimName, opts?)` |
|
|
56
|
+
|
|
57
|
+
## Constructors
|
|
58
|
+
|
|
59
|
+
Workload-tier (`Deployment.make`, `StatefulSet.make`, `Job.make`,
|
|
60
|
+
`CronJob.make`) accept typed containers, `Volume`s,
|
|
61
|
+
`imagePullSecrets: { name: SecretRef<N> }[]`, and
|
|
62
|
+
`serviceAccountName: ServiceAccountRef | string`.
|
|
63
|
+
|
|
64
|
+
Network-tier (`Service.make`, `Ingress.make`). Ingress TLS uses the
|
|
65
|
+
`ingressTLS(secretName, hosts?)` helper for branded refs.
|
|
66
|
+
|
|
67
|
+
Identity-tier (`Namespace.make`, `ServiceAccount.make`, `ConfigMap.make`,
|
|
68
|
+
`Secret.make`) return constructors whose returned record carries the
|
|
69
|
+
typed `.ref`.
|
|
70
|
+
|
|
71
|
+
Policy-tier (`PersistentVolume.make`, `PersistentVolumeClaim.make`,
|
|
72
|
+
`NetworkPolicy.make`, `ClusterRole.make`, `ClusterRoleBinding.make`,
|
|
73
|
+
`Role.make`, `RoleBinding.make`) — simple constructors with no extra
|
|
74
|
+
dependencies.
|
|
75
|
+
|
|
76
|
+
## Higher-level: `Workload.web` and `Workload.cron`
|
|
77
|
+
|
|
78
|
+
Built for the workload shapes consumers need today — not speculative.
|
|
79
|
+
|
|
80
|
+
`Workload.web(...)` composes Deployment + Service + (optional) Ingress
|
|
81
|
+
with derived labels (`app: <name>`). `Workload.cron(...)` composes
|
|
82
|
+
CronJob + ServiceAccount (the SA is private to the cron).
|
|
83
|
+
|
|
84
|
+
## Secret rotation — build-time hash vs runtime Reloader
|
|
85
|
+
|
|
86
|
+
konfig provides two complementary stories for restarting pods when a
|
|
87
|
+
Secret or ConfigMap they consume changes:
|
|
88
|
+
|
|
89
|
+
- **Build time** (`hashSecretValues` / `pod-hash` annotation). A SHA of
|
|
90
|
+
the secret material lives on the pod spec; re-rendering after a
|
|
91
|
+
rotation produces a new hash, so the Deployment's pod template
|
|
92
|
+
changes and Kubernetes rolls. Fast feedback, deterministic — but only
|
|
93
|
+
fires when konfig re-renders.
|
|
94
|
+
|
|
95
|
+
- **Runtime** (`Workload.web({ reloader: "stakater" })`). Emits
|
|
96
|
+
`reloader.stakater.com/auto: "true"` on the Deployment so
|
|
97
|
+
[Stakater Reloader](https://github.com/stakater/Reloader) watches
|
|
98
|
+
every referenced Secret/ConfigMap and patches the workload when the
|
|
99
|
+
in-cluster object changes. Pair with `ExternalSecrets` or a
|
|
100
|
+
controller that updates Secrets out-of-band.
|
|
101
|
+
|
|
102
|
+
Pick build-time hashes for stateless render → apply pipelines; pick
|
|
103
|
+
Reloader when secrets rotate independently of CI. They compose — a
|
|
104
|
+
config-managed Secret with `reloader: "stakater"` rolls on both
|
|
105
|
+
re-render and on out-of-band updates.
|
|
106
|
+
|
|
107
|
+
## Secret backends — `SecretBackend<N, K>`
|
|
108
|
+
|
|
109
|
+
`SecretBackend` is the contract Sops / SealedSecrets / ExternalSecrets
|
|
110
|
+
implement. `Secret.bind({ secret, backend, source? })` ties an env-bundle
|
|
111
|
+
secret to a backend emission. `backend.requiresSource` is `true` for
|
|
112
|
+
Sops and SealedSecrets (they need plaintext at render time) and `false`
|
|
113
|
+
for ExternalSecrets (it just emits an `ExternalSecret` CR pointing at a
|
|
114
|
+
remote store).
|
|
115
|
+
|
|
116
|
+
## k8s 1.30 types
|
|
117
|
+
|
|
118
|
+
Re-exported from `kubernetes-types@1.30.0` under
|
|
119
|
+
`src/.generated/k8s-types/`. We pin to the cluster minor and ship the
|
|
120
|
+
re-export rather than rolling our own OpenAPI codegen.
|
|
121
|
+
|
|
122
|
+
## Layout
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/
|
|
126
|
+
├── index.ts barrel
|
|
127
|
+
├── .generated/k8s-types/ thin re-export from kubernetes-types
|
|
128
|
+
├── refs.ts SecretRef, ConfigMapRef, ServiceAccountRef
|
|
129
|
+
├── env.ts secretEnv, configMapEnv, valueEnv, rawEnv
|
|
130
|
+
├── volume.ts volumeFromSecret, volumeFromConfigMap, ...
|
|
131
|
+
├── container.ts ContainerInput, PodSpecInput, imagePullSecret
|
|
132
|
+
├── identity.ts Namespace, ServiceAccount, ConfigMap, Secret
|
|
133
|
+
├── workload.ts Deployment, StatefulSet, Job, CronJob
|
|
134
|
+
├── network.ts Service, Ingress, ingressTLS
|
|
135
|
+
├── policy.ts PV, PVC, NetworkPolicy, RBAC
|
|
136
|
+
├── workloadHelpers.ts Workload.web, Workload.cron
|
|
137
|
+
└── backend.ts SecretBackend contract + Secret.bind
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Requirements
|
|
141
|
+
|
|
142
|
+
konfig.ts builds on [Effect](https://effect.website/), which is still in
|
|
143
|
+
beta. Until Effect ships a stable 4.x, you must install the exact beta
|
|
144
|
+
konfig is developed against:
|
|
145
|
+
|
|
146
|
+
- **`effect@4.0.0-beta.70`** — required.
|
|
147
|
+
- **`@effect/platform-node@4.0.0-beta.70`** — required only for `render()`
|
|
148
|
+
(the Node filesystem/subprocess entrypoint); manifest-only consumers can
|
|
149
|
+
omit it.
|
|
150
|
+
|
|
151
|
+
The peer dependency is pinned to the exact version on purpose: Effect's beta
|
|
152
|
+
line makes breaking changes between builds, so a looser range would surface
|
|
153
|
+
as `ERESOLVE` install conflicts rather than a working install. This pin will
|
|
154
|
+
relax to a caret range once Effect reaches a stable 4.x.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { Deployment, DeploymentSpec, StatefulSet, StatefulSetSpec, } from "kubernetes-types/apps/v1";
|
|
2
|
+
export type { CronJob, Job, JobSpec, } from "kubernetes-types/batch/v1";
|
|
3
|
+
export type { ConfigMap, Container, ContainerPort, EnvVar, EnvVarSource, LocalObjectReference, Namespace, PersistentVolume, PersistentVolumeClaim, PodSpec, PodTemplateSpec, Secret, Service, ServiceAccount, ServicePort, Volume, VolumeMount, } from "kubernetes-types/core/v1";
|
|
4
|
+
export type { ObjectMeta } from "kubernetes-types/meta/v1";
|
|
5
|
+
export type { Ingress, IngressRule, IngressSpec, IngressTLS, NetworkPolicy, } from "kubernetes-types/networking/v1";
|
|
6
|
+
export type { ClusterRole, ClusterRoleBinding, Role, RoleBinding, } from "kubernetes-types/rbac/v1";
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { Manifest } from "@konfig.ts/core";
|
|
2
|
+
import type { SecretSource } from "@konfig.ts/env";
|
|
3
|
+
export type BackendTag = "Sops" | "Sops.passthrough" | "SealedSecrets" | "ExternalSecrets" | "NativeSecret";
|
|
4
|
+
export interface BackendEmitInput<N extends string, K extends string> {
|
|
5
|
+
readonly name: N;
|
|
6
|
+
readonly namespace: string;
|
|
7
|
+
readonly keys: ReadonlyArray<K>;
|
|
8
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
9
|
+
readonly annotations?: Readonly<Record<string, string>>;
|
|
10
|
+
readonly source?: SecretSource<K, Manifest.RenderServices>;
|
|
11
|
+
}
|
|
12
|
+
export interface SecretBackend<N extends string, K extends string, RequiresSource extends boolean = boolean> {
|
|
13
|
+
readonly _tag: BackendTag;
|
|
14
|
+
readonly requiresSource: RequiresSource;
|
|
15
|
+
readonly emit: (input: BackendEmitInput<N, K>) => Manifest.Manifest<unknown>;
|
|
16
|
+
}
|
|
17
|
+
declare const BackendSourceMissing_base: new <A extends Record<string, any> = {}>(args: import("effect/Types").VoidIfEmpty<{ readonly [P in keyof A as P extends "_tag" ? never : P]: A[P]; }>) => import("effect/Cause").YieldableError & {
|
|
18
|
+
readonly _tag: "BackendSourceMissing";
|
|
19
|
+
} & Readonly<A>;
|
|
20
|
+
export declare class BackendSourceMissing extends BackendSourceMissing_base<{
|
|
21
|
+
readonly backend: BackendTag;
|
|
22
|
+
readonly secret: string;
|
|
23
|
+
}> {
|
|
24
|
+
get message(): string;
|
|
25
|
+
}
|
|
26
|
+
export {};
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import type { BuiltImageRef, SecretRef, ServiceAccountRef } from "@konfig.ts/core";
|
|
2
|
+
import type { Container as K8sContainer, PodSpec as K8sPodSpec } from "./.generated/k8s-types";
|
|
3
|
+
import type { EnvVar } from "./env";
|
|
4
|
+
import type { ContainerPort, NamesOf, ProbeTarget } from "./ports";
|
|
5
|
+
import type { Volume, VolumeMount, VolumeNamesOf } from "./volume";
|
|
6
|
+
/**
|
|
7
|
+
* Container input — extends the full K8s Container API. Konfig's
|
|
8
|
+
* branded helpers (`EnvVar.value`, `EnvVar.fromSecret`,
|
|
9
|
+
* `EnvVar.fromConfigMap`) produce `EnvVar` instances whose runtime
|
|
10
|
+
* shape matches `K8sEnvVar`; the override
|
|
11
|
+
* here narrows the field to readonly + the branded helper output so
|
|
12
|
+
* `secretKeyRef.name` carries its `SecretRef<N>` brand at construction
|
|
13
|
+
* time.
|
|
14
|
+
*
|
|
15
|
+
* `image` is overridden as required (K8s lets it be optional for
|
|
16
|
+
* higher-level controllers that default it; konfig wants every
|
|
17
|
+
* container to have an explicit image).
|
|
18
|
+
*
|
|
19
|
+
* `ports[i].name` accepts the branded `PortName<string>` from `Port.make(...)`
|
|
20
|
+
* as well as a raw string; the typed builder `Container` is the
|
|
21
|
+
* link that captures the literal name union for cross-reference checks.
|
|
22
|
+
*/
|
|
23
|
+
export interface ContainerInput extends Omit<K8sContainer, "env" | "image" | "ports" | "readinessProbe" | "livenessProbe" | "startupProbe" | "volumeMounts"> {
|
|
24
|
+
/**
|
|
25
|
+
* Container image. Accepts a raw string (escape hatch for vendor
|
|
26
|
+
* images: `ghcr.io/bitnami/postgresql:16.0.0`) or a `BuiltImageRef<App>`
|
|
27
|
+
* produced by `Dep.builtImageRef` / `Dep.provideImage`. The branded
|
|
28
|
+
* path ties the workload into the dep graph — a workload referencing
|
|
29
|
+
* an image whose build module isn't in the composition fails at
|
|
30
|
+
* `AppOfApps.entrypoint`.
|
|
31
|
+
*/
|
|
32
|
+
readonly image: string | BuiltImageRef<string>;
|
|
33
|
+
readonly env?: ReadonlyArray<EnvVar>;
|
|
34
|
+
readonly ports?: ReadonlyArray<ContainerPort | {
|
|
35
|
+
readonly containerPort: number;
|
|
36
|
+
readonly name?: string;
|
|
37
|
+
readonly protocol?: "TCP" | "UDP" | "SCTP";
|
|
38
|
+
}>;
|
|
39
|
+
/**
|
|
40
|
+
* Probes accept either the upstream K8s shape or konfig's branded
|
|
41
|
+
* `ProbeTarget<string>` (from `Container`). The two are
|
|
42
|
+
* structurally close — the union lets a `ContainerSpec` flow
|
|
43
|
+
* through `Workload.web`'s containers slot without a cast.
|
|
44
|
+
*/
|
|
45
|
+
readonly readinessProbe?: K8sContainer["readinessProbe"] | ProbeTarget<string>;
|
|
46
|
+
readonly livenessProbe?: K8sContainer["livenessProbe"] | ProbeTarget<string>;
|
|
47
|
+
readonly startupProbe?: K8sContainer["startupProbe"] | ProbeTarget<string>;
|
|
48
|
+
/**
|
|
49
|
+
* Volume mounts accept the upstream K8s shape or konfig's branded
|
|
50
|
+
* `VolumeMount<string>` (from `Container` / `mountRef`). Same
|
|
51
|
+
* rationale as the probe widening above.
|
|
52
|
+
*/
|
|
53
|
+
readonly volumeMounts?: K8sContainer["volumeMounts"] | ReadonlyArray<VolumeMount<string>>;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Strongly-typed container builder. Captures the union of named ports
|
|
57
|
+
* declared via `port({ name, containerPort })` as a phantom type
|
|
58
|
+
* parameter, then constrains every probe's `port` field to that union.
|
|
59
|
+
*
|
|
60
|
+
* Pair with `Service.fromContainer({ forContainer })` in `network.ts`
|
|
61
|
+
* to link a Service's `targetPort` to the same union — a typo or
|
|
62
|
+
* undeclared port name is a compile error.
|
|
63
|
+
*/
|
|
64
|
+
export interface ContainerSpec<Ports extends string = string, Mounts extends string = string> extends Omit<ContainerInput, "ports" | "readinessProbe" | "livenessProbe" | "startupProbe" | "volumeMounts"> {
|
|
65
|
+
readonly name: string;
|
|
66
|
+
readonly image: string;
|
|
67
|
+
readonly ports: ReadonlyArray<ContainerPort<Ports>>;
|
|
68
|
+
readonly readinessProbe?: ProbeTarget<Ports>;
|
|
69
|
+
readonly livenessProbe?: ProbeTarget<Ports>;
|
|
70
|
+
readonly startupProbe?: ProbeTarget<Ports>;
|
|
71
|
+
readonly volumeMounts?: ReadonlyArray<VolumeMount<Mounts>>;
|
|
72
|
+
readonly __portNames?: Ports;
|
|
73
|
+
readonly __mountNames?: Mounts;
|
|
74
|
+
}
|
|
75
|
+
export interface DefineContainerInput<Ports extends ReadonlyArray<ContainerPort<string>>, Mounts extends ReadonlyArray<VolumeMount<string>>, Envs extends ReadonlyArray<EnvVar<string>>> extends Omit<ContainerInput, "ports" | "readinessProbe" | "livenessProbe" | "startupProbe" | "volumeMounts" | "env"> {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly image: string;
|
|
78
|
+
readonly ports: Ports;
|
|
79
|
+
readonly readinessProbe?: ProbeTarget<NamesOf<Ports>>;
|
|
80
|
+
readonly livenessProbe?: ProbeTarget<NamesOf<Ports>>;
|
|
81
|
+
readonly startupProbe?: ProbeTarget<NamesOf<Ports>>;
|
|
82
|
+
readonly volumeMounts?: Mounts;
|
|
83
|
+
readonly env?: Envs & EnvDupCheck<Envs>;
|
|
84
|
+
}
|
|
85
|
+
type MountNamesOf<M extends ReadonlyArray<VolumeMount<string>>> = {
|
|
86
|
+
readonly [I in keyof M]: M[I] extends VolumeMount<infer N> ? N : never;
|
|
87
|
+
}[number];
|
|
88
|
+
type _EnvNameOf<X> = X extends EnvVar<infer N> ? N : never;
|
|
89
|
+
/**
|
|
90
|
+
* Union of any literal env-var names that appear more than once in `Envs`.
|
|
91
|
+
* Computes the intersection of pairwise names — if `Envs[I]` and `Envs[J]`
|
|
92
|
+
* share a literal name (i ≠ j), that name leaks into the union. Empty
|
|
93
|
+
* when every name is unique.
|
|
94
|
+
*/
|
|
95
|
+
type DuplicateEnvNames<Envs extends ReadonlyArray<EnvVar<string>>> = {
|
|
96
|
+
[I in keyof Envs]: {
|
|
97
|
+
[J in keyof Envs]: J extends I ? never : _EnvNameOf<Envs[I]> & _EnvNameOf<Envs[J]> extends never ? never : _EnvNameOf<Envs[I]> & _EnvNameOf<Envs[J]>;
|
|
98
|
+
}[number];
|
|
99
|
+
}[number];
|
|
100
|
+
/**
|
|
101
|
+
* Empty (`unknown`) when `Envs` has no duplicate names; otherwise an
|
|
102
|
+
* inline object type whose required `_konfig_duplicate_env_names`
|
|
103
|
+
* property carries a template-literal hint sentence. Intersected with
|
|
104
|
+
* the `env` slot, an unmet property surfaces the sentence inline in the
|
|
105
|
+
* TS error — far more discoverable than a runtime "last-wins" surprise.
|
|
106
|
+
*/
|
|
107
|
+
type EnvDupCheck<Envs extends ReadonlyArray<EnvVar<string>>> = [
|
|
108
|
+
DuplicateEnvNames<Envs>
|
|
109
|
+
] extends [never] ? unknown : {
|
|
110
|
+
readonly _konfig_duplicate_env_names: `Duplicate env name(s): "${DuplicateEnvNames<Envs>}". K8s silently last-wins; rename one of the colliding entries or remove the manual valueEnv that shadows another producer.`;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* `Container` value namespace.
|
|
114
|
+
*
|
|
115
|
+
* const apiContainer = Container.define({
|
|
116
|
+
* name: "api",
|
|
117
|
+
* image: apiImage,
|
|
118
|
+
* ports: [Port.make({ name: "http", containerPort: 8080 })],
|
|
119
|
+
* readinessProbe: { httpGet: { port: Port.ref("http") } },
|
|
120
|
+
* env: [...],
|
|
121
|
+
* });
|
|
122
|
+
*
|
|
123
|
+
* `Container.define` captures the union of named ports (from
|
|
124
|
+
* `Port.make`) as `Ports`, the union of mounted volume names (from
|
|
125
|
+
* `Volume.mountRef`) as `Mounts`, and validates that the `env` list
|
|
126
|
+
* has no duplicate env-var names. The first two phantoms travel on
|
|
127
|
+
* `ContainerSpec`; `Pod.define` checks the container's Mounts against
|
|
128
|
+
* the pod's declared volume names.
|
|
129
|
+
*
|
|
130
|
+
* Duplicate env names are caught at the call site via a template-literal
|
|
131
|
+
* error message — see `EnvDupCheck`. With no volumeMounts and no env,
|
|
132
|
+
* all phantoms collapse to `never` so the container slots into any pod.
|
|
133
|
+
*/
|
|
134
|
+
export declare const Container: {
|
|
135
|
+
define: <const Ports extends ReadonlyArray<ContainerPort<string>>, const Mounts extends ReadonlyArray<VolumeMount<string>> = readonly [], const Envs extends ReadonlyArray<EnvVar<string>> = readonly []>(input: DefineContainerInput<Ports, Mounts, Envs>) => ContainerSpec<NamesOf<Ports>, MountNamesOf<Mounts>>;
|
|
136
|
+
};
|
|
137
|
+
/**
|
|
138
|
+
* `Pod.define` input. The pod declares a tuple of `Volume`s and lists
|
|
139
|
+
* containers whose `Mounts` phantom is checked against the declared
|
|
140
|
+
* volume names via `NoInfer`. A container referencing an undeclared
|
|
141
|
+
* volume — or a typo — fails at the call site rather than at pod-startup
|
|
142
|
+
* time with "container references volume not found."
|
|
143
|
+
*/
|
|
144
|
+
export interface DefinePodInput<V extends ReadonlyArray<Volume<string>>> {
|
|
145
|
+
readonly volumes: V;
|
|
146
|
+
readonly containers: ReadonlyArray<ContainerSpec<string, NoInfer<VolumeNamesOf<V>>>>;
|
|
147
|
+
readonly initContainers?: ReadonlyArray<ContainerSpec<string, NoInfer<VolumeNamesOf<V>>>>;
|
|
148
|
+
}
|
|
149
|
+
export interface DefinedPod<MountNames extends string> {
|
|
150
|
+
readonly volumes: ReadonlyArray<Volume<MountNames>>;
|
|
151
|
+
readonly containers: ReadonlyArray<ContainerSpec<string, MountNames>>;
|
|
152
|
+
readonly initContainers?: ReadonlyArray<ContainerSpec<string, MountNames>>;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Pod spec input — extends K8s PodSpec but tightens the fields where
|
|
156
|
+
* konfig adds brand checking: `imagePullSecrets`, `serviceAccountName`,
|
|
157
|
+
* and `volumes` (the helpers in `volume.ts` produce konfig `Volume`
|
|
158
|
+
* objects that lower to K8s `Volume`).
|
|
159
|
+
*/
|
|
160
|
+
export interface PodSpecInput extends Omit<K8sPodSpec, "containers" | "initContainers" | "volumes" | "imagePullSecrets" | "serviceAccountName"> {
|
|
161
|
+
readonly containers: ReadonlyArray<ContainerInput>;
|
|
162
|
+
readonly initContainers?: ReadonlyArray<ContainerInput>;
|
|
163
|
+
readonly volumes?: ReadonlyArray<Volume>;
|
|
164
|
+
readonly imagePullSecrets?: ReadonlyArray<{
|
|
165
|
+
readonly name: SecretRef<string>;
|
|
166
|
+
}>;
|
|
167
|
+
readonly serviceAccountName?: ServiceAccountRef<string> | string;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* `Pod` value namespace.
|
|
171
|
+
*
|
|
172
|
+
* const pod = Pod.define({
|
|
173
|
+
* volumes: [Volume.empty({ name: "config" })],
|
|
174
|
+
* containers: [Container.define({ ..., volumeMounts: [...] })],
|
|
175
|
+
* });
|
|
176
|
+
*
|
|
177
|
+
* imagePullSecrets: [Pod.imagePullSecret(ghcrRef)],
|
|
178
|
+
*
|
|
179
|
+
* - `Pod.define(input)` ties container `volumeMounts[i].name` to the
|
|
180
|
+
* pod's declared volume names via `NoInfer`.
|
|
181
|
+
* - `Pod.imagePullSecret(ref)` returns the `{ name: SecretRef }` entry
|
|
182
|
+
* consumed by Deployment / StatefulSet / Job / CronJob workloads.
|
|
183
|
+
*/
|
|
184
|
+
export declare const Pod: {
|
|
185
|
+
define: <const V extends ReadonlyArray<Volume<string>>>(input: DefinePodInput<V>) => DefinedPod<VolumeNamesOf<V>>;
|
|
186
|
+
imagePullSecret: (ref: SecretRef<string>) => {
|
|
187
|
+
readonly name: SecretRef<string>;
|
|
188
|
+
};
|
|
189
|
+
};
|
|
190
|
+
export {};
|
package/dist/env.d.ts
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { ConfigMapRef, SecretRef } from "@konfig.ts/core";
|
|
2
|
+
export interface EnvVarSource {
|
|
3
|
+
readonly secretKeyRef?: {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
readonly key: string;
|
|
6
|
+
readonly optional?: boolean;
|
|
7
|
+
};
|
|
8
|
+
readonly configMapKeyRef?: {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
readonly key: string;
|
|
11
|
+
readonly optional?: boolean;
|
|
12
|
+
};
|
|
13
|
+
readonly fieldRef?: {
|
|
14
|
+
readonly fieldPath: string;
|
|
15
|
+
readonly apiVersion?: string;
|
|
16
|
+
};
|
|
17
|
+
readonly resourceFieldRef?: {
|
|
18
|
+
readonly containerName?: string;
|
|
19
|
+
readonly resource: string;
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Container env-var entry. The phantom `N` records the literal env-var
|
|
24
|
+
* name at the type level so `Container` can detect duplicates
|
|
25
|
+
* across the entry list. The default `N = string` preserves loose-typed
|
|
26
|
+
* call sites.
|
|
27
|
+
*/
|
|
28
|
+
export interface EnvVarShape<N extends string = string> {
|
|
29
|
+
readonly name: N;
|
|
30
|
+
readonly value?: string;
|
|
31
|
+
readonly valueFrom?: EnvVarSource;
|
|
32
|
+
}
|
|
33
|
+
export interface ValueEnvInput<N extends string> {
|
|
34
|
+
readonly name: N;
|
|
35
|
+
readonly value: string;
|
|
36
|
+
}
|
|
37
|
+
export interface SecretEnvInput<EnvName extends string, N extends string, K extends string> {
|
|
38
|
+
readonly name: EnvName;
|
|
39
|
+
readonly ref: SecretRef<N, K>;
|
|
40
|
+
/**
|
|
41
|
+
* Constrained to the keys carried by `ref`. `NoInfer` locks `K` to
|
|
42
|
+
* whatever the ref declares, so the typo `key: "passowrd"` fails at
|
|
43
|
+
* compile time when the ref's K is `"url" | "username" | "password"`.
|
|
44
|
+
* Refs constructed with the default `K = string` accept any string.
|
|
45
|
+
*/
|
|
46
|
+
readonly key: NoInfer<K>;
|
|
47
|
+
readonly optional?: boolean;
|
|
48
|
+
}
|
|
49
|
+
export interface SecretEnvForPodInput<EnvName extends string, N extends string, K extends string, Ns extends string> {
|
|
50
|
+
readonly name: EnvName;
|
|
51
|
+
/**
|
|
52
|
+
* Ref whose namespace slot must match `podNamespace`. Pass refs that
|
|
53
|
+
* came from `Secret.make({ namespace })` in the same namespace as
|
|
54
|
+
* the consuming pod. For legitimate cross-namespace cases, widen
|
|
55
|
+
* via `SecretRef.unsafeReNamespace(ref)` first.
|
|
56
|
+
*/
|
|
57
|
+
readonly ref: SecretRef<N, K, NoInfer<Ns>>;
|
|
58
|
+
readonly key: NoInfer<K>;
|
|
59
|
+
readonly podNamespace: Ns;
|
|
60
|
+
readonly optional?: boolean;
|
|
61
|
+
}
|
|
62
|
+
export interface ConfigMapEnvInput<EnvName extends string, N extends string, K extends string> {
|
|
63
|
+
readonly name: EnvName;
|
|
64
|
+
readonly ref: ConfigMapRef<N, K>;
|
|
65
|
+
/**
|
|
66
|
+
* Constrained to the keys carried by `ref`. `NoInfer` locks `K` to
|
|
67
|
+
* whatever the ref declares — typos like `key: "passwrod"` fail at
|
|
68
|
+
* compile time when the ref's K is `"HOST" | "PORT" | "LOG_LEVEL"`.
|
|
69
|
+
* Refs constructed with the default `K = string` accept any string.
|
|
70
|
+
*/
|
|
71
|
+
readonly key: NoInfer<K>;
|
|
72
|
+
readonly optional?: boolean;
|
|
73
|
+
}
|
|
74
|
+
export interface RawEnvInput<N extends string> {
|
|
75
|
+
readonly name: N;
|
|
76
|
+
readonly value?: string;
|
|
77
|
+
readonly valueFrom?: EnvVarSource;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `EnvVar` value namespace.
|
|
81
|
+
*
|
|
82
|
+
* env: [
|
|
83
|
+
* EnvVar.value({ name: "PORT", value: "8080" }),
|
|
84
|
+
* EnvVar.fromSecret({ name: "DB_URL", ref: dbCreds.ref, key: "url" }),
|
|
85
|
+
* EnvVar.fromSecretForPod({
|
|
86
|
+
* name: "DB_URL_PRIMARY", ref: dbCreds.ref, key: "url",
|
|
87
|
+
* podNamespace: "app",
|
|
88
|
+
* }),
|
|
89
|
+
* EnvVar.fromConfigMap({ name: "NEW_UI", ref: flags.ref, key: "NEW_UI" }),
|
|
90
|
+
* EnvVar.raw({ name: "POD_NAME", valueFrom: { fieldRef: ... } }),
|
|
91
|
+
* ]
|
|
92
|
+
*
|
|
93
|
+
* Each constructor captures the literal env-var name (`N`) so
|
|
94
|
+
* `Container` can detect duplicate entries across the env list.
|
|
95
|
+
*/
|
|
96
|
+
export declare const EnvVar: {
|
|
97
|
+
value: <const N extends string>(input: ValueEnvInput<N>) => EnvVar<N>;
|
|
98
|
+
fromSecret: <const EnvName extends string, N extends string, K extends string = string>(input: SecretEnvInput<EnvName, N, K>) => EnvVar<EnvName>;
|
|
99
|
+
/**
|
|
100
|
+
* Namespace-checked variant. The ref's namespace slot (`Ns`) must
|
|
101
|
+
* match `podNamespace`. Catches "pod in namespace A references a
|
|
102
|
+
* Secret in namespace B" at compile time — kube-apiserver only
|
|
103
|
+
* resolves `valueFrom.secretKeyRef` against the pod's own namespace,
|
|
104
|
+
* so cross-namespace refs are runtime errors.
|
|
105
|
+
*/
|
|
106
|
+
fromSecretForPod: <const EnvName extends string, N extends string, K extends string, const Ns extends string>(input: SecretEnvForPodInput<EnvName, N, K, Ns>) => EnvVar<EnvName>;
|
|
107
|
+
fromConfigMap: <const EnvName extends string, N extends string, K extends string = string>(input: ConfigMapEnvInput<EnvName, N, K>) => EnvVar<EnvName>;
|
|
108
|
+
raw: <const N extends string>(input: RawEnvInput<N>) => EnvVar<N>;
|
|
109
|
+
};
|
|
110
|
+
export type EnvVar<N extends string = string> = EnvVarShape<N>;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { type Manifest, type RenderError } from "@konfig.ts/core";
|
|
2
|
+
import type { DownwardEntry, EnvMember, Environment, LiteralEntry, SecretEntry, SecretSource } from "@konfig.ts/env";
|
|
3
|
+
import { Layer } from "effect";
|
|
4
|
+
import type { SecretBackend } from "./backend";
|
|
5
|
+
import type { EnvVar } from "./env";
|
|
6
|
+
import { type DeclaredSecret } from "./secretBind";
|
|
7
|
+
export interface DeclaredLiteral<EnvName extends string, T> {
|
|
8
|
+
readonly envName: EnvName;
|
|
9
|
+
readonly value: T;
|
|
10
|
+
readonly envVar: EnvVar;
|
|
11
|
+
}
|
|
12
|
+
export interface DeclaredDownward<EnvName extends string> {
|
|
13
|
+
readonly envName: EnvName;
|
|
14
|
+
readonly fieldPath: string;
|
|
15
|
+
readonly envVar: EnvVar;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Per-member declared shape produced by `Environment.bind`. Mirrors the
|
|
19
|
+
* structure of the bundle: secrets give `DeclaredSecret`, literals
|
|
20
|
+
* `DeclaredLiteral`, downwards `DeclaredDownward`, and nested
|
|
21
|
+
* `Environment` members give a `members` sub-record with the same
|
|
22
|
+
* recursive shape.
|
|
23
|
+
*/
|
|
24
|
+
export type DeclaredMember<A extends EnvMember, Ns extends string = string> = A extends SecretEntry<infer N, infer K, infer _E> ? [N, K] extends [string, string] ? DeclaredSecret<N, K, Ns> : never : A extends LiteralEntry<infer EnvName, infer T> ? [EnvName] extends [string] ? DeclaredLiteral<EnvName, T> : never : A extends DownwardEntry<infer EnvName> ? [EnvName] extends [string] ? DeclaredDownward<EnvName> : never : A extends Environment<infer SubM> ? {
|
|
25
|
+
readonly [K in keyof SubM]: DeclaredMember<SubM[K], Ns>;
|
|
26
|
+
} : never;
|
|
27
|
+
export interface DeclaredEnvironment<M extends Readonly<Record<string, EnvMember>>, Ns extends string = string> {
|
|
28
|
+
readonly envVars: ReadonlyArray<EnvVar>;
|
|
29
|
+
readonly manifests: ReadonlyArray<Manifest.Manifest<unknown>>;
|
|
30
|
+
readonly members: {
|
|
31
|
+
readonly [K in keyof M]: DeclaredMember<M[K], Ns>;
|
|
32
|
+
};
|
|
33
|
+
readonly valuesLayer: Layer.Layer<unknown, RenderError, Manifest.RenderServices>;
|
|
34
|
+
}
|
|
35
|
+
interface _SecretMemberOptionsBase {
|
|
36
|
+
readonly labels?: Readonly<Record<string, string>>;
|
|
37
|
+
readonly annotations?: Readonly<Record<string, string>>;
|
|
38
|
+
}
|
|
39
|
+
interface _SecretMemberBackendRequiresSource<N extends string, K extends string> extends _SecretMemberOptionsBase {
|
|
40
|
+
readonly backend: SecretBackend<N, K, true>;
|
|
41
|
+
readonly source: SecretSource<K, Manifest.RenderServices>;
|
|
42
|
+
}
|
|
43
|
+
interface _SecretMemberBackendOptionalSource<N extends string, K extends string> extends _SecretMemberOptionsBase {
|
|
44
|
+
readonly backend: SecretBackend<N, K, false>;
|
|
45
|
+
readonly source?: SecretSource<K, Manifest.RenderServices>;
|
|
46
|
+
}
|
|
47
|
+
interface _SecretMemberSourceOnly<_N extends string, K extends string> extends _SecretMemberOptionsBase {
|
|
48
|
+
readonly backend?: undefined;
|
|
49
|
+
readonly source: SecretSource<K, Manifest.RenderServices>;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Per-secret-member options at bind time. Backends with `RequiresSource: true`
|
|
53
|
+
* (`Sops.backend`, `SealedSecrets.backend`, `NativeSecret.backend`) make
|
|
54
|
+
* `source` mandatory at the type level. Backends with `false`
|
|
55
|
+
* (`ExternalSecrets.backend`, `Sops.passthrough`) make it optional. With no
|
|
56
|
+
* backend, `source` becomes mandatory — used to feed the in-process
|
|
57
|
+
* `SecretValues` layer for tests / local renders.
|
|
58
|
+
*/
|
|
59
|
+
export type SecretMemberOptions<N extends string, K extends string> = _SecretMemberBackendRequiresSource<N, K> | _SecretMemberBackendOptionalSource<N, K> | _SecretMemberSourceOnly<N, K>;
|
|
60
|
+
export type SecretMemberOptionsFor<A> = A extends SecretEntry<infer N, infer K, infer _E> ? [N, K] extends [string, string] ? SecretMemberOptions<N, K> : never : never;
|
|
61
|
+
/**
|
|
62
|
+
* True iff `M` (recursively, via nested `Environment` members)
|
|
63
|
+
* contains at least one `SecretEntry`. Used to flip `secrets` between
|
|
64
|
+
* required (any secrets present) and optional (literals/downwards
|
|
65
|
+
* only) on `BindEnvironmentInput`.
|
|
66
|
+
*/
|
|
67
|
+
export type HasSecrets<M extends Readonly<Record<string, EnvMember>>> = true extends {
|
|
68
|
+
readonly [K in keyof M]: M[K] extends SecretEntry<infer _N, infer _K, infer _E> ? true : M[K] extends Environment<infer Sub> ? HasSecrets<Sub> : false;
|
|
69
|
+
}[keyof M] ? true : false;
|
|
70
|
+
/**
|
|
71
|
+
* Recursive per-member secret options. For a secret member, the option
|
|
72
|
+
* matches its `SecretMemberOptionsFor` and is required. For a nested
|
|
73
|
+
* `Environment` member that itself contains secrets, the option is
|
|
74
|
+
* `SecretMembersOpts<SubM>` and is also required. Nested environments
|
|
75
|
+
* with no secrets are omitted entirely (no key needed).
|
|
76
|
+
*
|
|
77
|
+
* Together with `HasSecrets`, this enforces at compile time that every
|
|
78
|
+
* `Secret` reachable from the bundle is acknowledged at bind
|
|
79
|
+
* time — adding a new secret to the env contract forces every call
|
|
80
|
+
* site to update.
|
|
81
|
+
*/
|
|
82
|
+
export type SecretMembersOpts<M extends Readonly<Record<string, EnvMember>>> = {
|
|
83
|
+
readonly [K in keyof M as M[K] extends SecretEntry<infer _N, infer _K, infer _E> ? K : M[K] extends Environment<infer SubM> ? HasSecrets<SubM> extends true ? K : never : never]: M[K] extends SecretEntry<infer _N, infer _K, infer _E> ? SecretMemberOptionsFor<M[K]> : M[K] extends Environment<infer SubM> ? SecretMembersOpts<SubM> : never;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* Per-literal value override for bind time. Keyed by member name, typed
|
|
87
|
+
* to each literal's declared `T`. Useful when a `Literal` is a
|
|
88
|
+
* runtime contract (carries a `schema: Config.string(envName)` for app
|
|
89
|
+
* code to yield) but the manifest's emitted value differs per env —
|
|
90
|
+
* e.g. `CLIENT_URL`, `S3_ENDPOINT`, host/URL literals.
|
|
91
|
+
*
|
|
92
|
+
* Recurses on nested `Environment` members — pass `{group: {host: "x"}}`
|
|
93
|
+
* to override a literal nested inside a sub-bundle.
|
|
94
|
+
*/
|
|
95
|
+
export type LiteralMembersOpts<M extends Readonly<Record<string, EnvMember>>> = {
|
|
96
|
+
readonly [K in keyof M as M[K] extends LiteralEntry<infer _EnvName, infer _T> ? K : M[K] extends Environment<infer _SubM> ? K : never]?: M[K] extends LiteralEntry<infer _EnvName, infer T> ? T : M[K] extends Environment<infer SubM> ? LiteralMembersOpts<SubM> : never;
|
|
97
|
+
};
|
|
98
|
+
interface _BindEnvironmentInputBase<M extends Readonly<Record<string, EnvMember>>, Ns extends string = string> {
|
|
99
|
+
readonly env: Environment<M>;
|
|
100
|
+
/**
|
|
101
|
+
* Per-literal value override for the manifest's emitted env var. The
|
|
102
|
+
* runtime read (via `entry.schema`, if provided) is unchanged —
|
|
103
|
+
* overrides only affect what the konfig module writes into the
|
|
104
|
+
* Deployment's `env` array.
|
|
105
|
+
*/
|
|
106
|
+
readonly literals?: LiteralMembersOpts<M>;
|
|
107
|
+
/**
|
|
108
|
+
* Override every secret member's namespace for this bind. Useful when
|
|
109
|
+
* the bundle is consumed across multiple k8s namespaces (e.g. prod /
|
|
110
|
+
* staging / local) without redeclaring each contract. Recurses into
|
|
111
|
+
* nested `Environment` members.
|
|
112
|
+
*
|
|
113
|
+
* When passed as a string literal, the literal flows into each
|
|
114
|
+
* `DeclaredSecret.ref`'s brand so `secretEnvForPod` can enforce
|
|
115
|
+
* cross-namespace coherence on hand-spliced env entries.
|
|
116
|
+
*/
|
|
117
|
+
readonly namespace?: Ns;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* `secrets` flips between required and optional based on whether `M`
|
|
121
|
+
* actually contains any secrets. A bundle of literals/downwards only
|
|
122
|
+
* has nothing to bind, so `secrets` stays optional. A bundle with even
|
|
123
|
+
* one `Secret` (directly or nested) forces the caller to supply
|
|
124
|
+
* every secret member — and for each member, either a `backend` or a
|
|
125
|
+
* `source` (see `SecretMemberOptions`).
|
|
126
|
+
*/
|
|
127
|
+
export type BindEnvironmentInput<M extends Readonly<Record<string, EnvMember>>, Ns extends string = string> = _BindEnvironmentInputBase<M, Ns> & (HasSecrets<M> extends true ? {
|
|
128
|
+
readonly secrets: SecretMembersOpts<M>;
|
|
129
|
+
} : {
|
|
130
|
+
readonly secrets?: SecretMembersOpts<M>;
|
|
131
|
+
});
|
|
132
|
+
export declare const bindEnvironment: <const M extends Readonly<Record<string, EnvMember>>, const Ns extends string = string>(input: BindEnvironmentInput<M, Ns>) => DeclaredEnvironment<M, Ns>;
|
|
133
|
+
export {};
|