@alchemy.run/kubernetes 0.0.0 → 2.0.0-beta.85

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.
Files changed (94) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +41 -4
  3. package/lib/BuiltinAdapters.d.ts +43 -0
  4. package/lib/BuiltinAdapters.d.ts.map +1 -0
  5. package/lib/BuiltinAdapters.js +129 -0
  6. package/lib/BuiltinAdapters.js.map +1 -0
  7. package/lib/Client.d.ts +97 -0
  8. package/lib/Client.d.ts.map +1 -0
  9. package/lib/Client.js +312 -0
  10. package/lib/Client.js.map +1 -0
  11. package/lib/ClusterAdapter.d.ts +324 -0
  12. package/lib/ClusterAdapter.d.ts.map +1 -0
  13. package/lib/ClusterAdapter.js +65 -0
  14. package/lib/ClusterAdapter.js.map +1 -0
  15. package/lib/Connection.d.ts +210 -0
  16. package/lib/Connection.d.ts.map +1 -0
  17. package/lib/Connection.js +58 -0
  18. package/lib/Connection.js.map +1 -0
  19. package/lib/Deployment.d.ts +401 -0
  20. package/lib/Deployment.d.ts.map +1 -0
  21. package/lib/Deployment.js +550 -0
  22. package/lib/Deployment.js.map +1 -0
  23. package/lib/HelmChart.d.ts +147 -0
  24. package/lib/HelmChart.d.ts.map +1 -0
  25. package/lib/HelmChart.js +248 -0
  26. package/lib/HelmChart.js.map +1 -0
  27. package/lib/Job.d.ts +333 -0
  28. package/lib/Job.d.ts.map +1 -0
  29. package/lib/Job.js +443 -0
  30. package/lib/Job.js.map +1 -0
  31. package/lib/LocalCluster.d.ts +243 -0
  32. package/lib/LocalCluster.d.ts.map +1 -0
  33. package/lib/LocalCluster.js +359 -0
  34. package/lib/LocalCluster.js.map +1 -0
  35. package/lib/Manifest.d.ts +157 -0
  36. package/lib/Manifest.d.ts.map +1 -0
  37. package/lib/Manifest.js +205 -0
  38. package/lib/Manifest.js.map +1 -0
  39. package/lib/Objects.d.ts +60 -0
  40. package/lib/Objects.d.ts.map +1 -0
  41. package/lib/Objects.js +132 -0
  42. package/lib/Objects.js.map +1 -0
  43. package/lib/Providers.d.ts +24 -0
  44. package/lib/Providers.d.ts.map +1 -0
  45. package/lib/Providers.js +36 -0
  46. package/lib/Providers.js.map +1 -0
  47. package/lib/index.d.ts +10 -0
  48. package/lib/index.d.ts.map +1 -0
  49. package/lib/index.js +10 -0
  50. package/lib/index.js.map +1 -0
  51. package/lib/internal/declared.d.ts +19 -0
  52. package/lib/internal/declared.d.ts.map +1 -0
  53. package/lib/internal/declared.js +435 -0
  54. package/lib/internal/declared.js.map +1 -0
  55. package/lib/internal/helm.d.ts +65 -0
  56. package/lib/internal/helm.d.ts.map +1 -0
  57. package/lib/internal/helm.js +173 -0
  58. package/lib/internal/helm.js.map +1 -0
  59. package/lib/internal/kind.d.ts +17 -0
  60. package/lib/internal/kind.d.ts.map +1 -0
  61. package/lib/internal/kind.js +31 -0
  62. package/lib/internal/kind.js.map +1 -0
  63. package/lib/internal/kubeconfig.d.ts +79 -0
  64. package/lib/internal/kubeconfig.d.ts.map +1 -0
  65. package/lib/internal/kubeconfig.js +222 -0
  66. package/lib/internal/kubeconfig.js.map +1 -0
  67. package/lib/internal/registry.d.ts +44 -0
  68. package/lib/internal/registry.d.ts.map +1 -0
  69. package/lib/internal/registry.js +72 -0
  70. package/lib/internal/registry.js.map +1 -0
  71. package/lib/internal/workload.d.ts +127 -0
  72. package/lib/internal/workload.d.ts.map +1 -0
  73. package/lib/internal/workload.js +411 -0
  74. package/lib/internal/workload.js.map +1 -0
  75. package/package.json +55 -3
  76. package/src/BuiltinAdapters.ts +202 -0
  77. package/src/Client.ts +496 -0
  78. package/src/ClusterAdapter.ts +373 -0
  79. package/src/Connection.ts +234 -0
  80. package/src/Deployment.ts +872 -0
  81. package/src/HelmChart.ts +388 -0
  82. package/src/Job.ts +709 -0
  83. package/src/LocalCluster.ts +631 -0
  84. package/src/Manifest.ts +301 -0
  85. package/src/Objects.ts +213 -0
  86. package/src/Providers.ts +52 -0
  87. package/src/globals.d.ts +2 -0
  88. package/src/index.ts +9 -0
  89. package/src/internal/declared.ts +471 -0
  90. package/src/internal/helm.ts +245 -0
  91. package/src/internal/kind.ts +43 -0
  92. package/src/internal/kubeconfig.ts +364 -0
  93. package/src/internal/registry.ts +108 -0
  94. package/src/internal/workload.ts +504 -0
@@ -0,0 +1,108 @@
1
+ import type { RegistryCredentials } from "@alchemy.run/docker/Docker";
2
+ import {
3
+ makeContainerImageSource,
4
+ type ImageRegistryTarget,
5
+ } from "@alchemy.run/docker/ImageSource";
6
+ import { createPhysicalName } from "alchemy/PhysicalName";
7
+ /**
8
+ * Image publishing for connections that declare a
9
+ * {@link Connection.registry}: `main` programs and `context` Dockerfiles are
10
+ * built on the deploying machine and pushed to `<server>/<name>:<hash>`,
11
+ * where the cluster's nodes pull them. Used when the cluster's platform
12
+ * adapter has no managed registry of its own (EKS pushes to ECR instead).
13
+ */
14
+ import * as Effect from "effect/Effect";
15
+ import type { RegistryState, WorkloadImageSource } from "../ClusterAdapter.ts";
16
+ import type { ContainerRegistry } from "../Connection.ts";
17
+
18
+ /** Docker credentials for a registry, or `undefined` to use `docker login`. */
19
+ const credentialsOf = (registry: ContainerRegistry): RegistryCredentials | undefined =>
20
+ registry.username !== undefined && registry.password !== undefined
21
+ ? {
22
+ // Docker keys credentials by host; `server` may carry a namespace.
23
+ server: registry.server.split("/")[0]!,
24
+ username: registry.username,
25
+ password: registry.password,
26
+ }
27
+ : undefined;
28
+
29
+ const trimServer = (server: string) => server.replace(/\/+$/, "");
30
+
31
+ export const makeConnectionRegistry = Effect.gen(function* () {
32
+ const images = yield* makeContainerImageSource;
33
+
34
+ const resolve = Effect.fn(function* (options: {
35
+ id: string;
36
+ registry: ContainerRegistry;
37
+ source: WorkloadImageSource;
38
+ platform: string;
39
+ port?: number | undefined;
40
+ isExternal?: boolean | undefined;
41
+ bootstrap: (importPath: string) => string;
42
+ state: Record<string, unknown> | undefined;
43
+ session: { note: (message: string) => Effect.Effect<void> };
44
+ }) {
45
+ const server = trimServer(options.registry.server);
46
+ // Keep the repository stable across updates; a changed `server` starts
47
+ // a new one.
48
+ const repository =
49
+ options.state?.kind === "registry" &&
50
+ options.state.server === server &&
51
+ typeof options.state.repository === "string"
52
+ ? options.state.repository
53
+ : `${server}/${yield* createPhysicalName({
54
+ id: options.id,
55
+ maxLength: 96,
56
+ lowercase: true,
57
+ })}`;
58
+ const target: ImageRegistryTarget = {
59
+ repositoryUri: Effect.succeed(repository),
60
+ // Generic registries have no cheap existence probe; resolve only runs
61
+ // when the workload changes, and the local build cache makes a
62
+ // rebuild of unchanged content fast.
63
+ hasTag: () => Effect.succeed(false),
64
+ credentials: Effect.succeed(credentialsOf(options.registry)),
65
+ };
66
+ const resolved = yield* images.resolve(
67
+ {
68
+ id: options.id,
69
+ source: options.source,
70
+ platform: options.platform,
71
+ port: options.port,
72
+ isExternal: options.isExternal,
73
+ bootstrap: options.bootstrap,
74
+ session: options.session,
75
+ },
76
+ target,
77
+ );
78
+ return {
79
+ imageUri: resolved.imageUri,
80
+ codeHash: resolved.codeHash,
81
+ state: {
82
+ kind: "registry" as const,
83
+ server,
84
+ repository,
85
+ } satisfies RegistryState,
86
+ };
87
+ });
88
+
89
+ const hash = (options: {
90
+ source: WorkloadImageSource;
91
+ platform: string;
92
+ port?: number | undefined;
93
+ isExternal?: boolean | undefined;
94
+ bootstrap: (importPath: string) => string;
95
+ }) =>
96
+ images.hash({
97
+ source: options.source,
98
+ platform: options.platform,
99
+ port: options.port,
100
+ isExternal: options.isExternal,
101
+ bootstrap: options.bootstrap,
102
+ });
103
+
104
+ return { resolve, hash };
105
+ });
106
+
107
+ /** The publisher returned by {@link makeConnectionRegistry}. */
108
+ export interface ConnectionRegistry extends Effect.Success<typeof makeConnectionRegistry> {}
@@ -0,0 +1,504 @@
1
+ import { isInlineDockerfile } from "@alchemy.run/docker/Dockerfile";
2
+ import { hashDirectory } from "alchemy/Command/Memo";
3
+ import type { ResourceBinding } from "alchemy/Resource";
4
+ import { Self } from "alchemy/Self";
5
+ import { unwrapRedacted } from "alchemy/Util/data";
6
+ import { collectRedactedSecrets } from "alchemy/Util/Redaction";
7
+ import { sha256Object } from "alchemy/Util/sha256";
8
+ /**
9
+ * Internal helpers shared by the Kubernetes workload platforms
10
+ * (`Kubernetes.Deployment`, `Kubernetes.Job`): image-source dispatch
11
+ * through the cluster adapter, binding-env collection, connection
12
+ * identity, and the platform-neutral generated container entries.
13
+ */
14
+ import * as Effect from "effect/Effect";
15
+ import * as FileSystem from "effect/FileSystem";
16
+ import * as Path from "effect/Path";
17
+ import * as Redacted from "effect/Redacted";
18
+ import type {
19
+ ClusterAdapterService,
20
+ IdentityState,
21
+ WorkloadBindingContract,
22
+ WorkloadImageSource,
23
+ } from "../ClusterAdapter.ts";
24
+ import type { ClusterLike, Connection, ConnectionAuth } from "../Connection.ts";
25
+ import type { ConnectionRegistry } from "./registry.ts";
26
+
27
+ /**
28
+ * Structural deep merge: objects merge recursively; arrays and primitives
29
+ * from `override` replace the base value wholesale. Powers escape hatches
30
+ * like `Kubernetes.Deployment.podTemplate`.
31
+ */
32
+ export const deepMerge = <T>(base: T, override: unknown): T => {
33
+ if (override === undefined) return base;
34
+ if (
35
+ base === null ||
36
+ override === null ||
37
+ typeof base !== "object" ||
38
+ typeof override !== "object" ||
39
+ Array.isArray(base) ||
40
+ Array.isArray(override)
41
+ ) {
42
+ return override as T;
43
+ }
44
+ const out: Record<string, unknown> = { ...(base as Record<string, unknown>) };
45
+ for (const [key, value] of Object.entries(override)) {
46
+ out[key] =
47
+ key in (base as Record<string, unknown>)
48
+ ? deepMerge((base as Record<string, unknown>)[key], value)
49
+ : value;
50
+ }
51
+ return out as T;
52
+ };
53
+
54
+ /**
55
+ * The image build platform of a workload: its own `architecture`, else the
56
+ * connection's node architecture, else `linux/amd64`.
57
+ */
58
+ export const imagePlatformOf = (
59
+ architecture: "amd64" | "arm64" | undefined,
60
+ connection?: Connection,
61
+ ): string =>
62
+ (architecture ?? connection?.architecture) === "arm64" ? "linux/arm64" : "linux/amd64";
63
+
64
+ /**
65
+ * Best-effort {@link Connection} of a `cluster` prop value — `undefined`
66
+ * instead of throwing, for plan-time diffs where the referenced resource
67
+ * may resolve to stables-only (or `{}`).
68
+ */
69
+ export const tryConnectionOf = (cluster: ClusterLike | undefined): Connection | undefined => {
70
+ if (cluster === undefined) return undefined;
71
+ if ("auth" in cluster && cluster.auth !== undefined) return cluster;
72
+ if ("connection" in cluster && cluster.connection !== undefined) {
73
+ return cluster.connection;
74
+ }
75
+ return undefined;
76
+ };
77
+
78
+ const sortKeysDeep = (value: unknown): unknown => {
79
+ if (Array.isArray(value)) return value.map(sortKeysDeep);
80
+ if (value === null || typeof value !== "object") return value;
81
+ return Object.fromEntries(
82
+ Object.entries(value as Record<string, unknown>)
83
+ .filter(([, entry]) => entry !== undefined)
84
+ .sort(([a], [b]) => a.localeCompare(b))
85
+ .map(([key, entry]) => [key, sortKeysDeep(entry)]),
86
+ );
87
+ };
88
+
89
+ /**
90
+ * A stable identity string for a connection's *target cluster* — the auth
91
+ * descriptor (which names the cluster for managed clouds and the
92
+ * kubeconfig context otherwise). Changing it means the workload moves
93
+ * clusters, which is a replacement. Deliberately excludes `endpoint` /
94
+ * CA: managed clusters can rotate those in place.
95
+ */
96
+ export const connectionIdentity = (connection: Connection | undefined): string | undefined =>
97
+ connection === undefined ? undefined : JSON.stringify(sortKeysDeep(connection.auth));
98
+
99
+ /**
100
+ * The persisted connection of a workload's attributes, tolerating legacy
101
+ * pre-rename rows (`AWS.EKS.*` attributes carried a flat `clusterName`
102
+ * instead of a `connection`) by synthesizing an `aws-eks` connection —
103
+ * the only platform those legacy types could target.
104
+ */
105
+ export const connectionOfOutput = (output: Record<string, unknown>): Connection | undefined => {
106
+ const connection = output.connection as Connection | undefined;
107
+ if (connection?.auth !== undefined) return connection;
108
+ if (typeof output.clusterName === "string") {
109
+ return {
110
+ auth: { kind: "aws-eks", clusterName: output.clusterName } as unknown as ConnectionAuth,
111
+ };
112
+ }
113
+ return undefined;
114
+ };
115
+
116
+ /**
117
+ * Collect environment variables from a host's active bindings and report
118
+ * which cloud-grant channels (any binding-data key besides `env`, e.g.
119
+ * AWS `policyStatements`) are in play — the caller fails when grants
120
+ * exist but the cluster has no identity adapter to materialize them.
121
+ */
122
+ export const collectBindingEnv = (
123
+ bindings: ResourceBinding<WorkloadBindingContract>[],
124
+ ): { env: Record<string, any>; grantKeys: string[] } => {
125
+ const activeBindings = bindings.filter(
126
+ (binding: ResourceBinding<WorkloadBindingContract> & { action?: string }) =>
127
+ binding.action !== "delete",
128
+ );
129
+
130
+ const env = activeBindings
131
+ .map((binding) => binding?.data?.env)
132
+ .reduce<Record<string, any>>((acc, value) => ({ ...acc, ...value }), {});
133
+
134
+ const grantKeys = [
135
+ ...new Set(
136
+ activeBindings.flatMap((binding) =>
137
+ Object.keys(binding?.data ?? {}).filter(
138
+ (key) => key !== "env" && (binding.data as Record<string, unknown>)[key] !== undefined,
139
+ ),
140
+ ),
141
+ ),
142
+ ];
143
+
144
+ return { env, grantKeys };
145
+ };
146
+
147
+ /**
148
+ * Kubernetes env values are strings. A {@link Redacted} string stays wrapped
149
+ * so the request serializer unwraps it. Any other value that holds a secret
150
+ * is JSON, still wrapped, so a failed apply can scrub the inner strings.
151
+ */
152
+ export const containerEnvValue = (value: unknown): string | Redacted.Redacted<string> => {
153
+ if (typeof value === "string") return value;
154
+ if (Redacted.isRedacted(value)) {
155
+ const inner = Redacted.value(value);
156
+ if (typeof inner === "string") return Redacted.make(inner);
157
+ }
158
+ const json = JSON.stringify(unwrapRedacted(value));
159
+ return collectRedactedSecrets(value).length > 0 ? Redacted.make(json) : json;
160
+ };
161
+
162
+ export type ImageSourceKind = "main" | "context" | "image";
163
+
164
+ /** Which image source a props bag declares (`main` always wins). */
165
+ export const imageSourceKind = (source: WorkloadImageSource): ImageSourceKind | undefined =>
166
+ source.main !== undefined
167
+ ? "main"
168
+ : source.image !== undefined
169
+ ? "image"
170
+ : source.context !== undefined || source.dockerfile !== undefined
171
+ ? "context"
172
+ : undefined;
173
+
174
+ /**
175
+ * Content hash for image sources whose identity is computable without a
176
+ * bundler: `image` (the ref + platform) and `context` (the build-context
177
+ * directory + Dockerfile content + platform). `main` returns `undefined`
178
+ * — its hash comes from the bundle output inside the registry adapter.
179
+ */
180
+ export const computeStaticWorkloadImageHash = Effect.fn(function* (
181
+ source: WorkloadImageSource,
182
+ platform: string,
183
+ ) {
184
+ const kind = imageSourceKind(source);
185
+ if (kind === "image") {
186
+ return (yield* sha256Object({ image: source.image!, platform })).slice(0, 16);
187
+ }
188
+ if (kind === "context") {
189
+ if (source.dockerfile !== undefined && isInlineDockerfile(source.dockerfile)) {
190
+ if (typeof source.dockerfile.content !== "string") return undefined;
191
+ return (yield* sha256Object({ dockerfile: source.dockerfile.content, platform })).slice(
192
+ 0,
193
+ 16,
194
+ );
195
+ }
196
+ const fs = yield* FileSystem.FileSystem;
197
+ const path = yield* Path.Path;
198
+ const context = path.resolve(source.context ?? ".");
199
+ const dockerfile =
200
+ typeof source.dockerfile === "string"
201
+ ? path.resolve(source.dockerfile)
202
+ : path.join(context, "Dockerfile");
203
+ if (!(yield* fs.exists(context)) || !(yield* fs.exists(dockerfile))) {
204
+ return undefined;
205
+ }
206
+ const contextHash = yield* hashDirectory({ cwd: context });
207
+ const dockerfileContent = yield* fs.readFileString(dockerfile);
208
+ return (yield* sha256Object({ contextHash, dockerfile: dockerfileContent, platform })).slice(
209
+ 0,
210
+ 16,
211
+ );
212
+ }
213
+ return undefined;
214
+ });
215
+
216
+ export interface ResolveWorkloadImageOptions {
217
+ adapter: ClusterAdapterService;
218
+ /** The target connection — its `registry`, if any, receives builds. */
219
+ connection: Connection;
220
+ /** Publisher for connection-level registries. */
221
+ connectionRegistry: ConnectionRegistry;
222
+ id: string;
223
+ source: WorkloadImageSource;
224
+ platform: string;
225
+ port?: number | undefined;
226
+ isExternal?: boolean | undefined;
227
+ bootstrap: (importPath: string) => string;
228
+ tags: Record<string, string>;
229
+ /** Persisted registry state hints (legacy-shape tolerant). */
230
+ state: Record<string, unknown> | undefined;
231
+ session: { note: (message: string) => Effect.Effect<void> };
232
+ }
233
+
234
+ /**
235
+ * Resolve the container image for a workload: through the cluster
236
+ * adapter's managed registry when it has one (build/mirror + push). Other
237
+ * clusters run a pre-built `image` reference verbatim and build `main` /
238
+ * `context` sources into the connection's `registry`, which those sources
239
+ * require.
240
+ */
241
+ export const resolveWorkloadImage = Effect.fn(function* (options: ResolveWorkloadImageOptions) {
242
+ const { adapter, source } = options;
243
+ if (adapter.registry !== undefined) {
244
+ return yield* adapter.registry.resolve({
245
+ id: options.id,
246
+ source,
247
+ platform: options.platform,
248
+ port: options.port,
249
+ isExternal: options.isExternal,
250
+ bootstrap: options.bootstrap,
251
+ tags: options.tags,
252
+ state: options.state,
253
+ session: options.session,
254
+ });
255
+ }
256
+
257
+ const kind = imageSourceKind(source);
258
+ if (kind === "image") {
259
+ const codeHash = (yield* computeStaticWorkloadImageHash(source, options.platform))!;
260
+ return { imageUri: source.image!, codeHash, state: undefined };
261
+ }
262
+
263
+ const registry = options.connection.registry;
264
+ if (registry !== undefined) {
265
+ return yield* options.connectionRegistry.resolve({
266
+ id: options.id,
267
+ registry,
268
+ source,
269
+ platform: options.platform,
270
+ port: options.port,
271
+ isExternal: options.isExternal,
272
+ bootstrap: options.bootstrap,
273
+ state: options.state,
274
+ session: options.session,
275
+ });
276
+ }
277
+
278
+ return yield* Effect.die(
279
+ new Error(
280
+ `'${options.id}': 'main' and 'context' image sources are built and ` +
281
+ "pushed to a container registry, and this cluster connection has " +
282
+ "none. Add a `registry` to the connection (e.g. " +
283
+ '`Kubernetes.KubeConfig({ context, registry: { server: "ghcr.io/acme" } })`), ' +
284
+ "use `Kubernetes.LocalCluster`, which includes one, or run a " +
285
+ "pre-built `image` reference the cluster can pull.",
286
+ ),
287
+ );
288
+ });
289
+
290
+ /** Plan-time content hash for `diff` — adapter-aware. */
291
+ export const workloadImageHash = Effect.fn(function* (options: {
292
+ adapter: ClusterAdapterService;
293
+ connection: Connection;
294
+ connectionRegistry: ConnectionRegistry;
295
+ source: WorkloadImageSource;
296
+ platform: string;
297
+ port?: number | undefined;
298
+ isExternal?: boolean | undefined;
299
+ bootstrap: (importPath: string) => string;
300
+ }) {
301
+ if (options.adapter.registry !== undefined) {
302
+ return yield* options.adapter.registry.hash({
303
+ source: options.source,
304
+ platform: options.platform,
305
+ port: options.port,
306
+ isExternal: options.isExternal,
307
+ bootstrap: options.bootstrap,
308
+ });
309
+ }
310
+ if (options.connection.registry !== undefined && imageSourceKind(options.source) === "main") {
311
+ return yield* options.connectionRegistry.hash({
312
+ source: options.source,
313
+ platform: options.platform,
314
+ port: options.port,
315
+ isExternal: options.isExternal,
316
+ bootstrap: options.bootstrap,
317
+ });
318
+ }
319
+ return yield* computeStaticWorkloadImageHash(options.source, options.platform);
320
+ });
321
+
322
+ /** The identity-adapter state persisted on workload attributes. */
323
+ // oxlint-disable-next-line typescript/no-redundant-type-constituents -- registry type is `never` until cloud packages augment it
324
+ export type PersistedIdentityState = IdentityState | undefined;
325
+
326
+ /**
327
+ * Platform-neutral generated container entry for an Effect-native server
328
+ * workload: resolves the program's registered runners and serves the
329
+ * returned `{ fetch }` handler on `PORT`. Cloud adapters override this
330
+ * through {@link ClusterAdapterService.bootstrap} to wire their runtime
331
+ * credential chains (e.g. EKS Pod Identity).
332
+ */
333
+ export const makeServerBootstrap =
334
+ (handler: string) =>
335
+ (importPath: string): string =>
336
+ `
337
+ import { BunServices } from "@effect/platform-bun";
338
+ import { BunHttpServer } from "alchemy/Http";
339
+ import { Stack } from "alchemy/Stack";
340
+ import { Stage } from "alchemy/Stage";
341
+ import { makeEntrypointLayer, reifyBoundConfigProvider } from "alchemy/Runtime";
342
+ import { provideProcessTelemetry } from "alchemy/Telemetry";
343
+ import * as Context from "effect/Context";
344
+ import * as Config from "effect/Config";
345
+ import * as ConfigProvider from "effect/ConfigProvider";
346
+ import * as Effect from "effect/Effect";
347
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
348
+ import * as Layer from "effect/Layer";
349
+ import * as Logger from "effect/Logger";
350
+
351
+ import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
352
+
353
+ // Normalize the entrypoint export: an inline-effect class default export is
354
+ // an Effect resolving the platform instance, while the tagged form
355
+ // (X.make(props, impl)) exports a Layer providing the Self tag. Both fold
356
+ // into a Layer via makeEntrypointLayer (same pattern as the ECS/Lambda/
357
+ // Cloudflare Container bridges).
358
+ const tag = Context.Service("${Self.key}");
359
+ const layer = makeEntrypointLayer(tag, entrypoint);
360
+
361
+ const platform = Layer.mergeAll(
362
+ BunServices.layer,
363
+ FetchHttpClient.layer,
364
+ Logger.layer([Logger.consolePretty()]),
365
+ );
366
+
367
+ // Resolve the bundled program (the runners registered via host.run / serve)
368
+ // and run it with a Bun HTTP server bound to PORT, so a returned { fetch }
369
+ // handler is served and host.run loops stay alive.
370
+ const program = tag.pipe(
371
+ // Process-lifetime telemetry: built once into the root scope; exporters
372
+ // batch on their intervals and flush when the scope closes on graceful
373
+ // shutdown.
374
+ Effect.flatMap((host) =>
375
+ host.RuntimeContext.exports.pipe(
376
+ Effect.flatMap((exports) => exports.program),
377
+ provideProcessTelemetry(host.RuntimeContext),
378
+ ),
379
+ ),
380
+ Effect.provide(
381
+ layer.pipe(Layer.provideMerge(Layer.mergeAll(
382
+ Layer.effect(
383
+ Stack,
384
+ Effect.all([
385
+ Config.String("ALCHEMY_STACK_NAME"),
386
+ Config.String("ALCHEMY_STAGE")
387
+ ]).pipe(
388
+ Effect.map(([name, stage]) => ({
389
+ name,
390
+ stage,
391
+ bindings: {},
392
+ resources: {}
393
+ }))
394
+ )
395
+ ),
396
+ // Module-scope declarations shared with the Stack may read the stage.
397
+ Layer.effect(Stage, Config.String("ALCHEMY_STAGE")),
398
+ )),
399
+ Layer.provideMerge(BunHttpServer()),
400
+ Layer.provideMerge(platform),
401
+ Layer.provideMerge(
402
+ Layer.succeed(
403
+ ConfigProvider.ConfigProvider,
404
+ reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env)
405
+ )
406
+ ),
407
+ )
408
+ ),
409
+ Effect.scoped
410
+ );
411
+
412
+ console.log(\`Kubernetes Deployment bootstrap starting on port \${process.env.PORT ?? 3000}...\`);
413
+ await Effect.runPromise(program).catch((err) => {
414
+ console.error("Kubernetes Deployment bootstrap failed:", err);
415
+ process.exit(1);
416
+ });
417
+ `;
418
+
419
+ /**
420
+ * Platform-neutral generated container entry for an Effect-native one-shot
421
+ * Job: resolves the program's `run` effect, executes it to completion, and
422
+ * exits.
423
+ */
424
+ export const makeJobBootstrap =
425
+ (handler: string) =>
426
+ (importPath: string): string =>
427
+ `
428
+ import { BunServices } from "@effect/platform-bun";
429
+ import { Stack } from "alchemy/Stack";
430
+ import { Stage } from "alchemy/Stage";
431
+ import { makeEntrypointLayer, reifyBoundConfigProvider } from "alchemy/Runtime";
432
+ import { provideProcessTelemetry } from "alchemy/Telemetry";
433
+ import * as Context from "effect/Context";
434
+ import * as Config from "effect/Config";
435
+ import * as ConfigProvider from "effect/ConfigProvider";
436
+ import * as Effect from "effect/Effect";
437
+ import * as FetchHttpClient from "effect/http/FetchHttpClient";
438
+ import * as Layer from "effect/Layer";
439
+ import * as Logger from "effect/Logger";
440
+
441
+ import { ${handler} as entrypoint } from ${JSON.stringify(importPath)};
442
+
443
+ // Normalize the entrypoint export (see the server bootstrap).
444
+ const tag = Context.Service("${Self.key}");
445
+ const layer = makeEntrypointLayer(tag, entrypoint);
446
+
447
+ const platform = Layer.mergeAll(
448
+ BunServices.layer,
449
+ FetchHttpClient.layer,
450
+ Logger.layer([Logger.consolePretty()]),
451
+ );
452
+
453
+ // Resolve the bundled program's registered one-shot runners (the shape's
454
+ // \`run\` effect and any host.run work) and execute them to completion.
455
+ const program = tag.pipe(
456
+ // Process-lifetime telemetry: built once into the root scope; exporters
457
+ // batch on their intervals and flush when the scope closes as the
458
+ // one-shot program completes.
459
+ Effect.flatMap((host) =>
460
+ host.RuntimeContext.exports.pipe(
461
+ Effect.flatMap((exports) => exports.program),
462
+ provideProcessTelemetry(host.RuntimeContext),
463
+ ),
464
+ ),
465
+ Effect.provide(
466
+ layer.pipe(Layer.provideMerge(Layer.mergeAll(
467
+ Layer.effect(
468
+ Stack,
469
+ Effect.all([
470
+ Config.String("ALCHEMY_STACK_NAME"),
471
+ Config.String("ALCHEMY_STAGE")
472
+ ]).pipe(
473
+ Effect.map(([name, stage]) => ({
474
+ name,
475
+ stage,
476
+ bindings: {},
477
+ resources: {}
478
+ }))
479
+ )
480
+ ),
481
+ // Module-scope declarations shared with the Stack may read the stage.
482
+ Layer.effect(Stage, Config.String("ALCHEMY_STAGE")),
483
+ )),
484
+ Layer.provideMerge(platform),
485
+ Layer.provideMerge(
486
+ Layer.succeed(
487
+ ConfigProvider.ConfigProvider,
488
+ reifyBoundConfigProvider(ConfigProvider.fromEnv(), process.env)
489
+ )
490
+ ),
491
+ )
492
+ ),
493
+ Effect.scoped
494
+ );
495
+
496
+ console.log("Kubernetes Job bootstrap starting...");
497
+ await Effect.runPromise(program).catch((err) => {
498
+ console.error("Kubernetes Job bootstrap failed:", err);
499
+ process.exit(1);
500
+ });
501
+ // Run-to-completion semantics: exit 0 explicitly so lingering handles
502
+ // (sockets, timers) never keep the pod alive after the work is done.
503
+ process.exit(0);
504
+ `;