1import * as dataplex from "@distilled.cloud/gcp/dataplex_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 { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 DataplexNotResolved,
12 normalizeLocation,
13 parseName,
14 replaceOnIdentity,
15 retryQuota,
16 waitForOperation,
17 waitUntilExists,
18 waitUntilGone,
19} from "./internal.ts";
20
21const DEFAULT_ENCRYPTION_CONFIG_ID = "default";
22
23export type EncryptionConfigFailureDetails =
24 dataplex.GoogleCloudDataplexV1EncryptionConfigFailureDetails;
25
26export type EncryptionConfigProps = {
27 /**
28 * Organization id (the `{organization}` segment of
29 * `organizations/{organization}/locations/{location}/encryptionConfigs/{encryptionConfig}`).
30 * Immutable — changing it replaces the config.
31 */
32 organizationId: string;
33 /**
34 * Region (`us-central1`, …). Global is not supported. Immutable —
35 * changing it replaces the config.
36 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
37 */
38 location?: string;
39 /**
40 * EncryptionConfig id. Currently only `"default"` is supported.
41 * Immutable — changing it replaces the config.
42 * @default "default"
43 */
44 encryptionConfigId?: string;
45 /**
46 * Cloud KMS key for CMEK. Omit to use Google-managed encryption.
47 */
48 key?: string;
49 /**
50 * Opt the metastore into CMEK.
51 * @default false
52 */
53 enableMetastoreEncryption?: boolean;
54};
55
56export type EncryptionConfig = Resource<
57 "GCP.Dataplex.EncryptionConfig",
58 EncryptionConfigProps,
59 {
60 /** Full resource name. */
61 name: string;
62 /** EncryptionConfig id (last path segment). */
63 encryptionConfigId: string;
64 /** Organization id. */
65 organizationId: string;
66 /** Location id. */
67 location: string;
68 /** CMEK key, if any. */
69 key: string | undefined;
70 /** Whether metastore CMEK is enabled. */
71 enableMetastoreEncryption: boolean;
72 /** Encryption state. */
73 encryptionState: string | undefined;
74 /** Failure details, if any. */
75 failureDetails: EncryptionConfigFailureDetails | undefined;
76 /** Server etag. */
77 etag: string | undefined;
78 /** RFC3339 creation timestamp. */
79 createTime: string | undefined;
80 /** RFC3339 last-update timestamp. */
81 updateTime: string | undefined;
82 },
83 never,
84 Providers
85>;
86
87/**
88 * A Dataplex EncryptionConfig that opts an organization location into
89 * customer-managed encryption keys (CMEK).
90 *
91 * EncryptionConfigs have no labels field. Organization, location, and id
92 * are immutable (`default` is the only supported id). Key and metastore
93 * encryption update in place. `list` returns an empty set because
94 * ownership cannot be stamped on this org singleton.
95 *
96 * ### Creating an EncryptionConfig
97 * **Example:** Google-managed encryption at us-central1
98 * ```typescript
99 * const config = yield* GCP.Dataplex.EncryptionConfig("Default", {
100 * organizationId: "1234567890",
101 * location: "us-central1",
102 * });
103 * ```
104 *
105 * @resource
106 * @category Dataplex
107 */
108export const EncryptionConfig = Resource<EncryptionConfig>(
109 "GCP.Dataplex.EncryptionConfig",
110);
111
112const orgParent = (organizationId: string, location: string) =>
113 `organizations/${organizationId}/locations/${location}`;
114
115const resourceName = (
116 organizationId: string,
117 location: string,
118 encryptionConfigId: string,
119) =>
120 `${orgParent(organizationId, location)}/encryptionConfigs/${encryptionConfigId}`;
121
122const toId = (encryptionConfigId: string | undefined, existing?: string) =>
123 encryptionConfigId ?? existing ?? DEFAULT_ENCRYPTION_CONFIG_ID;
124
125const toAttrs = (
126 config: dataplex.GoogleCloudDataplexV1EncryptionConfig,
127 organizationId: string,
128) => {
129 const name = config.name ?? "";
130 const parsed = parseName(name, "encryptionConfigs");
131 return {
132 name,
133 encryptionConfigId: parsed.id || DEFAULT_ENCRYPTION_CONFIG_ID,
134 organizationId: parsed.organization || organizationId,
135 location: parsed.location,
136 key: config.key,
137 enableMetastoreEncryption: config.enableMetastoreEncryption === true,
138 encryptionState: config.encryptionState,
139 failureDetails: config.failureDetails,
140 etag: config.etag,
141 createTime: config.createTime,
142 updateTime: config.updateTime,
143 };
144};
145
146const getByName = (name: string) =>
147 name.length === 0
148 ? Effect.succeed(undefined)
149 : retryQuota(
150 dataplex.getOrganizationsLocationsEncryptionConfigs({ name }),
151 ).pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
152
153export const EncryptionConfigProvider = () =>
154 Provider.succeed(EncryptionConfig, {
155 stables: [
156 "name",
157 "encryptionConfigId",
158 "organizationId",
159 "location",
160 "createTime",
161 ],
162
163 diff: Effect.fn(function* ({ news, olds, output }) {
164 if (!isResolved(news)) return undefined;
165 const env = yield* GcpEnvironment.current;
166 return replaceOnIdentity({
167 previousId:
168 olds?.encryptionConfigId ??
169 output?.encryptionConfigId ??
170 DEFAULT_ENCRYPTION_CONFIG_ID,
171 nextId: toId(
172 news.encryptionConfigId,
173 olds?.encryptionConfigId ?? output?.encryptionConfigId,
174 ),
175 previousLocation: normalizeLocation(
176 olds?.location ?? output?.location,
177 env.region,
178 ),
179 nextLocation: normalizeLocation(
180 news.location ?? olds?.location ?? output?.location,
181 env.region,
182 ),
183 previousParent: olds?.organizationId ?? output?.organizationId,
184 nextParent: news.organizationId,
185 });
186 }),
187
188 read: Effect.fn(function* ({ olds, output }) {
189 const env = yield* GcpEnvironment.current;
190 const encryptionConfigId = toId(
191 olds?.encryptionConfigId,
192 output?.encryptionConfigId,
193 );
194 const organizationId = olds?.organizationId ?? output?.organizationId;
195 if (organizationId === undefined) return undefined;
196 const location = normalizeLocation(
197 olds?.location ?? output?.location,
198 env.region,
199 );
200 const name =
201 output?.name ??
202 resourceName(organizationId, location, encryptionConfigId);
203 const existing = yield* getByName(name);
204 if (existing === undefined) return undefined;
205 const attrs = toAttrs(existing, organizationId);
206 return output !== undefined ? attrs : Unowned(attrs);
207 }),
208
209 list: () => Effect.succeed([]),
210
211 reconcile: Effect.fn(function* ({ news, output }) {
212 const env = yield* GcpEnvironment.current;
213 const encryptionConfigId = toId(
214 news.encryptionConfigId,
215 output?.encryptionConfigId,
216 );
217 const location = normalizeLocation(
218 news.location ?? output?.location,
219 env.region,
220 );
221 const name = resourceName(
222 news.organizationId,
223 location,
224 encryptionConfigId,
225 );
226
227 let current = yield* getByName(output?.name ?? name);
228
229 if (current === undefined) {
230 const created = yield* retryQuota(
231 dataplex.createOrganizationsLocationsEncryptionConfigs({
232 parent: orgParent(news.organizationId, location),
233 encryptionConfigId,
234 body: {
235 key: news.key,
236 enableMetastoreEncryption:
237 news.enableMetastoreEncryption === true ? true : undefined,
238 },
239 }),
240 ).pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
241 if (created !== undefined) {
242 yield* waitForOperation(created);
243 }
244 current = yield* waitUntilExists(getByName(name), name);
245 }
246
247 if (current === undefined) {
248 return yield* new DataplexNotResolved({ name });
249 }
250
251 const keyChanged = (current.key ?? "") !== (news.key ?? "");
252 const metastoreChanged =
253 (current.enableMetastoreEncryption === true) !==
254 (news.enableMetastoreEncryption === true);
255
256 if (keyChanged || metastoreChanged) {
257 const operation =
258 yield* dataplex.patchOrganizationsLocationsEncryptionConfigs({
259 name: current.name ?? name,
260 updateMask: [
261 keyChanged ? "key" : undefined,
262 metastoreChanged ? "enableMetastoreEncryption" : undefined,
263 ]
264 .filter((field): field is string => field !== undefined)
265 .join(","),
266 body: {
267 name: current.name ?? name,
268 key: news.key,
269 enableMetastoreEncryption:
270 news.enableMetastoreEncryption === true,
271 etag: current.etag,
272 },
273 });
274 yield* waitForOperation(operation);
275 current = yield* waitUntilExists(
276 getByName(current.name ?? name),
277 current.name ?? name,
278 );
279 }
280
281 return toAttrs(current, news.organizationId);
282 }),
283
284 delete: Effect.fn(function* ({ output }) {
285 const operation = yield* dataplex
286 .deleteOrganizationsLocationsEncryptionConfigs({
287 name: output.name,
288 etag: output.etag,
289 })
290 .pipe(
291 Effect.retry({
292 while: (error) => error._tag === "Conflict",
293 times: 8,
294 schedule: Schedule.spaced("2 seconds"),
295 }),
296 Effect.catchTag("NotFound", () => Effect.succeed(undefined)),
297 );
298 if (operation !== undefined) {
299 yield* waitForOperation(operation, { notFoundOk: true });
300 }
301 yield* waitUntilGone(getByName(output.name), output.name);
302 }),
303 });