@alchemy.run/kubernetes 2.0.0-beta.84 → 2.0.0-beta.86

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 (67) 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 +24 -7
  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 +7 -4
  10. package/lib/ClusterAdapter.d.ts.map +1 -1
  11. package/lib/ClusterAdapter.js.map +1 -1
  12. package/lib/Connection.d.ts.map +1 -1
  13. package/lib/Connection.js.map +1 -1
  14. package/lib/Deployment.d.ts +75 -3
  15. package/lib/Deployment.d.ts.map +1 -1
  16. package/lib/Deployment.js +64 -2
  17. package/lib/Deployment.js.map +1 -1
  18. package/lib/HelmChart.d.ts.map +1 -1
  19. package/lib/HelmChart.js +1 -1
  20. package/lib/HelmChart.js.map +1 -1
  21. package/lib/Job.d.ts +62 -3
  22. package/lib/Job.d.ts.map +1 -1
  23. package/lib/Job.js +51 -1
  24. package/lib/Job.js.map +1 -1
  25. package/lib/LocalCluster.d.ts +22 -1
  26. package/lib/LocalCluster.d.ts.map +1 -1
  27. package/lib/LocalCluster.js +26 -4
  28. package/lib/LocalCluster.js.map +1 -1
  29. package/lib/Manifest.d.ts +42 -1
  30. package/lib/Manifest.d.ts.map +1 -1
  31. package/lib/Manifest.js +33 -2
  32. package/lib/Manifest.js.map +1 -1
  33. package/lib/Objects.d.ts.map +1 -1
  34. package/lib/Objects.js.map +1 -1
  35. package/lib/Providers.d.ts.map +1 -1
  36. package/lib/Providers.js.map +1 -1
  37. package/lib/index.d.ts.map +1 -1
  38. package/lib/index.js.map +1 -1
  39. package/lib/internal/declared.d.ts.map +1 -1
  40. package/lib/internal/declared.js.map +1 -1
  41. package/lib/internal/helm.d.ts.map +1 -1
  42. package/lib/internal/helm.js +1 -1
  43. package/lib/internal/helm.js.map +1 -1
  44. package/lib/internal/kind.d.ts.map +1 -1
  45. package/lib/internal/kind.js.map +1 -1
  46. package/lib/internal/kubeconfig.d.ts.map +1 -1
  47. package/lib/internal/kubeconfig.js +17 -8
  48. package/lib/internal/kubeconfig.js.map +1 -1
  49. package/lib/internal/registry.d.ts +1 -1
  50. package/lib/internal/registry.d.ts.map +1 -1
  51. package/lib/internal/registry.js.map +1 -1
  52. package/lib/internal/workload.d.ts +1 -1
  53. package/lib/internal/workload.d.ts.map +1 -1
  54. package/lib/internal/workload.js +4 -0
  55. package/lib/internal/workload.js.map +1 -1
  56. package/package.json +9 -9
  57. package/src/BuiltinAdapters.ts +11 -3
  58. package/src/Client.ts +39 -8
  59. package/src/ClusterAdapter.ts +8 -4
  60. package/src/Deployment.ts +80 -5
  61. package/src/HelmChart.ts +1 -1
  62. package/src/Job.ts +66 -4
  63. package/src/LocalCluster.ts +39 -5
  64. package/src/Manifest.ts +58 -6
  65. package/src/internal/helm.ts +1 -1
  66. package/src/internal/kubeconfig.ts +17 -8
  67. package/src/internal/workload.ts +6 -1
@@ -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";
@@ -178,9 +178,21 @@ export interface LocalCluster extends Resource<
178
178
  connection: Connection;
179
179
  },
180
180
  {},
181
- Providers
181
+ Providers,
182
+ LocalClusterLookup
182
183
  > {}
183
184
 
185
+ /**
186
+ * Props for {@link LocalCluster.lookup}: find a kind cluster this
187
+ * stack doesn't manage by its name.
188
+ */
189
+ export interface LocalClusterLookup {
190
+ /** kind cluster name. */
191
+ name: string;
192
+ /** Kubeconfig file the cluster's context is in. */
193
+ kubeconfig?: string;
194
+ }
195
+
184
196
  /**
185
197
  * A Kubernetes cluster on your machine, for trying Alchemy's Kubernetes
186
198
  * resources and developing against them without a hosted cluster.
@@ -252,6 +264,17 @@ export interface LocalCluster extends Resource<
252
264
  * });
253
265
  * ```
254
266
  *
267
+ * ### Looking up an existing cluster
268
+ * `LocalCluster.lookup` references a kind cluster Alchemy doesn't manage (applied with
269
+ * `kubectl`, Helm or another stack). It is read during plan/deploy, never
270
+ * applied, updated or deleted.
271
+ *
272
+ * **Example:** Apply to an existing kind cluster
273
+ * ```typescript
274
+ * const cluster = Kubernetes.LocalCluster.lookup({ name: "dev" });
275
+ * yield* Kubernetes.Manifest("Settings", { cluster, manifest });
276
+ * ```
277
+ *
255
278
  * @resource
256
279
  * @product LocalCluster
257
280
  */
@@ -308,7 +331,7 @@ export const LocalClusterProvider = () =>
308
331
  detached: false,
309
332
  extendEnv: true,
310
333
  }).pipe(
311
- spawner.spawn,
334
+ (command) => spawner.spawn(command),
312
335
  Effect.flatMap((child) =>
313
336
  Effect.all(
314
337
  {
@@ -559,7 +582,18 @@ export const LocalClusterProvider = () =>
559
582
  return { action: "replace", deleteFirst: true } as const;
560
583
  }
561
584
  }),
562
- read: Effect.fn(function* ({ id, olds, output }) {
585
+ lookup: Effect.fn(function* ({ name, kubeconfig }) {
586
+ if (!(yield* clusterExists(name))) return undefined;
587
+ const registryInfo = yield* inspectRegistry(`${name}-registry`);
588
+ return toAttributes({
589
+ name,
590
+ kubeconfig,
591
+ registryPort: registryHostPort(registryInfo) ?? DEFAULT_REGISTRY_PORT,
592
+ architecture: yield* hostArchitecture,
593
+ });
594
+ }),
595
+
596
+ read: Effect.fn(function* ({ id, olds, output, previous }) {
563
597
  const name = yield* clusterName(id, olds, output);
564
598
  if (!(yield* clusterExists(name))) return undefined;
565
599
  const registryInfo = yield* inspectRegistry(`${name}-registry`);
@@ -574,7 +608,7 @@ export const LocalClusterProvider = () =>
574
608
  // kind clusters carry no ownership marker; the registry container
575
609
  // we start next to them does. Anything else is foreign.
576
610
  const owned = yield* hasAlchemyTags(id, registryInfo?.Config?.Labels ?? undefined);
577
- return owned ? attrs : Unowned(attrs);
611
+ return owned ? attrs : unownedUnlessPrevious(attrs, previous, "name");
578
612
  }),
579
613
  reconcile: Effect.fn(function* ({ id, news, output, session }) {
580
614
  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
@@ -85,9 +91,27 @@ export interface Manifest extends Resource<
85
91
  observedHash?: string;
86
92
  },
87
93
  {},
88
- Providers
94
+ Providers,
95
+ ManifestLookup
89
96
  > {}
90
97
 
98
+ /**
99
+ * Props for {@link Manifest.lookup}: find an object this stack
100
+ * doesn't manage by its cluster, kind and name.
101
+ */
102
+ export interface ManifestLookup {
103
+ /** The cluster the object lives in. */
104
+ cluster: ClusterLike;
105
+ /** API version, e.g. `apps/v1`. */
106
+ apiVersion: string;
107
+ /** Object kind, e.g. `Deployment`. */
108
+ kind: string;
109
+ /** Object name. */
110
+ name: string;
111
+ /** Namespace, for namespaced kinds. */
112
+ namespace?: string;
113
+ }
114
+
91
115
  /**
92
116
  * Applies a raw Kubernetes manifest onto any cluster via server-side
93
117
  * apply.
@@ -161,6 +185,22 @@ export interface Manifest extends Resource<
161
185
  * });
162
186
  * ```
163
187
  *
188
+ * ### Looking up an existing object
189
+ * `Manifest.lookup` references an object Alchemy doesn't manage (applied with
190
+ * `kubectl`, Helm or another stack). It is read during plan/deploy, never
191
+ * applied, updated or deleted.
192
+ *
193
+ * **Example:** Reference an existing ConfigMap
194
+ * ```typescript
195
+ * const settings = Kubernetes.Manifest.lookup({
196
+ * cluster,
197
+ * apiVersion: "v1",
198
+ * kind: "ConfigMap",
199
+ * name: "app-settings",
200
+ * namespace: "default",
201
+ * });
202
+ * ```
203
+ *
164
204
  * @resource
165
205
  * @product Manifest
166
206
  */
@@ -170,13 +210,13 @@ export const Manifest = Resource<Manifest>("Kubernetes.Manifest", {
170
210
 
171
211
  const toObjectDefinition = (
172
212
  manifest: KubernetesManifest,
173
- ): Effect.Effect<KubernetesObjectDefinition, Error> => {
213
+ ): Effect.Effect<KubernetesObjectDefinition, ManifestInvalidProps> => {
174
214
  const name = manifest.metadata?.name;
175
215
  if (!name) {
176
216
  return Effect.fail(
177
- new Error(
178
- `Kubernetes.Manifest requires manifest.metadata.name (got ${manifest.apiVersion}/${manifest.kind})`,
179
- ),
217
+ new ManifestInvalidProps({
218
+ message: `Kubernetes.Manifest requires manifest.metadata.name (got ${manifest.apiVersion}/${manifest.kind})`,
219
+ }),
180
220
  );
181
221
  }
182
222
  return Effect.succeed(manifest as KubernetesObjectDefinition);
@@ -210,6 +250,18 @@ export const ManifestProvider = () =>
210
250
  return { action: "replace" } as const;
211
251
  }
212
252
  }),
253
+ lookup: Effect.fn(function* ({ cluster, apiVersion, kind, name, namespace }) {
254
+ const connection = toConnection(cluster);
255
+ const ref: KubernetesObjectRef = { apiVersion, kind, name, namespace };
256
+ const transport = yield* connectCluster(connection);
257
+ const observed = yield* readObject({ transport, object: ref }).pipe(
258
+ Effect.catchTag("KubernetesNotFound", () => Effect.undefined),
259
+ );
260
+ if (!observed) return undefined;
261
+ const uid = (observed as { metadata?: { uid?: string } }).metadata?.uid;
262
+ return { connection, apiVersion, kind, name, namespace, ref, uid };
263
+ }),
264
+
213
265
  read: Effect.fn(function* ({ output, olds }) {
214
266
  if (!output) return undefined;
215
267
  const connection = connectionOfOutput(output);
@@ -219,7 +271,7 @@ export const ManifestProvider = () =>
219
271
  const observed = yield* readObject({
220
272
  transport,
221
273
  object: output.ref,
222
- }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.succeed(undefined)));
274
+ }).pipe(Effect.catchTag("KubernetesNotFound", () => Effect.undefined));
223
275
  if (!observed) return undefined;
224
276
  const uid = (observed as { metadata?: { uid?: string } }).metadata?.uid;
225
277
  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
  {
@@ -6,10 +6,13 @@
6
6
  * path: AKS/GKE/EKS CLI-managed contexts work through their exec plugins
7
7
  * (`kubelogin`, `gke-gcloud-auth-plugin`, `aws eks get-token`).
8
8
  */
9
+ import * as Clock from "effect/Clock";
9
10
  import * as Config from "effect/Config";
10
11
  import * as Data from "effect/Data";
12
+ import * as DateTime from "effect/DateTime";
11
13
  import * as Effect from "effect/Effect";
12
14
  import * as FileSystem from "effect/FileSystem";
15
+ import * as Option from "effect/Option";
13
16
  import * as Path from "effect/Path";
14
17
  import * as ChildProcess from "effect/process/ChildProcess";
15
18
  import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
@@ -76,6 +79,8 @@ const decodeBase64 = (value: string) => Buffer.from(value, "base64").toString("u
76
79
 
77
80
  const expandHome = Effect.fn(function* (input: string) {
78
81
  if (!input.startsWith("~")) return input;
82
+ // `~` is the user's real home directory, not a configured value.
83
+ // oxlint-disable-next-line effecttsgo/process-env-in-effect -- home expansion reads the real process env
79
84
  const home = yield* Effect.sync(() => process.env.HOME ?? process.env.USERPROFILE);
80
85
  if (!home) {
81
86
  return yield* new KubeConfigError({
@@ -199,7 +204,7 @@ export const runExecCredential = Effect.fn(function* (options: {
199
204
  }) {
200
205
  const cacheKey = JSON.stringify([options.command, options.args ?? [], options.env ?? {}]);
201
206
  const cached = execCache.get(cacheKey);
202
- const now = yield* Effect.sync(() => Date.now());
207
+ const now = yield* Clock.currentTimeMillis;
203
208
  if (cached && cached.expiresAt > now) {
204
209
  return cached.credentials;
205
210
  }
@@ -215,7 +220,7 @@ export const runExecCredential = Effect.fn(function* (options: {
215
220
  extendEnv: true,
216
221
  env: options.env,
217
222
  }).pipe(
218
- spawner.spawn,
223
+ (command) => spawner.spawn(command),
219
224
  Effect.flatMap((child) =>
220
225
  Effect.all(
221
226
  {
@@ -270,12 +275,16 @@ export const runExecCredential = Effect.fn(function* (options: {
270
275
  : undefined,
271
276
  };
272
277
 
273
- const expiresAt = status.expirationTimestamp
274
- ? // Re-mint 30s before the declared expiry.
275
- new Date(status.expirationTimestamp).getTime() - 30_000
276
- : // No declared expiry: cache briefly so bursts of requests share one
277
- // plugin invocation without holding a stale credential for long.
278
- now + 60_000;
278
+ const declaredExpiry = status.expirationTimestamp
279
+ ? DateTime.make(status.expirationTimestamp)
280
+ : Option.none();
281
+ const expiresAt = Option.match(declaredExpiry, {
282
+ // Re-mint 30s before the declared expiry.
283
+ onSome: (expiry) => DateTime.toEpochMillis(expiry) - 30_000,
284
+ // No (valid) declared expiry: cache briefly so bursts of requests share
285
+ // one plugin invocation without holding a stale credential for long.
286
+ onNone: () => now + 60_000,
287
+ });
279
288
  execCache.set(cacheKey, { credentials, expiresAt });
280
289
 
281
290
  return credentials;
@@ -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
  /**
@@ -343,6 +344,7 @@ import * as Context from "effect/Context";
343
344
  import * as Config from "effect/Config";
344
345
  import * as ConfigProvider from "effect/ConfigProvider";
345
346
  import * as Effect from "effect/Effect";
347
+ import * as SecureRandom from "effect/crypto/SecureRandom";
346
348
  import * as FetchHttpClient from "effect/http/FetchHttpClient";
347
349
  import * as Layer from "effect/Layer";
348
350
  import * as Logger from "effect/Logger";
@@ -360,6 +362,7 @@ const layer = makeEntrypointLayer(tag, entrypoint);
360
362
  const platform = Layer.mergeAll(
361
363
  BunServices.layer,
362
364
  FetchHttpClient.layer,
365
+ SecureRandom.layerWebCrypto,
363
366
  Logger.layer([Logger.consolePretty()]),
364
367
  );
365
368
 
@@ -433,6 +436,7 @@ import * as Context from "effect/Context";
433
436
  import * as Config from "effect/Config";
434
437
  import * as ConfigProvider from "effect/ConfigProvider";
435
438
  import * as Effect from "effect/Effect";
439
+ import * as SecureRandom from "effect/crypto/SecureRandom";
436
440
  import * as FetchHttpClient from "effect/http/FetchHttpClient";
437
441
  import * as Layer from "effect/Layer";
438
442
  import * as Logger from "effect/Logger";
@@ -446,6 +450,7 @@ const layer = makeEntrypointLayer(tag, entrypoint);
446
450
  const platform = Layer.mergeAll(
447
451
  BunServices.layer,
448
452
  FetchHttpClient.layer,
453
+ SecureRandom.layerWebCrypto,
449
454
  Logger.layer([Logger.consolePretty()]),
450
455
  );
451
456