@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/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,181 @@
|
|
|
1
|
+
# @konfig.ts/core
|
|
2
|
+
|
|
3
|
+
The Kubernetes-agnostic primitives that the rest of konfig.ts builds on:
|
|
4
|
+
the `Manifest<A>` carrier, the `Dep.*` kinds that drive compile-time
|
|
5
|
+
dependency tracking, a stable YAML serializer, structural diff, an Effect
|
|
6
|
+
`Schema` boundary helper, and the `Helm.release` integration.
|
|
7
|
+
|
|
8
|
+
Higher-level surfaces — workloads, services, env contracts, ArgoCD
|
|
9
|
+
Application aggregation — live in `@konfig.ts/k8s`, `@konfig.ts/env`,
|
|
10
|
+
and `@konfig.ts/argocd`.
|
|
11
|
+
|
|
12
|
+
## `Manifest<A>` — the carrier
|
|
13
|
+
|
|
14
|
+
A `Manifest<A>` is a thunk from a `RenderContext` to an Effect that
|
|
15
|
+
produces an `A`:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
interface Manifest<A> {
|
|
19
|
+
readonly render: (
|
|
20
|
+
ctx: RenderContext
|
|
21
|
+
) => Effect.Effect<A, AnyRenderError, RenderServices>
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
`RenderServices` is `FileSystem | Path | ChildProcessSpawner | Scope`.
|
|
26
|
+
Errors are a tagged union: `RenderError`, `EmbedYamlReadError`,
|
|
27
|
+
`BoundaryDecodeError`, `HelmVersionTooLow`, `HelmRenderError`,
|
|
28
|
+
`HelmDigestMismatch`, `CrdExtractError`.
|
|
29
|
+
|
|
30
|
+
Constructors:
|
|
31
|
+
|
|
32
|
+
| Op | Behaviour |
|
|
33
|
+
| ------------------------------------ | ---------------------------------------------- |
|
|
34
|
+
| `Manifest.make(run)` | Lift a `ctx → A \| Effect<A>` into a Manifest. |
|
|
35
|
+
| `Manifest.combine({ a, b })` | Render `a` and `b` concurrently into a tuple. |
|
|
36
|
+
| `Manifest.concat(...ms)` | Flatten an array of Manifests into one. |
|
|
37
|
+
| `Manifest.whenever({ cond, thunk })` | Optional inclusion. |
|
|
38
|
+
| `Manifest.embedYaml(source)` | Pass through verbatim YAML (file or literal). |
|
|
39
|
+
|
|
40
|
+
The `Manifest<A>` itself is not where dependency-tracking happens — it
|
|
41
|
+
just carries data and effect failures. The compile-time graph lives
|
|
42
|
+
one level up; see below.
|
|
43
|
+
|
|
44
|
+
## Where the type-safety lives
|
|
45
|
+
|
|
46
|
+
konfig's dep-graph claim has two boundaries:
|
|
47
|
+
|
|
48
|
+
1. **`AppOfApps.entrypoint(program)`** in `@konfig.ts/argocd`. Every
|
|
49
|
+
`Application.define({...})` produces an Effect `Layer.Layer<Out, _, In>`
|
|
50
|
+
that propagates the `Dep.Provide<K, N>` it produces in `Out` and the
|
|
51
|
+
`Dep.Need<K, N>` it consumes in `In`. Compose with `Layer.provideMerge`,
|
|
52
|
+
pass to `entrypoint`, and TypeScript checks `In === never`. A missing
|
|
53
|
+
provider is a compile error — see
|
|
54
|
+
[`examples/full-stack/infra/envs/broken.ts`](../../examples/full-stack/infra/envs/broken.ts).
|
|
55
|
+
|
|
56
|
+
2. **`Environment.bind({ env, secrets })`** in `@konfig.ts/env`. The
|
|
57
|
+
bundle's secret atoms describe `{ envName, secret, key }` triples; the
|
|
58
|
+
`bind` checks at the type level that every secret has a backend, the
|
|
59
|
+
backend's `keys` set matches the bundle's keys, and a `requiresSource`
|
|
60
|
+
backend has a `source` of the right key shape.
|
|
61
|
+
|
|
62
|
+
Inside a single `Application.define({ build })` Effect, dep tracking is
|
|
63
|
+
declarative: when you `yield* Dep.Secret("ghcr-pull")` to obtain a
|
|
64
|
+
`SecretRef<"ghcr-pull">`, you add the `Need` to the requirements. Forget
|
|
65
|
+
to yield, and nothing complains — the system catches misses at
|
|
66
|
+
composition, not inside an Application's body.
|
|
67
|
+
|
|
68
|
+
## `Dep.*` — tracked kinds
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { Dep } from "@konfig.ts/core"
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
| Kind | Service | Provided value |
|
|
75
|
+
| --------------------------------- | ------------------------------ | ----------------------------------------- |
|
|
76
|
+
| `Dep.App<Name>(name)` | `Need<"App", Name>` | `Application` record |
|
|
77
|
+
| `Dep.Application<Name>(name)` | `Need<"Application", Name>` | `Name` literal |
|
|
78
|
+
| `Dep.Namespace<Name>(name)` | `Need<"Namespace", Name>` | `Name` literal |
|
|
79
|
+
| `Dep.Secret<Name>(name)` | `Need<"Secret", Name>` | `SecretRef<Name>` brand |
|
|
80
|
+
| `Dep.SecretValues<Name, K>(name)` | `Need<"SecretValues", Name>` | `{ readonly [k in K]: Redacted<string> }` |
|
|
81
|
+
| `Dep.ConfigMap<Name>(name)` | `Need<"ConfigMap", Name>` | `ConfigMapRef<Name>` brand |
|
|
82
|
+
| `Dep.ServiceAccount<Name>(name)` | `Need<"ServiceAccount", Name>` | `ServiceAccountRef<Name>` brand |
|
|
83
|
+
| `Dep.Pvc<Name>(name)` | `Need<"Pvc", Name>` | `PvcRef<Name>` brand |
|
|
84
|
+
|
|
85
|
+
For each kind there's a matching `provideX` helper (`provideSecret`,
|
|
86
|
+
`provideNamespace`, …) that emits a `Layer.Layer<Provide<K, N>>`.
|
|
87
|
+
|
|
88
|
+
`brand(value)` and `unsafeCoerce(value, reason)` from `@konfig.ts/core`
|
|
89
|
+
are escape hatches. `brand` is fine — it's a nominal-typing primitive
|
|
90
|
+
for the `*Ref` types. `unsafeCoerce` is the unchecked cast and takes a
|
|
91
|
+
mandatory reason string so every call site documents WHY the cast is
|
|
92
|
+
sound; prefer the schema boundary below for values crossing a trust
|
|
93
|
+
boundary. (`coerce` is deprecated and will be removed before 1.0.)
|
|
94
|
+
|
|
95
|
+
## Stable YAML — `Yaml.serialize({ value })`
|
|
96
|
+
|
|
97
|
+
Output rules:
|
|
98
|
+
|
|
99
|
+
- Top-level keys ordered: `apiVersion`, `kind`, `metadata`, `spec`,
|
|
100
|
+
`status`, then alphabetical.
|
|
101
|
+
- Inside `metadata`: `name`, `namespace`, `labels`, `annotations`, then
|
|
102
|
+
alphabetical.
|
|
103
|
+
- Every other map: alphabetical.
|
|
104
|
+
- Lists preserve insertion order.
|
|
105
|
+
- LF endings, 2-space indent, single trailing newline.
|
|
106
|
+
- YAML 1.1 explicit (kubectl/ArgoCD compatibility).
|
|
107
|
+
|
|
108
|
+
`Yaml.filenameFor(resource)` returns `<Kind>-<metadata.name>.yaml` —
|
|
109
|
+
deterministic per-resource filenames for ArgoCD-friendly diffs.
|
|
110
|
+
|
|
111
|
+
## Structural diff — `diffFiles({ left, right })`
|
|
112
|
+
|
|
113
|
+
Each file's YAML is parsed and redacted (Helm-volatile metadata stripped)
|
|
114
|
+
before deep-equality. Output is `Map<filename, FileDiff>`, formatted via
|
|
115
|
+
`formatDiff(result, "summary" | "detail" | "json")`. Today's diff is
|
|
116
|
+
two-way only and treats each file as a single document; richer per-doc
|
|
117
|
+
and anchor-aware diffing is on the roadmap.
|
|
118
|
+
|
|
119
|
+
## `Helm.release({...})`
|
|
120
|
+
|
|
121
|
+
Calls `helm pull` and `helm template`, lifts each emitted YAML document
|
|
122
|
+
as a `RawYaml` Manifest under the parent Application. SHA-256 of the
|
|
123
|
+
`.tgz` is verified against `opts.digest` after pull **and** on every
|
|
124
|
+
cache hit — flipping a byte in a cached tarball fails the next render
|
|
125
|
+
with `HelmDigestMismatch`.
|
|
126
|
+
|
|
127
|
+
`opts.digest` must include the `sha256:` prefix; the cache file name
|
|
128
|
+
includes the first 12 hex digits, but the verifier compares the full
|
|
129
|
+
hash.
|
|
130
|
+
|
|
131
|
+
## Boundary decode — `boundary({ schema, label })`
|
|
132
|
+
|
|
133
|
+
Wraps `Schema.decodeUnknownEffect` so that any decode failure becomes
|
|
134
|
+
a `BoundaryDecodeError` with `schema: label`. Use at the seams where
|
|
135
|
+
untyped input enters a module:
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
const decodeApiOptions = boundary({ schema: ApiOptions, label: "api" })
|
|
139
|
+
const cfg = yield * decodeApiOptions(input)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
Every place that previously did `coerce<T>(YAML.parse(stdout))` over
|
|
143
|
+
untrusted external output is now expected to use this helper.
|
|
144
|
+
|
|
145
|
+
## Layout
|
|
146
|
+
|
|
147
|
+
```
|
|
148
|
+
src/
|
|
149
|
+
├── index.ts barrel
|
|
150
|
+
├── _cast.ts brand + coerce (escape hatch)
|
|
151
|
+
├── boundary.ts Schema.decode wrapper → BoundaryDecodeError
|
|
152
|
+
├── deps.ts Dep.* kinds + provide* helpers
|
|
153
|
+
├── diff.ts structural diff + redaction
|
|
154
|
+
├── Helm.ts Helm.release with digest verification
|
|
155
|
+
├── images.ts ImagesConfig + decoders
|
|
156
|
+
├── konfigConfig.ts KonfigConfig schema (top-level config)
|
|
157
|
+
├── Manifest.ts Manifest interface + combinators
|
|
158
|
+
├── render.ts render entrypoint
|
|
159
|
+
├── RenderContext.ts threaded through every render
|
|
160
|
+
├── RenderError.ts tagged error union
|
|
161
|
+
├── types.ts Kind enum (legacy; superseded by Dep.*)
|
|
162
|
+
└── yaml/
|
|
163
|
+
├── index.ts
|
|
164
|
+
└── serialize.ts stable YAML serializer + filenameFor
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Requirements
|
|
168
|
+
|
|
169
|
+
konfig.ts builds on [Effect](https://effect.website/), which is still in
|
|
170
|
+
beta. Until Effect ships a stable 4.x, you must install the exact beta
|
|
171
|
+
konfig is developed against:
|
|
172
|
+
|
|
173
|
+
- **`effect@4.0.0-beta.70`** — required.
|
|
174
|
+
- **`@effect/platform-node@4.0.0-beta.70`** — required only for `render()`
|
|
175
|
+
(the Node filesystem/subprocess entrypoint); manifest-only consumers can
|
|
176
|
+
omit it.
|
|
177
|
+
|
|
178
|
+
The peer dependency is pinned to the exact version on purpose: Effect's beta
|
|
179
|
+
line makes breaking changes between builds, so a looser range would surface
|
|
180
|
+
as `ERESOLVE` install conflicts rather than a working install. This pin will
|
|
181
|
+
relax to a caret range once Effect reaches a stable 4.x.
|
package/dist/Bundle.d.ts
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import * as Compose from "./Compose";
|
|
2
|
+
import * as Dep from "./deps";
|
|
3
|
+
import type * as CoreManifest from "./Manifest";
|
|
4
|
+
import type * as Module from "./Module";
|
|
5
|
+
import type { AnyRenderError } from "./RenderError";
|
|
6
|
+
import { type Context, Effect, Layer } from "effect";
|
|
7
|
+
export interface Bundle {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly namespace?: string;
|
|
10
|
+
readonly manifests: ReadonlyArray<unknown>;
|
|
11
|
+
}
|
|
12
|
+
export interface BundleMakeOptions {
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly namespace?: string;
|
|
15
|
+
readonly manifests: ReadonlyArray<unknown>;
|
|
16
|
+
}
|
|
17
|
+
export declare const make: (opts: BundleMakeOptions) => Bundle;
|
|
18
|
+
/**
|
|
19
|
+
* Handle returned by `Bundle.define`. Same yieldable-Context-Tag +
|
|
20
|
+
* `.layer` pattern as argocd's `ApplicationHandle`; only the carried
|
|
21
|
+
* value type differs (a plain `Bundle` with no argo source/syncPolicy).
|
|
22
|
+
* `Dep.Need<"App", Name>` keys the dep graph by literal name so
|
|
23
|
+
* sibling modules can `yield* bundleHandle` to consume it.
|
|
24
|
+
*/
|
|
25
|
+
export interface BundleHandle<Name extends string, Out, In> extends Context.Service<Dep.Need<"App", Name>, Bundle> {
|
|
26
|
+
readonly layer: Layer.Layer<Out, AnyRenderError, In>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Higher-kinded handle constructor that maps `Module`'s
|
|
30
|
+
* `(Name, Ns, R, Extra)` slots onto a `BundleHandle`. Lets
|
|
31
|
+
* `Module.fixedNs({ target: Bundle.target, … })` /
|
|
32
|
+
* `Module.dynamicNs({ target: Bundle.target, … })` return
|
|
33
|
+
* strongly-typed `BundleHandle`s.
|
|
34
|
+
*/
|
|
35
|
+
export interface HandleKind extends Module.HandleKind {
|
|
36
|
+
readonly Handle: BundleHandle<this["_Name"] & string, Dep.Provide<"App", this["_Name"] & string> | Dep.Provide<"Namespace", this["_Ns"] & string> | (this["_Extra"] & unknown), Exclude<this["_R"] & unknown, Dep.Need<"Namespace", this["_Ns"] & string> | (this["_Extra"] & unknown)>>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Re-export of {@link Module.LiteralName} — preserved as
|
|
40
|
+
* `Bundle.LiteralName` so existing wrapper code keeps working.
|
|
41
|
+
* konfig's dep graph keys every `Provide<"App", Name>` slot by literal
|
|
42
|
+
* `Name`; a wrapper that lets `Name` widen to `string` collapses every
|
|
43
|
+
* bundle into the same slot.
|
|
44
|
+
*/
|
|
45
|
+
export type LiteralName<T extends string> = Module.LiteralName<T>;
|
|
46
|
+
export interface BundleDefineOptions<Name extends string, Ns extends string, R, Extra> {
|
|
47
|
+
readonly name: LiteralName<Name>;
|
|
48
|
+
readonly namespace?: LiteralName<Ns>;
|
|
49
|
+
readonly build: Effect.Effect<ReadonlyArray<unknown>, AnyRenderError, R> | (() => ReadonlyArray<unknown>);
|
|
50
|
+
readonly provides?: Layer.Layer<Extra>;
|
|
51
|
+
}
|
|
52
|
+
type _NsProvides<Ns extends string> = [Ns] extends [never] ? never : Dep.Provide<"Namespace", Ns>;
|
|
53
|
+
type _NsExcludes<Ns extends string> = [Ns] extends [never] ? never : Dep.Need<"Namespace", Ns>;
|
|
54
|
+
/**
|
|
55
|
+
* Build a typed handle for a manifest bundle — a name + optional
|
|
56
|
+
* namespace + a set of manifests, plus dep-graph wiring. Same
|
|
57
|
+
* compile-time guarantees as argocd's `Application.define` minus
|
|
58
|
+
* `source: ArgoSource` / `syncPolicy` / sync-wave annotations:
|
|
59
|
+
* - the literal `Name` flows into `Dep.Provide<"App", Name>`,
|
|
60
|
+
* - the optional literal namespace flows into `Dep.Provide<"Namespace", Ns>`,
|
|
61
|
+
* - the build callback's `R` channel becomes the handle's `In` after
|
|
62
|
+
* subtracting what this bundle provides itself.
|
|
63
|
+
*
|
|
64
|
+
* Pair with `Bundle.fromModules` to compose multiple bundles
|
|
65
|
+
* and have the dep-graph residual checked at `Bundle.entrypoint`.
|
|
66
|
+
*
|
|
67
|
+
* For `Module.fixedNs` / `Module.dynamicNs` use, see `Bundle.target`
|
|
68
|
+
* — it adapts this `define` so namespace is required (Module wrappers
|
|
69
|
+
* always thread a namespace through), which lets the dep-graph drop
|
|
70
|
+
* the `Provide<"Namespace", never>` cell the optional shape needs.
|
|
71
|
+
*/
|
|
72
|
+
export declare const define: <const Name extends string, const Ns extends string = never, R = never, Extra = never>(opts: BundleDefineOptions<Name, Ns, R, Extra>) => BundleHandle<Name, Dep.Provide<"App", Name> | _NsProvides<Ns> | Extra, Exclude<R, _NsExcludes<Ns> | Extra>>;
|
|
73
|
+
/**
|
|
74
|
+
* `Module.Target` adapter for `Bundle.define`. `Module.fixedNs` /
|
|
75
|
+
* `Module.dynamicNs` always pass a namespace through, so this adapter
|
|
76
|
+
* requires `namespace` (whereas `Bundle.define` itself accepts it as
|
|
77
|
+
* optional) — pinning `Ns` to a real literal lets the dep-graph emit
|
|
78
|
+
* a concrete `Provide<"Namespace", Ns>` cell on the resulting handle.
|
|
79
|
+
*
|
|
80
|
+
* ```ts
|
|
81
|
+
* const defineCache = Module.fixedNs({
|
|
82
|
+
* target: Bundle.target,
|
|
83
|
+
* namespace: "cache",
|
|
84
|
+
* build: ({ name, namespace }) => [ ... ],
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare const target: Module.Target<HandleKind, Record<never, never>, Record<never, never>>;
|
|
89
|
+
export interface BundleSetResult {
|
|
90
|
+
readonly name: string;
|
|
91
|
+
readonly bundles: ReadonlyArray<Bundle>;
|
|
92
|
+
}
|
|
93
|
+
export interface BundleSetMakeOptions {
|
|
94
|
+
readonly name?: string;
|
|
95
|
+
readonly bundles: ReadonlyArray<Bundle>;
|
|
96
|
+
}
|
|
97
|
+
export declare const makeSet: (opts: BundleSetMakeOptions) => BundleSetResult;
|
|
98
|
+
/**
|
|
99
|
+
* Phantom check that rejects a `Bundle.fromModules` program whose `R`
|
|
100
|
+
* channel still carries unmet dep-graph Needs. Bound to the
|
|
101
|
+
* "Bundle.fromModules" API label so the `_konfig_unsatisfied` hint
|
|
102
|
+
* guides the user to the right call site.
|
|
103
|
+
*/
|
|
104
|
+
export declare const entrypoint: <A, E, R>(program: Effect.Effect<A, E, R> & ([Exclude<R, CoreManifest.RenderServices>] extends [never] ? unknown : {
|
|
105
|
+
readonly _konfig_unsatisfied: Exclude<R, CoreManifest.RenderServices> extends infer T ? T extends Exclude<R, CoreManifest.RenderServices> ? T extends Dep.Need<infer K extends string, infer V extends string> ? `Missing provider for ${K} "${V}". Add a module that provides it to Bundle.fromModules({ modules }), or check that providers come before consumers in the list.` : "Unsatisfied dep — see the Effect Layer error above." : never : never;
|
|
106
|
+
})) => Effect.Effect<A, E, R & CoreManifest.RenderServices>;
|
|
107
|
+
type AnyHandle = BundleHandle<any, any, any>;
|
|
108
|
+
export type ResidualIn<T extends ReadonlyArray<AnyHandle>> = Compose.ResidualIn<T>;
|
|
109
|
+
export interface FromModulesOptions<Ms extends ReadonlyArray<AnyHandle>> {
|
|
110
|
+
readonly name?: string;
|
|
111
|
+
readonly modules: Ms;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* One-list composition for a backend-agnostic bundle set. Yields each
|
|
115
|
+
* module's `Bundle` in tuple order, then wires the merged provider layer
|
|
116
|
+
* with `Compose.composeLayers`. The returned Effect's R channel is the
|
|
117
|
+
* residual unmet Needs after the fold (`Compose.ResidualIn<Ms>`),
|
|
118
|
+
* which `entrypoint` rejects unless empty.
|
|
119
|
+
*
|
|
120
|
+
* **Order matters.** List providers before their consumers. A consumer
|
|
121
|
+
* placed before its provider leaves an unmet `Need` in the residual,
|
|
122
|
+
* which surfaces at `entrypoint` as a `_konfig_unsatisfied` hint.
|
|
123
|
+
*/
|
|
124
|
+
export declare const fromModules: <const Ms extends ReadonlyArray<AnyHandle>>(opts: FromModulesOptions<Ms>) => Effect.Effect<BundleSetResult, AnyRenderError, ResidualIn<Ms> | CoreManifest.RenderServices>;
|
|
125
|
+
export {};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { type Effect, Layer } from "effect";
|
|
2
|
+
import type { Need } from "./deps";
|
|
3
|
+
import type { RenderServices } from "./Manifest";
|
|
4
|
+
import type { AnyRenderError } from "./RenderError";
|
|
5
|
+
/**
|
|
6
|
+
* The minimal shape `composeLayers` and the residual fold need: anything
|
|
7
|
+
* with a `.layer` field of the standard `(Out, AnyRenderError, In)`
|
|
8
|
+
* triple. Each runtime (argocd `ApplicationHandle`, k8s `BundleHandle`,
|
|
9
|
+
* …) supplies its own wider handle interface — the carried value type
|
|
10
|
+
* is irrelevant to the fold itself.
|
|
11
|
+
*/
|
|
12
|
+
export interface ComposeHandle<Out = any, In = any> {
|
|
13
|
+
readonly layer: Layer.Layer<Out, AnyRenderError, In>;
|
|
14
|
+
}
|
|
15
|
+
type AnyHandle = ComposeHandle<any, any>;
|
|
16
|
+
type OutOfHandle<H> = H extends ComposeHandle<infer Out, any> ? Out : never;
|
|
17
|
+
type InOfHandle<H> = H extends ComposeHandle<any, infer In> ? In : never;
|
|
18
|
+
/**
|
|
19
|
+
* Left-fold over the modules tuple, mirroring the runtime
|
|
20
|
+
* `reduce(Layer.provideMerge)` below. Each module's `In` is filtered
|
|
21
|
+
* against the union of every *prior* module's `Out`; whatever survives
|
|
22
|
+
* is residual. Tuple order matters: a consumer listed before its
|
|
23
|
+
* provider leaves its Need in the residual.
|
|
24
|
+
*/
|
|
25
|
+
type FoldResidualIn<T extends ReadonlyArray<AnyHandle>, AccIn, AccOut> = T extends readonly [infer H, ...infer Rest] ? H extends AnyHandle ? Rest extends ReadonlyArray<AnyHandle> ? FoldResidualIn<Rest, AccIn | Exclude<InOfHandle<H>, AccOut>, AccOut | OutOfHandle<H>> : never : never : AccIn;
|
|
26
|
+
/**
|
|
27
|
+
* After folding `Layer.provideMerge` over `Ms` in tuple order, the
|
|
28
|
+
* leftover `RIn` channel — the Needs that no preceding module's Out
|
|
29
|
+
* satisfies. Pair with `makeResidualEntrypoint` to surface a non-empty
|
|
30
|
+
* residual as a compile error at the entrypoint call site.
|
|
31
|
+
*/
|
|
32
|
+
export type ResidualIn<T extends ReadonlyArray<AnyHandle>> = FoldResidualIn<T, never, never>;
|
|
33
|
+
/**
|
|
34
|
+
* Runtime layer composition — `reduce(Layer.provideMerge)`. Each
|
|
35
|
+
* successive module receives every prior module's Out as available
|
|
36
|
+
* services. The runtime type collapses to a bottom Layer; the per-module
|
|
37
|
+
* Out/In is tracked statically by `ResidualIn`.
|
|
38
|
+
*/
|
|
39
|
+
export declare const composeLayers: (modules: ReadonlyArray<{
|
|
40
|
+
readonly layer: unknown;
|
|
41
|
+
}>) => Layer.Layer<never, AnyRenderError, never>;
|
|
42
|
+
/**
|
|
43
|
+
* Per-Need template-literal hint shown when a residual reaches the
|
|
44
|
+
* entrypoint. `Api` is the calling API's name ("AppOfApps.fromModules",
|
|
45
|
+
* "Bundle.fromModules", …) so the message points the user at the right
|
|
46
|
+
* call site.
|
|
47
|
+
*/
|
|
48
|
+
type UnsatisfiedHint<R, Api extends string> = R extends Need<infer K, infer V> ? `Missing provider for ${K} "${V}". Add a module that provides it to ${Api}({ modules }), or check that providers come before consumers in the list.` : "Unsatisfied dep — see the Effect Layer error above.";
|
|
49
|
+
/**
|
|
50
|
+
* Intersects the program type with a phantom `_konfig_unsatisfied`
|
|
51
|
+
* object when the residual `R` carries anything beyond
|
|
52
|
+
* `Manifest.RenderServices`. The program has no such property at runtime,
|
|
53
|
+
* so the call fails to typecheck and the user sees the hint.
|
|
54
|
+
*/
|
|
55
|
+
type ResidualHintCheck<R, Api extends string> = [
|
|
56
|
+
Exclude<R, RenderServices>
|
|
57
|
+
] extends [never] ? unknown : {
|
|
58
|
+
readonly _konfig_unsatisfied: UnsatisfiedHint<Exclude<R, RenderServices>, Api>;
|
|
59
|
+
};
|
|
60
|
+
/**
|
|
61
|
+
* Build an `entrypoint` function bound to a specific API name. The
|
|
62
|
+
* returned function accepts only programs whose `R` channel reduces to
|
|
63
|
+
* `Manifest.RenderServices`; otherwise the call fails at the program
|
|
64
|
+
* argument with a `_konfig_unsatisfied` hint that names the missing
|
|
65
|
+
* provider and the calling API.
|
|
66
|
+
*/
|
|
67
|
+
export declare const makeResidualEntrypoint: <const Api extends string>(_api: Api) => <A, E, R>(program: Effect.Effect<A, E, R> & ResidualHintCheck<R, Api>) => Effect.Effect<A, E, R & RenderServices>;
|
|
68
|
+
export {};
|
package/dist/Helm.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type Manifest, type RawYaml } from "./Manifest";
|
|
2
|
+
export interface HelmReleaseOptions {
|
|
3
|
+
readonly repo: string;
|
|
4
|
+
readonly chart: string;
|
|
5
|
+
readonly releaseName?: string;
|
|
6
|
+
readonly version: string;
|
|
7
|
+
readonly digest: string;
|
|
8
|
+
readonly namespace?: string;
|
|
9
|
+
readonly values: Record<string, unknown>;
|
|
10
|
+
readonly extraOpts?: readonly string[];
|
|
11
|
+
/**
|
|
12
|
+
* Minimum acceptable `helm` CLI version (semver, e.g. `"3.16.0"`).
|
|
13
|
+
* When set, `release` runs a `helm version --short` preflight before
|
|
14
|
+
* pulling/templating and fails with `HelmVersionTooLow` if the
|
|
15
|
+
* installed helm is older (or absent). Omit to skip the check.
|
|
16
|
+
*/
|
|
17
|
+
readonly minVersion?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare const release: (opts: HelmReleaseOptions) => Manifest<RawYaml[]>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { FileSystem } from "effect/FileSystem";
|
|
3
|
+
import type { Path } from "effect/Path";
|
|
4
|
+
import type * as Scope from "effect/Scope";
|
|
5
|
+
import type { ChildProcessSpawner } from "./_unstable";
|
|
6
|
+
import type { RenderContext } from "./RenderContext";
|
|
7
|
+
import { type AnyRenderError } from "./RenderError";
|
|
8
|
+
export type RenderServices = FileSystem | Path | ChildProcessSpawner | Scope.Scope;
|
|
9
|
+
export declare const ManifestTypeId: unique symbol;
|
|
10
|
+
export type ManifestTypeId = typeof ManifestTypeId;
|
|
11
|
+
interface Variance<out A> {
|
|
12
|
+
readonly _A: (_: never) => A;
|
|
13
|
+
}
|
|
14
|
+
export interface Manifest<out A> {
|
|
15
|
+
readonly [ManifestTypeId]: Variance<A>;
|
|
16
|
+
readonly render: (ctx: RenderContext) => Effect.Effect<A, AnyRenderError, RenderServices>;
|
|
17
|
+
}
|
|
18
|
+
export type MakeRun<A> = (ctx: RenderContext) => A | Effect.Effect<A, AnyRenderError, RenderServices>;
|
|
19
|
+
export declare const make: <A>(run: MakeRun<A>) => Manifest<A>;
|
|
20
|
+
export interface CombineInput<A1, A2> {
|
|
21
|
+
readonly a: Manifest<A1>;
|
|
22
|
+
readonly b: Manifest<A2>;
|
|
23
|
+
}
|
|
24
|
+
export declare const combine: <A1, A2>(input: CombineInput<A1, A2>) => Manifest<readonly [A1, A2]>;
|
|
25
|
+
export declare const concat: <A>(...manifests: Manifest<A | A[]>[]) => Manifest<A[]>;
|
|
26
|
+
export interface WheneverInput<A> {
|
|
27
|
+
readonly cond: boolean;
|
|
28
|
+
readonly thunk: () => Manifest<A>;
|
|
29
|
+
}
|
|
30
|
+
export declare const whenever: <A>(input: WheneverInput<A>) => Manifest<A | undefined>;
|
|
31
|
+
export type EmbedYamlSource = {
|
|
32
|
+
readonly path: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly literal: string;
|
|
35
|
+
};
|
|
36
|
+
export interface RawYaml {
|
|
37
|
+
readonly _tag: "RawYaml";
|
|
38
|
+
readonly content: string;
|
|
39
|
+
readonly origin?: string;
|
|
40
|
+
}
|
|
41
|
+
export declare const embedYaml: (source: EmbedYamlSource) => Manifest<RawYaml>;
|
|
42
|
+
export {};
|
package/dist/Module.d.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { Effect, type Layer } from "effect";
|
|
2
|
+
import type { AnyRenderError } from "./RenderError";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves to `T` if it is a string literal (or template-literal
|
|
5
|
+
* pattern), and to a structured error type when `T` widens to the bare
|
|
6
|
+
* `string`. Use as the field type on every name/namespace slot any
|
|
7
|
+
* module wrapper forwards.
|
|
8
|
+
*
|
|
9
|
+
* konfig's dep graph keys each provider by literal name; a wrapper that
|
|
10
|
+
* lets `Name` widen to `string` collapses every module into the same
|
|
11
|
+
* slot and silently masks unmet deps. This brand turns that into a
|
|
12
|
+
* compile error at the call site.
|
|
13
|
+
*/
|
|
14
|
+
export type LiteralName<T extends string> = string extends T ? {
|
|
15
|
+
readonly _konfig_error: "Module name/namespace must be a string literal. Make the wrapper generic (`<const Name extends string>`) and forward via `Module.LiteralName<Name>`.";
|
|
16
|
+
} : T;
|
|
17
|
+
/**
|
|
18
|
+
* Context passed to a module's `build` callback. Carries the
|
|
19
|
+
* per-instance identity (chosen `name` and the module's `namespace`)
|
|
20
|
+
* so the build can stamp them onto manifests without re-receiving
|
|
21
|
+
* them via `opts`.
|
|
22
|
+
*/
|
|
23
|
+
export interface BuildContext<Ns extends string = string> {
|
|
24
|
+
readonly name: string;
|
|
25
|
+
readonly namespace: Ns;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Allowed return shapes from a module `build` callback:
|
|
29
|
+
* - an `Effect` (when the build reads from Layers, files, etc.)
|
|
30
|
+
* - a plain `ReadonlyArray<unknown>` for pure synchronous builds.
|
|
31
|
+
*
|
|
32
|
+
* `Module.fixedNs` / `Module.dynamicNs` lift the array form into an
|
|
33
|
+
* `Effect` internally — wrapper authors don't need to wrap themselves.
|
|
34
|
+
*/
|
|
35
|
+
export type BuildResult<R = never> = Effect.Effect<ReadonlyArray<unknown>, AnyRenderError, R> | ReadonlyArray<unknown>;
|
|
36
|
+
/**
|
|
37
|
+
* Higher-kinded handle constructor: each backend declares a sub-interface
|
|
38
|
+
* that maps the four type parameters (`_Name`, `_Ns`, `_R`, `_Extra`)
|
|
39
|
+
* onto its native handle (`ApplicationHandle` / `BundleHandle` / …).
|
|
40
|
+
*
|
|
41
|
+
* `ApplyHandle` substitutes concrete types into the kind's `this`
|
|
42
|
+
* slots — the standard "type lambda" encoding Effect uses for HKTs.
|
|
43
|
+
*/
|
|
44
|
+
export interface HandleKind {
|
|
45
|
+
readonly _Name: string;
|
|
46
|
+
readonly _Ns: string;
|
|
47
|
+
readonly _R: unknown;
|
|
48
|
+
readonly _Extra: unknown;
|
|
49
|
+
readonly Handle: unknown;
|
|
50
|
+
}
|
|
51
|
+
export type ApplyHandle<K extends HandleKind, Name extends string, Ns extends string, R, Extra> = (K & {
|
|
52
|
+
readonly _Name: Name;
|
|
53
|
+
readonly _Ns: Ns;
|
|
54
|
+
readonly _R: R;
|
|
55
|
+
readonly _Extra: Extra;
|
|
56
|
+
})["Handle"];
|
|
57
|
+
/**
|
|
58
|
+
* Universal define-args every backend accepts. Each backend layers
|
|
59
|
+
* additional fields on via the `ExtraConfig` (config-time-only) and
|
|
60
|
+
* `ExtraCallArgs` (per-instance) generics on `Target`.
|
|
61
|
+
*/
|
|
62
|
+
export interface DefineBaseArgs<Name extends string, Ns extends string, R, Extra> {
|
|
63
|
+
readonly name: LiteralName<Name>;
|
|
64
|
+
readonly namespace: LiteralName<Ns>;
|
|
65
|
+
readonly build: Effect.Effect<ReadonlyArray<unknown>, AnyRenderError, R> | (() => ReadonlyArray<unknown>);
|
|
66
|
+
readonly provides?: Layer.Layer<Extra>;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Adapter contract a backend implements to plug into `Module.fixedNs` /
|
|
70
|
+
* `Module.dynamicNs`. Both `Application` (argocd) and `Bundle` (k8s)
|
|
71
|
+
* satisfy it via their existing `define` exports — pass the namespace
|
|
72
|
+
* itself as the first argument.
|
|
73
|
+
*
|
|
74
|
+
* - `Kind` — HKT mapping `(Name, Ns, R, Extra)` to the
|
|
75
|
+
* backend's native handle type.
|
|
76
|
+
* - `ExtraConfig` — config-time-only fields the backend accepts
|
|
77
|
+
* (e.g. argocd's `syncPolicy`, `annotations`).
|
|
78
|
+
* - `ExtraCallArgs` — per-instance fields the wrapper requires
|
|
79
|
+
* at the call site (e.g. argocd's `source`).
|
|
80
|
+
*/
|
|
81
|
+
export interface Target<Kind extends HandleKind = HandleKind, ExtraConfig extends object = Record<string, never>, ExtraCallArgs extends object = Record<string, never>> {
|
|
82
|
+
readonly define: <const Name extends string, const Ns extends string, R = never, Extra = never>(args: DefineBaseArgs<Name, Ns, R, Extra> & ExtraConfig & ExtraCallArgs) => ApplyHandle<Kind, Name, Ns, R, Extra>;
|
|
83
|
+
}
|
|
84
|
+
/** Config accepted by `Module.fixedNs` (namespace baked into the wrapper). */
|
|
85
|
+
export interface FixedNsConfig<Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, Ns extends string, Opts extends object, R, Extra> {
|
|
86
|
+
readonly target: Target<Kind, ExtraConfig, ExtraCallArgs>;
|
|
87
|
+
readonly namespace: Ns;
|
|
88
|
+
readonly provides?: Layer.Layer<Extra>;
|
|
89
|
+
readonly build: (ctx: BuildContext<Ns>, opts: Opts) => BuildResult<R>;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Build a typed wrapper for a module whose namespace is part of its
|
|
93
|
+
* identity (e.g. `cert-manager` always installs into `cert-manager`).
|
|
94
|
+
*
|
|
95
|
+
* `target` is the backend adapter — typically `Application.target`
|
|
96
|
+
* (argocd) or `Bundle.target` (k8s). The wrapper's call signature
|
|
97
|
+
* merges the backend's per-instance fields (`source` for argocd,
|
|
98
|
+
* none for bundle) with the user-defined `Opts`.
|
|
99
|
+
*
|
|
100
|
+
* ```ts
|
|
101
|
+
* const defineSops = Module.fixedNs({
|
|
102
|
+
* target: Application.target,
|
|
103
|
+
* namespace: "sops",
|
|
104
|
+
* annotations: Sync.wave(-1),
|
|
105
|
+
* build: ({ namespace }, _opts: Record<never, never>) => [
|
|
106
|
+
* Namespace.make({ name: namespace }),
|
|
107
|
+
* Helm.release({ ... }),
|
|
108
|
+
* ],
|
|
109
|
+
* });
|
|
110
|
+
*
|
|
111
|
+
* const sops = defineSops({ name: "sops-operator", source: src("sops") });
|
|
112
|
+
* ```
|
|
113
|
+
*/
|
|
114
|
+
export declare const fixedNs: <Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, const Ns extends string, Opts extends object = Record<never, never>, R = never, Extra = never>(config: FixedNsConfig<Kind, ExtraConfig, ExtraCallArgs, Ns, Opts, R, Extra> & ExtraConfig) => <const Name extends string>(args: {
|
|
115
|
+
readonly name: LiteralName<Name>;
|
|
116
|
+
} & ExtraCallArgs & Opts) => ApplyHandle<Kind, Name, Ns, R, Extra>;
|
|
117
|
+
/** Config accepted by `Module.dynamicNs` (namespace chosen per instance). */
|
|
118
|
+
export interface DynamicNsConfig<Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, Opts extends object, R, Extra> {
|
|
119
|
+
readonly target: Target<Kind, ExtraConfig, ExtraCallArgs>;
|
|
120
|
+
readonly provides?: Layer.Layer<Extra>;
|
|
121
|
+
readonly build: (ctx: BuildContext, opts: Opts) => BuildResult<R>;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* Build a typed wrapper for a module whose namespace is chosen per
|
|
125
|
+
* instance (e.g. an `api` module deployed into different namespaces
|
|
126
|
+
* per env).
|
|
127
|
+
*
|
|
128
|
+
* ```ts
|
|
129
|
+
* const defineApi = Module.dynamicNs({
|
|
130
|
+
* target: Application.target,
|
|
131
|
+
* annotations: Sync.wave(1),
|
|
132
|
+
* build: ({ name, namespace }, opts: ApiOpts) => [ ... ],
|
|
133
|
+
* });
|
|
134
|
+
*
|
|
135
|
+
* const api = defineApi({
|
|
136
|
+
* name: "api",
|
|
137
|
+
* namespace: "prod",
|
|
138
|
+
* source: src("api"),
|
|
139
|
+
* replicas: 2,
|
|
140
|
+
* });
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
export declare const dynamicNs: <Kind extends HandleKind, ExtraConfig extends object, ExtraCallArgs extends object, Opts extends object = Record<never, never>, R = never, Extra = never>(config: DynamicNsConfig<Kind, ExtraConfig, ExtraCallArgs, Opts, R, Extra> & ExtraConfig) => <const Name extends string, const Ns extends string>(args: {
|
|
144
|
+
readonly name: LiteralName<Name>;
|
|
145
|
+
readonly namespace: LiteralName<Ns>;
|
|
146
|
+
} & ExtraCallArgs & Opts) => ApplyHandle<Kind, Name, Ns, R, Extra>;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Context object threaded through every `Manifest.render(...)` call.
|
|
3
|
+
*
|
|
4
|
+
* `env` is the single load-bearing field: it keys both the file-output
|
|
5
|
+
* directory under `outDir.manifests` and the choice of bundle entry in
|
|
6
|
+
* `KonfigConfig.envs`. The optional fields below carry per-cluster
|
|
7
|
+
* context for renders where one logical env spans multiple clusters.
|
|
8
|
+
*
|
|
9
|
+
* **Precedence rules:**
|
|
10
|
+
* - The output directory is keyed on `env` alone when `cluster` is
|
|
11
|
+
* `undefined`; when set, builds write to `<outDir>/<env>/<cluster>/`.
|
|
12
|
+
* Tests that bypass the CLI write into the simpler `<outDir>/<env>/`
|
|
13
|
+
* path by construction.
|
|
14
|
+
* - `cluster` is informational inside `Manifest.make((ctx) => ...)` —
|
|
15
|
+
* no constructor mutates a manifest based on it implicitly; the
|
|
16
|
+
* caller branches.
|
|
17
|
+
* - `k8sVersion` is a hint for renderers that emit
|
|
18
|
+
* apiVersion-conditional resources (e.g. `policy/v1beta1` vs
|
|
19
|
+
* `policy/v1`).
|
|
20
|
+
* - `flags` is the escape hatch for one-off "render with X" toggles
|
|
21
|
+
* that don't deserve their own field. Use sparingly.
|
|
22
|
+
*/
|
|
23
|
+
export interface RenderContext {
|
|
24
|
+
readonly env: string;
|
|
25
|
+
readonly cluster?: string;
|
|
26
|
+
readonly k8sVersion?: string;
|
|
27
|
+
readonly flags?: ReadonlyMap<string, unknown>;
|
|
28
|
+
}
|
|
29
|
+
export interface RenderContextFullInput {
|
|
30
|
+
readonly env: string;
|
|
31
|
+
readonly cluster?: string;
|
|
32
|
+
readonly k8sVersion?: string;
|
|
33
|
+
readonly flags?: ReadonlyMap<string, unknown>;
|
|
34
|
+
}
|
|
35
|
+
export declare const RenderContext: {
|
|
36
|
+
make: (env: string) => RenderContext;
|
|
37
|
+
makeFull: (input: RenderContextFullInput) => RenderContext;
|
|
38
|
+
};
|