alchemy-kubernetes-addons 0.1.0-alpha.0

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 ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 alchemy-k3s contributors
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # alchemy-kubernetes-addons
2
+
3
+ Composable Kubernetes resources built on Alchemy's public cluster-adapter API.
4
+ They work with any `Kubernetes.ClusterLike`, including Hetzner K3s, Docker K3s,
5
+ EKS, and kubeconfig connections.
6
+
7
+ Add the provider beside Alchemy's Kubernetes provider:
8
+
9
+ ```ts
10
+ providers: Layer.mergeAll(
11
+ Kubernetes.providers(),
12
+ KubernetesAddons.providers(),
13
+ ),
14
+ ```
15
+
16
+ ## Secrets
17
+
18
+ `Secret` converts literal and lazy Effect values to Effect `Redacted`, maps
19
+ Config and Output values to `Redacted` before provider diffing and state
20
+ persistence, unwraps them only in the Kubernetes PATCH request, and never
21
+ returns Secret data from reads:
22
+
23
+ ```ts
24
+ const credentials =
25
+ yield *
26
+ KubernetesAddons.Secret("Credentials", {
27
+ cluster,
28
+ namespace: "external-dns",
29
+ name: "cloudflare-api-token",
30
+ stringData: { "api-token": token.value },
31
+ });
32
+ ```
33
+
34
+ Use `Kubernetes.Manifest` only for public manifest data. Redacted desired inputs
35
+ still exist in Alchemy state so updates can be detected; production stacks
36
+ require encrypted remote state.
37
+
38
+ ## Ready Helm charts
39
+
40
+ `ReadyHelmChart` delegates rendering and ownership to Alchemy's existing
41
+ `Kubernetes.HelmChart`, then waits with a bounded deadline for CRDs,
42
+ Deployments, DaemonSets, StatefulSets, and Jobs:
43
+
44
+ ```ts
45
+ const chart =
46
+ yield *
47
+ KubernetesAddons.ReadyHelmChart("Controller", {
48
+ cluster,
49
+ chart: "controller",
50
+ repo: "https://charts.example.com",
51
+ version: "1.2.3",
52
+ namespace: "controller",
53
+ timeoutSeconds: 300,
54
+ values: { existingSecret: credentials.name },
55
+ });
56
+ ```
57
+
58
+ Credentials must be referenced by Secret name/key rather than embedded in Helm
59
+ values. Readiness errors expose object identity, status codes, replica counts,
60
+ and condition type/status only; manifest bodies and condition text are not
61
+ included.
62
+
63
+ ## Cloudflare ExternalDNS
64
+
65
+ Create or explicitly adopt the Cloudflare zone separately, then pass that
66
+ resource to the add-on:
67
+
68
+ ```ts
69
+ import { adopt } from "alchemy/AdoptPolicy";
70
+
71
+ const zone =
72
+ yield *
73
+ Cloudflare.Zone.Zone("PublicZone", {
74
+ name: "example.com",
75
+ }).pipe(adopt(true));
76
+
77
+ const publicDns =
78
+ yield *
79
+ KubernetesAddons.CloudflareExternalDns("PublicDns", {
80
+ cluster,
81
+ zone,
82
+ policy: "sync",
83
+ proxied: true,
84
+ });
85
+ ```
86
+
87
+ Omit `adopt(true)` when Alchemy should create a new zone. Cloudflare zones are
88
+ retained by default, and this add-on never creates or destroys the zone
89
+ implicitly. Registrar nameserver delegation also remains external.
90
+
91
+ Unless `token` is supplied, Alchemy mints an account-owned runtime token with
92
+ only `Zone Read`, `DNS Read`, and `DNS Write`, scoped to exactly
93
+ `com.cloudflare.api.account.zone.<zoneId>`. The deployment credential therefore
94
+ needs `Account API Tokens Write`; it additionally needs `Zone Read` to adopt a
95
+ zone or `Zone Write` to create one. Configure it using `alchemy login` or
96
+ `CLOUDFLARE_ACCOUNT_ID` plus `CLOUDFLARE_API_TOKEN`, never as source code.
97
+
98
+ The runtime token is written through `KubernetesAddons.Secret`. Helm receives
99
+ only the Secret name/key, while the Secret resource version rolls and re-waits
100
+ the controller after rotation. A pre-created Redacted token can be passed with
101
+ an optional non-secret `tokenRevision` when its rotation cannot otherwise be
102
+ observed safely.
103
+
104
+ ExternalDNS watches only Services and Ingresses, filters both the zone ID and
105
+ domain, and records ownership with a stack/stage/resource-specific TXT owner ID.
106
+ It exclusively owns the A/AAAA/CNAME records it derives and their registry TXT
107
+ records. Do not declare the same records with `Cloudflare.DNS.Record`.
108
+ cert-manager separately owns `_acme-challenge` TXT records.
109
+
110
+ With `policy: "sync"`, remove an application's Service/Ingress DNS declaration
111
+ and wait for reconciliation before destroying ExternalDNS; then its owned
112
+ records are removed. Destroying the controller itself deliberately performs no
113
+ zone-wide sweep, so records left behind require explicit cleanup. Use
114
+ `upsert-only` when record deletion is not authorized.
115
+
116
+ ## cert-manager and Cloudflare ACME
117
+
118
+ Install cert-manager independently of DNS and application workloads, then add an
119
+ issuer for one explicit Cloudflare zone:
120
+
121
+ ```ts
122
+ const certManager =
123
+ yield *
124
+ KubernetesAddons.CertManager("Certificates", {
125
+ cluster,
126
+ });
127
+
128
+ const issuer =
129
+ yield *
130
+ KubernetesAddons.CloudflareAcmeIssuer("LetsEncrypt", {
131
+ cluster,
132
+ certManager,
133
+ zone,
134
+ email: "platform@example.com",
135
+ environment: "staging",
136
+ });
137
+ ```
138
+
139
+ `CertManager` installs the pinned upstream OCI chart, owns its CRDs, hardens the
140
+ controller, webhook, and CA injector containers, and returns only after all
141
+ three workloads and the CRDs are ready. `CloudflareAcmeIssuer` then creates a
142
+ zone-scoped account token with only `Zone Read` and `DNS Write`, writes it
143
+ through the write-only Secret resource, and waits for its `ClusterIssuer` to
144
+ become ready. The returned `issuerRef.name` carries that readiness dependency,
145
+ so a directly composed Certificate is not submitted against an unready issuer.
146
+
147
+ ExternalDNS and ACME receive separate tokens by default. That lets either
148
+ credential be rotated or revoked independently: ExternalDNS needs DNS Read for
149
+ its registry, while cert-manager does not. Supplying the same pre-created
150
+ `Redacted` token to both add-ons intentionally couples their permissions,
151
+ rotation, and outage domain.
152
+
153
+ Applications own their domains, Certificate manifests, TLS Secret names, and
154
+ Ingress/Gateway references:
155
+
156
+ ```ts
157
+ yield *
158
+ Kubernetes.Manifest("ApiCertificate", {
159
+ cluster,
160
+ manifest: {
161
+ apiVersion: "cert-manager.io/v1",
162
+ kind: "Certificate",
163
+ metadata: { name: "api-tls", namespace: "api" },
164
+ spec: {
165
+ secretName: "api-tls",
166
+ dnsNames: ["api.example.com"],
167
+ issuerRef: issuer.issuerRef,
168
+ },
169
+ },
170
+ });
171
+ ```
172
+
173
+ cert-manager generates and rotates the private key inside Kubernetes; Alchemy
174
+ state contains the public Certificate request but not the issued TLS Secret.
175
+ cert-manager exclusively owns temporary `_acme-challenge` records and removes
176
+ them after validation. It also owns the stable ACME account-key Secret named by
177
+ the issuer. Do not model either kind of record with `Cloudflare.DNS.Record`.
178
+
179
+ Always prove a new setup with `environment: "staging"`. The production smoke
180
+ check is deliberately manual to avoid consuming Let's Encrypt production rate
181
+ limits:
182
+
183
+ 1. Create a distinct production issuer logical resource with
184
+ `environment: "production"`.
185
+ 2. Request one Certificate for a unique hostname and wait for
186
+ `Certificate Ready=True`.
187
+ 3. Inspect the public certificate issuer and confirm it is not a staging chain;
188
+ never print the TLS private key.
189
+ 4. Remove that Certificate and issuer from the stack, then confirm the exact
190
+ `_acme-challenge` record and managed token are gone while the Zone remains.
191
+
192
+ ## Parseable
193
+
194
+ `Parseable` composes a Namespace, write-only Secret, pinned upstream Helm chart,
195
+ readiness gate, and optional Ingress. Permanent telemetry lives in the supplied
196
+ `S3BucketAccess`; the local PVC is only the durable staging queue:
197
+
198
+ ```ts
199
+ const parseable =
200
+ yield *
201
+ KubernetesAddons.Parseable("Observability", {
202
+ cluster,
203
+ storage: observabilityBucket,
204
+ staging: { size: "5Gi", storageClass: "hcloud-volumes" },
205
+ ingress: {
206
+ host: "observe.example.com",
207
+ className: "traefik",
208
+ tlsSecretName: "observe-tls",
209
+ },
210
+ });
211
+ ```
212
+
213
+ Omit `ingress` for the safer ClusterIP-only default. The add-on does not install
214
+ an ingress controller, manage DNS, or issue the referenced TLS Secret. The
215
+ bundled OSS UI and the ingestion/query APIs use the same service, so an Ingress
216
+ exposes all of them.
217
+
218
+ The flat `otel*Endpoint` outputs match standard OTEL environment-variable names.
219
+ `endpoints` implements the endpoint portion of `Alchemy.Telemetry.OtlpOptions`
220
+ and includes the non-secret `X-P-Stream` and `X-P-Log-Source` headers. Streams
221
+ default to `otel-logs`, `otel-traces`, and `otel-metrics` and can be renamed
222
+ with `streams`. `credentialsSecretRef` lets an in-cluster collector mount the
223
+ Parseable Basic credentials without putting them in Helm values. Temporary S3
224
+ session credentials are rejected because the pinned Parseable chart does not
225
+ support them.
226
+
227
+ ## S3-backed container registry
228
+
229
+ `ContainerRegistry` deploys a private Docker-compatible OCI registry using the
230
+ headless `zot-minimal` image. S3 is authoritative; the pod has no persistent
231
+ volume, and destroying the add-on never deletes the separately owned bucket or
232
+ its objects:
233
+
234
+ ```ts
235
+ const registry =
236
+ yield *
237
+ KubernetesAddons.ContainerRegistry("Images", {
238
+ cluster,
239
+ storage: registryBucket,
240
+ ingress: {
241
+ host: "registry.example.com",
242
+ className: "traefik",
243
+ tlsSecretName: "registry-tls",
244
+ },
245
+ pullSecrets: {
246
+ namespaces: ["api", "workers"],
247
+ name: "private-registry",
248
+ },
249
+ });
250
+ ```
251
+
252
+ The HTTPS Ingress is required because Docker basic credentials must not travel
253
+ over plaintext. The referenced TLS Secret must be in the registry namespace; the
254
+ add-on does not install an ingress controller, manage DNS, or issue the
255
+ certificate. Its Service remains `ClusterIP`, so the Ingress is the only public
256
+ exposure. Blob redirects are disabled: clients talk only to the registry and do
257
+ not need direct access to the S3 endpoint.
258
+
259
+ The returned Redacted `registry.credentials.password` and username are used for
260
+ external `docker login` and push. The add-on also writes
261
+ `kubernetes.io/dockerconfigjson` Secrets into the explicitly listed,
262
+ pre-existing application namespaces and returns their references. Kubernetes
263
+ pull Secrets are namespace-local, which is why namespaces must be named rather
264
+ than receiving one cluster-wide credential.
265
+
266
+ Anonymous access is denied. The single generated account has read, create,
267
+ update, and delete access. Its password and an independent bcrypt salt are
268
+ stable Alchemy Random resources; Zot receives only a cost-12 bcrypt entry.
269
+ Storage keys, optional session tokens, bcrypt data, and Docker configs go
270
+ through write-only Secrets and never Helm values. Rotating either credential
271
+ updates the corresponding Secret and rolls the one-replica deployment.
272
+
273
+ Garbage collection runs inside Zot every 24 hours by default, starts only in the
274
+ `02:00-04:00` UTC window, and waits 24 hours before reclaiming untagged,
275
+ unreferenced content. `garbageCollection` can override those Zot/Go durations
276
+ and the UTC window. `storagePrefix` defaults to `registry`, allowing the
277
+ consumer to reserve a collision-free key prefix without putting prefix policy in
278
+ `S3BucketAccess`.
279
+
280
+ By default the add-on owns a new `registry` namespace. Set
281
+ `createNamespace: false` when a Certificate or another stack already owns that
282
+ namespace. Destroying an owned registry namespace also removes its pull Secret
283
+ only when that pull Secret lives there; pull Secrets in application namespaces
284
+ are deleted individually without deleting those namespaces.
285
+
286
+ ## OpenTelemetry collector gateway
287
+
288
+ `OtelCollector` is a destination adapter for any `Kubernetes.ClusterLike`. It
289
+ accepts the endpoint shape from `Alchemy.Telemetry.OtlpOptions`, adds optional
290
+ Basic authentication from a namespaced Secret, and exposes Axiom-shaped
291
+ in-cluster OTLP/HTTP endpoints:
292
+
293
+ ```ts
294
+ const collector =
295
+ yield *
296
+ KubernetesAddons.OtelCollector("TelemetryGateway", {
297
+ cluster,
298
+ destination: {
299
+ endpoints: parseable.endpoints,
300
+ authentication: {
301
+ type: "basic",
302
+ secretRef: parseable.credentialsSecretRef,
303
+ },
304
+ },
305
+ });
306
+ ```
307
+
308
+ When authentication is configured, the collector runs in the Secret's namespace
309
+ because Kubernetes cannot reference Secrets across namespaces. It adopts that
310
+ namespace instead of claiming ownership. Destination headers are stored in a
311
+ collector-owned Secret; the Helm values and generated ConfigMap contain only
312
+ environment-variable placeholders. Secret resource versions are copied to pod
313
+ annotations so a credential update rolls the Deployment.
314
+
315
+ Applications send unauthenticated OTLP/HTTP to the ClusterIP-only gateway:
316
+
317
+ ```ts
318
+ vars: {
319
+ OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: collector.otelTracesEndpoint,
320
+ OTEL_EXPORTER_OTLP_LOGS_ENDPOINT: collector.otelLogsEndpoint,
321
+ }
322
+ ```
323
+
324
+ The first release deliberately enables only port 4318 and only destination
325
+ pipelines that were configured. It does not expose Ingress, OTLP/gRPC, host log
326
+ collection, Kubernetes event collection, or cluster-wide scraping; those need
327
+ different trust and RBAC boundaries.