| 1 | import * as dm from "@distilled.cloud/gcp/datamigration_v1"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import * as Schedule from "effect/Schedule"; |
| 4 | import * as Stream from "effect/Stream"; |
| 5 | import { Unowned } from "../../AdoptPolicy.ts"; |
| 6 | import { isResolved } from "../../Diff.ts"; |
| 7 | import * as Provider from "../../Provider.ts"; |
| 8 | import { Resource } from "../../Resource.ts"; |
| 9 | import { tagRecord } from "../../Tags.ts"; |
| 10 | import { GcpEnvironment } from "../Environment.ts"; |
| 11 | import { |
| 12 | createInternalLabels, |
| 13 | diffLabels, |
| 14 | hasAlchemyLabels, |
| 15 | toLabels, |
| 16 | } from "../Labels.ts"; |
| 17 | import type { Providers } from "../Providers.ts"; |
| 18 | import { |
| 19 | fieldMask, |
| 20 | fingerprint, |
| 21 | hasAlchemyLabelMap, |
| 22 | locationParent, |
| 23 | normalizeLocation, |
| 24 | parseName, |
| 25 | replaceOnIdentity, |
| 26 | ResourceNotResolved, |
| 27 | toPhysicalId, |
| 28 | userLabels, |
| 29 | waitForOperation, |
| 30 | waitUntilExists, |
| 31 | waitUntilGone, |
| 32 | } from "./internal.ts"; |
| 33 | |
| 34 | export type ConnectionProfileProvider = |
| 35 | | dm.ConnectionProfileProviderEnum |
| 36 | | (string & {}); |
| 37 | export type ConnectionProfileState = |
| 38 | | dm.ConnectionProfileStateEnum |
| 39 | | (string & {}); |
| 40 | export type ConnectionProfileRole = |
| 41 | | dm.ConnectionProfileRoleEnum |
| 42 | | (string & {}); |
| 43 | export type MySqlConnectionProfile = dm.MySqlConnectionProfile; |
| 44 | export type PostgreSqlConnectionProfile = dm.PostgreSqlConnectionProfile; |
| 45 | export type OracleConnectionProfile = dm.OracleConnectionProfile; |
| 46 | export type SqlServerConnectionProfile = dm.SqlServerConnectionProfile; |
| 47 | export type CloudSqlConnectionProfile = dm.CloudSqlConnectionProfile; |
| 48 | export type CloudSqlSettings = dm.CloudSqlSettings; |
| 49 | export type AlloyDbConnectionProfile = dm.AlloyDbConnectionProfile; |
| 50 | export type AlloyDbSettings = dm.AlloyDbSettings; |
| 51 | export type SslConfig = dm.SslConfig; |
| 52 | export type Status = dm.Status; |
| 53 | |
| 54 | export type ConnectionProfileProps = { |
| 55 | /** |
| 56 | * Connection profile id (the `{connectionProfile}` segment of |
| 57 | * `projects/{project}/locations/{location}/connectionProfiles/{connectionProfile}`). |
| 58 | * If omitted, a unique RFC1035 name is generated. Immutable — changing it |
| 59 | * replaces the profile. |
| 60 | */ |
| 61 | connectionProfileId?: string; |
| 62 | /** |
| 63 | * Region (`us-central1`, …). Immutable — changing it replaces the |
| 64 | * profile. `US-CENTRAL1` is accepted and normalized to `us-central1`. |
| 65 | * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`) |
| 66 | */ |
| 67 | location?: string; |
| 68 | /** |
| 69 | * User-friendly display name. |
| 70 | */ |
| 71 | displayName?: string; |
| 72 | /** |
| 73 | * User labels. Alchemy ownership labels are merged in automatically. |
| 74 | */ |
| 75 | labels?: Record<string, string>; |
| 76 | /** |
| 77 | * Database provider (`CLOUDSQL`, `RDS`, `AURORA`, `ALLOYDB`, |
| 78 | * `AZURE_DATABASE`). |
| 79 | */ |
| 80 | provider?: ConnectionProfileProvider; |
| 81 | /** |
| 82 | * Connection profile role (`SOURCE` or `DESTINATION`). |
| 83 | */ |
| 84 | role?: ConnectionProfileRole; |
| 85 | /** |
| 86 | * MySQL source/destination connection. Mutually exclusive with the other |
| 87 | * engine blocks. Switching engines replaces the profile. |
| 88 | */ |
| 89 | mysql?: MySqlConnectionProfile; |
| 90 | /** |
| 91 | * PostgreSQL source/destination connection. Mutually exclusive with the |
| 92 | * other engine blocks. Switching engines replaces the profile. |
| 93 | */ |
| 94 | postgresql?: PostgreSqlConnectionProfile; |
| 95 | /** |
| 96 | * Oracle source connection. Mutually exclusive with the other engine |
| 97 | * blocks. Switching engines replaces the profile. |
| 98 | */ |
| 99 | oracle?: OracleConnectionProfile; |
| 100 | /** |
| 101 | * SQL Server source connection. Mutually exclusive with the other engine |
| 102 | * blocks. Switching engines replaces the profile. |
| 103 | */ |
| 104 | sqlserver?: SqlServerConnectionProfile; |
| 105 | /** |
| 106 | * Cloud SQL destination. `settings` are immutable after create — |
| 107 | * changing them replaces the profile. Creating this engine provisions a |
| 108 | * Cloud SQL replica. Delete always force-deletes that replica. |
| 109 | */ |
| 110 | cloudsql?: CloudSqlConnectionProfile; |
| 111 | /** |
| 112 | * AlloyDB destination. `clusterId` and `settings` are immutable — |
| 113 | * changing them replaces the profile. |
| 114 | */ |
| 115 | alloydb?: AlloyDbConnectionProfile; |
| 116 | /** |
| 117 | * Oracle only: create without validating connectivity. |
| 118 | */ |
| 119 | skipValidation?: boolean; |
| 120 | /** |
| 121 | * Oracle only: validate without creating resources. |
| 122 | */ |
| 123 | validateOnly?: boolean; |
| 124 | }; |
| 125 | |
| 126 | export type ConnectionProfile = Resource< |
| 127 | "GCP.DataMigration.ConnectionProfile", |
| 128 | ConnectionProfileProps, |
| 129 | { |
| 130 | /** Full resource name. */ |
| 131 | name: string; |
| 132 | /** Connection profile id (last path segment). */ |
| 133 | connectionProfileId: string; |
| 134 | /** Project id. */ |
| 135 | project: string; |
| 136 | /** Location id. */ |
| 137 | location: string; |
| 138 | /** User-friendly display name. */ |
| 139 | displayName: string | undefined; |
| 140 | /** User labels (Alchemy ownership labels stripped). */ |
| 141 | labels: Record<string, string>; |
| 142 | /** Database provider. */ |
| 143 | provider: string | undefined; |
| 144 | /** Source or destination role. */ |
| 145 | role: string | undefined; |
| 146 | /** Server-reported state. */ |
| 147 | state: string | undefined; |
| 148 | /** MySQL connection (password omitted). */ |
| 149 | mysql: MySqlConnectionProfile | undefined; |
| 150 | /** PostgreSQL connection (password omitted). */ |
| 151 | postgresql: PostgreSqlConnectionProfile | undefined; |
| 152 | /** Oracle connection (password omitted). */ |
| 153 | oracle: OracleConnectionProfile | undefined; |
| 154 | /** SQL Server connection (password omitted). */ |
| 155 | sqlserver: SqlServerConnectionProfile | undefined; |
| 156 | /** Cloud SQL destination (root password omitted). */ |
| 157 | cloudsql: CloudSqlConnectionProfile | undefined; |
| 158 | /** AlloyDB destination (initial user password omitted). */ |
| 159 | alloydb: AlloyDbConnectionProfile | undefined; |
| 160 | /** Failure details when `state` is `FAILED`. */ |
| 161 | error: Status | undefined; |
| 162 | /** RFC3339 creation timestamp. */ |
| 163 | createTime: string | undefined; |
| 164 | /** RFC3339 last-update timestamp. */ |
| 165 | updateTime: string | undefined; |
| 166 | }, |
| 167 | never, |
| 168 | Providers |
| 169 | >; |
| 170 | |
| 171 | /** |
| 172 | * A Database Migration Service connection profile: credentials and |
| 173 | * connectivity for a source database, or settings used to provision a |
| 174 | * Cloud SQL / AlloyDB destination. |
| 175 | * |
| 176 | * `connectionProfileId`, `location`, and the engine kind |
| 177 | * (`mysql` / `postgresql` / `oracle` / `sqlserver` / `cloudsql` / |
| 178 | * `alloydb`) plus immutable Cloud SQL / AlloyDB settings are replacement |
| 179 | * triggers. Display name, labels, role, provider, and credential fields |
| 180 | * update in place. Passwords are input-only and never returned. |
| 181 | * |
| 182 | * ### Creating a Connection Profile |
| 183 | * **Example:** MySQL source |
| 184 | * ```typescript |
| 185 | * const source = yield* GCP.DataMigration.ConnectionProfile("MysqlSrc", { |
| 186 | * mysql: { |
| 187 | * host: "10.0.0.8", |
| 188 | * port: 3306, |
| 189 | * username: "dms", |
| 190 | * password: process.env.MYSQL_PASSWORD, |
| 191 | * }, |
| 192 | * labels: { env: "test" }, |
| 193 | * }); |
| 194 | * ``` |
| 195 | * |
| 196 | * **Example:** Cloud SQL destination |
| 197 | * ```typescript |
| 198 | * const dest = yield* GCP.DataMigration.ConnectionProfile("MysqlDest", { |
| 199 | * cloudsql: { |
| 200 | * settings: { |
| 201 | * sourceId: source.name, |
| 202 | * databaseVersion: "MYSQL_8_0", |
| 203 | * tier: "db-n1-standard-1", |
| 204 | * rootPassword: process.env.MYSQL_ROOT_PASSWORD, |
| 205 | * }, |
| 206 | * }, |
| 207 | * }); |
| 208 | * ``` |
| 209 | * |
| 210 | * @resource |
| 211 | * @category DataMigration |
| 212 | */ |
| 213 | export const ConnectionProfile = Resource<ConnectionProfile>( |
| 214 | "GCP.DataMigration.ConnectionProfile", |
| 215 | ); |
| 216 | |
| 217 | const resourceName = ( |
| 218 | project: string, |
| 219 | location: string, |
| 220 | connectionProfileId: string, |
| 221 | ) => |
| 222 | `${locationParent(project, location)}/connectionProfiles/${connectionProfileId}`; |
| 223 | |
| 224 | const kindOf = (value: { |
| 225 | mysql?: unknown; |
| 226 | postgresql?: unknown; |
| 227 | oracle?: unknown; |
| 228 | sqlserver?: unknown; |
| 229 | cloudsql?: unknown; |
| 230 | alloydb?: unknown; |
| 231 | }) => |
| 232 | value.mysql |
| 233 | ? "mysql" |
| 234 | : value.postgresql |
| 235 | ? "postgresql" |
| 236 | : value.oracle |
| 237 | ? "oracle" |
| 238 | : value.sqlserver |
| 239 | ? "sqlserver" |
| 240 | : value.cloudsql |
| 241 | ? "cloudsql" |
| 242 | : value.alloydb |
| 243 | ? "alloydb" |
| 244 | : ""; |
| 245 | |
| 246 | const publicSsl = (ssl: SslConfig | undefined) => |
| 247 | ssl === undefined |
| 248 | ? undefined |
| 249 | : { |
| 250 | type: ssl.type, |
| 251 | sslFlags: ssl.sslFlags, |
| 252 | }; |
| 253 | |
| 254 | const publicMysql = (mysql: MySqlConnectionProfile | undefined) => |
| 255 | mysql === undefined |
| 256 | ? undefined |
| 257 | : { |
| 258 | host: mysql.host, |
| 259 | port: mysql.port, |
| 260 | username: mysql.username, |
| 261 | cloudSqlId: mysql.cloudSqlId, |
| 262 | passwordSet: mysql.passwordSet, |
| 263 | ssl: publicSsl(mysql.ssl), |
| 264 | }; |
| 265 | |
| 266 | const publicPostgresql = ( |
| 267 | postgresql: PostgreSqlConnectionProfile | undefined, |
| 268 | ) => |
| 269 | postgresql === undefined |
| 270 | ? undefined |
| 271 | : { |
| 272 | host: postgresql.host, |
| 273 | port: postgresql.port, |
| 274 | username: postgresql.username, |
| 275 | database: postgresql.database, |
| 276 | cloudSqlId: postgresql.cloudSqlId, |
| 277 | alloydbClusterId: postgresql.alloydbClusterId, |
| 278 | enableIamAuthentication: postgresql.enableIamAuthentication, |
| 279 | privateConnectivity: postgresql.privateConnectivity, |
| 280 | privateServiceConnectConnectivity: |
| 281 | postgresql.privateServiceConnectConnectivity, |
| 282 | forwardSshConnectivity: postgresql.forwardSshConnectivity |
| 283 | ? { |
| 284 | hostname: postgresql.forwardSshConnectivity.hostname, |
| 285 | port: postgresql.forwardSshConnectivity.port, |
| 286 | username: postgresql.forwardSshConnectivity.username, |
| 287 | } |
| 288 | : undefined, |
| 289 | passwordSet: postgresql.passwordSet, |
| 290 | ssl: publicSsl(postgresql.ssl), |
| 291 | }; |
| 292 | |
| 293 | const publicOracle = (oracle: OracleConnectionProfile | undefined) => |
| 294 | oracle === undefined |
| 295 | ? undefined |
| 296 | : { |
| 297 | host: oracle.host, |
| 298 | port: oracle.port, |
| 299 | username: oracle.username, |
| 300 | databaseService: oracle.databaseService, |
| 301 | privateConnectivity: oracle.privateConnectivity, |
| 302 | forwardSshConnectivity: oracle.forwardSshConnectivity |
| 303 | ? { |
| 304 | hostname: oracle.forwardSshConnectivity.hostname, |
| 305 | port: oracle.forwardSshConnectivity.port, |
| 306 | username: oracle.forwardSshConnectivity.username, |
| 307 | } |
| 308 | : undefined, |
| 309 | oracleAsmConfig: oracle.oracleAsmConfig |
| 310 | ? { |
| 311 | hostname: oracle.oracleAsmConfig.hostname, |
| 312 | port: oracle.oracleAsmConfig.port, |
| 313 | asmService: oracle.oracleAsmConfig.asmService, |
| 314 | username: oracle.oracleAsmConfig.username, |
| 315 | ssl: publicSsl(oracle.oracleAsmConfig.ssl), |
| 316 | } |
| 317 | : undefined, |
| 318 | passwordSet: oracle.passwordSet, |
| 319 | ssl: publicSsl(oracle.ssl), |
| 320 | }; |
| 321 | |
| 322 | const publicSqlserver = (sqlserver: SqlServerConnectionProfile | undefined) => |
| 323 | sqlserver === undefined |
| 324 | ? undefined |
| 325 | : { |
| 326 | host: sqlserver.host, |
| 327 | port: sqlserver.port, |
| 328 | username: sqlserver.username, |
| 329 | database: sqlserver.database, |
| 330 | cloudSqlId: sqlserver.cloudSqlId, |
| 331 | cloudSqlProjectId: sqlserver.cloudSqlProjectId, |
| 332 | dbmPort: sqlserver.dbmPort, |
| 333 | backups: sqlserver.backups, |
| 334 | privateConnectivity: sqlserver.privateConnectivity, |
| 335 | privateServiceConnectConnectivity: |
| 336 | sqlserver.privateServiceConnectConnectivity, |
| 337 | passwordSet: sqlserver.passwordSet, |
| 338 | ssl: publicSsl(sqlserver.ssl), |
| 339 | }; |
| 340 | |
| 341 | const publicCloudsql = (cloudsql: CloudSqlConnectionProfile | undefined) => |
| 342 | cloudsql === undefined |
| 343 | ? undefined |
| 344 | : { |
| 345 | cloudSqlId: cloudsql.cloudSqlId, |
| 346 | publicIp: cloudsql.publicIp, |
| 347 | privateIp: cloudsql.privateIp, |
| 348 | additionalPublicIp: cloudsql.additionalPublicIp, |
| 349 | settings: cloudsql.settings |
| 350 | ? { |
| 351 | ...cloudsql.settings, |
| 352 | rootPassword: undefined, |
| 353 | } |
| 354 | : undefined, |
| 355 | }; |
| 356 | |
| 357 | const publicAlloydb = (alloydb: AlloyDbConnectionProfile | undefined) => |
| 358 | alloydb === undefined |
| 359 | ? undefined |
| 360 | : { |
| 361 | clusterId: alloydb.clusterId, |
| 362 | settings: alloydb.settings |
| 363 | ? { |
| 364 | ...alloydb.settings, |
| 365 | initialUser: alloydb.settings.initialUser |
| 366 | ? { |
| 367 | user: alloydb.settings.initialUser.user, |
| 368 | passwordSet: alloydb.settings.initialUser.passwordSet, |
| 369 | } |
| 370 | : undefined, |
| 371 | } |
| 372 | : undefined, |
| 373 | }; |
| 374 | |
| 375 | const toAttrs = (profile: dm.ConnectionProfile, project: string) => { |
| 376 | const name = profile.name ?? ""; |
| 377 | const parsed = parseName(name, "connectionProfiles"); |
| 378 | return { |
| 379 | name, |
| 380 | connectionProfileId: parsed.id, |
| 381 | project: parsed.project || project, |
| 382 | location: parsed.location, |
| 383 | displayName: profile.displayName, |
| 384 | labels: userLabels(profile.labels), |
| 385 | provider: profile.provider, |
| 386 | role: profile.role, |
| 387 | state: profile.state, |
| 388 | mysql: publicMysql(profile.mysql), |
| 389 | postgresql: publicPostgresql(profile.postgresql), |
| 390 | oracle: publicOracle(profile.oracle), |
| 391 | sqlserver: publicSqlserver(profile.sqlserver), |
| 392 | cloudsql: publicCloudsql(profile.cloudsql), |
| 393 | alloydb: publicAlloydb(profile.alloydb), |
| 394 | error: profile.error, |
| 395 | createTime: profile.createTime, |
| 396 | updateTime: profile.updateTime, |
| 397 | }; |
| 398 | }; |
| 399 | |
| 400 | const getByName = (name: string) => |
| 401 | name.length === 0 |
| 402 | ? Effect.succeed(undefined) |
| 403 | : dm |
| 404 | .getProjectsLocationsConnectionProfiles({ name }) |
| 405 | .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined))); |
| 406 | |
| 407 | const listOwned = (project: string, region: string) => |
| 408 | dm.listProjectsLocationsConnectionProfiles |
| 409 | .pages({ |
| 410 | parent: `projects/${project}/locations/-`, |
| 411 | pageSize: 1000, |
| 412 | }) |
| 413 | .pipe( |
| 414 | Stream.flatMap((page) => |
| 415 | Stream.fromIterable(page.connectionProfiles ?? []), |
| 416 | ), |
| 417 | Stream.filter((item) => hasAlchemyLabelMap(item.labels)), |
| 418 | Stream.runCollect, |
| 419 | Effect.map((chunk) => Array.from(chunk)), |
| 420 | Effect.catchTag("NotFound", () => |
| 421 | dm.listProjectsLocationsConnectionProfiles |
| 422 | .pages({ |
| 423 | parent: locationParent(project, region), |
| 424 | pageSize: 1000, |
| 425 | }) |
| 426 | .pipe( |
| 427 | Stream.flatMap((page) => |
| 428 | Stream.fromIterable(page.connectionProfiles ?? []), |
| 429 | ), |
| 430 | Stream.filter((item) => hasAlchemyLabelMap(item.labels)), |
| 431 | Stream.runCollect, |
| 432 | Effect.map((chunk) => Array.from(chunk)), |
| 433 | Effect.catchTag("NotFound", () => |
| 434 | Effect.succeed([] as dm.ConnectionProfile[]), |
| 435 | ), |
| 436 | ), |
| 437 | ), |
| 438 | ); |
| 439 | |
| 440 | const secretChanged = ( |
| 441 | newsHasSecret: boolean, |
| 442 | publicPrevious: unknown, |
| 443 | publicNext: unknown, |
| 444 | ) => newsHasSecret || fingerprint(publicPrevious) !== fingerprint(publicNext); |
| 445 | |
| 446 | export const ConnectionProfileProvider = () => |
| 447 | Provider.succeed(ConnectionProfile, { |
| 448 | stables: [ |
| 449 | "name", |
| 450 | "connectionProfileId", |
| 451 | "project", |
| 452 | "location", |
| 453 | "createTime", |
| 454 | ], |
| 455 | |
| 456 | diff: Effect.fn(function* ({ news, olds, output }) { |
| 457 | if (!isResolved(news)) return undefined; |
| 458 | const env = yield* GcpEnvironment.current; |
| 459 | const previousKind = kindOf({ |
| 460 | mysql: olds?.mysql ?? output?.mysql, |
| 461 | postgresql: olds?.postgresql ?? output?.postgresql, |
| 462 | oracle: olds?.oracle ?? output?.oracle, |
| 463 | sqlserver: olds?.sqlserver ?? output?.sqlserver, |
| 464 | cloudsql: olds?.cloudsql ?? output?.cloudsql, |
| 465 | alloydb: olds?.alloydb ?? output?.alloydb, |
| 466 | }); |
| 467 | const nextKind = kindOf(news) || previousKind; |
| 468 | const previousCloudsql = fingerprint( |
| 469 | publicCloudsql(olds?.cloudsql ?? output?.cloudsql)?.settings, |
| 470 | ); |
| 471 | const nextCloudsql = fingerprint(publicCloudsql(news.cloudsql)?.settings); |
| 472 | const previousAlloy = fingerprint({ |
| 473 | clusterId: olds?.alloydb?.clusterId ?? output?.alloydb?.clusterId, |
| 474 | settings: publicAlloydb(olds?.alloydb ?? output?.alloydb)?.settings, |
| 475 | }); |
| 476 | const nextAlloy = fingerprint({ |
| 477 | clusterId: news.alloydb?.clusterId, |
| 478 | settings: publicAlloydb(news.alloydb)?.settings, |
| 479 | }); |
| 480 | return replaceOnIdentity({ |
| 481 | previousId: olds?.connectionProfileId ?? output?.connectionProfileId, |
| 482 | nextId: |
| 483 | news.connectionProfileId ?? |
| 484 | olds?.connectionProfileId ?? |
| 485 | output?.connectionProfileId, |
| 486 | previousLocation: normalizeLocation( |
| 487 | olds?.location ?? output?.location, |
| 488 | env.region, |
| 489 | ), |
| 490 | nextLocation: normalizeLocation( |
| 491 | news.location ?? olds?.location ?? output?.location, |
| 492 | env.region, |
| 493 | ), |
| 494 | extra: |
| 495 | previousKind !== nextKind || |
| 496 | (nextKind === "cloudsql" && |
| 497 | news.cloudsql !== undefined && |
| 498 | previousCloudsql !== nextCloudsql) || |
| 499 | (nextKind === "alloydb" && |
| 500 | news.alloydb !== undefined && |
| 501 | previousAlloy !== nextAlloy), |
| 502 | }); |
| 503 | }), |
| 504 | |
| 505 | read: Effect.fn(function* ({ id, olds, output }) { |
| 506 | const env = yield* GcpEnvironment.current; |
| 507 | const connectionProfileId = yield* toPhysicalId( |
| 508 | id, |
| 509 | olds?.connectionProfileId, |
| 510 | output?.connectionProfileId, |
| 511 | "profile", |
| 512 | ); |
| 513 | const location = normalizeLocation( |
| 514 | olds?.location ?? output?.location, |
| 515 | env.region, |
| 516 | ); |
| 517 | const name = |
| 518 | output?.name ?? |
| 519 | resourceName(env.project, location, connectionProfileId); |
| 520 | const existing = yield* getByName(name); |
| 521 | if (existing === undefined) return undefined; |
| 522 | const attrs = toAttrs(existing, env.project); |
| 523 | return (yield* hasAlchemyLabels(id, tagRecord(existing.labels))) |
| 524 | ? attrs |
| 525 | : Unowned(attrs); |
| 526 | }), |
| 527 | |
| 528 | list: () => |
| 529 | Effect.gen(function* () { |
| 530 | const env = yield* GcpEnvironment.current; |
| 531 | const items = yield* listOwned(env.project, env.region); |
| 532 | return items.map((item) => toAttrs(item, env.project)); |
| 533 | }), |
| 534 | |
| 535 | reconcile: Effect.fn(function* ({ id, news, output }) { |
| 536 | const env = yield* GcpEnvironment.current; |
| 537 | const connectionProfileId = yield* toPhysicalId( |
| 538 | id, |
| 539 | news.connectionProfileId, |
| 540 | output?.connectionProfileId, |
| 541 | "profile", |
| 542 | ); |
| 543 | const location = normalizeLocation( |
| 544 | news.location ?? output?.location, |
| 545 | env.region, |
| 546 | ); |
| 547 | const name = resourceName(env.project, location, connectionProfileId); |
| 548 | const desiredLabels = { |
| 549 | ...toLabels(news.labels), |
| 550 | ...(yield* createInternalLabels(id)), |
| 551 | }; |
| 552 | const displayName = news.displayName ?? connectionProfileId; |
| 553 | const body: dm.ConnectionProfile = { |
| 554 | displayName, |
| 555 | labels: desiredLabels, |
| 556 | provider: news.provider, |
| 557 | role: news.role, |
| 558 | mysql: news.mysql, |
| 559 | postgresql: news.postgresql, |
| 560 | oracle: news.oracle, |
| 561 | sqlserver: news.sqlserver, |
| 562 | cloudsql: news.cloudsql, |
| 563 | alloydb: news.alloydb, |
| 564 | }; |
| 565 | |
| 566 | let current = yield* getByName(output?.name ?? name); |
| 567 | |
| 568 | if (current === undefined) { |
| 569 | const created = yield* dm |
| 570 | .createProjectsLocationsConnectionProfiles({ |
| 571 | parent: locationParent(env.project, location), |
| 572 | connectionProfileId, |
| 573 | skipValidation: news.skipValidation, |
| 574 | validateOnly: news.validateOnly, |
| 575 | body, |
| 576 | }) |
| 577 | .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined))); |
| 578 | if (created !== undefined) { |
| 579 | yield* waitForOperation(created); |
| 580 | } |
| 581 | current = yield* waitUntilExists(getByName(name), name); |
| 582 | } |
| 583 | |
| 584 | if (current === undefined) { |
| 585 | return yield* new ResourceNotResolved({ name }); |
| 586 | } |
| 587 | |
| 588 | const observedLabels = tagRecord(current.labels); |
| 589 | const { upsert, removed } = diffLabels(observedLabels, desiredLabels); |
| 590 | const labelsChanged = upsert.length > 0 || removed.length > 0; |
| 591 | const displayNameChanged = (current.displayName ?? "") !== displayName; |
| 592 | const providerChanged = |
| 593 | news.provider !== undefined && |
| 594 | (current.provider ?? "") !== news.provider; |
| 595 | const roleChanged = |
| 596 | news.role !== undefined && (current.role ?? "") !== news.role; |
| 597 | const mysqlChanged = |
| 598 | news.mysql !== undefined && |
| 599 | secretChanged( |
| 600 | news.mysql.password !== undefined || |
| 601 | news.mysql.ssl?.caCertificate !== undefined || |
| 602 | news.mysql.ssl?.clientCertificate !== undefined || |
| 603 | news.mysql.ssl?.clientKey !== undefined, |
| 604 | publicMysql(current.mysql), |
| 605 | publicMysql(news.mysql), |
| 606 | ); |
| 607 | const postgresqlChanged = |
| 608 | news.postgresql !== undefined && |
| 609 | secretChanged( |
| 610 | news.postgresql.password !== undefined || |
| 611 | news.postgresql.ssl?.caCertificate !== undefined || |
| 612 | news.postgresql.ssl?.clientCertificate !== undefined || |
| 613 | news.postgresql.ssl?.clientKey !== undefined || |
| 614 | news.postgresql.forwardSshConnectivity?.password !== undefined || |
| 615 | news.postgresql.forwardSshConnectivity?.privateKey !== undefined, |
| 616 | publicPostgresql(current.postgresql), |
| 617 | publicPostgresql(news.postgresql), |
| 618 | ); |
| 619 | const oracleChanged = |
| 620 | news.oracle !== undefined && |
| 621 | secretChanged( |
| 622 | news.oracle.password !== undefined || |
| 623 | news.oracle.ssl?.caCertificate !== undefined || |
| 624 | news.oracle.oracleAsmConfig?.password !== undefined, |
| 625 | publicOracle(current.oracle), |
| 626 | publicOracle(news.oracle), |
| 627 | ); |
| 628 | const sqlserverChanged = |
| 629 | news.sqlserver !== undefined && |
| 630 | secretChanged( |
| 631 | news.sqlserver.password !== undefined || |
| 632 | news.sqlserver.ssl?.caCertificate !== undefined, |
| 633 | publicSqlserver(current.sqlserver), |
| 634 | publicSqlserver(news.sqlserver), |
| 635 | ); |
| 636 | const mask = fieldMask([ |
| 637 | labelsChanged && "labels", |
| 638 | displayNameChanged && "displayName", |
| 639 | providerChanged && "provider", |
| 640 | roleChanged && "role", |
| 641 | mysqlChanged && "mysql", |
| 642 | postgresqlChanged && "postgresql", |
| 643 | oracleChanged && "oracle", |
| 644 | sqlserverChanged && "sqlserver", |
| 645 | ]); |
| 646 | |
| 647 | if (mask.length > 0) { |
| 648 | const operation = yield* dm.patchProjectsLocationsConnectionProfiles({ |
| 649 | name: current.name ?? name, |
| 650 | updateMask: mask, |
| 651 | skipValidation: news.skipValidation, |
| 652 | validateOnly: news.validateOnly, |
| 653 | body: { |
| 654 | name: current.name ?? name, |
| 655 | displayName, |
| 656 | labels: desiredLabels, |
| 657 | provider: news.provider, |
| 658 | role: news.role, |
| 659 | mysql: news.mysql, |
| 660 | postgresql: news.postgresql, |
| 661 | oracle: news.oracle, |
| 662 | sqlserver: news.sqlserver, |
| 663 | }, |
| 664 | }); |
| 665 | yield* waitForOperation(operation); |
| 666 | current = yield* waitUntilExists( |
| 667 | getByName(current.name ?? name), |
| 668 | current.name ?? name, |
| 669 | ); |
| 670 | } |
| 671 | |
| 672 | return toAttrs(current, env.project); |
| 673 | }), |
| 674 | |
| 675 | delete: Effect.fn(function* ({ output }) { |
| 676 | const operation = yield* dm |
| 677 | .deleteProjectsLocationsConnectionProfiles({ |
| 678 | name: output.name, |
| 679 | force: true, |
| 680 | }) |
| 681 | .pipe( |
| 682 | Effect.retry({ |
| 683 | while: (error) => error._tag === "Conflict", |
| 684 | times: 8, |
| 685 | schedule: Schedule.spaced("2 seconds"), |
| 686 | }), |
| 687 | Effect.catchTag("NotFound", () => Effect.succeed(undefined)), |
| 688 | ); |
| 689 | if (operation !== undefined) { |
| 690 | yield* waitForOperation(operation, { notFoundOk: true }); |
| 691 | } |
| 692 | yield* waitUntilGone(getByName(output.name), output.name); |
| 693 | }), |
| 694 | }); |