| 1 | import * as mcn from "@distilled.cloud/cloudflare/magic-cloud-networking"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Predicate from "effect/Predicate"; |
| 4 | import * as Stream from "effect/Stream"; |
| 5 | |
| 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 { CloudflareEnvironment } from "../CloudflareEnvironment.ts"; |
| 12 | import type { Providers } from "../Providers.ts"; |
| 13 | |
| 14 | const TypeId = "Cloudflare.MagicCloudNetworking.OnRamp" as const; |
| 15 | type TypeId = typeof TypeId; |
| 16 | |
| 17 | /** |
| 18 | * The cloud provider an on-ramp connects to Magic WAN. |
| 19 | */ |
| 20 | export type OnRampCloudType = "AWS" | "AZURE" | "GOOGLE"; |
| 21 | |
| 22 | /** |
| 23 | * Topology of an on-ramp: a single VPC/VNet, or a hub (e.g. AWS Transit |
| 24 | * Gateway) with attached VPCs. |
| 25 | */ |
| 26 | export type OnRampType = "OnrampTypeSingle" | "OnrampTypeHub"; |
| 27 | |
| 28 | export interface OnRampProps { |
| 29 | /** |
| 30 | * The cloud provider this on-ramp connects to. |
| 31 | * |
| 32 | * Immutable — changing the cloud type triggers a replacement. |
| 33 | */ |
| 34 | cloudType: OnRampCloudType; |
| 35 | /** |
| 36 | * Topology of the on-ramp: `OnrampTypeSingle` connects one VPC/VNet, |
| 37 | * `OnrampTypeHub` provisions a hub (e.g. AWS Transit Gateway) that VPCs |
| 38 | * attach to. |
| 39 | * |
| 40 | * Immutable — changing the topology triggers a replacement. |
| 41 | */ |
| 42 | type: OnRampType; |
| 43 | /** |
| 44 | * Enables BGP routing. When enabled, set both `installRoutesInCloud` and |
| 45 | * `installRoutesInMagicWan` to `false`. |
| 46 | * |
| 47 | * Immutable — the API offers no way to change it, so a change triggers a |
| 48 | * replacement. |
| 49 | */ |
| 50 | dynamicRouting: boolean; |
| 51 | /** |
| 52 | * Whether Cloudflare installs Magic WAN routes into the cloud route |
| 53 | * tables. Mutable. |
| 54 | */ |
| 55 | installRoutesInCloud: boolean; |
| 56 | /** |
| 57 | * Whether routes to the cloud networks are installed in Magic WAN. |
| 58 | * Mutable. |
| 59 | */ |
| 60 | installRoutesInMagicWan: boolean; |
| 61 | /** |
| 62 | * Human readable name. Used as the on-ramp's identity for cold-state |
| 63 | * recovery, so it should be unique within the account. If omitted, a |
| 64 | * unique name is generated from the app, stage, and logical ID. |
| 65 | * @default ${app}-${stage}-${id} |
| 66 | */ |
| 67 | name?: string; |
| 68 | /** |
| 69 | * Cloud region the on-ramp is provisioned in (e.g. `us-east-1`). |
| 70 | * |
| 71 | * Immutable — changing the region triggers a replacement. |
| 72 | */ |
| 73 | region?: string; |
| 74 | /** |
| 75 | * The discovered VPC/VNet resource id to connect |
| 76 | * (`type: "OnrampTypeSingle"`). Mutable. |
| 77 | */ |
| 78 | vpc?: string; |
| 79 | /** |
| 80 | * Cloud-side ASN for BGP. If unset or zero, the cloud's default ASN |
| 81 | * takes effect. |
| 82 | * |
| 83 | * Immutable — the API offers no way to change it, so a change triggers a |
| 84 | * replacement. |
| 85 | */ |
| 86 | cloudAsn?: number; |
| 87 | /** |
| 88 | * Free-form description of the on-ramp. Mutable. |
| 89 | */ |
| 90 | description?: string; |
| 91 | /** |
| 92 | * Adopt an existing hub (e.g. an existing Transit Gateway) instead of |
| 93 | * provisioning one (`type: "OnrampTypeHub"`). |
| 94 | * |
| 95 | * Immutable — hub identity cannot change, so a change triggers a |
| 96 | * replacement. |
| 97 | */ |
| 98 | adoptedHubId?: string; |
| 99 | /** |
| 100 | * Cloud integration that owns the hub when it lives in a different |
| 101 | * provider account. |
| 102 | * |
| 103 | * Immutable — hub identity cannot change, so a change triggers a |
| 104 | * replacement. |
| 105 | */ |
| 106 | hubProviderId?: string; |
| 107 | /** |
| 108 | * Hubs attached to this on-ramp. Mutable. |
| 109 | */ |
| 110 | attachedHubs?: string[]; |
| 111 | /** |
| 112 | * VPCs attached to this hub on-ramp (`type: "OnrampTypeHub"`). Mutable. |
| 113 | */ |
| 114 | attachedVpcs?: string[]; |
| 115 | /** |
| 116 | * Whether Cloudflare manages hub-to-hub attachments. Mutable. |
| 117 | */ |
| 118 | manageHubToHubAttachments?: boolean; |
| 119 | /** |
| 120 | * Whether Cloudflare manages VPC-to-hub attachments. Mutable. |
| 121 | */ |
| 122 | manageVpcToHubAttachments?: boolean; |
| 123 | /** |
| 124 | * Whether deleting the on-ramp also destroys the cloud-side resources it |
| 125 | * provisioned (VPN gateways, Transit Gateway, …). Consumed only at |
| 126 | * delete time. |
| 127 | * @default false |
| 128 | */ |
| 129 | destroyOnDelete?: boolean; |
| 130 | } |
| 131 | |
| 132 | export interface OnRampAttributes { |
| 133 | /** Cloudflare-assigned identifier of the on-ramp. */ |
| 134 | onRampId: string; |
| 135 | /** The Cloudflare account the on-ramp belongs to. */ |
| 136 | accountId: string; |
| 137 | /** Human readable name of the on-ramp. */ |
| 138 | name: string; |
| 139 | /** The cloud provider this on-ramp connects to. */ |
| 140 | cloudType: OnRampCloudType; |
| 141 | /** Topology of the on-ramp. */ |
| 142 | type: OnRampType; |
| 143 | /** Whether BGP routing is enabled. */ |
| 144 | dynamicRouting: boolean; |
| 145 | /** Whether Magic WAN routes are installed into the cloud route tables. */ |
| 146 | installRoutesInCloud: boolean; |
| 147 | /** Whether cloud routes are installed in Magic WAN. */ |
| 148 | installRoutesInMagicWan: boolean; |
| 149 | /** The connected VPC/VNet resource id, if set. */ |
| 150 | vpc: string | undefined; |
| 151 | /** Cloud-side ASN, if set. */ |
| 152 | cloudAsn: number | undefined; |
| 153 | /** Free-form description, if set. */ |
| 154 | description: string | undefined; |
| 155 | /** Hubs attached to this on-ramp. */ |
| 156 | attachedHubs: string[]; |
| 157 | /** VPCs attached to this hub on-ramp. */ |
| 158 | attachedVpcs: string[]; |
| 159 | /** Whether Cloudflare manages hub-to-hub attachments, if reported. */ |
| 160 | manageHubToHubAttachments: boolean | undefined; |
| 161 | /** Whether Cloudflare manages VPC-to-hub attachments, if reported. */ |
| 162 | manageVpcToHubAttachments: boolean | undefined; |
| 163 | /** ISO8601 timestamp of the last change to the on-ramp. */ |
| 164 | updatedAt: string; |
| 165 | /** Whether delete also destroys provisioned cloud-side resources. */ |
| 166 | destroyOnDelete: boolean; |
| 167 | } |
| 168 | |
| 169 | export type OnRamp = Resource< |
| 170 | TypeId, |
| 171 | OnRampProps, |
| 172 | OnRampAttributes, |
| 173 | never, |
| 174 | Providers |
| 175 | >; |
| 176 | |
| 177 | /** |
| 178 | * A Magic Cloud Networking on-ramp — connects cloud VPCs/VNets to Magic WAN |
| 179 | * by provisioning VPN/Transit-Gateway constructs inside the cloud account |
| 180 | * registered via a `CloudIntegration`. |
| 181 | * |
| 182 | * On-ramps are heavily eventually consistent: after create/update the |
| 183 | * on-ramp goes through plan/apply phases that provision real cloud |
| 184 | * infrastructure (minutes). This resource creates and patches the on-ramp |
| 185 | * configuration and returns immediately; the apply lifecycle is driven by |
| 186 | * Cloudflare. |
| 187 | * |
| 188 | * `name`, `description`, `vpc`, route-installation flags, and attachments |
| 189 | * are patched in place; `cloudType`, `type`, `dynamicRouting`, `region`, |
| 190 | * `cloudAsn`, and hub identity force a replacement. |
| 191 | * |
| 192 | * Magic Cloud Networking is an entitlement-gated add-on (Magic WAN family). |
| 193 | * On accounts without the entitlement every API call fails with the typed |
| 194 | * `FeatureNotEnabled` error (Cloudflare code 1012, "feature not enabled"). |
| 195 | * ### Connecting a single VPC |
| 196 | * **Example:** AWS VPC on-ramp |
| 197 | * ```typescript |
| 198 | * const onramp = yield* Cloudflare.MagicCloudNetworking.OnRamp("ProdVpc", { |
| 199 | * cloudType: "AWS", |
| 200 | * type: "OnrampTypeSingle", |
| 201 | * region: "us-east-1", |
| 202 | * vpc: discoveredVpcId, |
| 203 | * dynamicRouting: false, |
| 204 | * installRoutesInCloud: true, |
| 205 | * installRoutesInMagicWan: true, |
| 206 | * }); |
| 207 | * ``` |
| 208 | * |
| 209 | * ### Hub topologies |
| 210 | * **Example:** Transit Gateway hub with attached VPCs |
| 211 | * ```typescript |
| 212 | * yield* Cloudflare.MagicCloudNetworking.OnRamp("TgwHub", { |
| 213 | * cloudType: "AWS", |
| 214 | * type: "OnrampTypeHub", |
| 215 | * region: "us-east-1", |
| 216 | * dynamicRouting: true, |
| 217 | * installRoutesInCloud: false, |
| 218 | * installRoutesInMagicWan: false, |
| 219 | * attachedVpcs: [vpcA, vpcB], |
| 220 | * manageVpcToHubAttachments: true, |
| 221 | * }); |
| 222 | * ``` |
| 223 | * |
| 224 | * ### Destroy behavior |
| 225 | * **Example:** Tear down cloud-side resources on destroy |
| 226 | * ```typescript |
| 227 | * yield* Cloudflare.MagicCloudNetworking.OnRamp("ProdVpc", { |
| 228 | * cloudType: "AWS", |
| 229 | * type: "OnrampTypeSingle", |
| 230 | * region: "us-east-1", |
| 231 | * vpc: discoveredVpcId, |
| 232 | * dynamicRouting: false, |
| 233 | * installRoutesInCloud: true, |
| 234 | * installRoutesInMagicWan: true, |
| 235 | * destroyOnDelete: true, |
| 236 | * }); |
| 237 | * ``` |
| 238 | * |
| 239 | * @see https://developers.cloudflare.com/magic-cloud-networking/ |
| 240 | * |
| 241 | * @resource |
| 242 | * @product Magic Cloud Networking |
| 243 | * @category Network |
| 244 | */ |
| 245 | export const OnRamp = Resource<OnRamp>(TypeId); |
| 246 | |
| 247 | /** |
| 248 | * Returns true if the given value is an OnRamp resource. |
| 249 | */ |
| 250 | export const isOnRamp = (value: unknown): value is OnRamp => |
| 251 | Predicate.hasProperty(value, "Type") && value.Type === TypeId; |
| 252 | |
| 253 | export const OnRampProvider = () => |
| 254 | Provider.succeed(OnRamp, { |
| 255 | stables: [ |
| 256 | "onRampId", |
| 257 | "accountId", |
| 258 | "cloudType", |
| 259 | "type", |
| 260 | "dynamicRouting", |
| 261 | "cloudAsn", |
| 262 | ], |
| 263 | |
| 264 | diff: Effect.fn(function* ({ olds, news, output }) { |
| 265 | if (!isResolved(news)) return undefined; |
| 266 | const old = olds !== undefined && isResolved(olds) ? olds : undefined; |
| 267 | // Immutable identity/topology properties — the PATCH body accepts |
| 268 | // none of these, so any change is a replacement. |
| 269 | const oldCloudType = output?.cloudType ?? old?.cloudType; |
| 270 | if (oldCloudType !== undefined && oldCloudType !== news.cloudType) { |
| 271 | return { action: "replace" } as const; |
| 272 | } |
| 273 | const oldType = output?.type ?? old?.type; |
| 274 | if (oldType !== undefined && oldType !== news.type) { |
| 275 | return { action: "replace" } as const; |
| 276 | } |
| 277 | const oldDynamicRouting = output?.dynamicRouting ?? old?.dynamicRouting; |
| 278 | if ( |
| 279 | oldDynamicRouting !== undefined && |
| 280 | oldDynamicRouting !== news.dynamicRouting |
| 281 | ) { |
| 282 | return { action: "replace" } as const; |
| 283 | } |
| 284 | if (old !== undefined) { |
| 285 | if (old.region !== news.region) { |
| 286 | return { action: "replace" } as const; |
| 287 | } |
| 288 | if (old.cloudAsn !== news.cloudAsn) { |
| 289 | return { action: "replace" } as const; |
| 290 | } |
| 291 | if (old.adoptedHubId !== news.adoptedHubId) { |
| 292 | return { action: "replace" } as const; |
| 293 | } |
| 294 | if (old.hubProviderId !== news.hubProviderId) { |
| 295 | return { action: "replace" } as const; |
| 296 | } |
| 297 | } |
| 298 | return undefined; |
| 299 | }), |
| 300 | |
| 301 | read: Effect.fn(function* ({ id, output, olds }) { |
| 302 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 303 | const acct = output?.accountId ?? accountId; |
| 304 | const destroyOnDelete = |
| 305 | output?.destroyOnDelete ?? olds?.destroyOnDelete ?? false; |
| 306 | |
| 307 | // Owned path: refresh by our persisted on-ramp id. |
| 308 | if (output?.onRampId) { |
| 309 | const observed = yield* getOnRamp(acct, output.onRampId); |
| 310 | if (observed) return toAttributes(observed, acct, destroyOnDelete); |
| 311 | return undefined; |
| 312 | } |
| 313 | |
| 314 | // Cold read — recover from lost state by matching the deterministic |
| 315 | // physical name. On-ramps carry no ownership markers, so report a |
| 316 | // match as Unowned and let the engine gate adoption. |
| 317 | const name = yield* onRampName(id, olds?.name); |
| 318 | const match = yield* findByName(acct, name); |
| 319 | return match |
| 320 | ? Unowned(toAttributes(match, acct, destroyOnDelete)) |
| 321 | : undefined; |
| 322 | }), |
| 323 | |
| 324 | reconcile: Effect.fn(function* ({ id, news, output }) { |
| 325 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 326 | const name = yield* onRampName(id, news.name); |
| 327 | const destroyOnDelete = news.destroyOnDelete ?? false; |
| 328 | |
| 329 | // 1. Observe — the id cached on `output` is a hint, not a guarantee: |
| 330 | // a missing on-ramp falls through to the name scan and create. |
| 331 | let observed = output?.onRampId |
| 332 | ? yield* getOnRamp(output.accountId ?? accountId, output.onRampId) |
| 333 | : undefined; |
| 334 | if (!observed) { |
| 335 | observed = yield* findByName(accountId, name); |
| 336 | } |
| 337 | |
| 338 | // 2. Ensure — create when missing. Names are not unique on |
| 339 | // Cloudflare's side so there is no AlreadyExists race to tolerate. |
| 340 | if (!observed) { |
| 341 | observed = yield* mcn.createOnRamp({ |
| 342 | accountId, |
| 343 | name, |
| 344 | cloudType: news.cloudType, |
| 345 | type: news.type, |
| 346 | dynamicRouting: news.dynamicRouting, |
| 347 | installRoutesInCloud: news.installRoutesInCloud, |
| 348 | installRoutesInMagicWan: news.installRoutesInMagicWan, |
| 349 | region: news.region, |
| 350 | vpc: news.vpc, |
| 351 | cloudAsn: news.cloudAsn, |
| 352 | description: news.description, |
| 353 | adoptedHubId: news.adoptedHubId, |
| 354 | hubProviderId: news.hubProviderId, |
| 355 | attachedHubs: news.attachedHubs, |
| 356 | attachedVpcs: news.attachedVpcs, |
| 357 | manageHubToHubAttachments: news.manageHubToHubAttachments, |
| 358 | manageVpcToHubAttachments: news.manageVpcToHubAttachments, |
| 359 | }); |
| 360 | return toAttributes(observed, accountId, destroyOnDelete); |
| 361 | } |
| 362 | |
| 363 | // 3. Sync — diff observed cloud state against desired; PATCH only the |
| 364 | // delta and skip the call entirely on a no-op. |
| 365 | const patch: mcn.PatchOnRampRequest = { |
| 366 | accountId, |
| 367 | onrampId: observed.id, |
| 368 | }; |
| 369 | let dirty = false; |
| 370 | if (observed.name !== name) { |
| 371 | patch.name = name; |
| 372 | dirty = true; |
| 373 | } |
| 374 | if ( |
| 375 | news.description !== undefined && |
| 376 | (observed.description ?? "") !== news.description |
| 377 | ) { |
| 378 | patch.description = news.description; |
| 379 | dirty = true; |
| 380 | } |
| 381 | if (news.vpc !== undefined && (observed.vpc ?? "") !== news.vpc) { |
| 382 | patch.vpc = news.vpc; |
| 383 | dirty = true; |
| 384 | } |
| 385 | if (observed.installRoutesInCloud !== news.installRoutesInCloud) { |
| 386 | patch.installRoutesInCloud = news.installRoutesInCloud; |
| 387 | dirty = true; |
| 388 | } |
| 389 | if (observed.installRoutesInMagicWan !== news.installRoutesInMagicWan) { |
| 390 | patch.installRoutesInMagicWan = news.installRoutesInMagicWan; |
| 391 | dirty = true; |
| 392 | } |
| 393 | if ( |
| 394 | news.attachedHubs !== undefined && |
| 395 | !sameIds(observed.attachedHubs ?? [], news.attachedHubs) |
| 396 | ) { |
| 397 | patch.attachedHubs = news.attachedHubs; |
| 398 | dirty = true; |
| 399 | } |
| 400 | if ( |
| 401 | news.attachedVpcs !== undefined && |
| 402 | !sameIds(observed.attachedVpcs ?? [], news.attachedVpcs) |
| 403 | ) { |
| 404 | patch.attachedVpcs = news.attachedVpcs; |
| 405 | dirty = true; |
| 406 | } |
| 407 | if ( |
| 408 | news.manageHubToHubAttachments !== undefined && |
| 409 | (observed.manageHubToHubAttachments ?? false) !== |
| 410 | news.manageHubToHubAttachments |
| 411 | ) { |
| 412 | patch.manageHubToHubAttachments = news.manageHubToHubAttachments; |
| 413 | dirty = true; |
| 414 | } |
| 415 | if ( |
| 416 | news.manageVpcToHubAttachments !== undefined && |
| 417 | (observed.manageVpcToHubAttachments ?? false) !== |
| 418 | news.manageVpcToHubAttachments |
| 419 | ) { |
| 420 | patch.manageVpcToHubAttachments = news.manageVpcToHubAttachments; |
| 421 | dirty = true; |
| 422 | } |
| 423 | if (dirty) { |
| 424 | observed = yield* mcn.patchOnRamp(patch); |
| 425 | } |
| 426 | |
| 427 | return toAttributes(observed, accountId, destroyOnDelete); |
| 428 | }), |
| 429 | |
| 430 | delete: Effect.fn(function* ({ output }) { |
| 431 | // Deleting an on-ramp tears down cloud-side infrastructure when |
| 432 | // `destroy` is set — slow and eventually consistent on Cloudflare's |
| 433 | // side, but the DELETE call itself returns promptly. |
| 434 | yield* mcn |
| 435 | .deleteOnRamp({ |
| 436 | accountId: output.accountId, |
| 437 | onrampId: output.onRampId, |
| 438 | destroy: output.destroyOnDelete, |
| 439 | }) |
| 440 | .pipe(Effect.catchTag("OnRampNotFound", () => Effect.void)); |
| 441 | }), |
| 442 | |
| 443 | list: Effect.fn(function* () { |
| 444 | const { accountId } = yield* yield* CloudflareEnvironment; |
| 445 | // Account-scoped collection: exhaustively paginate the on-ramp list, |
| 446 | // then hydrate each id into the exact `read` Attributes shape via |
| 447 | // `getOnRamp`. The list item omits some fields (`vpc`, `region`), so |
| 448 | // a per-item get is required for a fully-faithful Attributes object. |
| 449 | // Magic Cloud Networking is an entitlement-gated add-on; on accounts |
| 450 | // without it every call fails with `FeatureNotEnabled` — treat that |
| 451 | // as a non-listable account and return `[]`. |
| 452 | const ids = yield* mcn.listOnRamps.items({ accountId }).pipe( |
| 453 | Stream.map((onramp) => onramp.id), |
| 454 | Stream.runCollect, |
| 455 | Effect.map((chunk) => Array.from(chunk)), |
| 456 | Effect.catchTag("FeatureNotEnabled", () => |
| 457 | Effect.succeed([] as string[]), |
| 458 | ), |
| 459 | ); |
| 460 | const rows = yield* Effect.forEach( |
| 461 | ids, |
| 462 | (onRampId) => |
| 463 | getOnRamp(accountId, onRampId).pipe( |
| 464 | Effect.map((observed) => |
| 465 | observed ? toAttributes(observed, accountId, false) : undefined, |
| 466 | ), |
| 467 | ), |
| 468 | { concurrency: 10 }, |
| 469 | ); |
| 470 | return rows.filter((row): row is OnRampAttributes => row !== undefined); |
| 471 | }), |
| 472 | }); |
| 473 | |
| 474 | type ObservedOnRamp = Pick< |
| 475 | mcn.GetOnRampResponse, |
| 476 | | "id" |
| 477 | | "name" |
| 478 | | "cloudType" |
| 479 | | "type" |
| 480 | | "dynamicRouting" |
| 481 | | "installRoutesInCloud" |
| 482 | | "installRoutesInMagicWan" |
| 483 | | "vpc" |
| 484 | | "cloudAsn" |
| 485 | | "description" |
| 486 | | "attachedHubs" |
| 487 | | "attachedVpcs" |
| 488 | | "manageHubToHubAttachments" |
| 489 | | "manageVpcToHubAttachments" |
| 490 | | "updatedAt" |
| 491 | | "region" |
| 492 | >; |
| 493 | |
| 494 | /** |
| 495 | * Read an on-ramp by id, mapping "gone" (`OnRampNotFound`) to `undefined`. |
| 496 | */ |
| 497 | const getOnRamp = (accountId: string, onrampId: string) => |
| 498 | mcn.getOnRamp({ accountId, onrampId }).pipe( |
| 499 | Effect.map((onramp): ObservedOnRamp | undefined => onramp), |
| 500 | Effect.catchTag("OnRampNotFound", () => Effect.succeed(undefined)), |
| 501 | ); |
| 502 | |
| 503 | /** |
| 504 | * Find an on-ramp by exact name. Names are not unique on Cloudflare's side; |
| 505 | * if several on-ramps carry the same name, pick the lexicographically-first |
| 506 | * id for determinism. |
| 507 | */ |
| 508 | const findByName = (accountId: string, name: string) => |
| 509 | mcn.listOnRamps.items({ accountId }).pipe( |
| 510 | Stream.runCollect, |
| 511 | Effect.map((chunk) => |
| 512 | Array.from(chunk) |
| 513 | .filter((onramp) => onramp.name === name) |
| 514 | .sort((a, b) => a.id.localeCompare(b.id)) |
| 515 | .at(0), |
| 516 | ), |
| 517 | ); |
| 518 | |
| 519 | const onRampName = (id: string, name: string | undefined) => |
| 520 | Effect.gen(function* () { |
| 521 | return name ?? (yield* createPhysicalName({ id, lowercase: true })); |
| 522 | }); |
| 523 | |
| 524 | const sameIds = (observed: readonly string[], desired: readonly string[]) => |
| 525 | observed.length === desired.length && |
| 526 | [...observed].sort().join(",") === [...desired].sort().join(","); |
| 527 | |
| 528 | const toAttributes = ( |
| 529 | onramp: ObservedOnRamp, |
| 530 | accountId: string, |
| 531 | destroyOnDelete: boolean, |
| 532 | ): OnRampAttributes => ({ |
| 533 | onRampId: onramp.id, |
| 534 | accountId, |
| 535 | name: onramp.name, |
| 536 | // Distilled widens generated string enums to open unions (`string & {}`). |
| 537 | cloudType: onramp.cloudType as OnRampCloudType, |
| 538 | type: onramp.type as OnRampType, |
| 539 | dynamicRouting: onramp.dynamicRouting, |
| 540 | installRoutesInCloud: onramp.installRoutesInCloud, |
| 541 | installRoutesInMagicWan: onramp.installRoutesInMagicWan, |
| 542 | vpc: onramp.vpc ?? undefined, |
| 543 | cloudAsn: onramp.cloudAsn ?? undefined, |
| 544 | description: onramp.description ?? undefined, |
| 545 | attachedHubs: [...(onramp.attachedHubs ?? [])], |
| 546 | attachedVpcs: [...(onramp.attachedVpcs ?? [])], |
| 547 | manageHubToHubAttachments: onramp.manageHubToHubAttachments ?? undefined, |
| 548 | manageVpcToHubAttachments: onramp.manageVpcToHubAttachments ?? undefined, |
| 549 | updatedAt: onramp.updatedAt, |
| 550 | destroyOnDelete, |
| 551 | }); |