1import * as dlp from "@distilled.cloud/gcp/dlp_v2";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 DEFAULT_LOCATION,
12 DlpNotResolved,
13 fingerprint,
14 lastSegment,
15 normalizeLocation,
16 organizationLocationParent,
17 organizationIdOf,
18 parseName,
19 replaceOn,
20 resolveOrganization,
21 sameText,
22 updateMaskOf,
23} from "./internal.ts";
24
25export type CloudSqlProperties = dlp.GooglePrivacyDlpV2CloudSqlProperties;
26export type ConnectionState =
27 | dlp.GooglePrivacyDlpV2ConnectionStateEnum
28 | (string & {});
29export type ConnectionError = dlp.GooglePrivacyDlpV2Error;
30
31export type OrganizationsLocationsConnectionProps = {
32 /**
33 * Parent organization (`organizations/{organization}` or the numeric
34 * id). Defaults to `GOOGLE_ORGANIZATION_ID` or the project's Resource
35 * Manager ancestor. Immutable — changing it replaces the connection.
36 */
37 organization?: string;
38 /**
39 * Location of the Cloud SQL instance (`us-central1`, …). Must match the
40 * region of `cloudSql.connectionName`. Immutable — changing it replaces
41 * the connection.
42 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
43 */
44 location?: string;
45 /**
46 * Connection lifecycle state.
47 * @default "MISSING_CREDENTIALS"
48 */
49 state?: ConnectionState;
50 /**
51 * Cloud SQL properties. `connectionName` and `databaseEngine` are
52 * identity — changing either replaces the connection.
53 */
54 cloudSql: CloudSqlProperties;
55};
56
57export type OrganizationsLocationsConnection = Resource<
58 "GCP.DLP.OrganizationsLocationsConnection",
59 OrganizationsLocationsConnectionProps,
60 {
61 /** Full resource name `organizations/{organization}/locations/{location}/connections/{connection}`. */
62 name: string;
63 /** Connection id (last path segment). Assigned by the API. */
64 connectionId: string;
65 /** Organization resource name. */
66 organization: string;
67 /** Organization id. */
68 organizationId: string;
69 /** Location id. */
70 location: string;
71 /** Project id of the deploying stack. */
72 project: string;
73 /** Connection lifecycle state. */
74 state: string;
75 /** Cloud SQL properties. */
76 cloudSql: CloudSqlProperties | undefined;
77 /** Recent connection errors, if any. */
78 errors: ConnectionError[] | undefined;
79 },
80 never,
81 Providers
82>;
83
84/**
85 * An organization-scoped Sensitive Data Protection connection to an
86 * external data source (Cloud SQL).
87 *
88 * Connections have no labels or description field, so `list` returns an
89 * empty set — nuke cannot discover them from the cloud. Organization,
90 * location, Cloud SQL `connectionName`, and `databaseEngine` are identity.
91 * State, credentials, and `maxConnections` update in place. The connection
92 * id is assigned by the API.
93 *
94 * ### Creating a Connection
95 * **Example:** Cloud SQL IAM connection
96 * ```typescript
97 * const connection = yield* GCP.DLP.OrganizationsLocationsConnection(
98 * "Warehouse",
99 * {
100 * location: "us-central1",
101 * state: "AVAILABLE",
102 * cloudSql: {
103 * connectionName: "my-project:us-central1:warehouse",
104 * databaseEngine: "DATABASE_ENGINE_POSTGRES",
105 * maxConnections: 2,
106 * cloudSqlIam: {},
107 * },
108 * },
109 * );
110 * ```
111 *
112 * @resource
113 * @category DLP
114 */
115export const OrganizationsLocationsConnection =
116 Resource<OrganizationsLocationsConnection>(
117 "GCP.DLP.OrganizationsLocationsConnection",
118 );
119
120const DEFAULT_STATE = "MISSING_CREDENTIALS" satisfies ConnectionState;
121
122const toAttrs = (
123 connection: dlp.GooglePrivacyDlpV2Connection,
124 organization: string,
125 project: string,
126) => {
127 const name = connection.name ?? "";
128 const parsed = parseName(name, "connections");
129 return {
130 name,
131 connectionId: parsed.id || lastSegment(name),
132 organization,
133 organizationId: organizationIdOf(organization),
134 location: parsed.location || DEFAULT_LOCATION,
135 project,
136 state: connection.state ?? DEFAULT_STATE,
137 cloudSql: connection.cloudSql,
138 errors: connection.errors,
139 };
140};
141
142const getByName = (name: string) =>
143 name.length === 0
144 ? Effect.succeed(undefined)
145 : dlp
146 .getOrganizationsLocationsConnections({ name })
147 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
148
149const findByConnectionName = (parent: string, connectionName: string) =>
150 connectionName.length === 0
151 ? Effect.succeed(undefined)
152 : dlp.listOrganizationsLocationsConnections
153 .pages({ parent, pageSize: 100 })
154 .pipe(
155 Stream.flatMap((page) => Stream.fromIterable(page.connections ?? [])),
156 Stream.filter(
157 (connection) =>
158 (connection.cloudSql?.connectionName ?? "") === connectionName,
159 ),
160 Stream.runHead,
161 Effect.map((option) =>
162 option._tag === "Some" ? option.value : undefined,
163 ),
164 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
165 );
166
167const stateOf = (value: string | undefined) => value ?? DEFAULT_STATE;
168
169export const OrganizationsLocationsConnectionProvider = () =>
170 Provider.succeed(OrganizationsLocationsConnection, {
171 stables: [
172 "name",
173 "connectionId",
174 "organization",
175 "organizationId",
176 "location",
177 "project",
178 ],
179
180 diff: Effect.fn(function* ({ news, olds, output }) {
181 if (!isResolved(news)) return undefined;
182 const env = yield* GcpEnvironment.current;
183 const previousLocation = normalizeLocation(
184 olds?.location ?? output?.location,
185 env.region,
186 );
187 const nextLocation = normalizeLocation(
188 news.location ?? olds?.location ?? output?.location,
189 env.region,
190 );
191 const previousInstance =
192 olds?.cloudSql?.connectionName ?? output?.cloudSql?.connectionName;
193 const nextInstance = news.cloudSql.connectionName ?? previousInstance;
194 const previousEngine =
195 olds?.cloudSql?.databaseEngine ?? output?.cloudSql?.databaseEngine;
196 const nextEngine = news.cloudSql.databaseEngine ?? previousEngine;
197 return (
198 replaceOn(
199 olds?.organization ?? output?.organization,
200 news.organization,
201 ) ??
202 replaceOn(previousLocation, nextLocation) ??
203 replaceOn(previousInstance, nextInstance) ??
204 replaceOn(previousEngine, nextEngine)
205 );
206 }),
207
208 read: Effect.fn(function* ({ olds, output }) {
209 const env = yield* GcpEnvironment.current;
210 const organization = yield* resolveOrganization(
211 olds?.organization ?? output?.organization,
212 output?.organization,
213 );
214 const location = normalizeLocation(
215 olds?.location ?? output?.location,
216 env.region,
217 );
218 const parent = organizationLocationParent(organization, location);
219 let existing = yield* getByName(output?.name ?? "");
220 if (existing === undefined) {
221 existing = yield* findByConnectionName(
222 parent,
223 olds?.cloudSql?.connectionName ??
224 output?.cloudSql?.connectionName ??
225 "",
226 );
227 }
228 if (existing === undefined) return undefined;
229 const attrs = toAttrs(existing, organization, env.project);
230 return output !== undefined ? attrs : Unowned(attrs);
231 }),
232
233 list: () => Effect.succeed([]),
234
235 reconcile: Effect.fn(function* ({ news, output }) {
236 const env = yield* GcpEnvironment.current;
237 const organization = yield* resolveOrganization(
238 news.organization,
239 output?.organization,
240 );
241 const location = normalizeLocation(
242 news.location ?? output?.location,
243 env.region,
244 );
245 const parent = organizationLocationParent(organization, location);
246 const state = stateOf(news.state);
247 const cloudSql = news.cloudSql;
248
249 let current = yield* getByName(output?.name ?? "");
250 if (current === undefined) {
251 current = yield* findByConnectionName(
252 parent,
253 cloudSql.connectionName ?? output?.cloudSql?.connectionName ?? "",
254 );
255 }
256
257 if (current === undefined) {
258 const created = yield* dlp
259 .createOrganizationsLocationsConnections({
260 parent,
261 body: {
262 connection: {
263 state,
264 cloudSql,
265 },
266 },
267 })
268 .pipe(
269 Effect.catchTag("Conflict", () =>
270 findByConnectionName(parent, cloudSql.connectionName ?? ""),
271 ),
272 );
273 current = created ?? undefined;
274 }
275
276 if (current === undefined) {
277 return yield* new DlpNotResolved({
278 name: `${parent}/connections/${cloudSql.connectionName ?? "unknown"}`,
279 });
280 }
281
282 const currentName = current.name ?? "";
283 const stateChanged = !sameText(current.state, state);
284 const sqlChanged =
285 fingerprint(current.cloudSql) !== fingerprint(cloudSql);
286 const updateMask = updateMaskOf(
287 stateChanged ? "state" : undefined,
288 sqlChanged ? "cloudSql" : undefined,
289 );
290
291 if (updateMask.length > 0) {
292 current = yield* dlp.patchOrganizationsLocationsConnections({
293 name: currentName,
294 body: {
295 updateMask,
296 connection: {
297 state,
298 cloudSql,
299 },
300 },
301 });
302 }
303
304 return toAttrs(current, organization, env.project);
305 }),
306
307 delete: Effect.fn(function* ({ output }) {
308 yield* dlp
309 .deleteOrganizationsLocationsConnections({ name: output.name })
310 .pipe(Effect.catchTag("NotFound", () => Effect.void));
311 }),
312 });