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 +17 -0
- package/README.md +327 -0
- package/dist/index.d.mts +527 -0
- package/dist/index.mjs +1600 -0
- package/package.json +53 -0
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.
|