1import * as ds from "@distilled.cloud/gcp/datastream_v1";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { tagRecord } from "../../Tags.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import {
11 createInternalLabels,
12 diffLabels,
13 hasAlchemyLabels,
14 toLabels,
15} from "../Labels.ts";
16import type { Providers } from "../Providers.ts";
17import {
18 collectPages,
19 emptyMessage,
20 fieldMask,
21 fingerprint,
22 hasAlchemyLabelMap,
23 listAtLocation,
24 locationParent,
25 normalizeLocation,
26 parseName,
27 privateConnectionOf,
28 replaceOnIdentity,
29 ResourceNotResolved,
30 toPhysicalId,
31 userLabels,
32 settleOperation,
33 waitUntilExists,
34 waitUntilGone,
35} from "./internal.ts";
36
37export type MysqlProfile = ds.MysqlProfile;
38export type PostgresqlProfile = ds.PostgresqlProfile;
39export type OracleProfile = ds.OracleProfile;
40export type SqlServerProfile = ds.SqlServerProfile;
41export type MongodbProfile = ds.MongodbProfile;
42export type SpannerProfile = ds.SpannerProfile;
43export type GcsProfile = ds.GcsProfile;
44export type BigQueryProfile = ds.BigQueryProfile;
45export type SalesforceProfile = ds.SalesforceProfile;
46export type SalesforceMarketingCloudProfile =
47 ds.SalesforceMarketingCloudProfile;
48export type ServiceNowProfile = ds.ServiceNowProfile;
49export type DataverseProfile = ds.DataverseProfile;
50export type ForwardSshTunnelConnectivity = ds.ForwardSshTunnelConnectivity;
51export type PrivateConnectivity = ds.PrivateConnectivity;
52export type MysqlSslConfig = ds.MysqlSslConfig;
53export type PostgresqlSslConfig = ds.PostgresqlSslConfig;
54export type OracleSslConfig = ds.OracleSslConfig;
55export type SqlServerSslConfig = ds.SqlServerSslConfig;
56export type MongodbSslConfig = ds.MongodbSslConfig;
57export type OracleAsmConfig = ds.OracleAsmConfig;
58export type Secret = ds.Secret;
59
60export type ConnectionProfileProps = {
61 /**
62 * Connection profile id (the `{connectionProfile}` segment of
63 * `projects/{project}/locations/{location}/connectionProfiles/{connectionProfile}`).
64 * If omitted, a unique RFC1035 name is generated. Immutable — changing
65 * it replaces the profile.
66 */
67 connectionProfileId?: string;
68 /**
69 * Region (`us-central1`, …). Immutable — changing it replaces the
70 * profile. `US-CENTRAL1` is accepted and normalized to `us-central1`.
71 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
72 */
73 location?: string;
74 /**
75 * User-friendly display name.
76 */
77 displayName?: string;
78 /**
79 * User labels. Alchemy ownership labels are merged in automatically.
80 */
81 labels?: Record<string, string>;
82 /**
83 * MySQL source profile. Mutually exclusive with the other engine
84 * blocks. Switching engines replaces the profile.
85 */
86 mysqlProfile?: MysqlProfile;
87 /**
88 * PostgreSQL source profile. Mutually exclusive with the other engine
89 * blocks. Switching engines replaces the profile.
90 */
91 postgresqlProfile?: PostgresqlProfile;
92 /**
93 * Oracle source profile. Mutually exclusive with the other engine
94 * blocks. Switching engines replaces the profile.
95 */
96 oracleProfile?: OracleProfile;
97 /**
98 * SQL Server source profile. Mutually exclusive with the other engine
99 * blocks. Switching engines replaces the profile.
100 */
101 sqlServerProfile?: SqlServerProfile;
102 /**
103 * MongoDB source profile. Mutually exclusive with the other engine
104 * blocks. Switching engines replaces the profile.
105 */
106 mongodbProfile?: MongodbProfile;
107 /**
108 * Spanner source profile. `database` is immutable — changing it
109 * replaces the profile.
110 */
111 spannerProfile?: SpannerProfile;
112 /**
113 * Cloud Storage destination profile. Mutually exclusive with the other
114 * engine blocks. Switching engines replaces the profile.
115 */
116 gcsProfile?: GcsProfile;
117 /**
118 * BigQuery destination profile (empty object). Mutually exclusive with
119 * the other engine blocks. Switching engines replaces the profile.
120 */
121 bigqueryProfile?: BigQueryProfile;
122 /**
123 * Salesforce source profile. Mutually exclusive with the other engine
124 * blocks. Switching engines replaces the profile.
125 */
126 salesforceProfile?: SalesforceProfile;
127 /**
128 * Salesforce Marketing Cloud source profile. Mutually exclusive with
129 * the other engine blocks. Switching engines replaces the profile.
130 */
131 salesforceMarketingCloudProfile?: SalesforceMarketingCloudProfile;
132 /**
133 * ServiceNow source profile. Mutually exclusive with the other engine
134 * blocks. Switching engines replaces the profile.
135 */
136 serviceNowProfile?: ServiceNowProfile;
137 /**
138 * Dataverse source profile. Mutually exclusive with the other engine
139 * blocks. Switching engines replaces the profile.
140 */
141 dataverseProfile?: DataverseProfile;
142 /**
143 * Static service IP connectivity (Datastream public IPs). Mutually
144 * exclusive with SSH tunnel and private connectivity.
145 */
146 staticServiceIpConnectivity?: Record<string, never>;
147 /**
148 * Forward SSH tunnel connectivity. Mutually exclusive with static IP
149 * and private connectivity. Passwords and private keys are input-only.
150 */
151 forwardSshConnectivity?: ForwardSshTunnelConnectivity;
152 /**
153 * Private connectivity via a Datastream private connection. Full name
154 * or id (combined with `location`). Mutually exclusive with static IP
155 * and SSH tunnel.
156 */
157 privateConnectivity?: {
158 privateConnection: string;
159 };
160 /**
161 * Create or update without validating connectivity.
162 */
163 force?: boolean;
164 /**
165 * Validate without creating or updating resources.
166 */
167 validateOnly?: boolean;
168};
169
170export type ConnectionProfile = Resource<
171 "GCP.Datastream.ConnectionProfile",
172 ConnectionProfileProps,
173 {
174 /** Full resource name. */
175 name: string;
176 /** Connection profile id (last path segment). */
177 connectionProfileId: string;
178 /** Project id. */
179 project: string;
180 /** Location id. */
181 location: string;
182 /** User-friendly display name. */
183 displayName: string | undefined;
184 /** User labels (Alchemy ownership labels stripped). */
185 labels: Record<string, string>;
186 /** MySQL profile (password omitted). */
187 mysqlProfile: MysqlProfile | undefined;
188 /** PostgreSQL profile (password omitted). */
189 postgresqlProfile: PostgresqlProfile | undefined;
190 /** Oracle profile (password omitted). */
191 oracleProfile: OracleProfile | undefined;
192 /** SQL Server profile (password omitted). */
193 sqlServerProfile: SqlServerProfile | undefined;
194 /** MongoDB profile (password omitted). */
195 mongodbProfile: MongodbProfile | undefined;
196 /** Spanner profile. */
197 spannerProfile: SpannerProfile | undefined;
198 /** Cloud Storage destination profile. */
199 gcsProfile: GcsProfile | undefined;
200 /** BigQuery destination profile. */
201 bigqueryProfile: BigQueryProfile | undefined;
202 /** Salesforce profile (secrets omitted). */
203 salesforceProfile: SalesforceProfile | undefined;
204 /** Salesforce Marketing Cloud profile (secrets omitted). */
205 salesforceMarketingCloudProfile:
206 | SalesforceMarketingCloudProfile
207 | undefined;
208 /** ServiceNow profile (secrets omitted). */
209 serviceNowProfile: ServiceNowProfile | undefined;
210 /** Dataverse profile (secrets omitted). */
211 dataverseProfile: DataverseProfile | undefined;
212 /** Static service IP connectivity. */
213 staticServiceIpConnectivity: Record<string, never> | undefined;
214 /** Forward SSH connectivity (password and key omitted). */
215 forwardSshConnectivity: ForwardSshTunnelConnectivity | undefined;
216 /** Private connectivity. */
217 privateConnectivity: PrivateConnectivity | undefined;
218 /** Whether the profile satisfies physical zone isolation. */
219 satisfiesPzi: boolean | undefined;
220 /** Whether the profile satisfies physical zone separation. */
221 satisfiesPzs: boolean | undefined;
222 /** RFC3339 creation timestamp. */
223 createTime: string | undefined;
224 /** RFC3339 last-update timestamp. */
225 updateTime: string | undefined;
226 },
227 never,
228 Providers
229>;
230
231/**
232 * A Datastream connection profile: credentials and connectivity for a
233 * source database or a BigQuery / Cloud Storage destination.
234 *
235 * `connectionProfileId`, `location`, and the engine kind
236 * (`mysqlProfile` / `postgresqlProfile` / `oracleProfile` /
237 * `sqlServerProfile` / `mongodbProfile` / `spannerProfile` /
238 * `gcsProfile` / `bigqueryProfile` / `salesforceProfile` /
239 * `salesforceMarketingCloudProfile` / `serviceNowProfile` /
240 * `dataverseProfile`) plus Spanner `database` are replacement triggers.
241 * Display name, labels, credentials, and connectivity update in place.
242 * Passwords, SSL keys, and OAuth secrets are input-only and never
243 * returned.
244 *
245 * ### Creating a Connection Profile
246 * **Example:** BigQuery destination
247 * ```typescript
248 * const dest = yield* GCP.Datastream.ConnectionProfile("BqDest", {
249 * bigqueryProfile: {},
250 * labels: { env: "test" },
251 * });
252 * ```
253 *
254 * **Example:** MySQL source
255 * ```typescript
256 * const source = yield* GCP.Datastream.ConnectionProfile("MysqlSrc", {
257 * mysqlProfile: {
258 * hostname: "10.0.0.8",
259 * port: 3306,
260 * username: "datastream",
261 * password: process.env.MYSQL_PASSWORD,
262 * },
263 * staticServiceIpConnectivity: {},
264 * force: true,
265 * });
266 * ```
267 *
268 * **Example:** Cloud Storage destination
269 * ```typescript
270 * const gcs = yield* GCP.Datastream.ConnectionProfile("GcsDest", {
271 * gcsProfile: { bucket: bucket.bucketName, rootPath: "/data" },
272 * });
273 * ```
274 *
275 * @resource
276 * @category Datastream
277 */
278export const ConnectionProfile = Resource<ConnectionProfile>(
279 "GCP.Datastream.ConnectionProfile",
280);
281
282const resourceName = (
283 project: string,
284 location: string,
285 connectionProfileId: string,
286) =>
287 `${locationParent(project, location)}/connectionProfiles/${connectionProfileId}`;
288
289const kindOf = (value: {
290 mysqlProfile?: unknown;
291 postgresqlProfile?: unknown;
292 oracleProfile?: unknown;
293 sqlServerProfile?: unknown;
294 mongodbProfile?: unknown;
295 spannerProfile?: unknown;
296 gcsProfile?: unknown;
297 bigqueryProfile?: unknown;
298 salesforceProfile?: unknown;
299 salesforceMarketingCloudProfile?: unknown;
300 serviceNowProfile?: unknown;
301 dataverseProfile?: unknown;
302}) =>
303 value.mysqlProfile
304 ? "mysql"
305 : value.postgresqlProfile
306 ? "postgresql"
307 : value.oracleProfile
308 ? "oracle"
309 : value.sqlServerProfile
310 ? "sqlserver"
311 : value.mongodbProfile
312 ? "mongodb"
313 : value.spannerProfile
314 ? "spanner"
315 : value.gcsProfile
316 ? "gcs"
317 : value.bigqueryProfile
318 ? "bigquery"
319 : value.salesforceProfile
320 ? "salesforce"
321 : value.salesforceMarketingCloudProfile
322 ? "sfmc"
323 : value.serviceNowProfile
324 ? "servicenow"
325 : value.dataverseProfile
326 ? "dataverse"
327 : "";
328
329const publicSecret = (secret: Secret | undefined) =>
330 secret === undefined
331 ? undefined
332 : {
333 secretVersion: secret.secretVersion,
334 };
335
336const publicMysqlSsl = (ssl: MysqlSslConfig | undefined) =>
337 ssl === undefined
338 ? undefined
339 : {
340 clientCertificateSet: ssl.clientCertificateSet,
341 caCertificateSet: ssl.caCertificateSet,
342 clientKeySet: ssl.clientKeySet,
343 };
344
345const publicPostgresSsl = (ssl: PostgresqlSslConfig | undefined) =>
346 ssl === undefined
347 ? undefined
348 : {
349 serverVerification: ssl.serverVerification
350 ? {
351 serverCertificateHostname:
352 ssl.serverVerification.serverCertificateHostname,
353 }
354 : undefined,
355 serverAndClientVerification: ssl.serverAndClientVerification
356 ? {
357 serverCertificateHostname:
358 ssl.serverAndClientVerification.serverCertificateHostname,
359 }
360 : undefined,
361 };
362
363const publicOracleSsl = (ssl: OracleSslConfig | undefined) =>
364 ssl === undefined
365 ? undefined
366 : {
367 caCertificateSet: ssl.caCertificateSet,
368 serverCertificateDistinguishedName:
369 ssl.serverCertificateDistinguishedName,
370 };
371
372const publicSqlServerSsl = (ssl: SqlServerSslConfig | undefined) =>
373 ssl === undefined
374 ? undefined
375 : {
376 encryptionNotEnforced: emptyMessage(ssl.encryptionNotEnforced),
377 basicEncryption: emptyMessage(ssl.basicEncryption),
378 encryptionAndServerValidation: ssl.encryptionAndServerValidation
379 ? {
380 serverCertificateHostname:
381 ssl.encryptionAndServerValidation.serverCertificateHostname,
382 }
383 : undefined,
384 };
385
386const publicMongodbSsl = (ssl: MongodbSslConfig | undefined) =>
387 ssl === undefined
388 ? undefined
389 : {
390 clientCertificateSet: ssl.clientCertificateSet,
391 clientKeySet: ssl.clientKeySet,
392 caCertificateSet: ssl.caCertificateSet,
393 secretManagerStoredClientKey: ssl.secretManagerStoredClientKey,
394 };
395
396const publicMysql = (mysql: MysqlProfile | undefined) =>
397 mysql === undefined
398 ? undefined
399 : {
400 username: mysql.username,
401 port: mysql.port,
402 hostname: mysql.hostname,
403 secretManagerStoredPassword: mysql.secretManagerStoredPassword,
404 sslConfig: publicMysqlSsl(mysql.sslConfig),
405 };
406
407const publicPostgresql = (postgresql: PostgresqlProfile | undefined) =>
408 postgresql === undefined
409 ? undefined
410 : {
411 hostname: postgresql.hostname,
412 database: postgresql.database,
413 username: postgresql.username,
414 port: postgresql.port,
415 secretManagerStoredPassword: postgresql.secretManagerStoredPassword,
416 sslConfig: publicPostgresSsl(postgresql.sslConfig),
417 };
418
419const publicOracleAsm = (asm: OracleAsmConfig | undefined) =>
420 asm === undefined
421 ? undefined
422 : {
423 port: asm.port,
424 connectionAttributes: asm.connectionAttributes,
425 username: asm.username,
426 secretManagerStoredPassword: asm.secretManagerStoredPassword,
427 oracleSslConfig: publicOracleSsl(asm.oracleSslConfig),
428 hostname: asm.hostname,
429 asmService: asm.asmService,
430 };
431
432const publicOracle = (oracle: OracleProfile | undefined) =>
433 oracle === undefined
434 ? undefined
435 : {
436 connectionAttributes: oracle.connectionAttributes,
437 databaseService: oracle.databaseService,
438 hostname: oracle.hostname,
439 oracleSslConfig: publicOracleSsl(oracle.oracleSslConfig),
440 oracleAsmConfig: publicOracleAsm(oracle.oracleAsmConfig),
441 username: oracle.username,
442 secretManagerStoredPassword: oracle.secretManagerStoredPassword,
443 port: oracle.port,
444 };
445
446const publicSqlServer = (sqlserver: SqlServerProfile | undefined) =>
447 sqlserver === undefined
448 ? undefined
449 : {
450 hostname: sqlserver.hostname,
451 sslConfig: publicSqlServerSsl(sqlserver.sslConfig),
452 database: sqlserver.database,
453 username: sqlserver.username,
454 secretManagerStoredPassword: sqlserver.secretManagerStoredPassword,
455 port: sqlserver.port,
456 };
457
458const publicMongodb = (mongodb: MongodbProfile | undefined) =>
459 mongodb === undefined
460 ? undefined
461 : {
462 hostAddresses: mongodb.hostAddresses,
463 username: mongodb.username,
464 secretManagerStoredPassword: mongodb.secretManagerStoredPassword,
465 additionalOptions: mongodb.additionalOptions,
466 replicaSet: mongodb.replicaSet,
467 srvConnectionFormat: emptyMessage(mongodb.srvConnectionFormat),
468 standardConnectionFormat: mongodb.standardConnectionFormat,
469 sslConfig: publicMongodbSsl(mongodb.sslConfig),
470 };
471
472const publicSalesforce = (salesforce: SalesforceProfile | undefined) =>
473 salesforce === undefined
474 ? undefined
475 : {
476 domain: salesforce.domain,
477 userCredentials: salesforce.userCredentials
478 ? {
479 username: salesforce.userCredentials.username,
480 secretManagerStoredPassword:
481 salesforce.userCredentials.secretManagerStoredPassword,
482 secretManagerStoredSecurityToken:
483 salesforce.userCredentials.secretManagerStoredSecurityToken,
484 }
485 : undefined,
486 oauth2ClientCredentials: salesforce.oauth2ClientCredentials
487 ? {
488 clientId: salesforce.oauth2ClientCredentials.clientId,
489 secretManagerStoredClientSecret:
490 salesforce.oauth2ClientCredentials
491 .secretManagerStoredClientSecret,
492 }
493 : undefined,
494 };
495
496const publicSfmc = (profile: SalesforceMarketingCloudProfile | undefined) =>
497 profile === undefined
498 ? undefined
499 : {
500 subdomain: profile.subdomain,
501 oauthClientCredentials: profile.oauthClientCredentials
502 ? {
503 clientId: profile.oauthClientCredentials.clientId,
504 clientSecret: publicSecret(
505 profile.oauthClientCredentials.clientSecret,
506 ),
507 }
508 : undefined,
509 };
510
511const publicServiceNow = (profile: ServiceNowProfile | undefined) =>
512 profile === undefined
513 ? undefined
514 : {
515 instance: profile.instance,
516 userPasswordCredentials: profile.userPasswordCredentials
517 ? {
518 username: profile.userPasswordCredentials.username,
519 password: publicSecret(profile.userPasswordCredentials.password),
520 }
521 : undefined,
522 oauthClientCredentials: profile.oauthClientCredentials
523 ? {
524 clientId: profile.oauthClientCredentials.clientId,
525 clientSecret: publicSecret(
526 profile.oauthClientCredentials.clientSecret,
527 ),
528 }
529 : undefined,
530 };
531
532const publicDataverse = (profile: DataverseProfile | undefined) =>
533 profile === undefined
534 ? undefined
535 : {
536 tenantId: profile.tenantId,
537 environmentUrl: profile.environmentUrl,
538 oauthClientCredentials: profile.oauthClientCredentials
539 ? {
540 clientId: profile.oauthClientCredentials.clientId,
541 clientSecret: publicSecret(
542 profile.oauthClientCredentials.clientSecret,
543 ),
544 }
545 : undefined,
546 };
547
548const publicForwardSsh = (ssh: ForwardSshTunnelConnectivity | undefined) =>
549 ssh === undefined
550 ? undefined
551 : {
552 hostname: ssh.hostname,
553 username: ssh.username,
554 port: ssh.port,
555 };
556
557const publicPrivateConnectivity = (
558 value: PrivateConnectivity | ConnectionProfileProps["privateConnectivity"],
559 project: string,
560 location: string,
561) =>
562 value?.privateConnection === undefined
563 ? undefined
564 : {
565 privateConnection: privateConnectionOf(
566 value.privateConnection,
567 project,
568 location,
569 ),
570 };
571
572const secretChanged = (
573 newsHasSecret: boolean,
574 publicPrevious: unknown,
575 publicNext: unknown,
576) => newsHasSecret || fingerprint(publicPrevious) !== fingerprint(publicNext);
577
578const mysqlHasSecret = (mysql: MysqlProfile | undefined) =>
579 mysql?.password !== undefined ||
580 mysql?.sslConfig?.clientCertificate !== undefined ||
581 mysql?.sslConfig?.clientKey !== undefined ||
582 mysql?.sslConfig?.caCertificate !== undefined;
583
584const postgresqlHasSecret = (postgresql: PostgresqlProfile | undefined) =>
585 postgresql?.password !== undefined ||
586 postgresql?.sslConfig?.serverVerification?.caCertificate !== undefined ||
587 postgresql?.sslConfig?.serverAndClientVerification?.caCertificate !==
588 undefined ||
589 postgresql?.sslConfig?.serverAndClientVerification?.clientCertificate !==
590 undefined ||
591 postgresql?.sslConfig?.serverAndClientVerification?.clientKey !== undefined;
592
593const oracleHasSecret = (oracle: OracleProfile | undefined) =>
594 oracle?.password !== undefined ||
595 oracle?.oracleSslConfig?.caCertificate !== undefined ||
596 oracle?.oracleAsmConfig?.password !== undefined ||
597 oracle?.oracleAsmConfig?.oracleSslConfig?.caCertificate !== undefined;
598
599const sqlServerHasSecret = (sqlserver: SqlServerProfile | undefined) =>
600 sqlserver?.password !== undefined ||
601 sqlserver?.sslConfig?.encryptionAndServerValidation?.caCertificate !==
602 undefined;
603
604const mongodbHasSecret = (mongodb: MongodbProfile | undefined) =>
605 mongodb?.password !== undefined ||
606 mongodb?.sslConfig?.clientCertificate !== undefined ||
607 mongodb?.sslConfig?.clientKey !== undefined ||
608 mongodb?.sslConfig?.caCertificate !== undefined;
609
610const salesforceHasSecret = (salesforce: SalesforceProfile | undefined) =>
611 salesforce?.userCredentials?.password !== undefined ||
612 salesforce?.userCredentials?.securityToken !== undefined ||
613 salesforce?.oauth2ClientCredentials?.clientSecret !== undefined;
614
615const sfmcHasSecret = (profile: SalesforceMarketingCloudProfile | undefined) =>
616 profile?.oauthClientCredentials?.clientSecret?.rawValue !== undefined;
617
618const serviceNowHasSecret = (profile: ServiceNowProfile | undefined) =>
619 profile?.userPasswordCredentials?.password?.rawValue !== undefined ||
620 profile?.oauthClientCredentials?.clientSecret?.rawValue !== undefined;
621
622const dataverseHasSecret = (profile: DataverseProfile | undefined) =>
623 profile?.oauthClientCredentials?.clientSecret?.rawValue !== undefined;
624
625const sshHasSecret = (ssh: ForwardSshTunnelConnectivity | undefined) =>
626 ssh?.password !== undefined || ssh?.privateKey !== undefined;
627
628const toAttrs = (
629 profile: ds.ConnectionProfile,
630 project: string,
631 locationHint: string,
632) => {
633 const name = profile.name ?? "";
634 const parsed = parseName(name, "connectionProfiles", locationHint);
635 return {
636 name,
637 connectionProfileId: parsed.id,
638 project: parsed.project || project,
639 location: parsed.location || locationHint,
640 displayName: profile.displayName,
641 labels: userLabels(profile.labels),
642 mysqlProfile: publicMysql(profile.mysqlProfile),
643 postgresqlProfile: publicPostgresql(profile.postgresqlProfile),
644 oracleProfile: publicOracle(profile.oracleProfile),
645 sqlServerProfile: publicSqlServer(profile.sqlServerProfile),
646 mongodbProfile: publicMongodb(profile.mongodbProfile),
647 spannerProfile: profile.spannerProfile,
648 gcsProfile: profile.gcsProfile,
649 bigqueryProfile: emptyMessage(profile.bigqueryProfile),
650 salesforceProfile: publicSalesforce(profile.salesforceProfile),
651 salesforceMarketingCloudProfile: publicSfmc(
652 profile.salesforceMarketingCloudProfile,
653 ),
654 serviceNowProfile: publicServiceNow(profile.serviceNowProfile),
655 dataverseProfile: publicDataverse(profile.dataverseProfile),
656 staticServiceIpConnectivity: emptyMessage(
657 profile.staticServiceIpConnectivity,
658 ),
659 forwardSshConnectivity: publicForwardSsh(profile.forwardSshConnectivity),
660 privateConnectivity: profile.privateConnectivity,
661 satisfiesPzi: profile.satisfiesPzi,
662 satisfiesPzs: profile.satisfiesPzs,
663 createTime: profile.createTime,
664 updateTime: profile.updateTime,
665 };
666};
667
668const getByName = (name: string) =>
669 name.length === 0
670 ? Effect.succeed(undefined)
671 : ds
672 .getProjectsLocationsConnectionProfiles({ name })
673 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
674
675const listOwned = (project: string, region: string) =>
676 listAtLocation(project, region, (parent) =>
677 collectPages(
678 ds.listProjectsLocationsConnectionProfiles.pages({
679 parent,
680 pageSize: 1000,
681 }),
682 (page) => page.connectionProfiles,
683 ).pipe(
684 Effect.map((items) =>
685 items.filter((item) => hasAlchemyLabelMap(item.labels)),
686 ),
687 ),
688 );
689
690const connectivityOf = (
691 news: ConnectionProfileProps,
692 project: string,
693 location: string,
694) => ({
695 staticServiceIpConnectivity: emptyMessage(news.staticServiceIpConnectivity),
696 forwardSshConnectivity: news.forwardSshConnectivity,
697 privateConnectivity: publicPrivateConnectivity(
698 news.privateConnectivity,
699 project,
700 location,
701 ),
702});
703
704export const ConnectionProfileProvider = () =>
705 Provider.succeed(ConnectionProfile, {
706 stables: [
707 "name",
708 "connectionProfileId",
709 "project",
710 "location",
711 "createTime",
712 ],
713
714 diff: Effect.fn(function* ({ news, olds, output }) {
715 if (!isResolved(news)) return undefined;
716 const env = yield* GcpEnvironment.current;
717 const previousKind = kindOf({
718 mysqlProfile: olds?.mysqlProfile ?? output?.mysqlProfile,
719 postgresqlProfile: olds?.postgresqlProfile ?? output?.postgresqlProfile,
720 oracleProfile: olds?.oracleProfile ?? output?.oracleProfile,
721 sqlServerProfile: olds?.sqlServerProfile ?? output?.sqlServerProfile,
722 mongodbProfile: olds?.mongodbProfile ?? output?.mongodbProfile,
723 spannerProfile: olds?.spannerProfile ?? output?.spannerProfile,
724 gcsProfile: olds?.gcsProfile ?? output?.gcsProfile,
725 bigqueryProfile: olds?.bigqueryProfile ?? output?.bigqueryProfile,
726 salesforceProfile: olds?.salesforceProfile ?? output?.salesforceProfile,
727 salesforceMarketingCloudProfile:
728 olds?.salesforceMarketingCloudProfile ??
729 output?.salesforceMarketingCloudProfile,
730 serviceNowProfile: olds?.serviceNowProfile ?? output?.serviceNowProfile,
731 dataverseProfile: olds?.dataverseProfile ?? output?.dataverseProfile,
732 });
733 const nextKind = kindOf(news) || previousKind;
734 const previousSpanner =
735 olds?.spannerProfile?.database ?? output?.spannerProfile?.database;
736 const nextSpanner = news.spannerProfile?.database ?? previousSpanner;
737 return replaceOnIdentity({
738 previousId: olds?.connectionProfileId ?? output?.connectionProfileId,
739 nextId:
740 news.connectionProfileId ??
741 olds?.connectionProfileId ??
742 output?.connectionProfileId,
743 previousLocation: normalizeLocation(
744 olds?.location ?? output?.location,
745 env.region,
746 ),
747 nextLocation: normalizeLocation(
748 news.location ?? olds?.location ?? output?.location,
749 env.region,
750 ),
751 extra:
752 previousKind !== nextKind ||
753 (nextKind === "spanner" &&
754 previousSpanner !== undefined &&
755 nextSpanner !== undefined &&
756 previousSpanner !== nextSpanner),
757 });
758 }),
759
760 read: Effect.fn(function* ({ id, olds, output }) {
761 const env = yield* GcpEnvironment.current;
762 const connectionProfileId = yield* toPhysicalId(
763 id,
764 olds?.connectionProfileId,
765 output?.connectionProfileId,
766 "profile",
767 );
768 const location = normalizeLocation(
769 olds?.location ?? output?.location,
770 env.region,
771 );
772 const name =
773 output?.name ??
774 resourceName(env.project, location, connectionProfileId);
775 const existing = yield* getByName(name);
776 if (existing === undefined) return undefined;
777 const attrs = toAttrs(existing, env.project, location);
778 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
779 ? attrs
780 : Unowned(attrs);
781 }),
782
783 list: () =>
784 Effect.gen(function* () {
785 const env = yield* GcpEnvironment.current;
786 const items = yield* listOwned(env.project, env.region);
787 return items.map((item) => toAttrs(item, env.project, env.region));
788 }),
789
790 reconcile: Effect.fn(function* ({ id, news, output }) {
791 const env = yield* GcpEnvironment.current;
792 const connectionProfileId = yield* toPhysicalId(
793 id,
794 news.connectionProfileId,
795 output?.connectionProfileId,
796 "profile",
797 );
798 const location = normalizeLocation(
799 news.location ?? output?.location,
800 env.region,
801 );
802 const name = resourceName(env.project, location, connectionProfileId);
803 const desiredLabels = {
804 ...toLabels(news.labels),
805 ...(yield* createInternalLabels(id)),
806 };
807 const displayName = news.displayName ?? connectionProfileId;
808 const connectivity = connectivityOf(news, env.project, location);
809 const body: ds.ConnectionProfile = {
810 displayName,
811 labels: desiredLabels,
812 mysqlProfile: news.mysqlProfile,
813 postgresqlProfile: news.postgresqlProfile,
814 oracleProfile: news.oracleProfile,
815 sqlServerProfile: news.sqlServerProfile,
816 mongodbProfile: news.mongodbProfile,
817 spannerProfile: news.spannerProfile,
818 gcsProfile: news.gcsProfile,
819 bigqueryProfile: emptyMessage(news.bigqueryProfile),
820 salesforceProfile: news.salesforceProfile,
821 salesforceMarketingCloudProfile: news.salesforceMarketingCloudProfile,
822 serviceNowProfile: news.serviceNowProfile,
823 dataverseProfile: news.dataverseProfile,
824 ...connectivity,
825 };
826
827 let current = yield* getByName(output?.name ?? name);
828
829 if (current === undefined) {
830 yield* ds
831 .createProjectsLocationsConnectionProfiles({
832 parent: locationParent(env.project, location),
833 connectionProfileId,
834 force: news.force,
835 validateOnly: news.validateOnly,
836 body,
837 })
838 .pipe(
839 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
840 Effect.flatMap((created) => settleOperation(created)),
841 // Datastream intermittently fails the create operation with
842 // INTERNAL ("an internal error has occurred") under load.
843 Effect.retry({
844 while: (error) =>
845 error._tag === "GCP.OperationFailed" && error.code === 13,
846 times: 4,
847 schedule: Schedule.exponential("5 seconds"),
848 }),
849 );
850 current = yield* waitUntilExists(getByName(name), name);
851 }
852
853 if (current === undefined) {
854 return yield* new ResourceNotResolved({ name });
855 }
856
857 const observedLabels = tagRecord(current.labels);
858 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
859 const labelsChanged = upsert.length > 0 || removed.length > 0;
860 const displayNameChanged = (current.displayName ?? "") !== displayName;
861 const mysqlChanged =
862 news.mysqlProfile !== undefined &&
863 secretChanged(
864 mysqlHasSecret(news.mysqlProfile),
865 publicMysql(current.mysqlProfile),
866 publicMysql(news.mysqlProfile),
867 );
868 const postgresqlChanged =
869 news.postgresqlProfile !== undefined &&
870 secretChanged(
871 postgresqlHasSecret(news.postgresqlProfile),
872 publicPostgresql(current.postgresqlProfile),
873 publicPostgresql(news.postgresqlProfile),
874 );
875 const oracleChanged =
876 news.oracleProfile !== undefined &&
877 secretChanged(
878 oracleHasSecret(news.oracleProfile),
879 publicOracle(current.oracleProfile),
880 publicOracle(news.oracleProfile),
881 );
882 const sqlServerChanged =
883 news.sqlServerProfile !== undefined &&
884 secretChanged(
885 sqlServerHasSecret(news.sqlServerProfile),
886 publicSqlServer(current.sqlServerProfile),
887 publicSqlServer(news.sqlServerProfile),
888 );
889 const mongodbChanged =
890 news.mongodbProfile !== undefined &&
891 secretChanged(
892 mongodbHasSecret(news.mongodbProfile),
893 publicMongodb(current.mongodbProfile),
894 publicMongodb(news.mongodbProfile),
895 );
896 const gcsChanged =
897 news.gcsProfile !== undefined &&
898 fingerprint(current.gcsProfile) !== fingerprint(news.gcsProfile);
899 const salesforceChanged =
900 news.salesforceProfile !== undefined &&
901 secretChanged(
902 salesforceHasSecret(news.salesforceProfile),
903 publicSalesforce(current.salesforceProfile),
904 publicSalesforce(news.salesforceProfile),
905 );
906 const sfmcChanged =
907 news.salesforceMarketingCloudProfile !== undefined &&
908 secretChanged(
909 sfmcHasSecret(news.salesforceMarketingCloudProfile),
910 publicSfmc(current.salesforceMarketingCloudProfile),
911 publicSfmc(news.salesforceMarketingCloudProfile),
912 );
913 const serviceNowChanged =
914 news.serviceNowProfile !== undefined &&
915 secretChanged(
916 serviceNowHasSecret(news.serviceNowProfile),
917 publicServiceNow(current.serviceNowProfile),
918 publicServiceNow(news.serviceNowProfile),
919 );
920 const dataverseChanged =
921 news.dataverseProfile !== undefined &&
922 secretChanged(
923 dataverseHasSecret(news.dataverseProfile),
924 publicDataverse(current.dataverseProfile),
925 publicDataverse(news.dataverseProfile),
926 );
927 const desiredPrivate = publicPrivateConnectivity(
928 news.privateConnectivity,
929 env.project,
930 location,
931 );
932 const staticChanged =
933 news.staticServiceIpConnectivity !== undefined &&
934 fingerprint(emptyMessage(current.staticServiceIpConnectivity)) !==
935 fingerprint(emptyMessage(news.staticServiceIpConnectivity));
936 const sshChanged =
937 news.forwardSshConnectivity !== undefined &&
938 secretChanged(
939 sshHasSecret(news.forwardSshConnectivity),
940 publicForwardSsh(current.forwardSshConnectivity),
941 publicForwardSsh(news.forwardSshConnectivity),
942 );
943 const privateChanged =
944 news.privateConnectivity !== undefined &&
945 fingerprint(current.privateConnectivity) !==
946 fingerprint(desiredPrivate);
947 const mask = fieldMask([
948 labelsChanged && "labels",
949 displayNameChanged && "displayName",
950 mysqlChanged && "mysqlProfile",
951 postgresqlChanged && "postgresqlProfile",
952 oracleChanged && "oracleProfile",
953 sqlServerChanged && "sqlServerProfile",
954 mongodbChanged && "mongodbProfile",
955 gcsChanged && "gcsProfile",
956 salesforceChanged && "salesforceProfile",
957 sfmcChanged && "salesforceMarketingCloudProfile",
958 serviceNowChanged && "serviceNowProfile",
959 dataverseChanged && "dataverseProfile",
960 staticChanged && "staticServiceIpConnectivity",
961 sshChanged && "forwardSshConnectivity",
962 privateChanged && "privateConnectivity",
963 ]);
964
965 if (mask.length > 0) {
966 const patch: ds.ConnectionProfile = { name: current.name ?? name };
967 if (labelsChanged) patch.labels = desiredLabels;
968 if (displayNameChanged) patch.displayName = displayName;
969 if (mysqlChanged) patch.mysqlProfile = news.mysqlProfile;
970 if (postgresqlChanged) patch.postgresqlProfile = news.postgresqlProfile;
971 if (oracleChanged) patch.oracleProfile = news.oracleProfile;
972 if (sqlServerChanged) patch.sqlServerProfile = news.sqlServerProfile;
973 if (mongodbChanged) patch.mongodbProfile = news.mongodbProfile;
974 if (gcsChanged) patch.gcsProfile = news.gcsProfile;
975 if (salesforceChanged) patch.salesforceProfile = news.salesforceProfile;
976 if (sfmcChanged) {
977 patch.salesforceMarketingCloudProfile =
978 news.salesforceMarketingCloudProfile;
979 }
980 if (serviceNowChanged) patch.serviceNowProfile = news.serviceNowProfile;
981 if (dataverseChanged) patch.dataverseProfile = news.dataverseProfile;
982 if (staticChanged) {
983 patch.staticServiceIpConnectivity =
984 connectivity.staticServiceIpConnectivity;
985 }
986 if (sshChanged) {
987 patch.forwardSshConnectivity = connectivity.forwardSshConnectivity;
988 }
989 if (privateChanged) {
990 patch.privateConnectivity = connectivity.privateConnectivity;
991 }
992 const operation = yield* ds.patchProjectsLocationsConnectionProfiles({
993 name: current.name ?? name,
994 updateMask: mask,
995 force: news.force,
996 validateOnly: news.validateOnly,
997 body: patch,
998 });
999 yield* settleOperation(operation);
1000 current = yield* waitUntilExists(
1001 getByName(current.name ?? name),
1002 current.name ?? name,
1003 );
1004 }
1005
1006 return toAttrs(current, env.project, location);
1007 }),
1008
1009 delete: Effect.fn(function* ({ output }) {
1010 const operation = yield* ds
1011 .deleteProjectsLocationsConnectionProfiles({
1012 name: output.name,
1013 })
1014 .pipe(
1015 Effect.retry({
1016 while: (error) => error._tag === "Conflict",
1017 times: 8,
1018 schedule: Schedule.spaced("2 seconds"),
1019 }),
1020 Effect.catchTag(["NotFound", "Conflict"], () =>
1021 Effect.succeed(undefined),
1022 ),
1023 );
1024 yield* settleOperation(operation, { notFoundOk: true });
1025 yield* waitUntilGone(getByName(output.name), output.name, {
1026 times: 15,
1027 interval: "2 seconds",
1028 }).pipe(
1029 Effect.catchTag(
1030 "GCP.Datastream.ResourceStillExists",
1031 () => Effect.void,
1032 ),
1033 );
1034 }),
1035 });