@microagi/alchemy-gcp 0.1.2

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 (123) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +88 -0
  3. package/lib/Auth/AuthProvider.d.ts +48 -0
  4. package/lib/Auth/AuthProvider.d.ts.map +1 -0
  5. package/lib/Auth/AuthProvider.js +84 -0
  6. package/lib/Auth/AuthProvider.js.map +1 -0
  7. package/lib/Auth/Credentials.d.ts +28 -0
  8. package/lib/Auth/Credentials.d.ts.map +1 -0
  9. package/lib/Auth/Credentials.js +41 -0
  10. package/lib/Auth/Credentials.js.map +1 -0
  11. package/lib/CloudResourceManager/Project.d.ts +143 -0
  12. package/lib/CloudResourceManager/Project.d.ts.map +1 -0
  13. package/lib/CloudResourceManager/Project.js +272 -0
  14. package/lib/CloudResourceManager/Project.js.map +1 -0
  15. package/lib/CloudResourceManager/index.d.ts +3 -0
  16. package/lib/CloudResourceManager/index.d.ts.map +1 -0
  17. package/lib/CloudResourceManager/index.js +2 -0
  18. package/lib/CloudResourceManager/index.js.map +1 -0
  19. package/lib/Compute/GlobalAddress.d.ts +101 -0
  20. package/lib/Compute/GlobalAddress.d.ts.map +1 -0
  21. package/lib/Compute/GlobalAddress.js +134 -0
  22. package/lib/Compute/GlobalAddress.js.map +1 -0
  23. package/lib/Compute/Network.d.ts +114 -0
  24. package/lib/Compute/Network.d.ts.map +1 -0
  25. package/lib/Compute/Network.js +145 -0
  26. package/lib/Compute/Network.js.map +1 -0
  27. package/lib/Compute/Operations.d.ts +34 -0
  28. package/lib/Compute/Operations.d.ts.map +1 -0
  29. package/lib/Compute/Operations.js +62 -0
  30. package/lib/Compute/Operations.js.map +1 -0
  31. package/lib/Compute/SharedVpcHost.d.ts +44 -0
  32. package/lib/Compute/SharedVpcHost.d.ts.map +1 -0
  33. package/lib/Compute/SharedVpcHost.js +71 -0
  34. package/lib/Compute/SharedVpcHost.js.map +1 -0
  35. package/lib/Compute/SharedVpcServiceProject.d.ts +46 -0
  36. package/lib/Compute/SharedVpcServiceProject.d.ts.map +1 -0
  37. package/lib/Compute/SharedVpcServiceProject.js +96 -0
  38. package/lib/Compute/SharedVpcServiceProject.js.map +1 -0
  39. package/lib/Compute/Subnetwork.d.ts +108 -0
  40. package/lib/Compute/Subnetwork.d.ts.map +1 -0
  41. package/lib/Compute/Subnetwork.js +201 -0
  42. package/lib/Compute/Subnetwork.js.map +1 -0
  43. package/lib/Compute/index.d.ts +6 -0
  44. package/lib/Compute/index.d.ts.map +1 -0
  45. package/lib/Compute/index.js +6 -0
  46. package/lib/Compute/index.js.map +1 -0
  47. package/lib/Container/Cluster.d.ts +216 -0
  48. package/lib/Container/Cluster.d.ts.map +1 -0
  49. package/lib/Container/Cluster.js +326 -0
  50. package/lib/Container/Cluster.js.map +1 -0
  51. package/lib/Container/NodePool.d.ts +214 -0
  52. package/lib/Container/NodePool.d.ts.map +1 -0
  53. package/lib/Container/NodePool.js +344 -0
  54. package/lib/Container/NodePool.js.map +1 -0
  55. package/lib/Container/Operations.d.ts +50 -0
  56. package/lib/Container/Operations.d.ts.map +1 -0
  57. package/lib/Container/Operations.js +51 -0
  58. package/lib/Container/Operations.js.map +1 -0
  59. package/lib/Container/index.d.ts +5 -0
  60. package/lib/Container/index.d.ts.map +1 -0
  61. package/lib/Container/index.js +3 -0
  62. package/lib/Container/index.js.map +1 -0
  63. package/lib/Parallelstore/Instance.d.ts +119 -0
  64. package/lib/Parallelstore/Instance.d.ts.map +1 -0
  65. package/lib/Parallelstore/Instance.js +180 -0
  66. package/lib/Parallelstore/Instance.js.map +1 -0
  67. package/lib/Parallelstore/index.d.ts +2 -0
  68. package/lib/Parallelstore/index.d.ts.map +1 -0
  69. package/lib/Parallelstore/index.js +2 -0
  70. package/lib/Parallelstore/index.js.map +1 -0
  71. package/lib/Providers.d.ts +24 -0
  72. package/lib/Providers.d.ts.map +1 -0
  73. package/lib/Providers.js +51 -0
  74. package/lib/Providers.js.map +1 -0
  75. package/lib/ServiceNetworking/PsaConnection.d.ts +84 -0
  76. package/lib/ServiceNetworking/PsaConnection.d.ts.map +1 -0
  77. package/lib/ServiceNetworking/PsaConnection.js +130 -0
  78. package/lib/ServiceNetworking/PsaConnection.js.map +1 -0
  79. package/lib/ServiceNetworking/index.d.ts +2 -0
  80. package/lib/ServiceNetworking/index.d.ts.map +1 -0
  81. package/lib/ServiceNetworking/index.js +2 -0
  82. package/lib/ServiceNetworking/index.js.map +1 -0
  83. package/lib/ServiceUsage/ApiEnable.d.ts +78 -0
  84. package/lib/ServiceUsage/ApiEnable.d.ts.map +1 -0
  85. package/lib/ServiceUsage/ApiEnable.js +132 -0
  86. package/lib/ServiceUsage/ApiEnable.js.map +1 -0
  87. package/lib/ServiceUsage/index.d.ts +3 -0
  88. package/lib/ServiceUsage/index.d.ts.map +1 -0
  89. package/lib/ServiceUsage/index.js +2 -0
  90. package/lib/ServiceUsage/index.js.map +1 -0
  91. package/lib/Tags.d.ts +57 -0
  92. package/lib/Tags.d.ts.map +1 -0
  93. package/lib/Tags.js +106 -0
  94. package/lib/Tags.js.map +1 -0
  95. package/lib/index.d.ts +11 -0
  96. package/lib/index.d.ts.map +1 -0
  97. package/lib/index.js +10 -0
  98. package/lib/index.js.map +1 -0
  99. package/package.json +70 -0
  100. package/src/Auth/AuthProvider.ts +180 -0
  101. package/src/Auth/Credentials.ts +69 -0
  102. package/src/CloudResourceManager/Project.ts +517 -0
  103. package/src/CloudResourceManager/index.ts +2 -0
  104. package/src/Compute/GlobalAddress.ts +283 -0
  105. package/src/Compute/Network.ts +306 -0
  106. package/src/Compute/Operations.ts +108 -0
  107. package/src/Compute/SharedVpcHost.ts +141 -0
  108. package/src/Compute/SharedVpcServiceProject.ts +185 -0
  109. package/src/Compute/Subnetwork.ts +375 -0
  110. package/src/Compute/index.ts +5 -0
  111. package/src/Container/Cluster.ts +599 -0
  112. package/src/Container/NodePool.ts +620 -0
  113. package/src/Container/Operations.ts +88 -0
  114. package/src/Container/index.ts +8 -0
  115. package/src/Parallelstore/Instance.ts +371 -0
  116. package/src/Parallelstore/index.ts +1 -0
  117. package/src/Providers.ts +84 -0
  118. package/src/ServiceNetworking/PsaConnection.ts +269 -0
  119. package/src/ServiceNetworking/index.ts +1 -0
  120. package/src/ServiceUsage/ApiEnable.ts +267 -0
  121. package/src/ServiceUsage/index.ts +2 -0
  122. package/src/Tags.ts +130 -0
  123. package/src/index.ts +16 -0
@@ -0,0 +1,88 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as cont from "@distilled.cloud/gcp/container-v1";
3
+ import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
4
+ import * as Duration from "effect/Duration";
5
+ import * as Effect from "effect/Effect";
6
+ import * as Schedule from "effect/Schedule";
7
+
8
+ /**
9
+ * Qualify a GKE operation name. The Container API returns
10
+ * `Operation.name` as a bare ID like `operation-1778057673333-...`,
11
+ * but `getProjectsLocationsOperations` expects the fully-qualified
12
+ * path `projects/{p}/locations/{l}/operations/{id}`. Already-qualified
13
+ * names are passed through unchanged so callers can stay agnostic.
14
+ *
15
+ * (CRM operations don't need this because CRM returns names as
16
+ * `operations/...` and its `getOperations` takes the bare path
17
+ * directly.)
18
+ */
19
+ export const qualifyOperationName = (
20
+ project: string,
21
+ location: string,
22
+ name: string,
23
+ ): string =>
24
+ name.startsWith("projects/")
25
+ ? name
26
+ : `projects/${project}/locations/${location}/operations/${name}`;
27
+
28
+ /**
29
+ * Resolved callable signature of `cont.getProjectsLocationsOperations`.
30
+ *
31
+ * `cont.getProjectsLocationsOperations` is itself an
32
+ * `Effect<callable, never, Credentials | HttpClient>`, so `Effect.Success`
33
+ * extracts the callable type — i.e. what you get back from
34
+ * `yield* cont.getProjectsLocationsOperations`. Using a derived alias
35
+ * keeps the helper aligned with the SDK without us hand-writing the
36
+ * input/output/error union, which would drift if the patch set changes.
37
+ */
38
+ type GetOperations = Effect.Success<typeof cont.getProjectsLocationsOperations>;
39
+
40
+ /**
41
+ * Build the GKE long-running-operation polling helper.
42
+ *
43
+ * Container `Operation`s carry `.status: "PENDING" | "RUNNING" | "DONE"
44
+ * | "ABORTING"` (NOT `.done: boolean` like Cloud Resource Manager —
45
+ * verified at `vendor/distilled/packages/gcp/src/services/container-v1.ts:919–976`).
46
+ * The doneness predicate compares `.status === "DONE"` accordingly.
47
+ *
48
+ * Schedule: exponential 2s → 1.5× growth, capped per-poll at 15s via
49
+ * `Schedule.either`, then a hard count cap of 80 retries via
50
+ * `Schedule.both(Schedule.recurs(80))` → ~20 min wall ceiling, well
51
+ * above the worst observed GKE op time (cluster create runs 5–10 min).
52
+ *
53
+ * The helper is a factory — the caller resolves
54
+ * `cont.getProjectsLocationsOperations` once at provider construction
55
+ * (see `Project.ts` for the same idiom) and passes the resulting
56
+ * callable here, so we never re-resolve services on each await.
57
+ */
58
+ export const makeAwaitOperation = (getOperations: GetOperations) =>
59
+ Effect.fn(function* (
60
+ operationName: string,
61
+ session: ScopedPlanStatusSession,
62
+ ) {
63
+ const op = yield* getOperations({ name: operationName }).pipe(
64
+ Effect.flatMap((current) =>
65
+ current.status === "DONE"
66
+ ? Effect.succeed(current)
67
+ : Effect.fail({ _tag: "OperationPending" as const }),
68
+ ),
69
+ Effect.retry({
70
+ while: (e: { _tag?: string }) => e?._tag === "OperationPending",
71
+ schedule: Schedule.exponential(Duration.seconds(2), 1.5).pipe(
72
+ Schedule.either(Schedule.spaced(Duration.seconds(15))),
73
+ Schedule.both(Schedule.recurs(80)),
74
+ Schedule.tapOutput(() =>
75
+ session.note(`Waiting for GKE operation ${operationName}…`),
76
+ ),
77
+ ),
78
+ }),
79
+ );
80
+ if (op.error) {
81
+ return yield* new ConfigError({
82
+ message: `GKE operation ${operationName} failed: ${
83
+ op.error.message ?? JSON.stringify(op.error)
84
+ }`,
85
+ });
86
+ }
87
+ return op;
88
+ });
@@ -0,0 +1,8 @@
1
+ export { Cluster, ClusterProvider } from "./Cluster.ts";
2
+ export type {
3
+ ClusterAttributes,
4
+ ClusterInitialNodePool,
5
+ ClusterProps,
6
+ } from "./Cluster.ts";
7
+ export { NodePool, NodePoolProvider } from "./NodePool.ts";
8
+ export type { NodePoolAttributes, NodePoolProps } from "./NodePool.ts";
@@ -0,0 +1,371 @@
1
+ import { ConfigError } from "@distilled.cloud/gcp";
2
+ import * as ps from "@distilled.cloud/gcp/parallelstore-v1";
3
+ import { Resource } from "alchemy";
4
+ import { Unowned } from "alchemy/AdoptPolicy";
5
+ import type { ScopedPlanStatusSession } from "alchemy/Cli/Cli";
6
+ import { deepEqual, isResolved, somePropsAreDifferent } from "alchemy/Diff";
7
+ import { createPhysicalName } from "alchemy/PhysicalName";
8
+ import * as Provider from "alchemy/Provider";
9
+ import { diffTags } from "alchemy/Tags";
10
+ import * as Duration from "effect/Duration";
11
+ import * as Effect from "effect/Effect";
12
+ import * as Schedule from "effect/Schedule";
13
+ import type { NetworkRef } from "../Compute/Network.ts";
14
+ import { gcpInternalLabels, hasAlchemyLabels } from "../Tags.ts";
15
+ import type * as GCP from "../Providers.ts";
16
+
17
+ /**
18
+ * A Parallelstore instance — a high-throughput, POSIX-style filesystem
19
+ * Google sells for AI training/inference shared storage. Backed by DAOS
20
+ * under the hood; mounted in clusters via the Parallelstore CSI driver.
21
+ *
22
+ * **Network preconditions.** The instance attaches to a VPC over PSA
23
+ * (Private Services Access). The consumer VPC must already have:
24
+ * 1. A `GCP.GlobalAddress` reservation (`purpose=VPC_PEERING`,
25
+ * `addressType=INTERNAL`, ≥/24 prefix length).
26
+ * 2. A `GCP.PsaConnection` to `servicenetworking.googleapis.com`
27
+ * consuming that range.
28
+ * See the Shared VPC + Parallelstore wiring in the apps/cluster stack.
29
+ *
30
+ * **Capacity.** `capacityGib` must be a multiple of 12000 GiB and
31
+ * between 12000 (12 TiB) and 100800 (98.4 TiB). Smaller deployments
32
+ * may use 100 GiB increments depending on the SKU; the API surfaces
33
+ * the floor at create time.
34
+ *
35
+ * Parallelstore operations use a CRM-style `Operation.done: boolean`
36
+ * (verified at `parallelstore-v1.ts`).
37
+ *
38
+ * @section Creating a Parallelstore instance
39
+ * @example 20 TiB scratch instance
40
+ * ```typescript
41
+ * const fs = yield* GCP.ParallelstoreInstance("ModelsFs", {
42
+ * project: hostProject.projectId,
43
+ * location: "us-central1-a",
44
+ * capacityGib: "20480",
45
+ * network: GCP.networkRef(hostProject.projectNumber, vpc.name),
46
+ * reservedIpRange: psaRange.name,
47
+ * deploymentType: "SCRATCH",
48
+ * fileStripeLevel: "FILE_STRIPE_LEVEL_BALANCED",
49
+ * directoryStripeLevel: "DIRECTORY_STRIPE_LEVEL_BALANCED",
50
+ * });
51
+ * ```
52
+ */
53
+ export type ParallelstoreInstanceProps = {
54
+ /** GCP project ID hosting the instance. Immutable — replace if changed. */
55
+ project: string;
56
+ /**
57
+ * Parallelstore location. **Zone, not region** — Parallelstore is a
58
+ * zonal product. Common: `us-central1-a`. Immutable — replace.
59
+ */
60
+ location: string;
61
+ /**
62
+ * Instance ID. Defaults to `createPhysicalName({ id, lowercase: true,
63
+ * maxLength: 63 })`. Lowercase letters/numbers/hyphens, must start
64
+ * with a letter, end with a letter or number. Immutable — replace.
65
+ */
66
+ instanceId?: string;
67
+ /** User-visible description. Mutable via `patch`. */
68
+ description?: string;
69
+ /**
70
+ * Capacity in **GiB** (1024-based). Sent to the API as a string per
71
+ * Parallelstore's int64-encoded-as-string convention. Replace-only —
72
+ * Parallelstore doesn't currently support online resize.
73
+ */
74
+ capacityGib: string;
75
+ /**
76
+ * Fully-qualified consumer VPC in project-number form. Build with
77
+ * `networkRef(project.projectNumber, vpc.name)` — the `NetworkRef`
78
+ * template literal type rejects project-ID-form strings at compile
79
+ * time. Immutable — replace.
80
+ */
81
+ network: NetworkRef;
82
+ /**
83
+ * Name (or self-link) of a `GCP.GlobalAddress` PSA reservation in
84
+ * the same VPC. Immutable — replace.
85
+ */
86
+ reservedIpRange?: string;
87
+ /** `SCRATCH` (default) or `PERSISTENT`. Immutable — replace. */
88
+ deploymentType?: "SCRATCH" | "PERSISTENT";
89
+ /** File stripe level. Replace-only. */
90
+ fileStripeLevel?:
91
+ | "FILE_STRIPE_LEVEL_MIN"
92
+ | "FILE_STRIPE_LEVEL_BALANCED"
93
+ | "FILE_STRIPE_LEVEL_MAX";
94
+ /** Directory stripe level. Replace-only. */
95
+ directoryStripeLevel?:
96
+ | "DIRECTORY_STRIPE_LEVEL_MIN"
97
+ | "DIRECTORY_STRIPE_LEVEL_BALANCED"
98
+ | "DIRECTORY_STRIPE_LEVEL_MAX";
99
+ /**
100
+ * Resource labels. Alchemy internal labels (`alchemy_app`,
101
+ * `alchemy_stage`, `alchemy_id`) are merged on top automatically.
102
+ * Mutable via `patch`.
103
+ */
104
+ labels?: Record<string, string>;
105
+ };
106
+
107
+ export type ParallelstoreInstanceAttributes = {
108
+ /** Bare instance id. */
109
+ instanceId: string;
110
+ /** Project ID. */
111
+ project: string;
112
+ /** Zone. */
113
+ location: string;
114
+ /** Resource name `projects/{p}/locations/{l}/instances/{id}`. */
115
+ name: string;
116
+ /** Description, mirroring props. */
117
+ description: string | undefined;
118
+ /** Capacity in GiB. */
119
+ capacityGib: string;
120
+ /** Consumer VPC URL (project-number form). */
121
+ network: NetworkRef;
122
+ /** Reserved IP range used (server-resolved). */
123
+ reservedIpRange: string | undefined;
124
+ /** Effective reserved range (post-allocation). */
125
+ effectiveReservedIpRange: string | undefined;
126
+ /** Lifecycle state — `ACTIVE` once ready. */
127
+ state: string | undefined;
128
+ /** Active deployment type. */
129
+ deploymentType: string | undefined;
130
+ /** DNS access points VMs mount via. */
131
+ accessPoints: ReadonlyArray<string>;
132
+ /** Labels currently set, including internals. */
133
+ labels: Record<string, string>;
134
+ };
135
+
136
+ export type ParallelstoreInstance = Resource<
137
+ "GCP.ParallelstoreInstance",
138
+ ParallelstoreInstanceProps,
139
+ ParallelstoreInstanceAttributes,
140
+ never,
141
+ GCP.Providers
142
+ >;
143
+ export const ParallelstoreInstance = Resource<ParallelstoreInstance>(
144
+ "GCP.ParallelstoreInstance",
145
+ );
146
+
147
+ const toAttributes = (
148
+ i: ps.Instance,
149
+ parent: {
150
+ project: string;
151
+ location: string;
152
+ instanceId: string;
153
+ network: NetworkRef;
154
+ },
155
+ ): ParallelstoreInstanceAttributes => ({
156
+ instanceId: parent.instanceId,
157
+ project: parent.project,
158
+ location: parent.location,
159
+ name: i.name ?? `projects/${parent.project}/locations/${parent.location}/instances/${parent.instanceId}`,
160
+ description: i.description,
161
+ capacityGib: i.capacityGib ?? "",
162
+ // The server-echoed `i.network` matches what we sent up at create
163
+ // time, but we thread `parent.network` through to preserve the
164
+ // `NetworkRef` type without a cast — same pattern as project /
165
+ // location, both of which would otherwise lose their typing through
166
+ // the SDK's `string | undefined` fields.
167
+ network: parent.network,
168
+ reservedIpRange: i.reservedIpRange,
169
+ effectiveReservedIpRange: i.effectiveReservedIpRange,
170
+ state: i.state,
171
+ deploymentType: i.deploymentType,
172
+ accessPoints: i.accessPoints ?? [],
173
+ labels: { ...(i.labels ?? {}) },
174
+ });
175
+
176
+ export const ParallelstoreInstanceProvider = () =>
177
+ Provider.effect(
178
+ ParallelstoreInstance,
179
+ Effect.gen(function* () {
180
+ const getInstance = yield* ps.getProjectsLocationsInstances;
181
+ const createInstance = yield* ps.createProjectsLocationsInstances;
182
+ const patchInstance = yield* ps.patchProjectsLocationsInstances;
183
+ const deleteInstance = yield* ps.deleteProjectsLocationsInstances;
184
+ const getOperation = yield* ps.getProjectsLocationsOperations;
185
+
186
+ const awaitOperation = Effect.fn(function* (
187
+ operationName: string,
188
+ session: ScopedPlanStatusSession,
189
+ ) {
190
+ const op = yield* getOperation({ name: operationName }).pipe(
191
+ Effect.flatMap((current) =>
192
+ current.done === true
193
+ ? Effect.succeed(current)
194
+ : Effect.fail({ _tag: "OperationPending" as const }),
195
+ ),
196
+ Effect.retry({
197
+ while: (e: { _tag?: string }) => e?._tag === "OperationPending",
198
+ schedule: Schedule.exponential(Duration.seconds(5), 1.5).pipe(
199
+ Schedule.either(Schedule.spaced(Duration.seconds(30))),
200
+ // Parallelstore creates routinely run 10–25 min for
201
+ // multi-TiB instances. 200 × 30s ≈ 100 min wall ceiling.
202
+ Schedule.both(Schedule.recurs(200)),
203
+ Schedule.tapOutput(() =>
204
+ session.note(`Waiting for Parallelstore operation ${operationName}…`),
205
+ ),
206
+ ),
207
+ }),
208
+ );
209
+ if (op.error) {
210
+ return yield* new ConfigError({
211
+ message: `Parallelstore operation ${operationName} failed: ${
212
+ op.error.message ?? JSON.stringify(op.error)
213
+ }`,
214
+ });
215
+ }
216
+ return op;
217
+ });
218
+
219
+ const fqName = (project: string, location: string, instanceId: string) =>
220
+ `projects/${project}/locations/${location}/instances/${instanceId}`;
221
+
222
+ const observe = (project: string, location: string, instanceId: string) =>
223
+ getInstance({ name: fqName(project, location, instanceId) }).pipe(
224
+ Effect.catchTag("NotFound", () =>
225
+ Effect.succeed(undefined as ps.Instance | undefined),
226
+ ),
227
+ Effect.catchTag("Forbidden", () =>
228
+ Effect.succeed(undefined as ps.Instance | undefined),
229
+ ),
230
+ );
231
+
232
+ const syncMutable = Effect.fn(function* (args: {
233
+ name: string;
234
+ observed: ps.Instance;
235
+ desired: { description: string | undefined; labels: Record<string, string> };
236
+ session: ScopedPlanStatusSession;
237
+ }) {
238
+ const updateMaskFields: string[] = [];
239
+ if ((args.observed.description ?? undefined) !== args.desired.description) {
240
+ updateMaskFields.push("description");
241
+ }
242
+ const observedLabels = { ...(args.observed.labels ?? {}) };
243
+ const labelDiff = diffTags(observedLabels, args.desired.labels);
244
+ if (labelDiff.removed.length > 0 || labelDiff.upsert.length > 0) {
245
+ updateMaskFields.push("labels");
246
+ }
247
+ if (updateMaskFields.length === 0) return;
248
+
249
+ const op = yield* patchInstance({
250
+ name: args.name,
251
+ updateMask: updateMaskFields.join(","),
252
+ body: {
253
+ description: args.desired.description,
254
+ labels: args.desired.labels,
255
+ },
256
+ });
257
+ if (op.name) yield* awaitOperation(op.name, args.session);
258
+ });
259
+
260
+ return {
261
+ stables: ["instanceId", "project", "location", "name"],
262
+ diff: Effect.fn(function* ({ news, olds = {} }) {
263
+ if (!isResolved(news)) return undefined;
264
+ if (
265
+ somePropsAreDifferent(olds as ParallelstoreInstanceProps, news, [
266
+ "project",
267
+ "location",
268
+ "instanceId",
269
+ "network",
270
+ "reservedIpRange",
271
+ "deploymentType",
272
+ "fileStripeLevel",
273
+ "directoryStripeLevel",
274
+ "capacityGib",
275
+ ])
276
+ ) {
277
+ return { action: "replace" } as const;
278
+ }
279
+ return undefined;
280
+ }),
281
+ reconcile: Effect.fn(function* ({ id, news, session }) {
282
+ const internalLabels = yield* gcpInternalLabels(id);
283
+ const desiredId =
284
+ news.instanceId ??
285
+ (yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
286
+ const parent = `projects/${news.project}/locations/${news.location}`;
287
+ const name = fqName(news.project, news.location, desiredId);
288
+ const desiredLabels: Record<string, string> = {
289
+ ...(news.labels ?? {}),
290
+ ...internalLabels,
291
+ };
292
+
293
+ let observed = yield* observe(news.project, news.location, desiredId);
294
+
295
+ if (!observed) {
296
+ const body: ps.Instance = {
297
+ capacityGib: news.capacityGib,
298
+ network: news.network,
299
+ ...(news.description ? { description: news.description } : {}),
300
+ ...(news.reservedIpRange ? { reservedIpRange: news.reservedIpRange } : {}),
301
+ ...(news.deploymentType ? { deploymentType: news.deploymentType } : {}),
302
+ ...(news.fileStripeLevel ? { fileStripeLevel: news.fileStripeLevel } : {}),
303
+ ...(news.directoryStripeLevel
304
+ ? { directoryStripeLevel: news.directoryStripeLevel }
305
+ : {}),
306
+ labels: desiredLabels,
307
+ };
308
+ const op = yield* createInstance({
309
+ parent,
310
+ instanceId: desiredId,
311
+ body,
312
+ }).pipe(
313
+ Effect.catchTag("Conflict", () =>
314
+ Effect.succeed(undefined as ps.Operation | undefined),
315
+ ),
316
+ );
317
+ if (op?.name) yield* awaitOperation(op.name, session);
318
+ observed = yield* getInstance({ name });
319
+ }
320
+
321
+ yield* syncMutable({
322
+ name,
323
+ observed,
324
+ desired: { description: news.description, labels: desiredLabels },
325
+ session,
326
+ });
327
+
328
+ const final = yield* getInstance({ name });
329
+ return toAttributes(final, {
330
+ project: news.project,
331
+ location: news.location,
332
+ instanceId: desiredId,
333
+ network: news.network,
334
+ });
335
+ }),
336
+ delete: Effect.fn(function* ({ output, session }) {
337
+ yield* deleteInstance({ name: output.name }).pipe(
338
+ Effect.flatMap((op) =>
339
+ op.name ? awaitOperation(op.name, session) : Effect.succeed(op),
340
+ ),
341
+ Effect.catchTag("NotFound", () => Effect.void),
342
+ );
343
+ }),
344
+ read: Effect.fn(function* ({ id, output, olds }) {
345
+ const project = output?.project ?? olds?.project;
346
+ const location = output?.location ?? olds?.location;
347
+ // `network` only narrows to NetworkRef when it comes from
348
+ // post-create `output`. `olds` is the snapshot of *props*
349
+ // including a NetworkRef, so both feed into the same typed
350
+ // pipeline.
351
+ const network = output?.network ?? olds?.network;
352
+ if (!project || !location || !network) return undefined;
353
+ const instanceId =
354
+ output?.instanceId ??
355
+ olds?.instanceId ??
356
+ (yield* createPhysicalName({ id, maxLength: 63 })).toLowerCase();
357
+ const observed = yield* observe(project, location, instanceId);
358
+ if (!observed) return undefined;
359
+ const attrs = toAttributes(observed, {
360
+ project,
361
+ location,
362
+ instanceId,
363
+ network,
364
+ });
365
+ return (yield* hasAlchemyLabels(id, observed.labels))
366
+ ? attrs
367
+ : Unowned(attrs);
368
+ }),
369
+ };
370
+ }),
371
+ );
@@ -0,0 +1 @@
1
+ export * from "./Instance.ts";
@@ -0,0 +1,84 @@
1
+ import * as Provider from "alchemy/Provider";
2
+ import * as Layer from "effect/Layer";
3
+ import { GCPAuth } from "./Auth/AuthProvider.ts";
4
+ import { fromAuthProvider } from "./Auth/Credentials.ts";
5
+ import { Project, ProjectProvider } from "./CloudResourceManager/Project.ts";
6
+ import { GlobalAddress, GlobalAddressProvider } from "./Compute/GlobalAddress.ts";
7
+ import { Network, NetworkProvider } from "./Compute/Network.ts";
8
+ import { SharedVpcHost, SharedVpcHostProvider } from "./Compute/SharedVpcHost.ts";
9
+ import {
10
+ SharedVpcServiceProject,
11
+ SharedVpcServiceProjectProvider,
12
+ } from "./Compute/SharedVpcServiceProject.ts";
13
+ import { Subnetwork, SubnetworkProvider } from "./Compute/Subnetwork.ts";
14
+ import { Cluster, ClusterProvider } from "./Container/Cluster.ts";
15
+ import { NodePool, NodePoolProvider } from "./Container/NodePool.ts";
16
+ import {
17
+ ParallelstoreInstance,
18
+ ParallelstoreInstanceProvider,
19
+ } from "./Parallelstore/Instance.ts";
20
+ import {
21
+ PsaConnection,
22
+ PsaConnectionProvider,
23
+ } from "./ServiceNetworking/PsaConnection.ts";
24
+ import { ApiEnable, ApiEnableProvider } from "./ServiceUsage/ApiEnable.ts";
25
+
26
+ export class Providers extends Provider.ProviderCollection<Providers>()("GCP") {}
27
+
28
+ export type ProviderRequirements = Layer.Services<ReturnType<typeof providers>>;
29
+
30
+ /**
31
+ * Bundle the GCP resource providers, AuthProvider-backed credentials, and an
32
+ * HTTP client into a single Layer suitable for `Alchemy.Stack({ providers })`.
33
+ *
34
+ * Credentials resolve through the Alchemy AuthProvider registry — the active
35
+ * profile (from `ALCHEMY_PROFILE`, default `"default"`) configures `project`
36
+ * and optional `keyFile` overrides; everything else (ADC discovery, env-var
37
+ * key files, metadata server) is delegated to `google-auth-library`. To
38
+ * bypass the registry, import {@link fromADC} directly.
39
+ *
40
+ * `FetchHttpClient.layer` is provided once at the Stack level (see
41
+ * `vendor/alchemy/packages/alchemy/src/Stack.ts`); we intentionally do NOT
42
+ * re-provide it here, mirroring `AWS/Providers.ts` and
43
+ * `Cloudflare/Providers.ts`.
44
+ */
45
+ export const providers = () =>
46
+ Layer.effect(
47
+ Providers,
48
+ Provider.collection([
49
+ Project,
50
+ Cluster,
51
+ NodePool,
52
+ ApiEnable,
53
+ Network,
54
+ Subnetwork,
55
+ GlobalAddress,
56
+ SharedVpcHost,
57
+ SharedVpcServiceProject,
58
+ PsaConnection,
59
+ ParallelstoreInstance,
60
+ ]),
61
+ ).pipe(
62
+ Layer.provide(
63
+ Layer.mergeAll(
64
+ ProjectProvider(),
65
+ ClusterProvider(),
66
+ NodePoolProvider(),
67
+ ApiEnableProvider(),
68
+ NetworkProvider(),
69
+ SubnetworkProvider(),
70
+ GlobalAddressProvider(),
71
+ SharedVpcHostProvider(),
72
+ SharedVpcServiceProjectProvider(),
73
+ PsaConnectionProvider(),
74
+ ParallelstoreInstanceProvider(),
75
+ ),
76
+ ),
77
+ Layer.provideMerge(fromAuthProvider()),
78
+ Layer.provideMerge(GCPAuth),
79
+ // Auth/config failures (missing ADC, malformed profile) become
80
+ // unrecoverable defects rather than typed errors — mirrors
81
+ // `AWS/Providers.ts` and `Cloudflare/Providers.ts`. Resource lifecycle
82
+ // handlers still surface their own typed errors normally.
83
+ Layer.orDie,
84
+ );