@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.
- package/lib/BuiltinAdapters.d.ts +20 -0
- package/lib/BuiltinAdapters.d.ts.map +1 -1
- package/lib/BuiltinAdapters.js +8 -3
- package/lib/BuiltinAdapters.js.map +1 -1
- package/lib/Client.d.ts +24 -7
- package/lib/Client.d.ts.map +1 -1
- package/lib/Client.js +22 -6
- package/lib/Client.js.map +1 -1
- package/lib/ClusterAdapter.d.ts +7 -4
- package/lib/ClusterAdapter.d.ts.map +1 -1
- package/lib/ClusterAdapter.js.map +1 -1
- package/lib/Connection.d.ts.map +1 -1
- package/lib/Connection.js.map +1 -1
- package/lib/Deployment.d.ts +75 -3
- package/lib/Deployment.d.ts.map +1 -1
- package/lib/Deployment.js +64 -2
- package/lib/Deployment.js.map +1 -1
- package/lib/HelmChart.d.ts.map +1 -1
- package/lib/HelmChart.js +1 -1
- package/lib/HelmChart.js.map +1 -1
- package/lib/Job.d.ts +62 -3
- package/lib/Job.d.ts.map +1 -1
- package/lib/Job.js +51 -1
- package/lib/Job.js.map +1 -1
- package/lib/LocalCluster.d.ts +22 -1
- package/lib/LocalCluster.d.ts.map +1 -1
- package/lib/LocalCluster.js +26 -4
- package/lib/LocalCluster.js.map +1 -1
- package/lib/Manifest.d.ts +42 -1
- package/lib/Manifest.d.ts.map +1 -1
- package/lib/Manifest.js +33 -2
- package/lib/Manifest.js.map +1 -1
- package/lib/Objects.d.ts.map +1 -1
- package/lib/Objects.js.map +1 -1
- package/lib/Providers.d.ts.map +1 -1
- package/lib/Providers.js.map +1 -1
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js.map +1 -1
- package/lib/internal/declared.d.ts.map +1 -1
- package/lib/internal/declared.js.map +1 -1
- package/lib/internal/helm.d.ts.map +1 -1
- package/lib/internal/helm.js +1 -1
- package/lib/internal/helm.js.map +1 -1
- package/lib/internal/kind.d.ts.map +1 -1
- package/lib/internal/kind.js.map +1 -1
- package/lib/internal/kubeconfig.d.ts.map +1 -1
- package/lib/internal/kubeconfig.js +17 -8
- package/lib/internal/kubeconfig.js.map +1 -1
- package/lib/internal/registry.d.ts +1 -1
- package/lib/internal/registry.d.ts.map +1 -1
- package/lib/internal/registry.js.map +1 -1
- package/lib/internal/workload.d.ts +1 -1
- package/lib/internal/workload.d.ts.map +1 -1
- package/lib/internal/workload.js +4 -0
- package/lib/internal/workload.js.map +1 -1
- package/package.json +9 -9
- package/src/BuiltinAdapters.ts +11 -3
- package/src/Client.ts +39 -8
- package/src/ClusterAdapter.ts +8 -4
- package/src/Deployment.ts +80 -5
- package/src/HelmChart.ts +1 -1
- package/src/Job.ts +66 -4
- package/src/LocalCluster.ts +39 -5
- package/src/Manifest.ts +58 -6
- package/src/internal/helm.ts +1 -1
- package/src/internal/kubeconfig.ts +17 -8
- package/src/internal/workload.ts +6 -1
package/src/LocalCluster.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
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
|
-
|
|
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 :
|
|
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,
|
|
213
|
+
): Effect.Effect<KubernetesObjectDefinition, ManifestInvalidProps> => {
|
|
174
214
|
const name = manifest.metadata?.name;
|
|
175
215
|
if (!name) {
|
|
176
216
|
return Effect.fail(
|
|
177
|
-
new
|
|
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.
|
|
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 };
|
package/src/internal/helm.ts
CHANGED
|
@@ -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*
|
|
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
|
|
274
|
-
?
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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;
|
package/src/internal/workload.ts
CHANGED
|
@@ -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
|
|
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
|
|