1import * as networkconnectivity from "@distilled.cloud/gcp/networkconnectivity_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 NetworkConnectivityNotResolved,
19 canonicalizeLink,
20 changedFields,
21 collectPages,
22 hasAlchemyLabelKeys,
23 lastSegment,
24 normalizeLocation,
25 parentOf,
26 parseName,
27 rfc1035,
28 sameJson,
29 toNetworkResource,
30 toPhysicalId,
31 toSubnetworkResource,
32 userLabels,
33 waitForOperation,
34 waitUntilGone,
35 waitUntilPresent,
36} from "./internal.ts";
37
38const COLLECTION = "serviceConnectionPolicies";
39
40export type ServiceConnectionPolicySubnetworkMode =
41 | networkconnectivity.CreateProjectsLocationsServiceConnectionPoliciesSubnetworkModeEnum
42 | (string & {});
43export type ServiceConnectionPolicyIpStack =
44 | networkconnectivity.CreateProjectsLocationsServiceConnectionPoliciesAutoSubnetworkConfig_ipStackEnum
45 | (string & {});
46export type PscConfigProducerInstanceLocation =
47 | networkconnectivity.PscConfigProducerInstanceLocationEnum
48 | (string & {});
49
50export type PscConfig = {
51 /**
52 * Subnetwork resource paths used for IPAM
53 * (`projects/{project}/regions/{region}/subnetworks/{subnetwork}`).
54 */
55 subnetworks?: string[];
56 /**
57 * Max number of PSC connections for this policy (int64 string).
58 */
59 limit?: string;
60 /**
61 * Authorization mechanism for producer instance location.
62 */
63 producerInstanceLocation?: PscConfigProducerInstanceLocation;
64 /**
65 * Projects, folders, or organizations the producer instance may live
66 * in, e.g. `"projects/my-project"`.
67 */
68 allowedGoogleProducersResourceHierarchyLevel?: string[];
69};
70
71export type AutoSubnetworkConfig = {
72 /** Requested IP stack. */
73 ipStack?: ServiceConnectionPolicyIpStack;
74 /** Desired prefix length for the auto-created subnet. */
75 prefixLength?: number;
76 /** CIDR spaces to search for a free range. */
77 allocRangeSpace?: string[];
78};
79
80export type AutoCreatedSubnetworkInfo = {
81 internalRange?: string;
82 internalRangeRef?: string;
83 subnetwork?: string;
84 subnetworkRef?: string;
85 delinked?: boolean;
86};
87
88export type ServiceConnectionPolicyProps = {
89 /**
90 * Policy id (the `{service_connection_policy}` segment). If omitted, a
91 * unique name is generated. Immutable — changing it replaces the
92 * policy.
93 */
94 serviceConnectionPolicyId?: string;
95 /**
96 * Location (`us-central1`, …). Immutable — changing it replaces the
97 * policy. `US-CENTRAL1` is accepted and normalized to `us-central1`.
98 * @default the stack's GCP region (`GCP.Region`, else the profile
99 * region, else `us-central1`)
100 */
101 location?: string;
102 /**
103 * Consumer VPC
104 * (`projects/{project}/global/networks/{network}` or a Compute
105 * self-link). Immutable — changing it replaces the policy.
106 */
107 network: string;
108 /**
109 * Service class identifier, e.g. `"gcp-memorystore-redis"` or
110 * `"gcp-cloud-sql"`.
111 */
112 serviceClass: string;
113 /**
114 * PSC connection configuration (subnets, connection limit, …).
115 */
116 pscConfig?: PscConfig;
117 /**
118 * Subnetwork mode. `USER_PROVIDED` uses `pscConfig.subnetworks`;
119 * `AUTO_CREATED` allocates a subnet from `autoSubnetworkConfig`.
120 * Create-only.
121 */
122 subnetworkMode?: ServiceConnectionPolicySubnetworkMode;
123 /**
124 * Auto-created subnet options. Create-only; used when
125 * `subnetworkMode` is `AUTO_CREATED`.
126 */
127 autoSubnetworkConfig?: AutoSubnetworkConfig;
128 /**
129 * Human-readable description.
130 */
131 description?: string;
132 /**
133 * User labels. Alchemy ownership labels are merged in automatically.
134 */
135 labels?: Record<string, string>;
136};
137
138export type ServiceConnectionPolicy = Resource<
139 "GCP.NetworkConnectivity.ServiceConnectionPolicy",
140 ServiceConnectionPolicyProps,
141 {
142 /** Full resource name `projects/{project}/locations/{location}/serviceConnectionPolicies/{id}`. */
143 name: string;
144 /** Policy id (last path segment). */
145 serviceConnectionPolicyId: string;
146 /** Project id. */
147 project: string;
148 /** Location id. */
149 location: string;
150 /** Consumer VPC resource path. */
151 network: string | undefined;
152 /** Service class identifier. */
153 serviceClass: string | undefined;
154 /** PSC configuration. */
155 pscConfig: PscConfig | undefined;
156 /** Infrastructure used for connections (`PSC`, …). */
157 infrastructure: string | undefined;
158 /** Auto-created subnet info, if any. */
159 autoCreatedSubnetInfo: AutoCreatedSubnetworkInfo | undefined;
160 /** User-provided description. */
161 description: string | undefined;
162 /** User labels (Alchemy ownership labels stripped). */
163 labels: Record<string, string>;
164 /** Server etag. */
165 etag: string | undefined;
166 /** RFC3339 creation timestamp. */
167 createTime: string | undefined;
168 /** RFC3339 last-update timestamp. */
169 updateTime: string | undefined;
170 },
171 never,
172 Providers
173>;
174
175/**
176 * A PSC Service Connection Policy that allows a consumer VPC to connect
177 * to a producer service class (Cloud SQL, Memorystore, …).
178 *
179 * Changing `serviceConnectionPolicyId`, `location`, or `network`
180 * replaces the policy. Description, labels, `serviceClass`, and
181 * `pscConfig` update in place.
182 *
183 * ### Creating a ServiceConnectionPolicy
184 * **Example:** User-provided subnet
185 * ```typescript
186 * const network = yield* GCP.Compute.Network("AppVpc", {
187 * autoCreateSubnetworks: false,
188 * });
189 * const subnet = yield* GCP.Compute.Subnetwork("PscSubnet", {
190 * network: network.selfLink ?? network.networkName,
191 * ipCidrRange: "10.20.0.0/24",
192 * });
193 * const policy = yield* GCP.NetworkConnectivity.ServiceConnectionPolicy(
194 * "Redis",
195 * {
196 * serviceClass: "gcp-memorystore-redis",
197 * network: network.selfLink ?? network.networkName,
198 * pscConfig: {
199 * subnetworks: [subnet.selfLink ?? subnet.subnetworkName],
200 * },
201 * labels: { env: "prod" },
202 * },
203 * );
204 * ```
205 *
206 * ### Updating a ServiceConnectionPolicy
207 * **Example:** Description, labels, and connection limit
208 * ```typescript
209 * const policy = yield* GCP.NetworkConnectivity.ServiceConnectionPolicy(
210 * "Redis",
211 * {
212 * serviceClass: "gcp-memorystore-redis",
213 * network: network.selfLink ?? network.networkName,
214 * description: "redis psc v2",
215 * pscConfig: {
216 * subnetworks: [subnet.selfLink ?? subnet.subnetworkName],
217 * limit: "4",
218 * },
219 * labels: { env: "prod", role: "psc" },
220 * },
221 * );
222 * ```
223 *
224 * @resource
225 * @category NetworkConnectivity
226 */
227export const ServiceConnectionPolicy = Resource<ServiceConnectionPolicy>(
228 "GCP.NetworkConnectivity.ServiceConnectionPolicy",
229);
230
231const resourceName = (
232 project: string,
233 location: string,
234 serviceConnectionPolicyId: string,
235) =>
236 `projects/${project}/locations/${location}/serviceConnectionPolicies/${serviceConnectionPolicyId}`;
237
238const toPscConfig = (
239 config: PscConfig | networkconnectivity.PscConfig | undefined,
240 project: string,
241 location: string,
242): PscConfig | undefined => {
243 if (config === undefined) return undefined;
244 return {
245 subnetworks: (config.subnetworks ?? []).map((subnetwork) =>
246 toSubnetworkResource(project, location, subnetwork),
247 ),
248 limit: config.limit,
249 producerInstanceLocation: config.producerInstanceLocation,
250 allowedGoogleProducersResourceHierarchyLevel:
251 config.allowedGoogleProducersResourceHierarchyLevel
252 ? [...config.allowedGoogleProducersResourceHierarchyLevel]
253 : undefined,
254 };
255};
256
257const toAutoCreated = (
258 info: networkconnectivity.AutoCreatedSubnetworkInfo | undefined,
259): AutoCreatedSubnetworkInfo | undefined => {
260 if (info === undefined) return undefined;
261 return {
262 internalRange: info.internalRange,
263 internalRangeRef: info.internalRangeRef,
264 subnetwork: info.subnetwork,
265 subnetworkRef: info.subnetworkRef,
266 delinked: info.delinked,
267 };
268};
269
270const toAttrs = (
271 policy: networkconnectivity.ServiceConnectionPolicy,
272 project: string,
273 region: string,
274) => {
275 const name = policy.name ?? "";
276 const parsed = parseName(name, COLLECTION, region);
277 const location = parsed.location || region;
278 return {
279 name,
280 serviceConnectionPolicyId: parsed.id,
281 project: parsed.project || project,
282 location,
283 network: policy.network,
284 serviceClass: policy.serviceClass,
285 pscConfig: toPscConfig(
286 policy.pscConfig,
287 parsed.project || project,
288 location,
289 ),
290 infrastructure: policy.infrastructure,
291 autoCreatedSubnetInfo: toAutoCreated(policy.autoCreatedSubnetInfo),
292 description: policy.description,
293 labels: userLabels(policy.labels),
294 etag: policy.etag,
295 createTime: policy.createTime,
296 updateTime: policy.updateTime,
297 };
298};
299
300const getByName = (name: string) =>
301 networkconnectivity
302 .getProjectsLocationsServiceConnectionPolicies({ name })
303 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
304
305export const ServiceConnectionPolicyProvider = () =>
306 Provider.succeed(ServiceConnectionPolicy, {
307 stables: [
308 "name",
309 "serviceConnectionPolicyId",
310 "project",
311 "location",
312 "network",
313 "createTime",
314 ],
315
316 diff: Effect.fn(function* ({ news, olds, output }) {
317 if (!isResolved(news)) return undefined;
318 const env = yield* GcpEnvironment.current;
319 const previousId =
320 olds?.serviceConnectionPolicyId ?? output?.serviceConnectionPolicyId;
321 const nextId = news.serviceConnectionPolicyId
322 ? rfc1035(news.serviceConnectionPolicyId, "service-connection-policy")
323 : previousId;
324 const previousLocation = normalizeLocation(
325 olds?.location ?? output?.location,
326 env.region,
327 );
328 const nextLocation = normalizeLocation(
329 news.location ?? olds?.location ?? output?.location,
330 env.region,
331 );
332 const previousNetwork = lastSegment(
333 canonicalizeLink(olds?.network ?? output?.network),
334 );
335 const nextNetwork = lastSegment(canonicalizeLink(news.network));
336 if (
337 (previousId !== undefined &&
338 nextId !== undefined &&
339 nextId !== previousId) ||
340 previousLocation !== nextLocation ||
341 (previousNetwork.length > 0 && previousNetwork !== nextNetwork)
342 ) {
343 return { action: "replace" as const };
344 }
345 return undefined;
346 }),
347
348 read: Effect.fn(function* ({ id, olds, output }) {
349 const env = yield* GcpEnvironment.current;
350 const serviceConnectionPolicyId = yield* toPhysicalId(
351 id,
352 olds?.serviceConnectionPolicyId,
353 output?.serviceConnectionPolicyId,
354 "service-connection-policy",
355 );
356 const location = normalizeLocation(
357 olds?.location ?? output?.location,
358 env.region,
359 );
360 const name =
361 output?.name ??
362 resourceName(env.project, location, serviceConnectionPolicyId);
363 const existing = yield* getByName(name);
364 if (existing === undefined) return undefined;
365 const attrs = toAttrs(existing, env.project, env.region);
366 return (yield* hasAlchemyLabels(id, tagRecord(existing.labels)))
367 ? attrs
368 : Unowned(attrs);
369 }),
370
371 list: () =>
372 Effect.gen(function* () {
373 const env = yield* GcpEnvironment.current;
374 const items = yield* collectPages(
375 networkconnectivity.listProjectsLocationsServiceConnectionPolicies.pages(
376 {
377 parent: parentOf(env.project, "-"),
378 pageSize: 1000,
379 },
380 ),
381 (page) => page.serviceConnectionPolicies,
382 );
383 return items
384 .filter((item) => hasAlchemyLabelKeys(item.labels))
385 .map((item) => toAttrs(item, env.project, env.region));
386 }),
387
388 reconcile: Effect.fn(function* ({ id, news, output }) {
389 const env = yield* GcpEnvironment.current;
390 const serviceConnectionPolicyId = yield* toPhysicalId(
391 id,
392 news.serviceConnectionPolicyId,
393 output?.serviceConnectionPolicyId,
394 "service-connection-policy",
395 );
396 const location = normalizeLocation(
397 news.location ?? output?.location,
398 env.region,
399 );
400 const name = resourceName(
401 env.project,
402 location,
403 serviceConnectionPolicyId,
404 );
405 const desiredLabels = {
406 ...toLabels(news.labels),
407 ...(yield* createInternalLabels(id)),
408 };
409 const network = toNetworkResource(env.project, news.network);
410 const pscConfig = toPscConfig(news.pscConfig, env.project, location);
411
412 let current = yield* getByName(name);
413
414 if (current === undefined) {
415 const created = yield* networkconnectivity
416 .createProjectsLocationsServiceConnectionPolicies({
417 parent: parentOf(env.project, location),
418 serviceConnectionPolicyId,
419 subnetworkMode: news.subnetworkMode,
420 "autoSubnetworkConfig.ipStack": news.autoSubnetworkConfig?.ipStack,
421 "autoSubnetworkConfig.prefixLength":
422 news.autoSubnetworkConfig?.prefixLength,
423 "autoSubnetworkConfig.allocRangeSpace": news.autoSubnetworkConfig
424 ?.allocRangeSpace
425 ? [...news.autoSubnetworkConfig.allocRangeSpace]
426 : undefined,
427 body: {
428 network,
429 serviceClass: news.serviceClass,
430 pscConfig,
431 description: news.description,
432 labels: desiredLabels,
433 },
434 })
435 .pipe(
436 Effect.retry({
437 while: (error) => error._tag === "Conflict",
438 times: 5,
439 schedule: Schedule.spaced("2 seconds"),
440 }),
441 Effect.catchTag("Conflict", () => Effect.succeed(undefined)),
442 );
443 if (created !== undefined) {
444 yield* waitForOperation(created);
445 }
446 current = yield* waitUntilPresent(getByName(name), name);
447 }
448
449 if (current === undefined) {
450 return yield* new NetworkConnectivityNotResolved({ name });
451 }
452
453 const observedLabels = tagRecord(current.labels);
454 const { upsert, removed } = diffLabels(observedLabels, desiredLabels);
455 const labelsChanged = upsert.length > 0 || removed.length > 0;
456 const descriptionChanged =
457 (current.description ?? "") !== (news.description ?? "");
458 const serviceClassChanged =
459 (current.serviceClass ?? "") !== news.serviceClass;
460 const observedPsc = toPscConfig(current.pscConfig, env.project, location);
461 const pscChanged = !sameJson(
462 {
463 subnetworks: [...(observedPsc?.subnetworks ?? [])]
464 .map((value) => lastSegment(canonicalizeLink(value)))
465 .sort(),
466 limit: observedPsc?.limit ?? "",
467 producerInstanceLocation: observedPsc?.producerInstanceLocation ?? "",
468 allowedGoogleProducersResourceHierarchyLevel: [
469 ...(observedPsc?.allowedGoogleProducersResourceHierarchyLevel ??
470 []),
471 ].sort(),
472 },
473 {
474 subnetworks: [...(pscConfig?.subnetworks ?? [])]
475 .map((value) => lastSegment(canonicalizeLink(value)))
476 .sort(),
477 limit: pscConfig?.limit ?? "",
478 producerInstanceLocation: pscConfig?.producerInstanceLocation ?? "",
479 allowedGoogleProducersResourceHierarchyLevel: [
480 ...(pscConfig?.allowedGoogleProducersResourceHierarchyLevel ?? []),
481 ].sort(),
482 },
483 );
484 const updateMask = changedFields([
485 ["labels", labelsChanged],
486 ["description", descriptionChanged],
487 ["serviceClass", serviceClassChanged],
488 ["pscConfig", pscChanged],
489 ]);
490
491 if (updateMask.length > 0) {
492 const operation =
493 yield* networkconnectivity.patchProjectsLocationsServiceConnectionPolicies(
494 {
495 name: current.name ?? name,
496 updateMask: updateMask.join(","),
497 body: {
498 name: current.name ?? name,
499 labels: desiredLabels,
500 description: news.description,
501 serviceClass: news.serviceClass,
502 pscConfig,
503 etag: current.etag,
504 },
505 },
506 );
507 yield* waitForOperation(operation);
508 current = yield* waitUntilPresent(
509 getByName(current.name ?? name),
510 current.name ?? name,
511 );
512 }
513
514 return toAttrs(current, env.project, env.region);
515 }),
516
517 delete: Effect.fn(function* ({ output }) {
518 const operation = yield* networkconnectivity
519 .deleteProjectsLocationsServiceConnectionPolicies({
520 name: output.name,
521 etag: output.etag,
522 })
523 .pipe(
524 Effect.retry({
525 while: (error) => error._tag === "Conflict",
526 times: 8,
527 schedule: Schedule.spaced("2 seconds"),
528 }),
529 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
530 );
531 if (operation !== undefined) {
532 yield* waitForOperation(operation, { notFoundOk: true });
533 }
534 yield* waitUntilGone(getByName(output.name), output.name);
535 }),
536 });