| 1 | import * as compute from "@distilled.cloud/gcp/compute_v1"; |
| 2 | import { waitRegionOperation } from "./operations.ts"; |
| 3 | import * as Data from "effect/Data"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Schedule from "effect/Schedule"; |
| 6 | import * as Stream from "effect/Stream"; |
| 7 | import { Unowned } from "../../AdoptPolicy.ts"; |
| 8 | import { isResolved } from "../../Diff.ts"; |
| 9 | import { createPhysicalName } from "../../PhysicalName.ts"; |
| 10 | import * as Provider from "../../Provider.ts"; |
| 11 | import { Resource } from "../../Resource.ts"; |
| 12 | import { tagRecord } from "../../Tags.ts"; |
| 13 | import { GcpEnvironment } from "../Environment.ts"; |
| 14 | import { |
| 15 | createInternalLabels, |
| 16 | diffLabels, |
| 17 | hasAlchemyLabels, |
| 18 | stripInternalLabels, |
| 19 | toLabels, |
| 20 | } from "../Labels.ts"; |
| 21 | import type { Providers } from "../Providers.ts"; |
| 22 | |
| 23 | export type RegionDiskProps = { |
| 24 | /** |
| 25 | * Disk name (RFC1035, 1-63 characters). If omitted, a unique name is |
| 26 | * generated from the stack, stage, and logical id. Changing the name |
| 27 | * replaces the disk. |
| 28 | */ |
| 29 | diskName?: string; |
| 30 | /** |
| 31 | * Region the disk lives in (e.g. `us-central1`). Immutable — changing |
| 32 | * it replaces the disk. `US-CENTRAL1` is accepted and normalized to |
| 33 | * `us-central1`. |
| 34 | * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`) |
| 35 | */ |
| 36 | region?: string; |
| 37 | /** |
| 38 | * Zones the disk is replicated to. Two zones in `region` are required. |
| 39 | * Short names (`us-central1-a`) or URLs are accepted. Immutable — |
| 40 | * changing them replaces the disk. |
| 41 | * @default ["{region}-a", "{region}-b"] |
| 42 | */ |
| 43 | replicaZones?: string[]; |
| 44 | /** |
| 45 | * Persistent disk type (`pd-standard`, `pd-balanced`, `pd-ssd`, |
| 46 | * `pd-extreme`, `hyperdisk-balanced`, …). Immutable — changing it |
| 47 | * replaces the disk. |
| 48 | * @default "pd-standard" |
| 49 | */ |
| 50 | type?: string; |
| 51 | /** |
| 52 | * Size in GB. Regional persistent disks require at least 200 GB. May |
| 53 | * only grow in place; shrinking replaces the disk. |
| 54 | * @default 200 |
| 55 | */ |
| 56 | sizeGb?: number; |
| 57 | /** |
| 58 | * Optional description. |
| 59 | */ |
| 60 | description?: string; |
| 61 | /** |
| 62 | * User labels. Alchemy ownership labels are merged in automatically. |
| 63 | */ |
| 64 | labels?: Record<string, string>; |
| 65 | /** |
| 66 | * Source image used to create the disk. Immutable — changing it replaces |
| 67 | * the disk. |
| 68 | */ |
| 69 | sourceImage?: string; |
| 70 | /** |
| 71 | * Source snapshot used to create the disk. Immutable — changing it |
| 72 | * replaces the disk. |
| 73 | */ |
| 74 | sourceSnapshot?: string; |
| 75 | /** |
| 76 | * Source disk used to create this disk. Immutable — changing it replaces |
| 77 | * the disk. |
| 78 | */ |
| 79 | sourceDisk?: string; |
| 80 | /** |
| 81 | * CPU architecture (`ARM64` or `X86_64`). Immutable — changing it |
| 82 | * replaces the disk. |
| 83 | */ |
| 84 | architecture?: string; |
| 85 | /** |
| 86 | * Physical block size in bytes (typically `4096`). Immutable — changing |
| 87 | * it replaces the disk. |
| 88 | */ |
| 89 | physicalBlockSizeBytes?: number; |
| 90 | /** |
| 91 | * Provisioned IOPS (Extreme PD / Hyperdisk). Mutable in place. |
| 92 | */ |
| 93 | provisionedIops?: number; |
| 94 | /** |
| 95 | * Provisioned throughput in MB/s (Hyperdisk). Mutable in place. |
| 96 | */ |
| 97 | provisionedThroughput?: number; |
| 98 | /** |
| 99 | * Whether confidential compute is enabled. Immutable — changing it |
| 100 | * replaces the disk. |
| 101 | */ |
| 102 | enableConfidentialCompute?: boolean; |
| 103 | }; |
| 104 | |
| 105 | export type RegionDisk = Resource< |
| 106 | "GCP.Compute.RegionDisk", |
| 107 | RegionDiskProps, |
| 108 | { |
| 109 | /** Disk name. */ |
| 110 | diskName: string; |
| 111 | /** Server-assigned numeric id. */ |
| 112 | diskId: string | undefined; |
| 113 | /** Project id. */ |
| 114 | project: string; |
| 115 | /** Region (short name, e.g. `us-central1`). */ |
| 116 | region: string; |
| 117 | /** Replica zone short names, sorted. */ |
| 118 | replicaZones: string[]; |
| 119 | /** Disk type (short name, e.g. `pd-standard`). */ |
| 120 | type: string; |
| 121 | /** Size in GB. */ |
| 122 | sizeGb: number; |
| 123 | /** Server-reported status (`READY`, `CREATING`, …). */ |
| 124 | status: string | undefined; |
| 125 | /** User labels (Alchemy ownership labels stripped). */ |
| 126 | labels: Record<string, string>; |
| 127 | /** Optional description. */ |
| 128 | description: string | undefined; |
| 129 | /** Compute Engine self-link. */ |
| 130 | selfLink: string | undefined; |
| 131 | /** RFC3339 creation timestamp. */ |
| 132 | creationTimestamp: string | undefined; |
| 133 | /** Source image, if any. */ |
| 134 | sourceImage: string | undefined; |
| 135 | /** Source snapshot, if any. */ |
| 136 | sourceSnapshot: string | undefined; |
| 137 | /** Source disk, if any. */ |
| 138 | sourceDisk: string | undefined; |
| 139 | /** Attached instance URLs. */ |
| 140 | users: ReadonlyArray<string>; |
| 141 | }, |
| 142 | never, |
| 143 | Providers |
| 144 | >; |
| 145 | |
| 146 | /** |
| 147 | * A regional Compute Engine persistent disk. |
| 148 | * |
| 149 | * Regional disks replicate across two zones in a region and require a |
| 150 | * minimum size of 200 GB. Changing |
| 151 | * `region`, `replicaZones`, `type`, `diskName`, `sourceImage`, |
| 152 | * `sourceSnapshot`, `sourceDisk`, `architecture`, |
| 153 | * `physicalBlockSizeBytes`, or `enableConfidentialCompute` replaces the |
| 154 | * disk. Growing `sizeGb` is applied in place via `regionDisks.resize`; |
| 155 | * shrinking it replaces the disk. |
| 156 | * |
| 157 | * ### Creating a RegionDisk |
| 158 | * **Example:** Generated name |
| 159 | * ```typescript |
| 160 | * const disk = yield* GCP.Compute.RegionDisk("data", {}); |
| 161 | * ``` |
| 162 | * |
| 163 | * **Example:** Explicit name, replica zones, type, size, and labels |
| 164 | * ```typescript |
| 165 | * const disk = yield* GCP.Compute.RegionDisk("data", { |
| 166 | * diskName: "app-data", |
| 167 | * region: "us-central1", |
| 168 | * replicaZones: ["us-central1-a", "us-central1-b"], |
| 169 | * type: "pd-balanced", |
| 170 | * sizeGb: 200, |
| 171 | * labels: { env: "prod" }, |
| 172 | * }); |
| 173 | * ``` |
| 174 | * |
| 175 | * ### Resizing a RegionDisk |
| 176 | * **Example:** Grow in place |
| 177 | * ```typescript |
| 178 | * const disk = yield* GCP.Compute.RegionDisk("data", { |
| 179 | * diskName: "app-data", |
| 180 | * region: "us-central1", |
| 181 | * sizeGb: 250, |
| 182 | * }); |
| 183 | * ``` |
| 184 | * |
| 185 | * @resource |
| 186 | * @category Compute |
| 187 | */ |
| 188 | export const RegionDisk = Resource<RegionDisk>("GCP.Compute.RegionDisk"); |
| 189 | |
| 190 | export class RegionDiskNotResolved extends Data.TaggedError( |
| 191 | "GCP.Compute.RegionDiskNotResolved", |
| 192 | )<{ |
| 193 | diskName: string; |
| 194 | region: string; |
| 195 | }> {} |
| 196 | |
| 197 | export class RegionDiskNotReady extends Data.TaggedError( |
| 198 | "GCP.Compute.RegionDiskNotReady", |
| 199 | )<{ |
| 200 | diskName: string; |
| 201 | status: string; |
| 202 | }> {} |
| 203 | |
| 204 | export class RegionDiskFailed extends Data.TaggedError( |
| 205 | "GCP.Compute.RegionDiskFailed", |
| 206 | )<{ |
| 207 | diskName: string; |
| 208 | status: string; |
| 209 | }> {} |
| 210 | |
| 211 | export class RegionDiskStillExists extends Data.TaggedError( |
| 212 | "GCP.Compute.RegionDiskStillExists", |
| 213 | )<{ |
| 214 | diskName: string; |
| 215 | status: string; |
| 216 | }> {} |
| 217 | |
| 218 | const DEFAULT_TYPE = "pd-standard"; |
| 219 | const DEFAULT_SIZE_GB = 200; |
| 220 | |
| 221 | const lastSegment = (value: string | undefined): string | undefined => { |
| 222 | if (value === undefined || value.length === 0) return undefined; |
| 223 | const parts = value.split("/"); |
| 224 | return parts[parts.length - 1] || value; |
| 225 | }; |
| 226 | |
| 227 | const normalizeRegion = (region: string | undefined, defaultRegion: string) => |
| 228 | (lastSegment(region ?? defaultRegion) ?? defaultRegion).toLowerCase(); |
| 229 | |
| 230 | const userLabels = ( |
| 231 | labels: Record<string, string | undefined> | null | undefined, |
| 232 | ): Record<string, string> => stripInternalLabels(tagRecord(labels)); |
| 233 | |
| 234 | const parseSizeGb = (value: string | undefined): number => { |
| 235 | const parsed = Number(value); |
| 236 | return Number.isFinite(parsed) ? parsed : 0; |
| 237 | }; |
| 238 | |
| 239 | const normalizeReplicaZones = ( |
| 240 | region: string, |
| 241 | zones: readonly string[] | undefined, |
| 242 | ): string[] => { |
| 243 | const raw = |
| 244 | zones && zones.length > 0 ? zones : [`${region}-a`, `${region}-b`]; |
| 245 | return [ |
| 246 | ...new Set( |
| 247 | raw |
| 248 | .map((zone) => lastSegment(zone)) |
| 249 | .filter((zone): zone is string => zone !== undefined), |
| 250 | ), |
| 251 | ].sort(); |
| 252 | }; |
| 253 | |
| 254 | const replicaZoneUrls = (project: string, zones: readonly string[]) => |
| 255 | zones.map((zone) => `projects/${project}/zones/${zone}`); |
| 256 | |
| 257 | const replicaZonesEqual = (left: readonly string[], right: readonly string[]) => |
| 258 | left.length === right.length && |
| 259 | left.every((zone, index) => zone === right[index]); |
| 260 | |
| 261 | const toName = (id: string, name: string | undefined, existing?: string) => |
| 262 | Effect.gen(function* () { |
| 263 | if (name !== undefined) return name; |
| 264 | if (existing !== undefined) return existing; |
| 265 | const generated = yield* createPhysicalName({ |
| 266 | id, |
| 267 | maxLength: 63, |
| 268 | lowercase: true, |
| 269 | }); |
| 270 | // RFC1035: disk names must start with a letter. |
| 271 | return /^[a-z]/.test(generated) ? generated : `d${generated}`.slice(0, 63); |
| 272 | }); |
| 273 | |
| 274 | const toAttrs = (disk: compute.Disk, project: string) => { |
| 275 | const region = (lastSegment(disk.region) ?? "").toLowerCase(); |
| 276 | return { |
| 277 | diskName: disk.name ?? disk.id ?? "", |
| 278 | diskId: disk.id, |
| 279 | project, |
| 280 | region, |
| 281 | replicaZones: normalizeReplicaZones(region, disk.replicaZones), |
| 282 | type: lastSegment(disk.type) ?? DEFAULT_TYPE, |
| 283 | sizeGb: parseSizeGb(disk.sizeGb), |
| 284 | status: disk.status, |
| 285 | labels: userLabels(disk.labels), |
| 286 | description: disk.description, |
| 287 | selfLink: disk.selfLink, |
| 288 | creationTimestamp: disk.creationTimestamp, |
| 289 | sourceImage: disk.sourceImage, |
| 290 | sourceSnapshot: disk.sourceSnapshot, |
| 291 | sourceDisk: disk.sourceDisk, |
| 292 | users: disk.users ?? [], |
| 293 | }; |
| 294 | }; |
| 295 | |
| 296 | const getByName = (project: string, region: string, disk: string) => |
| 297 | compute |
| 298 | .getRegionDisks({ project, region, disk }) |
| 299 | .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))); |
| 300 | |
| 301 | const waitDiskReady = (project: string, region: string, diskName: string) => |
| 302 | getByName(project, region, diskName).pipe( |
| 303 | Effect.flatMap((disk) => |
| 304 | disk?.status === "FAILED" |
| 305 | ? Effect.fail(new RegionDiskFailed({ diskName, status: "FAILED" })) |
| 306 | : Effect.succeed(disk), |
| 307 | ), |
| 308 | Effect.filterOrFail( |
| 309 | (disk): disk is compute.Disk => |
| 310 | disk !== undefined && disk.status === "READY", |
| 311 | (disk) => |
| 312 | new RegionDiskNotReady({ |
| 313 | diskName, |
| 314 | status: disk?.status ?? "MISSING", |
| 315 | }), |
| 316 | ), |
| 317 | Effect.retry({ |
| 318 | while: (error) => error._tag === "GCP.Compute.RegionDiskNotReady", |
| 319 | times: 10, |
| 320 | schedule: Schedule.spaced("2 seconds"), |
| 321 | }), |
| 322 | ); |
| 323 | |
| 324 | const waitDiskGone = (project: string, region: string, diskName: string) => |
| 325 | getByName(project, region, diskName).pipe( |
| 326 | Effect.flatMap((disk) => |
| 327 | disk === undefined |
| 328 | ? Effect.void |
| 329 | : Effect.fail( |
| 330 | new RegionDiskStillExists({ |
| 331 | diskName, |
| 332 | status: disk.status ?? "UNKNOWN", |
| 333 | }), |
| 334 | ), |
| 335 | ), |
| 336 | Effect.retry({ |
| 337 | while: (error) => error._tag === "GCP.Compute.RegionDiskStillExists", |
| 338 | times: 10, |
| 339 | schedule: Schedule.spaced("3 seconds"), |
| 340 | }), |
| 341 | ); |
| 342 | |
| 343 | const diskTypeUrl = (project: string, region: string, type: string) => |
| 344 | `projects/${project}/regions/${region}/diskTypes/${type}`; |
| 345 | |
| 346 | export const RegionDiskProvider = () => |
| 347 | Provider.succeed(RegionDisk, { |
| 348 | stables: [ |
| 349 | "diskName", |
| 350 | "diskId", |
| 351 | "project", |
| 352 | "region", |
| 353 | "replicaZones", |
| 354 | "type", |
| 355 | "selfLink", |
| 356 | "creationTimestamp", |
| 357 | "sourceImage", |
| 358 | "sourceSnapshot", |
| 359 | "sourceDisk", |
| 360 | ], |
| 361 | |
| 362 | diff: Effect.fn(function* ({ news, olds, output }) { |
| 363 | if (!isResolved(news)) return undefined; |
| 364 | const env = yield* GcpEnvironment.current; |
| 365 | |
| 366 | const previousRegion = normalizeRegion( |
| 367 | olds?.region ?? output?.region, |
| 368 | env.region, |
| 369 | ); |
| 370 | const nextRegion = normalizeRegion( |
| 371 | news.region ?? output?.region, |
| 372 | env.region, |
| 373 | ); |
| 374 | const previousType = |
| 375 | lastSegment(olds?.type) ?? lastSegment(output?.type) ?? DEFAULT_TYPE; |
| 376 | const nextType = lastSegment(news.type) ?? DEFAULT_TYPE; |
| 377 | const previousName = olds?.diskName ?? output?.diskName; |
| 378 | const nextName = news.diskName ?? previousName; |
| 379 | const previousSize = olds?.sizeGb ?? output?.sizeGb; |
| 380 | const nextSize = news.sizeGb ?? DEFAULT_SIZE_GB; |
| 381 | const previousReplicas = normalizeReplicaZones( |
| 382 | previousRegion, |
| 383 | olds?.replicaZones ?? output?.replicaZones, |
| 384 | ); |
| 385 | const nextReplicas = normalizeReplicaZones(nextRegion, news.replicaZones); |
| 386 | |
| 387 | const replace = |
| 388 | previousRegion !== nextRegion || |
| 389 | previousType !== nextType || |
| 390 | !replicaZonesEqual(previousReplicas, nextReplicas) || |
| 391 | (previousName !== undefined && |
| 392 | nextName !== undefined && |
| 393 | previousName !== nextName) || |
| 394 | (olds?.sourceImage ?? undefined) !== (news.sourceImage ?? undefined) || |
| 395 | (olds?.sourceSnapshot ?? undefined) !== |
| 396 | (news.sourceSnapshot ?? undefined) || |
| 397 | (olds?.sourceDisk ?? undefined) !== (news.sourceDisk ?? undefined) || |
| 398 | (olds?.architecture ?? undefined) !== |
| 399 | (news.architecture ?? undefined) || |
| 400 | (olds?.physicalBlockSizeBytes ?? undefined) !== |
| 401 | (news.physicalBlockSizeBytes ?? undefined) || |
| 402 | (olds?.enableConfidentialCompute ?? undefined) !== |
| 403 | (news.enableConfidentialCompute ?? undefined) || |
| 404 | (previousSize !== undefined && nextSize < previousSize); |
| 405 | |
| 406 | if (!replace) return undefined; |
| 407 | return { |
| 408 | action: "replace" as const, |
| 409 | deleteFirst: |
| 410 | previousName !== undefined && |
| 411 | nextName === previousName && |
| 412 | previousRegion === nextRegion, |
| 413 | }; |
| 414 | }), |
| 415 | |
| 416 | read: Effect.fn(function* ({ id, olds, output }) { |
| 417 | const env = yield* GcpEnvironment.current; |
| 418 | const diskName = yield* toName(id, olds?.diskName, output?.diskName); |
| 419 | const region = normalizeRegion( |
| 420 | olds?.region ?? output?.region, |
| 421 | env.region, |
| 422 | ); |
| 423 | const existing = yield* getByName(env.project, region, diskName); |
| 424 | if (existing === undefined) return undefined; |
| 425 | const attrs = toAttrs(existing, env.project); |
| 426 | return (yield* hasAlchemyLabels(id, tagRecord(existing.labels))) |
| 427 | ? attrs |
| 428 | : Unowned(attrs); |
| 429 | }), |
| 430 | |
| 431 | list: () => |
| 432 | Effect.gen(function* () { |
| 433 | const env = yield* GcpEnvironment.current; |
| 434 | const pages = yield* compute.aggregatedListDisks |
| 435 | .pages({ |
| 436 | project: env.project, |
| 437 | filter: "labels.alchemy-id:*", |
| 438 | returnPartialSuccess: true, |
| 439 | maxResults: 500, |
| 440 | }) |
| 441 | .pipe(Stream.runCollect); |
| 442 | return Array.from(pages).flatMap((page) => |
| 443 | Object.values(page.items ?? {}).flatMap((scoped) => |
| 444 | (scoped?.disks ?? []) |
| 445 | .filter((disk) => disk.region !== undefined) |
| 446 | .filter((disk) => |
| 447 | Object.keys(disk.labels ?? {}).some((key) => |
| 448 | key.startsWith("alchemy-"), |
| 449 | ), |
| 450 | ) |
| 451 | .map((disk) => toAttrs(disk, env.project)), |
| 452 | ), |
| 453 | ); |
| 454 | }), |
| 455 | |
| 456 | reconcile: Effect.fn(function* ({ id, news, output }) { |
| 457 | const env = yield* GcpEnvironment.current; |
| 458 | const diskName = yield* toName(id, news.diskName, output?.diskName); |
| 459 | const region = normalizeRegion(news.region ?? output?.region, env.region); |
| 460 | const diskType = lastSegment(news.type) ?? DEFAULT_TYPE; |
| 461 | const sizeGb = news.sizeGb ?? DEFAULT_SIZE_GB; |
| 462 | const replicaZones = normalizeReplicaZones(region, news.replicaZones); |
| 463 | const desiredLabels = { |
| 464 | ...toLabels(news.labels), |
| 465 | ...(yield* createInternalLabels(id)), |
| 466 | }; |
| 467 | |
| 468 | let current = yield* getByName(env.project, region, diskName); |
| 469 | if (current?.status === "DELETING") { |
| 470 | yield* waitDiskGone(env.project, region, diskName); |
| 471 | current = undefined; |
| 472 | } |
| 473 | |
| 474 | if (current === undefined) { |
| 475 | const inserted = yield* compute |
| 476 | .insertRegionDisks({ |
| 477 | project: env.project, |
| 478 | region, |
| 479 | body: { |
| 480 | name: diskName, |
| 481 | sizeGb: String(sizeGb), |
| 482 | type: diskTypeUrl(env.project, region, diskType), |
| 483 | replicaZones: replicaZoneUrls(env.project, replicaZones), |
| 484 | description: news.description, |
| 485 | labels: desiredLabels, |
| 486 | sourceImage: news.sourceImage, |
| 487 | sourceSnapshot: news.sourceSnapshot, |
| 488 | sourceDisk: news.sourceDisk, |
| 489 | architecture: news.architecture, |
| 490 | physicalBlockSizeBytes: |
| 491 | news.physicalBlockSizeBytes !== undefined |
| 492 | ? String(news.physicalBlockSizeBytes) |
| 493 | : undefined, |
| 494 | provisionedIops: |
| 495 | news.provisionedIops !== undefined |
| 496 | ? String(news.provisionedIops) |
| 497 | : undefined, |
| 498 | provisionedThroughput: |
| 499 | news.provisionedThroughput !== undefined |
| 500 | ? String(news.provisionedThroughput) |
| 501 | : undefined, |
| 502 | enableConfidentialCompute: news.enableConfidentialCompute, |
| 503 | }, |
| 504 | }) |
| 505 | .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined))); |
| 506 | if (inserted !== undefined) { |
| 507 | yield* waitRegionOperation(env.project, region, inserted, { |
| 508 | ignore: ["RESOURCE_ALREADY_EXISTS"], |
| 509 | }); |
| 510 | } |
| 511 | current = yield* waitDiskReady(env.project, region, diskName); |
| 512 | } |
| 513 | |
| 514 | if (current === undefined) { |
| 515 | return yield* new RegionDiskNotResolved({ diskName, region }); |
| 516 | } |
| 517 | |
| 518 | const observedSize = parseSizeGb(current.sizeGb); |
| 519 | if (sizeGb > observedSize) { |
| 520 | const resized = yield* compute.resizeRegionDisks({ |
| 521 | project: env.project, |
| 522 | region, |
| 523 | disk: diskName, |
| 524 | body: { sizeGb: String(sizeGb) }, |
| 525 | }); |
| 526 | yield* waitRegionOperation(env.project, region, resized); |
| 527 | current = yield* waitDiskReady(env.project, region, diskName); |
| 528 | } |
| 529 | |
| 530 | if (current === undefined) { |
| 531 | return yield* new RegionDiskNotResolved({ diskName, region }); |
| 532 | } |
| 533 | |
| 534 | const observedLabels = tagRecord(current.labels); |
| 535 | const { upsert, removed } = diffLabels(observedLabels, desiredLabels); |
| 536 | if (upsert.length > 0 || removed.length > 0) { |
| 537 | const labeled = yield* compute.setLabelsRegionDisks({ |
| 538 | project: env.project, |
| 539 | region, |
| 540 | resource: diskName, |
| 541 | body: { |
| 542 | labels: desiredLabels, |
| 543 | labelFingerprint: current.labelFingerprint, |
| 544 | }, |
| 545 | }); |
| 546 | yield* waitRegionOperation(env.project, region, labeled); |
| 547 | current = yield* getByName(env.project, region, diskName); |
| 548 | } |
| 549 | |
| 550 | if (current === undefined) { |
| 551 | return yield* new RegionDiskNotResolved({ diskName, region }); |
| 552 | } |
| 553 | |
| 554 | const descriptionChanged = |
| 555 | (current.description ?? "") !== (news.description ?? ""); |
| 556 | const iopsChanged = |
| 557 | news.provisionedIops !== undefined && |
| 558 | String(news.provisionedIops) !== (current.provisionedIops ?? ""); |
| 559 | const throughputChanged = |
| 560 | news.provisionedThroughput !== undefined && |
| 561 | String(news.provisionedThroughput) !== |
| 562 | (current.provisionedThroughput ?? ""); |
| 563 | |
| 564 | if (descriptionChanged || iopsChanged || throughputChanged) { |
| 565 | const updated = yield* compute.updateRegionDisks({ |
| 566 | project: env.project, |
| 567 | region, |
| 568 | disk: diskName, |
| 569 | updateMask: [ |
| 570 | descriptionChanged ? "description" : undefined, |
| 571 | iopsChanged ? "provisionedIops" : undefined, |
| 572 | throughputChanged ? "provisionedThroughput" : undefined, |
| 573 | ] |
| 574 | .filter((field): field is string => field !== undefined) |
| 575 | .join(","), |
| 576 | body: { |
| 577 | description: news.description, |
| 578 | provisionedIops: |
| 579 | news.provisionedIops !== undefined |
| 580 | ? String(news.provisionedIops) |
| 581 | : undefined, |
| 582 | provisionedThroughput: |
| 583 | news.provisionedThroughput !== undefined |
| 584 | ? String(news.provisionedThroughput) |
| 585 | : undefined, |
| 586 | }, |
| 587 | }); |
| 588 | yield* waitRegionOperation(env.project, region, updated); |
| 589 | current = yield* getByName(env.project, region, diskName); |
| 590 | } |
| 591 | |
| 592 | if (current === undefined) { |
| 593 | return yield* new RegionDiskNotResolved({ diskName, region }); |
| 594 | } |
| 595 | |
| 596 | return toAttrs(current, env.project); |
| 597 | }), |
| 598 | |
| 599 | delete: Effect.fn(function* ({ output }) { |
| 600 | const deleted = yield* compute |
| 601 | .deleteRegionDisks({ |
| 602 | project: output.project, |
| 603 | region: output.region, |
| 604 | disk: output.diskName, |
| 605 | }) |
| 606 | .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))); |
| 607 | if (deleted !== undefined) { |
| 608 | yield* waitRegionOperation(output.project, output.region, deleted, { |
| 609 | ignore: ["RESOURCE_NOT_FOUND"], |
| 610 | }); |
| 611 | } |
| 612 | yield* waitDiskGone(output.project, output.region, output.diskName); |
| 613 | }), |
| 614 | }); |