@konfig.ts/core 0.0.1 → 0.0.3
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/README.md +61 -160
- package/package.json +10 -10
package/README.md
CHANGED
|
@@ -1,181 +1,82 @@
|
|
|
1
1
|
# @konfig.ts/core
|
|
2
2
|
|
|
3
|
-
The Kubernetes-agnostic primitives
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The Kubernetes-agnostic primitives every other konfig.ts package builds on: the
|
|
4
|
+
`Manifest<A>` carrier, the `Dep.*` kinds that drive compile-time dependency
|
|
5
|
+
tracking, the `Module` factories, stable YAML, structural diff, a Helm
|
|
6
|
+
integration with digest verification, and the `render` entrypoint.
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
and
|
|
8
|
+
Most projects reach for the higher-level packages ([k8s](../k8s), [env](../env),
|
|
9
|
+
[argocd](../argocd)) and touch core directly only for `Helm.release`, `Dep.*`,
|
|
10
|
+
and `Module`.
|
|
11
11
|
|
|
12
|
-
##
|
|
12
|
+
## Install
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
```ts
|
|
18
|
-
interface Manifest<A> {
|
|
19
|
-
readonly render: (
|
|
20
|
-
ctx: RenderContext
|
|
21
|
-
) => Effect.Effect<A, AnyRenderError, RenderServices>
|
|
22
|
-
}
|
|
14
|
+
```bash
|
|
15
|
+
bun add @konfig.ts/core
|
|
23
16
|
```
|
|
24
17
|
|
|
25
|
-
|
|
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:
|
|
18
|
+
## Usage
|
|
47
19
|
|
|
48
|
-
|
|
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
|
|
20
|
+
Pull a digest-verified Helm chart inside a reusable module:
|
|
69
21
|
|
|
70
22
|
```ts
|
|
71
|
-
import {
|
|
23
|
+
import { Application } from "@konfig.ts/argocd"
|
|
24
|
+
import { Dep, Helm, Module } from "@konfig.ts/core"
|
|
25
|
+
import { Namespace } from "@konfig.ts/k8s"
|
|
26
|
+
|
|
27
|
+
export const definePostgres = Module.fixedNs({
|
|
28
|
+
target: Application.target,
|
|
29
|
+
namespace: "data",
|
|
30
|
+
build: ({ namespace }, opts: { storageGi: number }) => [
|
|
31
|
+
Namespace.make({ name: namespace }),
|
|
32
|
+
Helm.release({
|
|
33
|
+
repo: "https://charts.bitnami.com/bitnami",
|
|
34
|
+
chart: "postgresql",
|
|
35
|
+
version: "16.0.0",
|
|
36
|
+
digest: "sha256:…", // verified after pull AND on every cache hit
|
|
37
|
+
namespace,
|
|
38
|
+
values: { primary: { persistence: { size: `${opts.storageGi}Gi` } } }
|
|
39
|
+
})
|
|
40
|
+
]
|
|
41
|
+
})
|
|
72
42
|
```
|
|
73
43
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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:
|
|
44
|
+
Inside a build, `yield* Dep.Secret("ghcr-pull")` records a typed dependency that
|
|
45
|
+
another module must provide — the graph is checked when you compose everything
|
|
46
|
+
at `AppOfApps.entrypoint` (see [`@konfig.ts/argocd`](../argocd)).
|
|
136
47
|
|
|
137
|
-
|
|
138
|
-
const decodeApiOptions = boundary({ schema: ApiOptions, label: "api" })
|
|
139
|
-
const cfg = yield * decodeApiOptions(input)
|
|
140
|
-
```
|
|
48
|
+
## Surface
|
|
141
49
|
|
|
142
|
-
|
|
143
|
-
|
|
50
|
+
| Area | Exports |
|
|
51
|
+
| ----------- | ------------------------------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| Manifest | `Manifest` (`.make` / `.combine` / `.concat` / `.whenever` / `.embedYaml`), `render`, `renderManifest`, `RenderContext` |
|
|
53
|
+
| Deps | `Dep.*` kinds + `Dep.provide*` helpers; branded `SecretRef` / `ConfigMapRef` / `PvcRef` / `ServiceAccountRef` / `BuiltImageRef` |
|
|
54
|
+
| Modules | `Module.fixedNs`, `Module.dynamicNs` |
|
|
55
|
+
| Helm | `Helm.release` — chart pull + SHA-256 digest verification |
|
|
56
|
+
| YAML & diff | `Yaml.serialize` / `Yaml.filenameFor`; `diffFiles`, `formatDiff`, `parseYaml`, `redact` |
|
|
57
|
+
| Config | `KonfigConfig`, `ImagesConfig`, and their decoders |
|
|
58
|
+
| Boundaries | `boundary` (Schema decode → `BoundaryDecodeError`); `brand` / `unsafeCoerce` escape hatches |
|
|
59
|
+
| Errors | the tagged union `AnyRenderError` (`HelmDigestMismatch`, `BoundaryDecodeError`, …) |
|
|
144
60
|
|
|
145
|
-
##
|
|
61
|
+
## Internals
|
|
146
62
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
```
|
|
63
|
+
`Manifest<A>` is only a recipe from a `RenderContext` to an Effect that produces
|
|
64
|
+
an `A` — it does not track deps in its own type. Per-kind dependency tracking
|
|
65
|
+
lives one level up, in the Effect `Layer`s that `Module` and
|
|
66
|
+
`Application.define` compose. See
|
|
67
|
+
[`.docs/architecture.md`](../../.docs/architecture.md).
|
|
166
68
|
|
|
167
69
|
## Requirements
|
|
168
70
|
|
|
169
|
-
konfig.ts
|
|
170
|
-
|
|
171
|
-
|
|
71
|
+
konfig.ts is built on [Effect](https://effect.website/), currently in beta.
|
|
72
|
+
Until Effect ships a stable 4.x, install the exact beta konfig.ts is built
|
|
73
|
+
against:
|
|
172
74
|
|
|
173
|
-
- **`effect@4.0.0-beta.70`** — required.
|
|
174
|
-
- **`@effect/platform-node@4.0.0-beta.70`** — required only
|
|
175
|
-
(the Node filesystem/subprocess entrypoint); manifest-only
|
|
176
|
-
omit it.
|
|
75
|
+
- **`effect@4.0.0-beta.70`** — required by every package.
|
|
76
|
+
- **`@effect/platform-node@4.0.0-beta.70`** — required only when you call
|
|
77
|
+
`render()` (the Node filesystem/subprocess entrypoint); manifest-only
|
|
78
|
+
consumers can omit it (it is declared as an optional peer).
|
|
177
79
|
|
|
178
|
-
The
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
relax to a caret range once Effect reaches a stable 4.x.
|
|
80
|
+
The pin is exact on purpose: Effect's beta line makes breaking changes between
|
|
81
|
+
builds, so a looser range surfaces as `ERESOLVE` install conflicts. It relaxes
|
|
82
|
+
to a caret range once Effect reaches a stable 4.x.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@konfig.ts/core",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Core abstractions (Manifest, RenderContext, Helm, deps) for konfig.ts — typesafe Kubernetes + ArgoCD config in TypeScript.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "David Stahl-Gruber",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"postpack": "node ../../scripts/prepack-exports.mjs restore"
|
|
52
52
|
},
|
|
53
53
|
"dependencies": {
|
|
54
|
-
"yaml": "
|
|
54
|
+
"yaml": "^2.8.3"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@effect/platform-node": "4.0.0-beta.70",
|
|
@@ -63,13 +63,13 @@
|
|
|
63
63
|
}
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@effect/platform-node": "
|
|
67
|
-
"@effect/vitest": "
|
|
68
|
-
"@types/bun": "
|
|
69
|
-
"effect": "
|
|
70
|
-
"fast-check": "
|
|
71
|
-
"typescript": "
|
|
72
|
-
"vitest": "
|
|
73
|
-
"yaml": "
|
|
66
|
+
"@effect/platform-node": "4.0.0-beta.70",
|
|
67
|
+
"@effect/vitest": "4.0.0-beta.70",
|
|
68
|
+
"@types/bun": "^1.3.6",
|
|
69
|
+
"effect": "4.0.0-beta.70",
|
|
70
|
+
"fast-check": "^4.7.0",
|
|
71
|
+
"typescript": "^5.8.3",
|
|
72
|
+
"vitest": "^4.0.18",
|
|
73
|
+
"yaml": "^2.8.3"
|
|
74
74
|
}
|
|
75
75
|
}
|