alchemy 2.0.0-beta.64 → 2.0.0-beta.65

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 (165) hide show
  1. package/lib/AWS/EC2/InternetGateway.d.ts.map +1 -1
  2. package/lib/AWS/EC2/InternetGateway.js +8 -3
  3. package/lib/AWS/EC2/InternetGateway.js.map +1 -1
  4. package/lib/AWS/EC2/LingeringEnis.d.ts +26 -0
  5. package/lib/AWS/EC2/LingeringEnis.d.ts.map +1 -0
  6. package/lib/AWS/EC2/LingeringEnis.js +128 -0
  7. package/lib/AWS/EC2/LingeringEnis.js.map +1 -0
  8. package/lib/AWS/EC2/SecurityGroup.d.ts.map +1 -1
  9. package/lib/AWS/EC2/SecurityGroup.js +14 -14
  10. package/lib/AWS/EC2/SecurityGroup.js.map +1 -1
  11. package/lib/AWS/EC2/Subnet.d.ts.map +1 -1
  12. package/lib/AWS/EC2/Subnet.js +13 -21
  13. package/lib/AWS/EC2/Subnet.js.map +1 -1
  14. package/lib/AWS/ECR/Image.d.ts +1 -0
  15. package/lib/AWS/ECR/Image.d.ts.map +1 -1
  16. package/lib/AWS/EKS/Addon.d.ts.map +1 -1
  17. package/lib/AWS/EKS/Addon.js +15 -3
  18. package/lib/AWS/EKS/Addon.js.map +1 -1
  19. package/lib/AWS/EKS/Deployment.d.ts +10 -1
  20. package/lib/AWS/EKS/Deployment.d.ts.map +1 -1
  21. package/lib/AWS/EKS/Deployment.js +10 -4
  22. package/lib/AWS/EKS/Deployment.js.map +1 -1
  23. package/lib/AWS/EKS/HyperPod.d.ts +57 -0
  24. package/lib/AWS/EKS/HyperPod.d.ts.map +1 -0
  25. package/lib/AWS/EKS/HyperPod.js +46 -0
  26. package/lib/AWS/EKS/HyperPod.js.map +1 -0
  27. package/lib/AWS/EKS/Job.d.ts +10 -1
  28. package/lib/AWS/EKS/Job.d.ts.map +1 -1
  29. package/lib/AWS/EKS/Job.js +16 -6
  30. package/lib/AWS/EKS/Job.js.map +1 -1
  31. package/lib/AWS/EKS/internal/client.d.ts +4 -3
  32. package/lib/AWS/EKS/internal/client.d.ts.map +1 -1
  33. package/lib/AWS/EKS/internal/client.js +33 -2
  34. package/lib/AWS/EKS/internal/client.js.map +1 -1
  35. package/lib/AWS/Lambda/Function.d.ts.map +1 -1
  36. package/lib/AWS/Lambda/Function.js +1 -1
  37. package/lib/AWS/Lambda/Function.js.map +1 -1
  38. package/lib/AWS/Providers.d.ts.map +1 -1
  39. package/lib/AWS/Providers.js +4 -1
  40. package/lib/AWS/Providers.js.map +1 -1
  41. package/lib/AWS/SageMaker/Cluster.d.ts +327 -0
  42. package/lib/AWS/SageMaker/Cluster.d.ts.map +1 -0
  43. package/lib/AWS/SageMaker/Cluster.js +469 -0
  44. package/lib/AWS/SageMaker/Cluster.js.map +1 -0
  45. package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts +96 -0
  46. package/lib/AWS/SageMaker/ClusterSchedulerConfig.d.ts.map +1 -0
  47. package/lib/AWS/SageMaker/ClusterSchedulerConfig.js +270 -0
  48. package/lib/AWS/SageMaker/ClusterSchedulerConfig.js.map +1 -0
  49. package/lib/AWS/SageMaker/ComputeQuota.d.ts +106 -0
  50. package/lib/AWS/SageMaker/ComputeQuota.d.ts.map +1 -0
  51. package/lib/AWS/SageMaker/ComputeQuota.js +268 -0
  52. package/lib/AWS/SageMaker/ComputeQuota.js.map +1 -0
  53. package/lib/AWS/SageMaker/index.d.ts +3 -0
  54. package/lib/AWS/SageMaker/index.d.ts.map +1 -1
  55. package/lib/AWS/SageMaker/index.js +3 -0
  56. package/lib/AWS/SageMaker/index.js.map +1 -1
  57. package/lib/Bundle/PurePlugin.d.ts +29 -7
  58. package/lib/Bundle/PurePlugin.d.ts.map +1 -1
  59. package/lib/Bundle/PurePlugin.js +56 -33
  60. package/lib/Bundle/PurePlugin.js.map +1 -1
  61. package/lib/Cli/commands/nuke.d.ts.map +1 -1
  62. package/lib/Cli/commands/nuke.js +231 -37
  63. package/lib/Cli/commands/nuke.js.map +1 -1
  64. package/lib/Cloudflare/StateStore/Api.d.ts +1 -1
  65. package/lib/Cloudflare/Workers/Assets.d.ts +26 -8
  66. package/lib/Cloudflare/Workers/Assets.d.ts.map +1 -1
  67. package/lib/Cloudflare/Workers/Assets.js +46 -1
  68. package/lib/Cloudflare/Workers/Assets.js.map +1 -1
  69. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts +2 -2
  70. package/lib/Cloudflare/Workers/LocalWorkerProvider.d.ts.map +1 -1
  71. package/lib/Cloudflare/Workers/LocalWorkerProvider.js +39 -6
  72. package/lib/Cloudflare/Workers/LocalWorkerProvider.js.map +1 -1
  73. package/lib/Cloudflare/Workers/Worker.d.ts +7 -5
  74. package/lib/Cloudflare/Workers/Worker.d.ts.map +1 -1
  75. package/lib/Cloudflare/Workers/Worker.js +0 -1
  76. package/lib/Cloudflare/Workers/Worker.js.map +1 -1
  77. package/lib/Cloudflare/Workers/WorkerBinding.d.ts +2 -2
  78. package/lib/Cloudflare/Workers/WorkerBundle.d.ts +20 -1
  79. package/lib/Cloudflare/Workers/WorkerBundle.d.ts.map +1 -1
  80. package/lib/Cloudflare/Workers/WorkerBundle.js +11 -0
  81. package/lib/Cloudflare/Workers/WorkerBundle.js.map +1 -1
  82. package/lib/Cloudflare/Workers/WorkerProvider.d.ts.map +1 -1
  83. package/lib/Cloudflare/Workers/WorkerProvider.js +10 -4
  84. package/lib/Cloudflare/Workers/WorkerProvider.js.map +1 -1
  85. package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts +2 -1
  86. package/lib/Cloudflare/Workflows/WorkflowBridge.d.ts.map +1 -1
  87. package/lib/Cloudflare/Workflows/WorkflowBridge.js +5 -5
  88. package/lib/Cloudflare/Workflows/WorkflowBridge.js.map +1 -1
  89. package/lib/Cloudflare/Workflows/index.d.ts +1 -1
  90. package/lib/Cloudflare/Workflows/index.d.ts.map +1 -1
  91. package/lib/Cloudflare/Workflows/index.js +1 -1
  92. package/lib/Cloudflare/Workflows/index.js.map +1 -1
  93. package/lib/Docker/Container.d.ts +26 -0
  94. package/lib/Docker/Container.d.ts.map +1 -1
  95. package/lib/Docker/Container.js +20 -1
  96. package/lib/Docker/Container.js.map +1 -1
  97. package/lib/Docker/Docker.d.ts +2 -0
  98. package/lib/Docker/Docker.d.ts.map +1 -1
  99. package/lib/Docker/Docker.js.map +1 -1
  100. package/lib/Output.d.ts +4 -1
  101. package/lib/Output.d.ts.map +1 -1
  102. package/lib/Output.js.map +1 -1
  103. package/lib/Provider.d.ts +20 -0
  104. package/lib/Provider.d.ts.map +1 -1
  105. package/lib/Provider.js.map +1 -1
  106. package/lib/Random.d.ts +1 -1
  107. package/lib/Random.d.ts.map +1 -1
  108. package/lib/Resource.d.ts +14 -1
  109. package/lib/Resource.d.ts.map +1 -1
  110. package/lib/Resource.js.map +1 -1
  111. package/lib/SQL/D1.d.ts +1 -1
  112. package/lib/SQL/D1.js +1 -1
  113. package/lib/SQL/Postgres.d.ts +1 -1
  114. package/lib/SQL/Postgres.js +1 -1
  115. package/lib/State/HttpStateApi.d.ts +11 -11
  116. package/lib/Test/Alchemy.d.ts +1 -1
  117. package/lib/Test/Alchemy.js +1 -1
  118. package/lib/Test/Vitest.d.ts +58 -0
  119. package/lib/Test/Vitest.d.ts.map +1 -0
  120. package/lib/Test/Vitest.js +114 -0
  121. package/lib/Test/Vitest.js.map +1 -0
  122. package/lib/Util/proxy-chain.d.ts +5 -0
  123. package/lib/Util/proxy-chain.d.ts.map +1 -1
  124. package/lib/Util/proxy-chain.js +52 -4
  125. package/lib/Util/proxy-chain.js.map +1 -1
  126. package/package.json +23 -15
  127. package/src/AWS/EC2/InternetGateway.ts +8 -3
  128. package/src/AWS/EC2/LingeringEnis.ts +169 -0
  129. package/src/AWS/EC2/SecurityGroup.ts +24 -28
  130. package/src/AWS/EC2/Subnet.ts +23 -34
  131. package/src/AWS/EKS/Addon.ts +17 -4
  132. package/src/AWS/EKS/Deployment.ts +26 -5
  133. package/src/AWS/EKS/HyperPod.ts +97 -0
  134. package/src/AWS/EKS/Job.ts +32 -7
  135. package/src/AWS/EKS/internal/client.ts +40 -3
  136. package/src/AWS/Lambda/Function.ts +5 -3
  137. package/src/AWS/Providers.ts +6 -0
  138. package/src/AWS/SageMaker/Cluster.ts +775 -0
  139. package/src/AWS/SageMaker/ClusterSchedulerConfig.ts +430 -0
  140. package/src/AWS/SageMaker/ComputeQuota.ts +426 -0
  141. package/src/AWS/SageMaker/index.ts +3 -0
  142. package/src/Bundle/PurePlugin.ts +92 -39
  143. package/src/Cli/commands/nuke.ts +295 -56
  144. package/src/Cloudflare/Workers/Assets.ts +53 -1
  145. package/src/Cloudflare/Workers/LocalWorkerProvider.ts +46 -7
  146. package/src/Cloudflare/Workers/Worker.ts +7 -5
  147. package/src/Cloudflare/Workers/WorkerBundle.ts +31 -1
  148. package/src/Cloudflare/Workers/WorkerProvider.ts +18 -4
  149. package/src/Cloudflare/Workflows/WorkflowBridge.ts +5 -5
  150. package/src/Cloudflare/Workflows/index.ts +1 -1
  151. package/src/Docker/Container.ts +30 -1
  152. package/src/Docker/Docker.ts +2 -0
  153. package/src/Output.ts +19 -11
  154. package/src/Provider.ts +20 -0
  155. package/src/Resource.ts +18 -1
  156. package/src/SQL/D1.ts +1 -1
  157. package/src/SQL/Postgres.ts +1 -1
  158. package/src/Test/Alchemy.ts +1 -1
  159. package/src/Test/Vitest.ts +244 -0
  160. package/src/Util/proxy-chain.ts +73 -4
  161. package/lib/SQL/index.d.ts +0 -4
  162. package/lib/SQL/index.d.ts.map +0 -1
  163. package/lib/SQL/index.js +0 -4
  164. package/lib/SQL/index.js.map +0 -1
  165. package/src/SQL/index.ts +0 -3
@@ -0,0 +1,430 @@
1
+ import * as sagemaker from "@distilled.cloud/aws/sagemaker";
2
+ import * as Data from "effect/Data";
3
+ import * as Effect from "effect/Effect";
4
+ import * as Schedule from "effect/Schedule";
5
+ import * as EffectStream from "effect/Stream";
6
+ import { Unowned } from "../../AdoptPolicy.ts";
7
+ import { isResolved } from "../../Diff.ts";
8
+ import { createPhysicalName } from "../../PhysicalName.ts";
9
+ import * as Provider from "../../Provider.ts";
10
+ import { Resource } from "../../Resource.ts";
11
+ import {
12
+ createInternalTags,
13
+ diffTags,
14
+ hasAlchemyTags,
15
+ type Tags,
16
+ } from "../../Tags.ts";
17
+ import type { Providers } from "../Providers.ts";
18
+
19
+ export type SchedulerResourceStatus = sagemaker.SchedulerResourceStatus;
20
+
21
+ export interface ClusterSchedulerConfigProps {
22
+ /**
23
+ * Name of the cluster policy. Maximum 63 characters, alphanumeric and
24
+ * hyphens.
25
+ * @default ${app}-${stage}-${id}
26
+ */
27
+ name?: string;
28
+ /**
29
+ * ARN of the EKS-orchestrated HyperPod cluster the policy applies to.
30
+ * Changing the cluster replaces the policy. AWS allows ONE cluster
31
+ * policy per cluster — creating a second fails with the typed
32
+ * `ClusterSchedulerConfigAlreadyExists` error.
33
+ */
34
+ clusterArn: string;
35
+ /**
36
+ * The policy itself: task priority classes, fair-share allocation, and
37
+ * idle-resource sharing.
38
+ */
39
+ schedulerConfig?: sagemaker.SchedulerConfig;
40
+ /**
41
+ * A description of the cluster policy.
42
+ */
43
+ description?: string;
44
+ /**
45
+ * Tags to associate with the cluster policy. Merged with internal Alchemy
46
+ * tags.
47
+ */
48
+ tags?: Record<string, string>;
49
+ }
50
+
51
+ export interface ClusterSchedulerConfig extends Resource<
52
+ "AWS.SageMaker.ClusterSchedulerConfig",
53
+ ClusterSchedulerConfigProps,
54
+ {
55
+ /**
56
+ * The cluster policy's ID.
57
+ */
58
+ clusterSchedulerConfigId: string;
59
+ /**
60
+ * ARN of the cluster policy.
61
+ */
62
+ clusterSchedulerConfigArn: string;
63
+ /**
64
+ * The cluster policy's name.
65
+ */
66
+ name: string;
67
+ /**
68
+ * ARN of the HyperPod cluster the policy applies to.
69
+ */
70
+ clusterArn: string;
71
+ /**
72
+ * The policy's current version (incremented on every update).
73
+ */
74
+ clusterSchedulerConfigVersion: number;
75
+ },
76
+ never,
77
+ Providers
78
+ > {}
79
+
80
+ /**
81
+ * A SageMaker HyperPod cluster policy (task governance) — configures how an
82
+ * EKS-orchestrated HyperPod cluster prioritizes tasks and allocates idle
83
+ * compute across teams via priority classes and fair-share weights.
84
+ * @resource
85
+ * @section Creating Cluster Policies
86
+ * @example Priority Classes with Fair-Share
87
+ * ```typescript
88
+ * import * as AWS from "alchemy/AWS";
89
+ *
90
+ * const policy = yield* AWS.SageMaker.ClusterSchedulerConfig("Scheduler", {
91
+ * clusterArn: hyperpod.clusterArn,
92
+ * schedulerConfig: {
93
+ * PriorityClasses: [
94
+ * { Name: "inference", Weight: 100 },
95
+ * { Name: "training", Weight: 75 },
96
+ * ],
97
+ * FairShare: "Enabled",
98
+ * },
99
+ * description: "Prioritize inference over training",
100
+ * });
101
+ * ```
102
+ */
103
+ export const ClusterSchedulerConfig = Resource<ClusterSchedulerConfig>(
104
+ "AWS.SageMaker.ClusterSchedulerConfig",
105
+ );
106
+
107
+ const createConfigName = (id: string, props: { name?: string | undefined }) =>
108
+ props.name
109
+ ? Effect.succeed(props.name)
110
+ : createPhysicalName({ id, maxLength: 63 });
111
+
112
+ const describeConfigOrUndefined = (configId: string) =>
113
+ sagemaker
114
+ .describeClusterSchedulerConfig({ ClusterSchedulerConfigId: configId })
115
+ .pipe(Effect.catchTag("ResourceNotFound", () => Effect.succeed(undefined)));
116
+
117
+ /**
118
+ * Look a cluster policy up by exact name — used when state was lost (read
119
+ * without output) or a create raced.
120
+ */
121
+ const findConfigByName = Effect.fn(function* (name: string) {
122
+ const summaries = yield* sagemaker.listClusterSchedulerConfigs
123
+ .pages({ NameContains: name })
124
+ .pipe(
125
+ EffectStream.runCollect,
126
+ Effect.map((chunk) =>
127
+ Array.from(chunk).flatMap(
128
+ (page) => page.ClusterSchedulerConfigSummaries ?? [],
129
+ ),
130
+ ),
131
+ );
132
+ return summaries.find((s) => s.Name === name && s.Status !== "Deleted");
133
+ });
134
+
135
+ const fetchConfigTags = Effect.fn(function* (arn: string) {
136
+ const response = yield* sagemaker
137
+ .listTags({ ResourceArn: arn })
138
+ .pipe(
139
+ Effect.catchTag("AccessDeniedException", () => Effect.succeed(undefined)),
140
+ );
141
+ return Object.fromEntries(
142
+ (response?.Tags ?? []).flatMap((tag) =>
143
+ tag.Key !== undefined ? [[tag.Key, tag.Value ?? ""]] : [],
144
+ ),
145
+ );
146
+ });
147
+
148
+ const toAttrs = (
149
+ described: sagemaker.DescribeClusterSchedulerConfigResponse,
150
+ ): ClusterSchedulerConfig["Attributes"] => ({
151
+ clusterSchedulerConfigId: described.ClusterSchedulerConfigId,
152
+ clusterSchedulerConfigArn: described.ClusterSchedulerConfigArn,
153
+ name: described.Name,
154
+ clusterArn: described.ClusterArn ?? "",
155
+ clusterSchedulerConfigVersion: described.ClusterSchedulerConfigVersion,
156
+ });
157
+
158
+ /**
159
+ * The cluster policy is still transitioning toward the awaited state —
160
+ * retried by the bounded wait schedule.
161
+ */
162
+ class SchedulerConfigNotReady extends Data.TaggedError(
163
+ "SchedulerConfigNotReady",
164
+ )<{
165
+ readonly configId: string;
166
+ readonly status: string | undefined;
167
+ }> {}
168
+
169
+ /**
170
+ * The cluster policy converged to a terminal failed status.
171
+ */
172
+ export class SchedulerConfigFailed extends Data.TaggedError(
173
+ "SchedulerConfigFailed",
174
+ )<{
175
+ readonly configId: string;
176
+ readonly status: string | undefined;
177
+ readonly message: string | undefined;
178
+ }> {}
179
+
180
+ const FAILED_STATUSES: string[] = [
181
+ "CreateFailed",
182
+ "CreateRollbackFailed",
183
+ "UpdateFailed",
184
+ "UpdateRollbackFailed",
185
+ "DeleteFailed",
186
+ "DeleteRollbackFailed",
187
+ ];
188
+
189
+ // Explicitly-typed retry wrapper — an inline `Effect.retry` in provider
190
+ // lifecycle code leaks `Retry.Return`'s conditional type into declaration
191
+ // emit and widens the provider layer to `unknown` for every consumer of
192
+ // `AWS.providers()`.
193
+ const retryWhileNotReady = <A, E extends { readonly _tag: string }, R>(
194
+ self: Effect.Effect<A, E, R>,
195
+ ): Effect.Effect<A, E, R> =>
196
+ Effect.retry(self, {
197
+ while: (e) => e._tag === "SchedulerConfigNotReady",
198
+ schedule: Schedule.max([Schedule.spaced("5 seconds"), Schedule.recurs(60)]),
199
+ });
200
+
201
+ const waitForConfig = (configId: string, target: "Ready" | "Gone") =>
202
+ retryWhileNotReady(
203
+ Effect.gen(function* () {
204
+ const described = yield* describeConfigOrUndefined(configId);
205
+ if (target === "Gone") {
206
+ if (described === undefined || described.Status === "Deleted") return;
207
+ if (FAILED_STATUSES.includes(described.Status)) {
208
+ return yield* Effect.fail(
209
+ new SchedulerConfigFailed({
210
+ configId,
211
+ status: described.Status,
212
+ message: described.FailureReason,
213
+ }),
214
+ );
215
+ }
216
+ return yield* Effect.fail(
217
+ new SchedulerConfigNotReady({ configId, status: described.Status }),
218
+ );
219
+ }
220
+ if (described?.Status === "Created" || described?.Status === "Updated") {
221
+ return;
222
+ }
223
+ if (
224
+ described !== undefined &&
225
+ FAILED_STATUSES.includes(described.Status)
226
+ ) {
227
+ return yield* Effect.fail(
228
+ new SchedulerConfigFailed({
229
+ configId,
230
+ status: described.Status,
231
+ message: described.FailureReason,
232
+ }),
233
+ );
234
+ }
235
+ return yield* Effect.fail(
236
+ new SchedulerConfigNotReady({ configId, status: described?.Status }),
237
+ );
238
+ }),
239
+ );
240
+
241
+ export const ClusterSchedulerConfigProvider = () =>
242
+ Provider.effect(
243
+ ClusterSchedulerConfig,
244
+ Effect.gen(function* () {
245
+ return {
246
+ stables: [
247
+ "clusterSchedulerConfigId",
248
+ "clusterSchedulerConfigArn",
249
+ "name",
250
+ "clusterArn",
251
+ ],
252
+ list: () =>
253
+ Effect.gen(function* () {
254
+ const summaries = yield* sagemaker.listClusterSchedulerConfigs
255
+ .pages({})
256
+ .pipe(
257
+ EffectStream.runCollect,
258
+ Effect.map((chunk) =>
259
+ Array.from(chunk).flatMap(
260
+ (page) => page.ClusterSchedulerConfigSummaries ?? [],
261
+ ),
262
+ ),
263
+ );
264
+ return summaries.flatMap((s) =>
265
+ s.ClusterSchedulerConfigId !== undefined && s.Status !== "Deleted"
266
+ ? [
267
+ {
268
+ clusterSchedulerConfigId: s.ClusterSchedulerConfigId,
269
+ clusterSchedulerConfigArn:
270
+ s.ClusterSchedulerConfigArn ?? "",
271
+ name: s.Name ?? "",
272
+ clusterArn: s.ClusterArn ?? "",
273
+ clusterSchedulerConfigVersion:
274
+ s.ClusterSchedulerConfigVersion ?? 1,
275
+ },
276
+ ]
277
+ : [],
278
+ );
279
+ }),
280
+ read: Effect.fn(function* ({ id, olds, output }) {
281
+ const configId =
282
+ output?.clusterSchedulerConfigId ??
283
+ (yield* findConfigByName(yield* createConfigName(id, olds ?? {})))
284
+ ?.ClusterSchedulerConfigId;
285
+ if (configId === undefined) return undefined;
286
+ const described = yield* describeConfigOrUndefined(configId);
287
+ if (
288
+ !described ||
289
+ described.Status === "Deleting" ||
290
+ described.Status === "Deleted"
291
+ ) {
292
+ return undefined;
293
+ }
294
+ const attrs = toAttrs(described);
295
+ const tags = yield* fetchConfigTags(attrs.clusterSchedulerConfigArn);
296
+ return (yield* hasAlchemyTags(id, tags as Tags))
297
+ ? attrs
298
+ : Unowned(attrs);
299
+ }),
300
+ diff: Effect.fn(function* ({ id, news, olds }) {
301
+ if (!isResolved(news)) return;
302
+ if (olds === undefined) return;
303
+ const oldName = yield* createConfigName(id, olds);
304
+ const newName = yield* createConfigName(id, news);
305
+ // The name and target cluster are fixed at creation.
306
+ if (oldName !== newName || olds.clusterArn !== news.clusterArn) {
307
+ return { action: "replace" } as const;
308
+ }
309
+ }),
310
+ reconcile: Effect.fn(function* ({ id, news, output, session }) {
311
+ if (!news) {
312
+ return yield* Effect.fail(
313
+ new Error("SageMaker ClusterSchedulerConfig requires props"),
314
+ );
315
+ }
316
+ const name = yield* createConfigName(id, news);
317
+ const internalTags = yield* createInternalTags(id);
318
+ const desiredTags = { ...internalTags, ...news.tags };
319
+
320
+ // Observe — by cached id first, then by name (lost state or race).
321
+ let configId = output?.clusterSchedulerConfigId;
322
+ let described =
323
+ configId !== undefined
324
+ ? yield* describeConfigOrUndefined(configId)
325
+ : undefined;
326
+ if (described === undefined) {
327
+ const found = yield* findConfigByName(name);
328
+ described =
329
+ found?.ClusterSchedulerConfigId !== undefined
330
+ ? yield* describeConfigOrUndefined(
331
+ found.ClusterSchedulerConfigId,
332
+ )
333
+ : undefined;
334
+ }
335
+
336
+ // Ensure — create if missing; a Conflict means a concurrent
337
+ // create won the race, so re-observe by name.
338
+ if (described === undefined) {
339
+ const created = yield* sagemaker
340
+ .createClusterSchedulerConfig({
341
+ Name: name,
342
+ ClusterArn: news.clusterArn,
343
+ SchedulerConfig: news.schedulerConfig,
344
+ Description: news.description,
345
+ Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
346
+ Key,
347
+ Value,
348
+ })),
349
+ })
350
+ .pipe(
351
+ Effect.catchTag("ConflictException", () =>
352
+ Effect.succeed(undefined),
353
+ ),
354
+ );
355
+ configId =
356
+ created?.ClusterSchedulerConfigId ??
357
+ (yield* findConfigByName(name))?.ClusterSchedulerConfigId;
358
+ if (configId === undefined) {
359
+ return yield* Effect.fail(
360
+ new Error(`failed to create cluster scheduler config ${name}`),
361
+ );
362
+ }
363
+ yield* session.note(`Creating cluster policy ${name}...`);
364
+ yield* waitForConfig(configId, "Ready");
365
+ } else {
366
+ configId = described.ClusterSchedulerConfigId;
367
+ // Wait out any in-flight transition before diffing.
368
+ yield* waitForConfig(configId, "Ready");
369
+ described = yield* describeConfigOrUndefined(configId);
370
+ // Sync — diff observed policy against desired.
371
+ if (
372
+ described !== undefined &&
373
+ (JSON.stringify(described.SchedulerConfig) !==
374
+ JSON.stringify(news.schedulerConfig) ||
375
+ (described.Description ?? undefined) !==
376
+ (news.description ?? undefined))
377
+ ) {
378
+ yield* sagemaker.updateClusterSchedulerConfig({
379
+ ClusterSchedulerConfigId: configId,
380
+ TargetVersion: described.ClusterSchedulerConfigVersion,
381
+ SchedulerConfig: news.schedulerConfig,
382
+ Description: news.description,
383
+ });
384
+ yield* session.note(`Updating cluster policy ${name}...`);
385
+ yield* waitForConfig(configId, "Ready");
386
+ }
387
+ }
388
+
389
+ described = yield* describeConfigOrUndefined(configId);
390
+ if (described === undefined) {
391
+ return yield* Effect.fail(
392
+ new Error(
393
+ `failed to read reconciled cluster scheduler config ${name}`,
394
+ ),
395
+ );
396
+ }
397
+ const attrs = toAttrs(described);
398
+
399
+ // Sync tags — diff against OBSERVED cloud tags.
400
+ const currentTags = yield* fetchConfigTags(
401
+ attrs.clusterSchedulerConfigArn,
402
+ );
403
+ const { removed, upsert } = diffTags(currentTags, desiredTags);
404
+ if (removed.length > 0) {
405
+ yield* sagemaker.deleteTags({
406
+ ResourceArn: attrs.clusterSchedulerConfigArn,
407
+ TagKeys: removed,
408
+ });
409
+ }
410
+ if (upsert.length > 0) {
411
+ yield* sagemaker.addTags({
412
+ ResourceArn: attrs.clusterSchedulerConfigArn,
413
+ Tags: upsert.map(({ Key, Value }) => ({ Key, Value })),
414
+ });
415
+ }
416
+
417
+ yield* session.note(attrs.clusterSchedulerConfigArn);
418
+ return attrs;
419
+ }),
420
+ delete: Effect.fn(function* ({ output }) {
421
+ yield* sagemaker
422
+ .deleteClusterSchedulerConfig({
423
+ ClusterSchedulerConfigId: output.clusterSchedulerConfigId,
424
+ })
425
+ .pipe(Effect.catchTag("ResourceNotFound", () => Effect.void));
426
+ yield* waitForConfig(output.clusterSchedulerConfigId, "Gone");
427
+ }),
428
+ };
429
+ }),
430
+ );