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 +18 -0
- package/dist/index.d.mts +32 -1
- package/dist/index.mjs +115 -2
- package/package.json +1 -1
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 };
|