@alchemy.run/kubernetes 2.0.0-beta.84 → 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 (46) hide show
  1. package/lib/BuiltinAdapters.d.ts +20 -0
  2. package/lib/BuiltinAdapters.d.ts.map +1 -1
  3. package/lib/BuiltinAdapters.js +8 -3
  4. package/lib/BuiltinAdapters.js.map +1 -1
  5. package/lib/Client.d.ts +17 -0
  6. package/lib/Client.d.ts.map +1 -1
  7. package/lib/Client.js +22 -6
  8. package/lib/Client.js.map +1 -1
  9. package/lib/ClusterAdapter.d.ts +4 -2
  10. package/lib/ClusterAdapter.d.ts.map +1 -1
  11. package/lib/ClusterAdapter.js.map +1 -1
  12. package/lib/Deployment.d.ts +21 -2
  13. package/lib/Deployment.d.ts.map +1 -1
  14. package/lib/Deployment.js +19 -2
  15. package/lib/Deployment.js.map +1 -1
  16. package/lib/HelmChart.js +1 -1
  17. package/lib/HelmChart.js.map +1 -1
  18. package/lib/Job.d.ts +21 -2
  19. package/lib/Job.d.ts.map +1 -1
  20. package/lib/Job.js +18 -1
  21. package/lib/Job.js.map +1 -1
  22. package/lib/LocalCluster.js +4 -4
  23. package/lib/LocalCluster.js.map +1 -1
  24. package/lib/Manifest.d.ts +9 -0
  25. package/lib/Manifest.d.ts.map +1 -1
  26. package/lib/Manifest.js +7 -2
  27. package/lib/Manifest.js.map +1 -1
  28. package/lib/internal/helm.js +1 -1
  29. package/lib/internal/helm.js.map +1 -1
  30. package/lib/internal/kubeconfig.js +1 -1
  31. package/lib/internal/kubeconfig.js.map +1 -1
  32. package/lib/internal/workload.d.ts +1 -1
  33. package/lib/internal/workload.d.ts.map +1 -1
  34. package/lib/internal/workload.js.map +1 -1
  35. package/package.json +8 -8
  36. package/src/BuiltinAdapters.ts +11 -3
  37. package/src/Client.ts +39 -8
  38. package/src/ClusterAdapter.ts +5 -2
  39. package/src/Deployment.ts +25 -4
  40. package/src/HelmChart.ts +1 -1
  41. package/src/Job.ts +23 -3
  42. package/src/LocalCluster.ts +4 -4
  43. package/src/Manifest.ts +10 -4
  44. package/src/internal/helm.ts +1 -1
  45. package/src/internal/kubeconfig.ts +1 -1
  46. package/src/internal/workload.ts +2 -1
package/src/Client.ts CHANGED
@@ -36,6 +36,13 @@ import {
36
36
  type KubernetesObjectRef,
37
37
  } from "./Objects.ts";
38
38
 
39
+ export class KubernetesClientOperationFailed extends Data.TaggedError(
40
+ "KubernetesClientOperationFailed",
41
+ )<{
42
+ readonly message: string;
43
+ readonly cause?: unknown;
44
+ }> {}
45
+
39
46
  export class KubernetesApiError extends Data.TaggedError("KubernetesApiError")<{
40
47
  method: string;
41
48
  path: string;
@@ -49,6 +56,12 @@ export class KubernetesApiError extends Data.TaggedError("KubernetesApiError")<{
49
56
  }
50
57
  }
51
58
 
59
+ /** An object ref that cannot be mapped to a REST path (e.g. a namespaced kind without a namespace). */
60
+ export class KubernetesObjectPathError extends Data.TaggedError("KubernetesObjectPathError")<{
61
+ message: string;
62
+ cause?: unknown;
63
+ }> {}
64
+
52
65
  const fieldManager = "alchemy";
53
66
 
54
67
  /** Deadline for auth-header minting and for each HTTP attempt. */
@@ -81,7 +94,7 @@ export class KubernetesNotFound extends Data.TaggedError("KubernetesNotFound")<{
81
94
  * becomes `undefined`; auth and HTTP failures stay failures.
82
95
  */
83
96
  export const ifClusterExists = <A, E, R>(effect: Effect.Effect<A, ClusterNotFoundError | E, R>) =>
84
- effect.pipe(Effect.catchTag("Kubernetes.ClusterNotFoundError", () => Effect.succeed(undefined)));
97
+ effect.pipe(Effect.catchTag("Kubernetes.ClusterNotFoundError", () => Effect.undefined));
85
98
 
86
99
  const requestJson = Effect.fn(function* ({
87
100
  transport,
@@ -156,13 +169,21 @@ const requestJson = Effect.fn(function* ({
156
169
  resume(Effect.fail(error));
157
170
  });
158
171
  incoming.on("aborted", () => {
159
- resume(Effect.fail(new Error(`Kubernetes ${method} ${path} response aborted`)));
172
+ resume(
173
+ Effect.fail(
174
+ new KubernetesClientOperationFailed({
175
+ message: `Kubernetes ${method} ${path} response aborted`,
176
+ }),
177
+ ),
178
+ );
160
179
  });
161
180
  incoming.on("end", () => {
162
181
  if (incoming.complete === false) {
163
182
  resume(
164
183
  Effect.fail(
165
- new Error(`Kubernetes ${method} ${path} response ended before completion`),
184
+ new KubernetesClientOperationFailed({
185
+ message: `Kubernetes ${method} ${path} response ended before completion`,
186
+ }),
166
187
  ),
167
188
  );
168
189
  return;
@@ -178,7 +199,13 @@ const requestJson = Effect.fn(function* ({
178
199
  },
179
200
  );
180
201
  request.on("error", (error) => {
181
- resume(Effect.fail(error instanceof Error ? error : new Error(String(error))));
202
+ resume(
203
+ Effect.fail(
204
+ error instanceof Error
205
+ ? error
206
+ : new KubernetesClientOperationFailed({ message: String(error) }),
207
+ ),
208
+ );
182
209
  });
183
210
  if (prepared.payload !== undefined) request.write(prepared.payload);
184
211
  request.end();
@@ -193,9 +220,9 @@ const requestJson = Effect.fn(function* ({
193
220
  Effect.timeout(requestTimeout),
194
221
  Effect.mapError(
195
222
  (error) =>
196
- new Error(
197
- `Failed Kubernetes ${method} ${path}: ${error instanceof Error ? error.message : String(error)}`,
198
- ),
223
+ new KubernetesClientOperationFailed({
224
+ message: `Failed Kubernetes ${method} ${path}: ${error instanceof Error ? error.message : String(error)}`,
225
+ }),
199
226
  ),
200
227
  // Transport-level failures (ECONNREFUSED/ECONNRESET/ETIMEDOUT/DNS,
201
228
  // the per-attempt deadline) are transient — a fresh managed endpoint's
@@ -310,7 +337,11 @@ const buildPath = Effect.fn(function* ({
310
337
  const spec = yield* resolveKindSpec({ transport, input: object });
311
338
  return yield* Effect.try({
312
339
  try: () => buildKubernetesObjectPathWithSpec(object, spec),
313
- catch: (error) => (error instanceof Error ? error : new Error(String(error))),
340
+ catch: (error) =>
341
+ new KubernetesObjectPathError({
342
+ message: error instanceof Error ? error.message : String(error),
343
+ cause: error,
344
+ }),
314
345
  });
315
346
  });
316
347
 
@@ -1,4 +1,5 @@
1
1
  import type { InlineDockerfile } from "@alchemy.run/docker/Dockerfile";
2
+ import type { DockerfileBuild } from "@alchemy.run/docker/ImageSource";
2
3
  import type * as Bundle from "alchemy/Bundle/Bundle";
3
4
  import type { ResourceBinding } from "alchemy/Resource";
4
5
  import type { ResourceContext } from "alchemy/ResourceContext";
@@ -170,12 +171,13 @@ export interface WorkloadImageSource {
170
171
  main?: string;
171
172
  handler?: string;
172
173
  /**
173
- * Bundler configuration for `main`. Unused code is tree-shaken.
174
+ * Bundler configuration for `main`, or Docker build args and secrets for
175
+ * a `context`/`dockerfile` build. Unused bundled code is tree-shaken.
174
176
  * `effect`, alchemy, and `@distilled.cloud` are marked pure so unused
175
177
  * parts prune more aggressively. List extra packages with
176
178
  * `pure.packages`, or disable with `pure: false`.
177
179
  */
178
- build?: Bundle.BundleConfig;
180
+ build?: Bundle.BundleConfig & DockerfileBuild;
179
181
  context?: string;
180
182
  dockerfile?: string | InlineDockerfile;
181
183
  image?: string;
@@ -210,6 +212,7 @@ export interface WorkloadIdentityResult {
210
212
  /** Annotations to stamp on the workload's ServiceAccount object. */
211
213
  serviceAccountAnnotations?: Record<string, string>;
212
214
  /** Serializable state persisted on the workload's attributes. */
215
+ // oxlint-disable-next-line typescript/no-redundant-type-constituents -- registry type is `never` until cloud packages augment it
213
216
  state: IdentityState | undefined;
214
217
  }
215
218
 
package/src/Deployment.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { DockerfileBuildOptions } from "@alchemy.run/docker/ImageSource";
2
+ import type * as Bundle from "alchemy/Bundle/Bundle";
1
3
  import { isResolved } from "alchemy/Diff";
2
4
  import type { Input } from "alchemy/Input";
3
5
  import { createPhysicalName } from "alchemy/PhysicalName";
@@ -174,7 +176,7 @@ export interface BundledDeploymentProps extends DeploymentPropsBase {
174
176
  /** Named export to load from `main`. @default "default" */
175
177
  handler?: string;
176
178
  /** Bundler configuration for the entrypoint. */
177
- build?: WorkloadImageSource["build"];
179
+ build?: Bundle.BundleConfig;
178
180
  /** Environment Dockerfile (path or inline content). */
179
181
  dockerfile?: WorkloadImageSource["dockerfile"];
180
182
  /** Build context for a path {@link dockerfile} environment. */
@@ -182,7 +184,7 @@ export interface BundledDeploymentProps extends DeploymentPropsBase {
182
184
  }
183
185
 
184
186
  /** Build the user's own Dockerfile (`context` + optional `dockerfile` path). */
185
- export interface DockerfileDeploymentProps extends DeploymentPropsBase {
187
+ export interface DockerfileDeploymentProps extends DeploymentPropsBase, DockerfileBuildOptions {
186
188
  /** Docker build context directory. */
187
189
  context?: string;
188
190
  /**
@@ -229,6 +231,7 @@ export interface Deployment extends Resource<
229
231
  * adapter (on EKS: the pod-identity role + association; on GKE: the
230
232
  * Workload Identity principal and the IAM grants applied to it).
231
233
  */
234
+ // oxlint-disable-next-line typescript/no-redundant-type-constituents -- registry type is `never` until cloud packages augment it
232
235
  identity: IdentityState | undefined;
233
236
  /**
234
237
  * Image-registry state provisioned by the cluster platform's adapter
@@ -252,6 +255,7 @@ export interface Deployment extends Resource<
252
255
  Providers
253
256
  > {}
254
257
 
258
+ // oxlint-disable-next-line typescript/no-redundant-type-constituents -- registry type is `never` until cloud packages augment it
255
259
  export type DeploymentServices = ServerHostService | WorkloadServices;
256
260
 
257
261
  export type DeploymentShape = Main<DeploymentServices>;
@@ -320,6 +324,23 @@ export interface DeploymentRuntimeContext extends HostRuntimeContext {
320
324
  * });
321
325
  * ```
322
326
  *
327
+ * **Example:** Build args and secrets
328
+ * ```typescript
329
+ * // Dockerfile:
330
+ * // ARG NODE_ENV
331
+ * // RUN --mount=type=secret,id=NPM_TOKEN,env=NPM_TOKEN npm ci
332
+ * const api = yield* Kubernetes.Deployment("Api", {
333
+ * cluster,
334
+ * context: "./api",
335
+ * build: {
336
+ * args: { NODE_ENV: "production" },
337
+ * // Mounted only into the RUN step that asks for it; never in the image.
338
+ * secrets: { NPM_TOKEN: Config.Redacted("NPM_TOKEN") },
339
+ * },
340
+ * port: 8080,
341
+ * });
342
+ * ```
343
+ *
323
344
  * ### Effect Servers
324
345
  * **Example:** Inline Effect server
325
346
  * ```typescript
@@ -538,7 +559,7 @@ export const DeploymentProvider = () =>
538
559
  hostname ? Effect.succeed(hostname) : Effect.fail(new ServiceNotReady()),
539
560
  ),
540
561
  retryUntilServiceReady,
541
- Effect.catchTag("Kubernetes.ServiceNotReady", () => Effect.succeed(undefined)),
562
+ Effect.catchTag("Kubernetes.ServiceNotReady", () => Effect.undefined),
542
563
  );
543
564
 
544
565
  return {
@@ -613,7 +634,7 @@ export const DeploymentProvider = () =>
613
634
  const anchor = (output.kubernetesObjects ?? [])[0];
614
635
  if (!anchor) return output;
615
636
  const observed = yield* readObject({ transport, object: anchor }).pipe(
616
- Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)),
637
+ Effect.catchTag("KubernetesNotFound", () => Effect.undefined),
617
638
  );
618
639
  if (observed === undefined) return undefined;
619
640
  return output;
package/src/HelmChart.ts CHANGED
@@ -355,7 +355,7 @@ export const HelmChartProvider = () =>
355
355
  Effect.gen(function* () {
356
356
  const key = kubernetesObjectKey(object);
357
357
  const observed = yield* readObject({ transport, object }).pipe(
358
- Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)),
358
+ Effect.catchTag("KubernetesNotFound", () => Effect.undefined),
359
359
  );
360
360
  if (!observed) return `missing:${key}`;
361
361
  // Older rows stored no selection. Re-rendering the chart is
package/src/Job.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import type { DockerfileBuildOptions } from "@alchemy.run/docker/ImageSource";
2
+ import type * as Bundle from "alchemy/Bundle/Bundle";
1
3
  import { isResolved } from "alchemy/Diff";
2
4
  import * as Output from "alchemy/Output";
3
5
  import { createPhysicalName } from "alchemy/PhysicalName";
@@ -155,7 +157,7 @@ export interface BundledJobProps extends JobPropsBase {
155
157
  /** Named export to load from `main`. @default "default" */
156
158
  handler?: string;
157
159
  /** Bundler configuration for the entrypoint. */
158
- build?: WorkloadImageSource["build"];
160
+ build?: Bundle.BundleConfig;
159
161
  /** Environment Dockerfile (path or inline content). */
160
162
  dockerfile?: WorkloadImageSource["dockerfile"];
161
163
  /** Build context for a path {@link dockerfile} environment. */
@@ -163,7 +165,7 @@ export interface BundledJobProps extends JobPropsBase {
163
165
  }
164
166
 
165
167
  /** Build the user's own Dockerfile (`context` + optional `dockerfile` path). */
166
- export interface DockerfileJobProps extends JobPropsBase {
168
+ export interface DockerfileJobProps extends JobPropsBase, DockerfileBuildOptions {
167
169
  /** Docker build context directory. */
168
170
  context?: string;
169
171
  /**
@@ -207,6 +209,7 @@ export interface Job extends Resource<
207
209
  * adapter (on EKS: the pod-identity role + association; on GKE: the
208
210
  * Workload Identity principal and the IAM grants applied to it).
209
211
  */
212
+ // oxlint-disable-next-line typescript/no-redundant-type-constituents -- registry type is `never` until cloud packages augment it
210
213
  identity: IdentityState | undefined;
211
214
  /**
212
215
  * Image-registry state provisioned by the cluster platform's adapter
@@ -267,6 +270,23 @@ export interface JobRuntimeContext extends HostRuntimeContext {
267
270
  * });
268
271
  * ```
269
272
  *
273
+ * **Example:** Your own Dockerfile with build args and secrets
274
+ * ```typescript
275
+ * // Dockerfile:
276
+ * // ARG NODE_ENV
277
+ * // RUN --mount=type=secret,id=NPM_TOKEN,env=NPM_TOKEN npm ci
278
+ * const migrate = yield* Kubernetes.Job("DbMigrate", {
279
+ * cluster,
280
+ * context: "./migrator",
281
+ * build: {
282
+ * args: { NODE_ENV: "production" },
283
+ * // Mounted only into the RUN step that asks for it; never in the image.
284
+ * secrets: { NPM_TOKEN: Config.Redacted("NPM_TOKEN") },
285
+ * },
286
+ * backoffLimit: 2,
287
+ * });
288
+ * ```
289
+ *
270
290
  * **Example:** Inline Effect program
271
291
  * ```typescript
272
292
  * const cluster = yield* Kubernetes.LocalCluster("Cluster", {
@@ -477,7 +497,7 @@ export const JobProvider = () =>
477
497
  const observed = yield* readObject({
478
498
  transport,
479
499
  object: anchor,
480
- }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)));
500
+ }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.undefined));
481
501
  if (observed === undefined) return undefined;
482
502
  return output;
483
503
  }),
@@ -1,4 +1,4 @@
1
- import { Unowned } from "alchemy/AdoptPolicy";
1
+ import { unownedUnlessPrevious } from "alchemy/AdoptPolicy";
2
2
  import { deepEqual, isResolved } from "alchemy/Diff";
3
3
  import { createPhysicalName } from "alchemy/PhysicalName";
4
4
  import * as Provider from "alchemy/Provider";
@@ -308,7 +308,7 @@ export const LocalClusterProvider = () =>
308
308
  detached: false,
309
309
  extendEnv: true,
310
310
  }).pipe(
311
- spawner.spawn,
311
+ (command) => spawner.spawn(command),
312
312
  Effect.flatMap((child) =>
313
313
  Effect.all(
314
314
  {
@@ -559,7 +559,7 @@ export const LocalClusterProvider = () =>
559
559
  return { action: "replace", deleteFirst: true } as const;
560
560
  }
561
561
  }),
562
- read: Effect.fn(function* ({ id, olds, output }) {
562
+ read: Effect.fn(function* ({ id, olds, output, previous }) {
563
563
  const name = yield* clusterName(id, olds, output);
564
564
  if (!(yield* clusterExists(name))) return undefined;
565
565
  const registryInfo = yield* inspectRegistry(`${name}-registry`);
@@ -574,7 +574,7 @@ export const LocalClusterProvider = () =>
574
574
  // kind clusters carry no ownership marker; the registry container
575
575
  // we start next to them does. Anything else is foreign.
576
576
  const owned = yield* hasAlchemyTags(id, registryInfo?.Config?.Labels ?? undefined);
577
- return owned ? attrs : Unowned(attrs);
577
+ return owned ? attrs : unownedUnlessPrevious(attrs, previous, "name");
578
578
  }),
579
579
  reconcile: Effect.fn(function* ({ id, news, output, session }) {
580
580
  const name = yield* clusterName(id, news, output);
package/src/Manifest.ts CHANGED
@@ -2,6 +2,7 @@ import { isResolved } from "alchemy/Diff";
2
2
  import * as Provider from "alchemy/Provider";
3
3
  import { Resource } from "alchemy/Resource";
4
4
  import { isPlainObject } from "alchemy/Util/data";
5
+ import * as Data from "effect/Data";
5
6
  import * as Effect from "effect/Effect";
6
7
  import {
7
8
  applyObject,
@@ -16,6 +17,11 @@ import { connectionIdentity, connectionOfOutput, tryConnectionOf } from "./inter
16
17
  import type { KubernetesObjectDefinition, KubernetesObjectRef } from "./Objects.ts";
17
18
  import type { Providers } from "./Providers.ts";
18
19
 
20
+ export class ManifestInvalidProps extends Data.TaggedError("ManifestInvalidProps")<{
21
+ readonly message: string;
22
+ readonly cause?: unknown;
23
+ }> {}
24
+
19
25
  /**
20
26
  * A literal Kubernetes object: `apiVersion` + `kind` + `metadata`, with the
21
27
  * rest of the object's fields (`spec`, `data`, …) carried as-is. Any kind is
@@ -174,9 +180,9 @@ const toObjectDefinition = (
174
180
  const name = manifest.metadata?.name;
175
181
  if (!name) {
176
182
  return Effect.fail(
177
- new Error(
178
- `Kubernetes.Manifest requires manifest.metadata.name (got ${manifest.apiVersion}/${manifest.kind})`,
179
- ),
183
+ new ManifestInvalidProps({
184
+ message: `Kubernetes.Manifest requires manifest.metadata.name (got ${manifest.apiVersion}/${manifest.kind})`,
185
+ }),
180
186
  );
181
187
  }
182
188
  return Effect.succeed(manifest as KubernetesObjectDefinition);
@@ -219,7 +225,7 @@ export const ManifestProvider = () =>
219
225
  const observed = yield* readObject({
220
226
  transport,
221
227
  object: output.ref,
222
- }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)));
228
+ }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.undefined));
223
229
  if (!observed) return undefined;
224
230
  const uid = (observed as { metadata?: { uid?: string } }).metadata?.uid;
225
231
  const attrs = { ...output, uid };
@@ -116,7 +116,7 @@ export const renderHelmChart = Effect.fn(function* (options: RenderHelmChartOpti
116
116
  detached: false,
117
117
  extendEnv: true,
118
118
  }).pipe(
119
- spawner.spawn,
119
+ (command) => spawner.spawn(command),
120
120
  Effect.flatMap((child) =>
121
121
  Effect.all(
122
122
  {
@@ -215,7 +215,7 @@ export const runExecCredential = Effect.fn(function* (options: {
215
215
  extendEnv: true,
216
216
  env: options.env,
217
217
  }).pipe(
218
- spawner.spawn,
218
+ (command) => spawner.spawn(command),
219
219
  Effect.flatMap((child) =>
220
220
  Effect.all(
221
221
  {
@@ -57,7 +57,7 @@ export const deepMerge = <T>(base: T, override: unknown): T => {
57
57
  */
58
58
  export const imagePlatformOf = (
59
59
  architecture: "amd64" | "arm64" | undefined,
60
- connection?: Connection | undefined,
60
+ connection?: Connection,
61
61
  ): string =>
62
62
  (architecture ?? connection?.architecture) === "arm64" ? "linux/arm64" : "linux/amd64";
63
63
 
@@ -320,6 +320,7 @@ export const workloadImageHash = Effect.fn(function* (options: {
320
320
  });
321
321
 
322
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
323
324
  export type PersistedIdentityState = IdentityState | undefined;
324
325
 
325
326
  /**