alchemy-kubernetes-addons 0.1.1 → 0.1.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 CHANGED
@@ -4,6 +4,24 @@ Composable Kubernetes resources built on Alchemy's public cluster-adapter API.
4
4
  They work with any `Kubernetes.ClusterLike`, including Hetzner K3s, Docker K3s,
5
5
  EKS, and kubeconfig connections.
6
6
 
7
+ ## Cloudflare Tunnel ingress
8
+
9
+ `CloudflareTunnelIngress` installs STRRL's pinned ingress controller, a
10
+ namespace, and a separate scoped Cloudflare token. Supply `cluster`,
11
+ `accountId`, `zoneId`, and a cluster-unique `tunnelName`; optionally supply an
12
+ external Redacted `token`. It returns the chart, namespace, release name, tunnel
13
+ name, and `ingressClassName: "cloudflare-tunnel"`. It never makes that class
14
+ default.
15
+
16
+ `dependsOn` accepts a non-secret resource dependency, such as a hostname
17
+ admission binding. Helm receives only a Secret reference; Secret revisions
18
+ restart the controller after rotation. The upstream controller can overwrite an
19
+ existing CNAME for a claimed hostname. Check ownership and gate allowed
20
+ hostnames before using it in a shared zone; the
21
+ [Kairos stack](../../examples/kairos-platform/README.md) implements both. Do not
22
+ run ExternalDNS against the same hosts. Remove ingress routes and wait for DNS
23
+ cleanup before uninstalling; the tunnel is retained.
24
+
7
25
  Add the provider beside Alchemy's Kubernetes provider:
8
26
 
9
27
  ```ts
package/dist/index.d.mts CHANGED
@@ -28,6 +28,37 @@ declare class KubernetesReadinessError extends Error {
28
28
  constructor(message: string, object: KubernetesObjectRef, timedOut: boolean);
29
29
  }
30
30
  //#endregion
31
+ //#region src/cloudflare-tunnel-ingress.d.ts
32
+ declare const CLOUDFLARE_TUNNEL_CHART = "cloudflare-tunnel-ingress-controller";
33
+ declare const CLOUDFLARE_TUNNEL_REPO = "https://helm.strrl.dev";
34
+ declare const CLOUDFLARE_TUNNEL_VERSION = "0.1.0";
35
+ declare const CLOUDFLARED_VERSION = "2026.7.3-host-metrics.1";
36
+ interface CloudflareTunnelIngressProps {
37
+ cluster: Input<Kubernetes.ClusterLike>;
38
+ accountId: string;
39
+ zoneId: string;
40
+ tunnelName: string;
41
+ namespace?: string;
42
+ releaseName?: string;
43
+ token?: Input<Redacted.Redacted<string>>;
44
+ /** Non-secret dependency, e.g. the hostname admission binding revision. */
45
+ dependsOn?: Input<string>;
46
+ timeoutSeconds?: number;
47
+ }
48
+ declare const cloudflareTunnelTokenPolicies: (accountId: string, zoneId: string) => Cloudflare.ApiToken.Policy[];
49
+ declare const cloudflareTunnelHelmValues: (releaseName: string, secretName: string, secretRevision: Input<string>, dependsOn?: Input<string>) => Record<string, unknown>;
50
+ /** Install an explicit-class tunnel controller with a separate scoped token.
51
+ * The upstream controller can overwrite existing CNAMEs: callers must check
52
+ * hostname ownership and restrict admission before handing it a shared zone.
53
+ */
54
+ declare const CloudflareTunnelIngress: (id: string, props: CloudflareTunnelIngressProps) => Effect.Effect<{
55
+ namespace: string;
56
+ releaseName: string;
57
+ tunnelName: string;
58
+ ingressClassName: string;
59
+ chart: Kubernetes.HelmChart;
60
+ }, import("effect/Cause").UnknownError, Providers | Kubernetes.Providers | Cloudflare.Providers>;
61
+ //#endregion
31
62
  //#region src/cloudflare-external-dns.d.ts
32
63
  declare const EXTERNAL_DNS_CHART_VERSION = "1.21.1";
33
64
  declare const EXTERNAL_DNS_IMAGE = "registry.k8s.io/external-dns/external-dns:v0.21.0@sha256:f53faaf71cb270d1ca9dce6ea0c94bfebf1a18696263487f0fbc74b9bf2bd7ff";
@@ -524,4 +555,4 @@ interface ReadyHelmChartProps extends Omit<Kubernetes.HelmChartProps, "cluster"
524
555
  declare const ReadyHelmChart: (id: string, props: ReadyHelmChartProps) => Effect.Effect<Kubernetes.HelmChart, never, Providers | Kubernetes.Providers>;
525
556
  declare const providers: () => Layer.Layer<Providers | Provider.Provider<import("alchemy").Random>, never, never>;
526
557
  //#endregion
527
- export { CERT_MANAGER_CHART, CERT_MANAGER_CHART_VERSION, CERT_MANAGER_IMAGE_VERSION, CONTAINER_REGISTRY_CHART, CONTAINER_REGISTRY_CHART_VERSION, CONTAINER_REGISTRY_IMAGE, CertManager, type CertManagerProps, type CertManagerResult, CloudflareAcmeIssuer, type CloudflareAcmeIssuerProps, type CloudflareAcmeIssuerResult, CloudflareExternalDns, type CloudflareExternalDnsPlan, type CloudflareExternalDnsProps, type CloudflareExternalDnsResult, ContainerRegistry, type ContainerRegistryGarbageCollectionProps, type ContainerRegistryIngressProps, type ContainerRegistryProps, type ContainerRegistryPullSecretRef, type ContainerRegistryPullSecretsProps, type ContainerRegistryResult, EXTERNAL_DNS_CHART_VERSION, EXTERNAL_DNS_IMAGE, type KubernetesObjectReadiness, type KubernetesObjectRef, KubernetesReadinessError, LETSENCRYPT_PRODUCTION_URL, LETSENCRYPT_STAGING_URL, OTEL_COLLECTOR_CHART_VERSION, OTEL_COLLECTOR_IMAGE, OtelCollector, type OtelCollectorBasicAuthSecretRef, type OtelCollectorDestination, type OtelCollectorPlan, type OtelCollectorProps, type OtelCollectorResult, type OtelSignal, PARSEABLE_CHART_VERSION, PARSEABLE_IMAGE, Parseable, type ParseableIngressProps, type ParseableOtlpEndpoints, type ParseableProps, type ParseableResult, type ParseableStagingProps, Providers, Readiness, ReadinessProps, ReadinessProvider, ReadyHelmChart, ReadyHelmChartProps, Secret, SecretProps, SecretProvider, certManagerHelmValues, cloudflareAcmeIssuerManifest, cloudflareAcmeIssuerName, cloudflareAcmeIssuerTokenPolicies, containerRegistryDockerConfig, containerRegistryHelmValues, containerRegistryHtpasswd, kubernetesObjectReadiness, parseableHelmValues, planCloudflareExternalDns, planOtelCollector, providers, validateCertManagerProps, validateCloudflareAcmeIssuerProps, validateContainerRegistryProps };
558
+ export { CERT_MANAGER_CHART, CERT_MANAGER_CHART_VERSION, CERT_MANAGER_IMAGE_VERSION, CLOUDFLARED_VERSION, CLOUDFLARE_TUNNEL_CHART, CLOUDFLARE_TUNNEL_REPO, CLOUDFLARE_TUNNEL_VERSION, CONTAINER_REGISTRY_CHART, CONTAINER_REGISTRY_CHART_VERSION, CONTAINER_REGISTRY_IMAGE, CertManager, type CertManagerProps, type CertManagerResult, CloudflareAcmeIssuer, type CloudflareAcmeIssuerProps, type CloudflareAcmeIssuerResult, CloudflareExternalDns, type CloudflareExternalDnsPlan, type CloudflareExternalDnsProps, type CloudflareExternalDnsResult, CloudflareTunnelIngress, type CloudflareTunnelIngressProps, ContainerRegistry, type ContainerRegistryGarbageCollectionProps, type ContainerRegistryIngressProps, type ContainerRegistryProps, type ContainerRegistryPullSecretRef, type ContainerRegistryPullSecretsProps, type ContainerRegistryResult, EXTERNAL_DNS_CHART_VERSION, EXTERNAL_DNS_IMAGE, type KubernetesObjectReadiness, type KubernetesObjectRef, KubernetesReadinessError, LETSENCRYPT_PRODUCTION_URL, LETSENCRYPT_STAGING_URL, OTEL_COLLECTOR_CHART_VERSION, OTEL_COLLECTOR_IMAGE, OtelCollector, type OtelCollectorBasicAuthSecretRef, type OtelCollectorDestination, type OtelCollectorPlan, type OtelCollectorProps, type OtelCollectorResult, type OtelSignal, PARSEABLE_CHART_VERSION, PARSEABLE_IMAGE, Parseable, type ParseableIngressProps, type ParseableOtlpEndpoints, type ParseableProps, type ParseableResult, type ParseableStagingProps, Providers, Readiness, ReadinessProps, ReadinessProvider, ReadyHelmChart, ReadyHelmChartProps, Secret, SecretProps, SecretProvider, certManagerHelmValues, cloudflareAcmeIssuerManifest, cloudflareAcmeIssuerName, cloudflareAcmeIssuerTokenPolicies, cloudflareTunnelHelmValues, cloudflareTunnelTokenPolicies, containerRegistryDockerConfig, containerRegistryHelmValues, containerRegistryHtpasswd, kubernetesObjectReadiness, parseableHelmValues, planCloudflareExternalDns, planOtelCollector, providers, validateCertManagerProps, validateCloudflareAcmeIssuerProps, validateContainerRegistryProps };
package/dist/index.mjs CHANGED
@@ -10,8 +10,8 @@ import * as Redacted from "effect/Redacted";
10
10
  import * as Duration from "effect/Duration";
11
11
  import { request } from "node:http";
12
12
  import { request as request$1 } from "node:https";
13
- import { createHash } from "node:crypto";
14
13
  import * as Cloudflare from "alchemy/Cloudflare";
14
+ import { createHash } from "node:crypto";
15
15
  import { encodeBase64, hashSync, truncates } from "bcryptjs";
16
16
  //#region src/client.ts
17
17
  var KubernetesApiError = class extends Error {
@@ -248,6 +248,119 @@ const waitForObjectsReady = Effect.fn(function* ({ transport, objects, timeoutSe
248
248
  }
249
249
  });
250
250
  //#endregion
251
+ //#region src/cloudflare-tunnel-ingress.ts
252
+ const CLOUDFLARE_TUNNEL_CHART = "cloudflare-tunnel-ingress-controller";
253
+ const CLOUDFLARE_TUNNEL_REPO = "https://helm.strrl.dev";
254
+ const CLOUDFLARE_TUNNEL_VERSION = "0.1.0";
255
+ const CLOUDFLARED_VERSION = "2026.7.3-host-metrics.1";
256
+ const cloudflareTunnelTokenPolicies = (accountId, zoneId) => [{
257
+ effect: "allow",
258
+ permissionGroups: [
259
+ "Zone Read",
260
+ "DNS Read",
261
+ "DNS Write"
262
+ ],
263
+ resources: { [`com.cloudflare.api.account.zone.${zoneId}`]: "*" }
264
+ }, {
265
+ effect: "allow",
266
+ permissionGroups: ["Cloudflare Tunnel Write"],
267
+ resources: { [`com.cloudflare.api.account.${accountId}`]: "*" }
268
+ }];
269
+ const cloudflareTunnelHelmValues = (releaseName, secretName, secretRevision, dependsOn = "none") => ({
270
+ fullnameOverride: releaseName,
271
+ cloudflare: { secretRef: {
272
+ name: secretName,
273
+ accountIDKey: "account-id",
274
+ tunnelNameKey: "tunnel-name",
275
+ apiTokenKey: "api-token"
276
+ } },
277
+ ingressClass: {
278
+ name: "cloudflare-tunnel",
279
+ isDefaultClass: false
280
+ },
281
+ replicaCount: 1,
282
+ image: { tag: CLOUDFLARE_TUNNEL_VERSION },
283
+ podAnnotations: {
284
+ "alchemy.run/secret-revision": secretRevision,
285
+ "alchemy.run/dependency": dependsOn
286
+ },
287
+ securityContext: {
288
+ allowPrivilegeEscalation: false,
289
+ capabilities: { drop: ["ALL"] },
290
+ readOnlyRootFilesystem: true
291
+ },
292
+ cloudflared: {
293
+ image: { tag: CLOUDFLARED_VERSION },
294
+ replicaCount: 1,
295
+ podAntiAffinity: false,
296
+ resources: {
297
+ requests: {
298
+ cpu: "50m",
299
+ memory: "64Mi"
300
+ },
301
+ limits: { memory: "256Mi" }
302
+ }
303
+ },
304
+ serviceMonitor: { create: false }
305
+ });
306
+ /** Install an explicit-class tunnel controller with a separate scoped token.
307
+ * The upstream controller can overwrite existing CNAMEs: callers must check
308
+ * hostname ownership and restrict admission before handing it a shared zone.
309
+ */
310
+ const CloudflareTunnelIngress = (id, props) => Effect.gen(function* () {
311
+ const namespaceName = props.namespace ?? "cloudflare-tunnel-system";
312
+ const releaseName = props.releaseName ?? "cloudflare-tunnel";
313
+ yield* Effect.try(() => {
314
+ for (const name of [
315
+ namespaceName,
316
+ releaseName,
317
+ props.tunnelName
318
+ ]) if (!/^[a-z0-9](?:[-a-z0-9]{0,51}[a-z0-9])?$/.test(name)) throw new Error(`Invalid Cloudflare tunnel resource name: ${name}`);
319
+ for (const value of [props.accountId, props.zoneId]) if (!/^[a-f0-9]{32}$/.test(value)) throw new Error("Cloudflare accountId and zoneId must be 32 hex digits");
320
+ });
321
+ const namespace = yield* Kubernetes.Manifest(`${id}Namespace`, {
322
+ cluster: props.cluster,
323
+ manifest: {
324
+ apiVersion: "v1",
325
+ kind: "Namespace",
326
+ metadata: { name: namespaceName }
327
+ }
328
+ });
329
+ const token = props.token ?? (yield* Cloudflare.ApiToken.AccountApiToken(`${id}Token`, {
330
+ accountId: props.accountId,
331
+ policies: cloudflareTunnelTokenPolicies(props.accountId, props.zoneId)
332
+ })).value;
333
+ const secretName = `${releaseName}-credentials`;
334
+ const secret = yield* Secret(`${id}Credentials`, {
335
+ cluster: props.cluster,
336
+ namespace: namespace.name,
337
+ name: secretName,
338
+ stringData: {
339
+ "account-id": props.accountId,
340
+ "tunnel-name": props.tunnelName,
341
+ "api-token": token
342
+ }
343
+ });
344
+ const chart = yield* ReadyHelmChart(`${id}Chart`, {
345
+ cluster: props.cluster,
346
+ chart: CLOUDFLARE_TUNNEL_CHART,
347
+ repo: CLOUDFLARE_TUNNEL_REPO,
348
+ version: CLOUDFLARE_TUNNEL_VERSION,
349
+ namespace: namespaceName,
350
+ releaseName,
351
+ createNamespace: false,
352
+ timeoutSeconds: props.timeoutSeconds ?? 300,
353
+ values: cloudflareTunnelHelmValues(releaseName, secretName, Output.map(secret.resourceVersion, (version) => version ?? "created"), props.dependsOn)
354
+ });
355
+ return {
356
+ namespace: namespaceName,
357
+ releaseName,
358
+ tunnelName: props.tunnelName,
359
+ ingressClassName: "cloudflare-tunnel",
360
+ chart
361
+ };
362
+ });
363
+ //#endregion
251
364
  //#region src/cloudflare-external-dns.ts
252
365
  const EXTERNAL_DNS_CHART_VERSION = "1.21.1";
253
366
  const EXTERNAL_DNS_IMAGE_REPOSITORY = "registry.k8s.io/external-dns/external-dns";
@@ -1597,4 +1710,4 @@ const ReadyHelmChart = (id, props) => Effect.gen(function* () {
1597
1710
  });
1598
1711
  const providers = () => Layer.effect(Providers, Provider.collection([Secret, Readiness])).pipe(Layer.provide(Layer.mergeAll(SecretProvider(), ReadinessProvider())), Layer.provideMerge(RandomProvider()));
1599
1712
  //#endregion
1600
- export { CERT_MANAGER_CHART, CERT_MANAGER_CHART_VERSION, CERT_MANAGER_IMAGE_VERSION, CONTAINER_REGISTRY_CHART, CONTAINER_REGISTRY_CHART_VERSION, CONTAINER_REGISTRY_IMAGE, CertManager, CloudflareAcmeIssuer, CloudflareExternalDns, ContainerRegistry, EXTERNAL_DNS_CHART_VERSION, EXTERNAL_DNS_IMAGE, KubernetesReadinessError, LETSENCRYPT_PRODUCTION_URL, LETSENCRYPT_STAGING_URL, OTEL_COLLECTOR_CHART_VERSION, OTEL_COLLECTOR_IMAGE, OtelCollector, PARSEABLE_CHART_VERSION, PARSEABLE_IMAGE, Parseable, Providers, Readiness, ReadinessProvider, ReadyHelmChart, Secret, SecretProvider, certManagerHelmValues, cloudflareAcmeIssuerManifest, cloudflareAcmeIssuerName, cloudflareAcmeIssuerTokenPolicies, containerRegistryDockerConfig, containerRegistryHelmValues, containerRegistryHtpasswd, kubernetesObjectReadiness, parseableHelmValues, planCloudflareExternalDns, planOtelCollector, providers, validateCertManagerProps, validateCloudflareAcmeIssuerProps, validateContainerRegistryProps };
1713
+ export { CERT_MANAGER_CHART, CERT_MANAGER_CHART_VERSION, CERT_MANAGER_IMAGE_VERSION, CLOUDFLARED_VERSION, CLOUDFLARE_TUNNEL_CHART, CLOUDFLARE_TUNNEL_REPO, CLOUDFLARE_TUNNEL_VERSION, CONTAINER_REGISTRY_CHART, CONTAINER_REGISTRY_CHART_VERSION, CONTAINER_REGISTRY_IMAGE, CertManager, CloudflareAcmeIssuer, CloudflareExternalDns, CloudflareTunnelIngress, ContainerRegistry, EXTERNAL_DNS_CHART_VERSION, EXTERNAL_DNS_IMAGE, KubernetesReadinessError, LETSENCRYPT_PRODUCTION_URL, LETSENCRYPT_STAGING_URL, OTEL_COLLECTOR_CHART_VERSION, OTEL_COLLECTOR_IMAGE, OtelCollector, PARSEABLE_CHART_VERSION, PARSEABLE_IMAGE, Parseable, Providers, Readiness, ReadinessProvider, ReadyHelmChart, Secret, SecretProvider, certManagerHelmValues, cloudflareAcmeIssuerManifest, cloudflareAcmeIssuerName, cloudflareAcmeIssuerTokenPolicies, cloudflareTunnelHelmValues, cloudflareTunnelTokenPolicies, containerRegistryDockerConfig, containerRegistryHelmValues, containerRegistryHtpasswd, kubernetesObjectReadiness, parseableHelmValues, planCloudflareExternalDns, planOtelCollector, providers, validateCertManagerProps, validateCloudflareAcmeIssuerProps, validateContainerRegistryProps };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alchemy-kubernetes-addons",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Composable Kubernetes add-ons and safe primitives for Alchemy",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",