1import * as developerconnect from "@distilled.cloud/gcp/developerconnect_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { tagRecord } from "../../Tags.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import {
10 createInternalLabels,
11 diffLabels,
12 hasAlchemyLabels,
13 toLabels,
14} from "../Labels.ts";
15import type { Providers } from "../Providers.ts";
16import {
17 compact,
18 fieldMask,
19 fingerprint,
20 listAtLocation,
21 listLabeledPages,
22 normalizeLocation,
23 parentOf,
24 parseName,
25 replaceOnIdentity,
26 ResourceNotResolved,
27 sameBool,
28 toPhysicalId,
29 userAnnotations,
30 userLabels,
31 waitForOperation,
32 waitUntilExists,
33 waitUntilGone,
34} from "./internal.ts";
35
36export type ServiceDirectoryConfig = {
37 /**
38 * Service Directory service used to reach a private host, as
39 * `projects/{project}/locations/{location}/namespaces/{namespace}/services/{service}`.
40 */
41 service?: string;
42};
43
44export type UserCredential = {
45 /**
46 * Secret Manager resource containing the user token, as
47 * `projects/{project}/secrets/{secret}/versions/{version}`.
48 */
49 userTokenSecretVersion?: string;
50};
51
52export type OAuthCredential = {
53 /**
54 * Secret Manager resource containing the OAuth token that authorizes
55 * the connection.
56 */
57 oauthTokenSecretVersion?: string;
58};
59
60export type GitHubConfig = {
61 /**
62 * GitHub App that was installed (`DEVELOPER_CONNECT`, `FIREBASE`,
63 * `GEMINI_CODE_ASSIST`, `DATAFORM`). Immutable — changing it
64 * replaces the connection.
65 * @default "DEVELOPER_CONNECT"
66 */
67 githubApp?: developerconnect.GitHubConfigGithubAppEnum | (string & {});
68 /**
69 * OAuth credential of the account that authorized the GitHub App.
70 */
71 authorizerCredential?: OAuthCredential;
72 /**
73 * GitHub App installation id.
74 */
75 appInstallationId?: string;
76};
77
78export type GitHubEnterpriseConfig = {
79 /**
80 * URI of the GitHub Enterprise host. Immutable — changing it
81 * replaces the connection.
82 */
83 hostUri?: string;
84 /**
85 * Installation id of the GitHub App.
86 */
87 appInstallationId?: string;
88 /**
89 * SSL certificate used for requests to GitHub Enterprise.
90 */
91 sslCaCertificate?: string;
92 /**
93 * GitHub Enterprise organization in which the GitHub App is created.
94 * Immutable.
95 */
96 organization?: string;
97 /**
98 * Secret Manager resource containing the GitHub App private key.
99 */
100 privateKeySecretVersion?: string;
101 /**
102 * Id of the GitHub App created from the manifest.
103 */
104 appId?: string;
105 /**
106 * Secret Manager resource containing the webhook secret.
107 */
108 webhookSecretSecretVersion?: string;
109 /**
110 * Service Directory config for a private GitHub Enterprise host.
111 */
112 serviceDirectoryConfig?: ServiceDirectoryConfig;
113};
114
115export type GitLabConfig = {
116 /**
117 * GitLab personal access token with `api` scope.
118 */
119 authorizerCredential?: UserCredential;
120 /**
121 * Secret Manager resource containing the webhook secret. Immutable —
122 * changing it replaces the connection.
123 */
124 webhookSecretSecretVersion?: string;
125 /**
126 * GitLab personal access token with `read_api` scope.
127 */
128 readAuthorizerCredential?: UserCredential;
129};
130
131export type GitLabEnterpriseConfig = {
132 /**
133 * URI of the GitLab Enterprise host. Immutable — changing it
134 * replaces the connection.
135 */
136 hostUri?: string;
137 /**
138 * Secret Manager resource containing the webhook secret. Immutable —
139 * changing it replaces the connection.
140 */
141 webhookSecretSecretVersion?: string;
142 /**
143 * GitLab personal access token with `read_api` scope.
144 */
145 readAuthorizerCredential?: UserCredential;
146 /**
147 * GitLab personal access token with `api` scope.
148 */
149 authorizerCredential?: UserCredential;
150 /**
151 * Service Directory config for a private GitLab Enterprise host.
152 */
153 serviceDirectoryConfig?: ServiceDirectoryConfig;
154 /**
155 * SSL certificate used for requests to GitLab Enterprise.
156 */
157 sslCaCertificate?: string;
158};
159
160export type BitbucketCloudConfig = {
161 /**
162 * Bitbucket Cloud workspace id. Immutable — changing it replaces the
163 * connection.
164 */
165 workspace?: string;
166 /**
167 * Secret Manager resource containing the webhook secret. Immutable —
168 * changing it replaces the connection.
169 */
170 webhookSecretSecretVersion?: string;
171 /**
172 * Access token with `repository` access.
173 */
174 readAuthorizerCredential?: UserCredential;
175 /**
176 * Access token with `webhook`, `repository`, and `pullrequest` scope.
177 */
178 authorizerCredential?: UserCredential;
179};
180
181export type BitbucketDataCenterConfig = {
182 /**
183 * URI of the Bitbucket Data Center host. Immutable — changing it
184 * replaces the connection.
185 */
186 hostUri?: string;
187 /**
188 * Secret Manager resource containing the webhook secret. Immutable —
189 * changing it replaces the connection.
190 */
191 webhookSecretSecretVersion?: string;
192 /**
193 * HTTP access token with `Repository read` access.
194 */
195 readAuthorizerCredential?: UserCredential;
196 /**
197 * HTTP access token with `Repository admin` access.
198 */
199 authorizerCredential?: UserCredential;
200 /**
201 * Service Directory config for a private Bitbucket Data Center host.
202 */
203 serviceDirectoryConfig?: ServiceDirectoryConfig;
204 /**
205 * SSL certificate used for requests to Bitbucket Data Center.
206 */
207 sslCaCertificate?: string;
208};
209
210export type SecureSourceManagerInstanceConfig = {
211 /**
212 * Secure Source Manager instance, as
213 * `projects/{project}/locations/{location}/instances/{instance}`.
214 * Immutable — changing it replaces the connection.
215 */
216 instance?: string;
217};
218
219export type GitProxyConfig = {
220 /**
221 * Enable the git proxy for repositories linked on this connection.
222 * @default false
223 */
224 enabled?: boolean;
225};
226
227export type CryptoKeyConfig = {
228 /**
229 * Cloud KMS key used to encrypt customer data, as
230 * `projects/{project}/locations/{location}/keyRings/{ring}/cryptoKeys/{key}`.
231 */
232 keyReference?: string;
233};
234
235export type InstallationState = {
236 /** Current step of the installation process. */
237 stage?: string;
238 /** Message describing the next user action, if any. */
239 message?: string;
240 /** Link for the next user action, if any. */
241 actionUri?: string;
242};
243
244export type ConnectionProps = {
245 /**
246 * Connection id (the `{connection}` segment of
247 * `projects/{project}/locations/{location}/connections/{connection}`).
248 * If omitted, a unique RFC1035 name is generated. Immutable —
249 * changing it replaces the connection.
250 */
251 connectionId?: string;
252 /**
253 * Region (`us-central1`, `us-east1`, …). Immutable — changing it
254 * replaces the connection. `US-CENTRAL1` is accepted and normalized
255 * to `us-central1`.
256 * @default the stack's GCP region (`GCP.Region`, else the profile
257 * region, else `us-central1`)
258 */
259 location?: string;
260 /**
261 * When true, repository APIs and webhook processing for this
262 * connection are disabled.
263 * @default false
264 */
265 disabled?: boolean;
266 /**
267 * User labels. Alchemy ownership labels are merged in automatically.
268 */
269 labels?: Record<string, string>;
270 /**
271 * User annotations (AIP-148).
272 */
273 annotations?: Record<string, string>;
274 /**
275 * Configuration for github.com. If no SCM config is set, Alchemy
276 * defaults to `githubApp: "DEVELOPER_CONNECT"` on create.
277 */
278 githubConfig?: GitHubConfig;
279 /**
280 * Configuration for a GitHub Enterprise host. Mutually exclusive
281 * with the other SCM configs.
282 */
283 githubEnterpriseConfig?: GitHubEnterpriseConfig;
284 /**
285 * Configuration for gitlab.com. Mutually exclusive with the other
286 * SCM configs.
287 */
288 gitlabConfig?: GitLabConfig;
289 /**
290 * Configuration for GitLab Enterprise. Mutually exclusive with the
291 * other SCM configs.
292 */
293 gitlabEnterpriseConfig?: GitLabEnterpriseConfig;
294 /**
295 * Configuration for Bitbucket Cloud. Mutually exclusive with the
296 * other SCM configs.
297 */
298 bitbucketCloudConfig?: BitbucketCloudConfig;
299 /**
300 * Configuration for Bitbucket Data Center. Mutually exclusive with
301 * the other SCM configs.
302 */
303 bitbucketDataCenterConfig?: BitbucketDataCenterConfig;
304 /**
305 * Configuration for a Secure Source Manager instance. Mutually
306 * exclusive with the other SCM configs.
307 */
308 secureSourceManagerInstanceConfig?: SecureSourceManagerInstanceConfig;
309 /**
310 * Git proxy configuration.
311 */
312 gitProxyConfig?: GitProxyConfig;
313 /**
314 * Customer-managed encryption.
315 */
316 cryptoKeyConfig?: CryptoKeyConfig;
317};
318
319export type Connection = Resource<
320 "GCP.DeveloperConnect.Connection",
321 ConnectionProps,
322 {
323 /** Full resource name. */
324 name: string;
325 /** Connection id (last path segment). */
326 connectionId: string;
327 /** Project id. */
328 project: string;
329 /** Location id. */
330 location: string;
331 /** Whether repository APIs and webhooks are disabled. */
332 disabled: boolean;
333 /** User labels (Alchemy ownership labels stripped). */
334 labels: Record<string, string>;
335 /** User annotations. */
336 annotations: Record<string, string>;
337 /** github.com configuration, if this is a GitHub connection. */
338 githubConfig: GitHubConfig | undefined;
339 /** GitHub Enterprise configuration, if any. */
340 githubEnterpriseConfig: GitHubEnterpriseConfig | undefined;
341 /** GitLab configuration, if any. */
342 gitlabConfig: GitLabConfig | undefined;
343 /** GitLab Enterprise configuration, if any. */
344 gitlabEnterpriseConfig: GitLabEnterpriseConfig | undefined;
345 /** Bitbucket Cloud configuration, if any. */
346 bitbucketCloudConfig: BitbucketCloudConfig | undefined;
347 /** Bitbucket Data Center configuration, if any. */
348 bitbucketDataCenterConfig: BitbucketDataCenterConfig | undefined;
349 /** Secure Source Manager configuration, if any. */
350 secureSourceManagerInstanceConfig:
351 | SecureSourceManagerInstanceConfig
352 | undefined;
353 /** Git proxy configuration. */
354 gitProxyConfig: GitProxyConfig | undefined;
355 /** Customer-managed encryption, if any. */
356 cryptoKeyConfig: CryptoKeyConfig | undefined;
357 /** Installation progress for GitHub-based connections. */
358 installationState: InstallationState | undefined;
359 /** True while Developer Connect is applying the connection. */
360 reconciling: boolean;
361 /** Server checksum of the resource. */
362 etag: string | undefined;
363 /** Server-generated resource uid. */
364 uid: string | undefined;
365 /** RFC3339 creation timestamp. */
366 createTime: string | undefined;
367 /** RFC3339 last-update timestamp. */
368 updateTime: string | undefined;
369 },
370 never,
371 Providers
372>;
373
374/**
375 * A Developer Connect connection to a source-control host (GitHub,
376 * GitHub Enterprise, GitLab, Bitbucket, or Secure Source Manager).
377 *
378 * Changing `connectionId`, `location`, SCM kind, `githubApp`, or
379 * immutable host fields (`hostUri`, `workspace`,
380 * `webhookSecretSecretVersion`, SSM `instance`) replaces the
381 * connection. `disabled`, labels, annotations, credentials, and
382 * `gitProxyConfig` update in place.
383 *
384 * ### Creating a Connection
385 * **Example:** Generated GitHub connection
386 * ```typescript
387 * const github = yield* GCP.DeveloperConnect.Connection("Github", {
388 * githubConfig: { githubApp: "DEVELOPER_CONNECT" },
389 * });
390 * ```
391 *
392 * **Example:** Named GitHub connection with labels
393 * ```typescript
394 * const github = yield* GCP.DeveloperConnect.Connection("Github", {
395 * connectionId: "app-github",
396 * location: "us-central1",
397 * labels: { env: "prod" },
398 * githubConfig: { githubApp: "DEVELOPER_CONNECT" },
399 * });
400 * ```
401 *
402 * ### Disabling a Connection
403 * **Example:** Disable webhook processing
404 * ```typescript
405 * const github = yield* GCP.DeveloperConnect.Connection("Github", {
406 * connectionId: "app-github",
407 * githubConfig: { githubApp: "DEVELOPER_CONNECT" },
408 * disabled: true,
409 * });
410 * ```
411 *
412 * @resource
413 * @category DeveloperConnect
414 */
415export const Connection = Resource<Connection>(
416 "GCP.DeveloperConnect.Connection",
417);
418
419type ScmKind =
420 | "github"
421 | "githubEnterprise"
422 | "gitlab"
423 | "gitlabEnterprise"
424 | "bitbucketCloud"
425 | "bitbucketDataCenter"
426 | "secureSourceManager";
427
428const resourceName = (
429 project: string,
430 location: string,
431 connectionId: string,
432) => `projects/${project}/locations/${location}/connections/${connectionId}`;
433
434const toUserCredential = (
435 credential: UserCredential | undefined,
436): developerconnect.UserCredential | undefined => {
437 if (credential === undefined) return undefined;
438 return compact({ userTokenSecretVersion: credential.userTokenSecretVersion });
439};
440
441const toOAuthCredential = (
442 credential: OAuthCredential | undefined,
443): developerconnect.OAuthCredential | undefined => {
444 if (credential === undefined) return undefined;
445 return compact({
446 oauthTokenSecretVersion: credential.oauthTokenSecretVersion,
447 });
448};
449
450const toServiceDirectory = (
451 config: ServiceDirectoryConfig | undefined,
452): developerconnect.ServiceDirectoryConfig | undefined => {
453 if (config === undefined) return undefined;
454 return compact({ service: config.service });
455};
456
457const toGithub = (
458 config: GitHubConfig | undefined,
459): developerconnect.GitHubConfig | undefined => {
460 if (config === undefined) return undefined;
461 return compact({
462 githubApp: config.githubApp ?? "DEVELOPER_CONNECT",
463 authorizerCredential: toOAuthCredential(config.authorizerCredential),
464 appInstallationId: config.appInstallationId,
465 });
466};
467
468const toGithubEnterprise = (
469 config: GitHubEnterpriseConfig | undefined,
470): developerconnect.GitHubEnterpriseConfig | undefined => {
471 if (config === undefined) return undefined;
472 return compact({
473 hostUri: config.hostUri,
474 appInstallationId: config.appInstallationId,
475 sslCaCertificate: config.sslCaCertificate,
476 organization: config.organization,
477 privateKeySecretVersion: config.privateKeySecretVersion,
478 appId: config.appId,
479 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
480 serviceDirectoryConfig: toServiceDirectory(config.serviceDirectoryConfig),
481 });
482};
483
484const toGitlab = (
485 config: GitLabConfig | undefined,
486): developerconnect.GitLabConfig | undefined => {
487 if (config === undefined) return undefined;
488 return compact({
489 authorizerCredential: toUserCredential(config.authorizerCredential),
490 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
491 readAuthorizerCredential: toUserCredential(config.readAuthorizerCredential),
492 });
493};
494
495const toGitlabEnterprise = (
496 config: GitLabEnterpriseConfig | undefined,
497): developerconnect.GitLabEnterpriseConfig | undefined => {
498 if (config === undefined) return undefined;
499 return compact({
500 hostUri: config.hostUri,
501 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
502 readAuthorizerCredential: toUserCredential(config.readAuthorizerCredential),
503 authorizerCredential: toUserCredential(config.authorizerCredential),
504 serviceDirectoryConfig: toServiceDirectory(config.serviceDirectoryConfig),
505 sslCaCertificate: config.sslCaCertificate,
506 });
507};
508
509const toBitbucketCloud = (
510 config: BitbucketCloudConfig | undefined,
511): developerconnect.BitbucketCloudConfig | undefined => {
512 if (config === undefined) return undefined;
513 return compact({
514 workspace: config.workspace,
515 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
516 readAuthorizerCredential: toUserCredential(config.readAuthorizerCredential),
517 authorizerCredential: toUserCredential(config.authorizerCredential),
518 });
519};
520
521const toBitbucketDataCenter = (
522 config: BitbucketDataCenterConfig | undefined,
523): developerconnect.BitbucketDataCenterConfig | undefined => {
524 if (config === undefined) return undefined;
525 return compact({
526 hostUri: config.hostUri,
527 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
528 readAuthorizerCredential: toUserCredential(config.readAuthorizerCredential),
529 authorizerCredential: toUserCredential(config.authorizerCredential),
530 serviceDirectoryConfig: toServiceDirectory(config.serviceDirectoryConfig),
531 sslCaCertificate: config.sslCaCertificate,
532 });
533};
534
535const toSsm = (
536 config: SecureSourceManagerInstanceConfig | undefined,
537): developerconnect.SecureSourceManagerInstanceConfig | undefined => {
538 if (config === undefined) return undefined;
539 return compact({ instance: config.instance });
540};
541
542const fromGithub = (
543 config: developerconnect.GitHubConfig | undefined,
544): GitHubConfig | undefined => {
545 if (config === undefined) return undefined;
546 return compact({
547 githubApp: config.githubApp,
548 authorizerCredential: config.authorizerCredential
549 ? compact({
550 oauthTokenSecretVersion:
551 config.authorizerCredential.oauthTokenSecretVersion,
552 })
553 : undefined,
554 appInstallationId: config.appInstallationId,
555 });
556};
557
558const fromGithubEnterprise = (
559 config: developerconnect.GitHubEnterpriseConfig | undefined,
560): GitHubEnterpriseConfig | undefined => {
561 if (config === undefined) return undefined;
562 return compact({
563 hostUri: config.hostUri,
564 appInstallationId: config.appInstallationId,
565 sslCaCertificate: config.sslCaCertificate,
566 organization: config.organization,
567 privateKeySecretVersion: config.privateKeySecretVersion,
568 appId: config.appId,
569 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
570 serviceDirectoryConfig: config.serviceDirectoryConfig
571 ? compact({ service: config.serviceDirectoryConfig.service })
572 : undefined,
573 });
574};
575
576const fromGitlab = (
577 config: developerconnect.GitLabConfig | undefined,
578): GitLabConfig | undefined => {
579 if (config === undefined) return undefined;
580 return compact({
581 authorizerCredential: config.authorizerCredential
582 ? compact({
583 userTokenSecretVersion:
584 config.authorizerCredential.userTokenSecretVersion,
585 })
586 : undefined,
587 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
588 readAuthorizerCredential: config.readAuthorizerCredential
589 ? compact({
590 userTokenSecretVersion:
591 config.readAuthorizerCredential.userTokenSecretVersion,
592 })
593 : undefined,
594 });
595};
596
597const fromGitlabEnterprise = (
598 config: developerconnect.GitLabEnterpriseConfig | undefined,
599): GitLabEnterpriseConfig | undefined => {
600 if (config === undefined) return undefined;
601 return compact({
602 hostUri: config.hostUri,
603 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
604 readAuthorizerCredential: config.readAuthorizerCredential
605 ? compact({
606 userTokenSecretVersion:
607 config.readAuthorizerCredential.userTokenSecretVersion,
608 })
609 : undefined,
610 authorizerCredential: config.authorizerCredential
611 ? compact({
612 userTokenSecretVersion:
613 config.authorizerCredential.userTokenSecretVersion,
614 })
615 : undefined,
616 serviceDirectoryConfig: config.serviceDirectoryConfig
617 ? compact({ service: config.serviceDirectoryConfig.service })
618 : undefined,
619 sslCaCertificate: config.sslCaCertificate,
620 });
621};
622
623const fromBitbucketCloud = (
624 config: developerconnect.BitbucketCloudConfig | undefined,
625): BitbucketCloudConfig | undefined => {
626 if (config === undefined) return undefined;
627 return compact({
628 workspace: config.workspace,
629 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
630 readAuthorizerCredential: config.readAuthorizerCredential
631 ? compact({
632 userTokenSecretVersion:
633 config.readAuthorizerCredential.userTokenSecretVersion,
634 })
635 : undefined,
636 authorizerCredential: config.authorizerCredential
637 ? compact({
638 userTokenSecretVersion:
639 config.authorizerCredential.userTokenSecretVersion,
640 })
641 : undefined,
642 });
643};
644
645const fromBitbucketDataCenter = (
646 config: developerconnect.BitbucketDataCenterConfig | undefined,
647): BitbucketDataCenterConfig | undefined => {
648 if (config === undefined) return undefined;
649 return compact({
650 hostUri: config.hostUri,
651 webhookSecretSecretVersion: config.webhookSecretSecretVersion,
652 readAuthorizerCredential: config.readAuthorizerCredential
653 ? compact({
654 userTokenSecretVersion:
655 config.readAuthorizerCredential.userTokenSecretVersion,
656 })
657 : undefined,
658 authorizerCredential: config.authorizerCredential
659 ? compact({
660 userTokenSecretVersion:
661 config.authorizerCredential.userTokenSecretVersion,
662 })
663 : undefined,
664 serviceDirectoryConfig: config.serviceDirectoryConfig
665 ? compact({ service: config.serviceDirectoryConfig.service })
666 : undefined,
667 sslCaCertificate: config.sslCaCertificate,
668 });
669};
670
671const fromSsm = (
672 config: developerconnect.SecureSourceManagerInstanceConfig | undefined,
673): SecureSourceManagerInstanceConfig | undefined => {
674 if (config === undefined) return undefined;
675 return compact({ instance: config.instance });
676};
677
678const scmKindOf = (value: {
679 githubConfig?: unknown;
680 githubEnterpriseConfig?: unknown;
681 gitlabConfig?: unknown;
682 gitlabEnterpriseConfig?: unknown;
683 bitbucketCloudConfig?: unknown;
684 bitbucketDataCenterConfig?: unknown;
685 secureSourceManagerInstanceConfig?: unknown;
686}): ScmKind | undefined => {
687 if (value.githubEnterpriseConfig !== undefined) return "githubEnterprise";
688 if (value.gitlabEnterpriseConfig !== undefined) return "gitlabEnterprise";
689 if (value.gitlabConfig !== undefined) return "gitlab";
690 if (value.bitbucketDataCenterConfig !== undefined)
691 return "bitbucketDataCenter";
692 if (value.bitbucketCloudConfig !== undefined) return "bitbucketCloud";
693 if (value.secureSourceManagerInstanceConfig !== undefined)
694 return "secureSourceManager";
695 if (value.githubConfig !== undefined) return "github";
696 return undefined;
697};
698
699const desiredScmKind = (news: ConnectionProps): ScmKind | undefined =>
700 scmKindOf(news);
701
702const toScmBody = (
703 news: ConnectionProps,
704): Partial<developerconnect.Connection> => {
705 const kind = desiredScmKind(news) ?? "github";
706 switch (kind) {
707 case "githubEnterprise":
708 return {
709 githubEnterpriseConfig: toGithubEnterprise(news.githubEnterpriseConfig),
710 };
711 case "gitlab":
712 return { gitlabConfig: toGitlab(news.gitlabConfig) };
713 case "gitlabEnterprise":
714 return {
715 gitlabEnterpriseConfig: toGitlabEnterprise(news.gitlabEnterpriseConfig),
716 };
717 case "bitbucketCloud":
718 return {
719 bitbucketCloudConfig: toBitbucketCloud(news.bitbucketCloudConfig),
720 };
721 case "bitbucketDataCenter":
722 return {
723 bitbucketDataCenterConfig: toBitbucketDataCenter(
724 news.bitbucketDataCenterConfig,
725 ),
726 };
727 case "secureSourceManager":
728 return {
729 secureSourceManagerInstanceConfig: toSsm(
730 news.secureSourceManagerInstanceConfig,
731 ),
732 };
733 default:
734 return { githubConfig: toGithub(news.githubConfig ?? {}) };
735 }
736};
737
738const scmField = (kind: ScmKind | undefined) => {
739 switch (kind) {
740 case "githubEnterprise":
741 return "githubEnterpriseConfig";
742 case "gitlab":
743 return "gitlabConfig";
744 case "gitlabEnterprise":
745 return "gitlabEnterpriseConfig";
746 case "bitbucketCloud":
747 return "bitbucketCloudConfig";
748 case "bitbucketDataCenter":
749 return "bitbucketDataCenterConfig";
750 case "secureSourceManager":
751 return "secureSourceManagerInstanceConfig";
752 case "github":
753 return "githubConfig";
754 default:
755 return undefined;
756 }
757};
758
759const immutableHostOf = (value: {
760 githubEnterpriseConfig?: { hostUri?: string };
761 gitlabEnterpriseConfig?: { hostUri?: string };
762 bitbucketDataCenterConfig?: { hostUri?: string };
763 bitbucketCloudConfig?: { workspace?: string };
764 secureSourceManagerInstanceConfig?: { instance?: string };
765 githubConfig?: { githubApp?: string };
766}) =>
767 value.githubEnterpriseConfig?.hostUri ??
768 value.gitlabEnterpriseConfig?.hostUri ??
769 value.bitbucketDataCenterConfig?.hostUri ??
770 value.bitbucketCloudConfig?.workspace ??
771 value.secureSourceManagerInstanceConfig?.instance ??
772 value.githubConfig?.githubApp;
773
774const immutableWebhookOf = (value: {
775 githubEnterpriseConfig?: { webhookSecretSecretVersion?: string };
776 gitlabConfig?: { webhookSecretSecretVersion?: string };
777 gitlabEnterpriseConfig?: { webhookSecretSecretVersion?: string };
778 bitbucketDataCenterConfig?: { webhookSecretSecretVersion?: string };
779 bitbucketCloudConfig?: { webhookSecretSecretVersion?: string };
780}) =>
781 value.githubEnterpriseConfig?.webhookSecretSecretVersion ??
782 value.gitlabConfig?.webhookSecretSecretVersion ??
783 value.gitlabEnterpriseConfig?.webhookSecretSecretVersion ??
784 value.bitbucketDataCenterConfig?.webhookSecretSecretVersion ??
785 value.bitbucketCloudConfig?.webhookSecretSecretVersion;
786
787const toAttrs = (
788 item: developerconnect.Connection,
789 project: string,
790 region: string,
791) => {
792 const name = item.name ?? "";
793 const parsed = parseName(name, "connections", region);
794 return {
795 name,
796 connectionId: parsed.id,
797 project: parsed.project || project,
798 location: parsed.location,
799 disabled: item.disabled === true,
800 labels: userLabels(item.labels),
801 annotations: userAnnotations(item.annotations),
802 githubConfig: fromGithub(item.githubConfig),
803 githubEnterpriseConfig: fromGithubEnterprise(item.githubEnterpriseConfig),
804 gitlabConfig: fromGitlab(item.gitlabConfig),
805 gitlabEnterpriseConfig: fromGitlabEnterprise(item.gitlabEnterpriseConfig),
806 bitbucketCloudConfig: fromBitbucketCloud(item.bitbucketCloudConfig),
807 bitbucketDataCenterConfig: fromBitbucketDataCenter(
808 item.bitbucketDataCenterConfig,
809 ),
810 secureSourceManagerInstanceConfig: fromSsm(
811 item.secureSourceManagerInstanceConfig,
812 ),
813 gitProxyConfig: item.gitProxyConfig
814 ? compact({ enabled: item.gitProxyConfig.enabled === true })
815 : undefined,
816 cryptoKeyConfig: item.cryptoKeyConfig
817 ? compact({ keyReference: item.cryptoKeyConfig.keyReference })
818 : undefined,
819 installationState: item.installationState
820 ? compact({
821 stage: item.installationState.stage,
822 message: item.installationState.message,
823 actionUri: item.installationState.actionUri,
824 })
825 : undefined,
826 reconciling: item.reconciling === true,
827 etag: item.etag,
828 uid: item.uid,
829 createTime: item.createTime,
830 updateTime: item.updateTime,
831 };
832};
833
834const getByName = (name: string) =>
835 developerconnect
836 .getProjectsLocationsConnections({ name })
837 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
838
839const listOwned = (project: string, region: string) =>
840 listAtLocation(project, region, (parent) =>
841 listLabeledPages(
842 developerconnect.listProjectsLocationsConnections.pages({
843 parent,
844 pageSize: 1000,
845 }),
846 (page) => page.connections,
847 (item) => item.labels,
848 ),
849 );
850
851export const ConnectionProvider = () =>
852 Provider.succeed(Connection, {
853 stables: [
854 "name",
855 "connectionId",
856 "project",
857 "location",
858 "uid",
859 "createTime",
860 ],
861
862 diff: Effect.fn(function* ({ news, olds, output }) {
863 if (!isResolved(news)) return undefined;
864 const env = yield* GcpEnvironment.current;
865 const previousKind =
866 desiredScmKind(olds ?? {}) ??
867 (output === undefined ? undefined : scmKindOf(output));
868 const nextKind = desiredScmKind(news);
869 const previousHost =
870 immutableHostOf(olds ?? {}) ?? immutableHostOf(output ?? {});
871 const nextHost = immutableHostOf(news);
872 const previousWebhook =
873 immutableWebhookOf(olds ?? {}) ?? immutableWebhookOf(output ?? {});
874 const nextWebhook = immutableWebhookOf(news);
875 return replaceOnIdentity({
876 previousId: olds?.connectionId ?? output?.connectionId,
877 nextId: news.connectionId ?? olds?.connectionId ?? output?.connectionId,
878 previousLocation: normalizeLocation(
879 olds?.location ?? output?.location,
880 env.region,
881 ),
882 nextLocation: normalizeLocation(
883 news.location ?? olds?.location ?? output?.location,
884 env.region,
885 ),
886 extra:
887 (previousKind !== undefined &&
888 nextKind !== undefined &&
889 nextKind !== previousKind) ||
890 (previousHost !== undefined &&
891 nextHost !== undefined &&
892 nextHost !== previousHost) ||
893 (previousWebhook !== undefined &&
894 nextWebhook !== undefined &&
895 nextWebhook !== previousWebhook),
896 });
897 }),
898
899 read: Effect.fn(function* ({ id, olds, output }) {
900 const env = yield* GcpEnvironment.current;
901 const connectionId = yield* toPhysicalId(
902 id,
903 olds?.connectionId,
904 output?.connectionId,
905 "connection",
906 );
907 const location = normalizeLocation(
908 olds?.location ?? output?.location,
909 env.region,
910 );
911 const name =
912 output?.name ?? resourceName(env.project, location, connectionId);
913 const existing = yield* getByName(name);
914 if (existing === undefined) return undefined;
915 const attrs = toAttrs(existing, env.project, env.region);
916 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
917 ? attrs
918 : Unowned(attrs);
919 }),
920
921 list: () =>
922 Effect.gen(function* () {
923 const env = yield* GcpEnvironment.current;
924 const items = yield* listOwned(env.project, env.region);
925 return items.map((item) => toAttrs(item, env.project, env.region));
926 }),
927
928 reconcile: Effect.fn(function* ({ id, news, output }) {
929 const env = yield* GcpEnvironment.current;
930 const connectionId = yield* toPhysicalId(
931 id,
932 news.connectionId,
933 output?.connectionId,
934 "connection",
935 );
936 const location = normalizeLocation(
937 news.location ?? output?.location,
938 env.region,
939 );
940 const name = resourceName(env.project, location, connectionId);
941 const desiredLabels = {
942 ...toLabels(news.labels),
943 ...(yield* createInternalLabels(id)),
944 };
945 const annotations = news.annotations;
946 const desiredDisabled = news.disabled === true;
947 const scmBody = toScmBody(news);
948 const body = compact({
949 ...scmBody,
950 disabled: desiredDisabled ? true : undefined,
951 labels: desiredLabels,
952 annotations,
953 gitProxyConfig: news.gitProxyConfig
954 ? compact({ enabled: news.gitProxyConfig.enabled })
955 : undefined,
956 cryptoKeyConfig: news.cryptoKeyConfig
957 ? compact({ keyReference: news.cryptoKeyConfig.keyReference })
958 : undefined,
959 });
960
961 let current = yield* getByName(output?.name ?? name);
962
963 if (current === undefined) {
964 const created = yield* developerconnect
965 .createProjectsLocationsConnections({
966 parent: parentOf(env.project, location),
967 connectionId,
968 body,
969 })
970 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
971 if (created !== undefined) {
972 yield* waitForOperation(created);
973 }
974 current = yield* waitUntilExists(getByName(name), name);
975 }
976
977 if (current === undefined) {
978 return yield* new ResourceNotResolved({ name });
979 }
980
981 const observedLabels = tagRecord(current.labels);
982 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
983 const nextKind = desiredScmKind(news) ?? "github";
984 const observedKind = scmKindOf(toAttrs(current, env.project, env.region));
985 const observedScm = compact({
986 githubConfig: fromGithub(current.githubConfig),
987 githubEnterpriseConfig: fromGithubEnterprise(
988 current.githubEnterpriseConfig,
989 ),
990 gitlabConfig: fromGitlab(current.gitlabConfig),
991 gitlabEnterpriseConfig: fromGitlabEnterprise(
992 current.gitlabEnterpriseConfig,
993 ),
994 bitbucketCloudConfig: fromBitbucketCloud(current.bitbucketCloudConfig),
995 bitbucketDataCenterConfig: fromBitbucketDataCenter(
996 current.bitbucketDataCenterConfig,
997 ),
998 secureSourceManagerInstanceConfig: fromSsm(
999 current.secureSourceManagerInstanceConfig,
1000 ),
1001 });
1002 const desiredScm = compact({
1003 githubConfig:
1004 nextKind === "github" ? toGithub(news.githubConfig ?? {}) : undefined,
1005 githubEnterpriseConfig: toGithubEnterprise(news.githubEnterpriseConfig),
1006 gitlabConfig: toGitlab(news.gitlabConfig),
1007 gitlabEnterpriseConfig: toGitlabEnterprise(news.gitlabEnterpriseConfig),
1008 bitbucketCloudConfig: toBitbucketCloud(news.bitbucketCloudConfig),
1009 bitbucketDataCenterConfig: toBitbucketDataCenter(
1010 news.bitbucketDataCenterConfig,
1011 ),
1012 secureSourceManagerInstanceConfig: toSsm(
1013 news.secureSourceManagerInstanceConfig,
1014 ),
1015 });
1016 const scmChanged = fingerprint(observedScm) !== fingerprint(desiredScm);
1017 const mask = fieldMask([
1018 (upsert.length > 0 || removed.length > 0) && "labels",
1019 fingerprint(userAnnotations(current.annotations)) !==
1020 fingerprint(userAnnotations(annotations)) && "annotations",
1021 !sameBool(current.disabled, desiredDisabled) && "disabled",
1022 scmChanged && scmField(nextKind ?? observedKind),
1023 !sameBool(
1024 current.gitProxyConfig?.enabled,
1025 news.gitProxyConfig?.enabled,
1026 ) && "gitProxyConfig",
1027 fingerprint(current.cryptoKeyConfig) !==
1028 fingerprint(news.cryptoKeyConfig) && "cryptoKeyConfig",
1029 ]);
1030
1031 if (mask.length > 0) {
1032 const operation = yield* developerconnect
1033 .patchProjectsLocationsConnections({
1034 name: current.name ?? name,
1035 updateMask: mask,
1036 body: compact({
1037 etag: current.etag,
1038 ...body,
1039 disabled: desiredDisabled,
1040 }),
1041 })
1042 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
1043 if (operation === undefined) {
1044 const created = yield* developerconnect
1045 .createProjectsLocationsConnections({
1046 parent: parentOf(env.project, location),
1047 connectionId,
1048 body,
1049 })
1050 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
1051 if (created !== undefined) {
1052 yield* waitForOperation(created);
1053 }
1054 } else {
1055 yield* waitForOperation(operation);
1056 }
1057 current = yield* waitUntilExists(
1058 getByName(current.name ?? name),
1059 current.name ?? name,
1060 );
1061 }
1062
1063 return toAttrs(current, env.project, env.region);
1064 }),
1065
1066 delete: Effect.fn(function* ({ output }) {
1067 const operation = yield* developerconnect
1068 .deleteProjectsLocationsConnections({ name: output.name })
1069 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
1070 if (operation !== undefined) {
1071 yield* waitForOperation(operation, { notFoundOk: true });
1072 }
1073 yield* waitUntilGone(getByName(output.name), output.name);
1074 }),
1075 });