1import * as apigee from "@distilled.cloud/gcp/apigee_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import type { Providers } from "../Providers.ts";
9import {
10 childName,
11 collectPages,
12 defaultOrgName,
13 hasOwnershipMarker,
14 lastSegment,
15 listOrgNames,
16 orgIdOf,
17 orgNameOf,
18 stringField,
19 waitForOperation,
20} from "./operations.ts";
21
22export type EnvgroupsAttachmentProps = {
23 /**
24 * Apigee organization id or `organizations/{org}`. Defaults to the
25 * current GCP project id. Immutable — changing it replaces the
26 * attachment.
27 */
28 organization?: string;
29 /**
30 * Parent environment group id or
31 * `organizations/{org}/envgroups/{envgroup}`. Immutable — changing it
32 * replaces the attachment.
33 */
34 envgroup: string;
35 /**
36 * Attached environment id (the `{env}` segment of
37 * `organizations/{org}/environments/{env}`). Immutable — changing it
38 * replaces the attachment.
39 */
40 environment: string;
41};
42
43export type EnvgroupsAttachment = Resource<
44 "GCP.Apigee.EnvgroupsAttachment",
45 EnvgroupsAttachmentProps,
46 {
47 /** Full resource name `organizations/{org}/envgroups/{envgroup}/attachments/{attachment}`. */
48 name: string;
49 /** Attachment id (last path segment). */
50 attachmentId: string;
51 /** Parent environment group id. */
52 envgroup: string;
53 /** Attached environment id. */
54 environment: string;
55 /** Apigee organization id. */
56 organization: string;
57 /** Server-reported environment group id. */
58 environmentGroupId: string | undefined;
59 /** Creation time in milliseconds since epoch. */
60 createdAt: string | undefined;
61 },
62 never,
63 Providers
64>;
65
66/**
67 * An attachment of an Apigee environment to an environment group.
68 *
69 * Existence-only: the identity is the (envgroup, environment) pair. The
70 * API has no labels or description, so `list` / nuke returns attachments
71 * whose attached environment carries an Alchemy description marker.
72 *
73 * ### Creating an Environment Group Attachment
74 * **Example:** Attach an environment to a group
75 * ```typescript
76 * const attachment = yield* GCP.Apigee.EnvgroupsAttachment("ProdApi", {
77 * envgroup: group.envgroupId,
78 * environment: environment.environmentId,
79 * });
80 * ```
81 *
82 * @resource
83 * @category Apigee
84 */
85export const EnvgroupsAttachment = Resource<EnvgroupsAttachment>(
86 "GCP.Apigee.EnvgroupsAttachment",
87);
88
89export class EnvgroupsAttachmentNotResolved extends Data.TaggedError(
90 "GCP.Apigee.EnvgroupsAttachmentNotResolved",
91)<{
92 parent: string;
93 environment: string;
94}> {}
95
96const envgroupIdOf = (envgroup: string) =>
97 envgroup.includes("/envgroups/") ? lastSegment(envgroup) : envgroup;
98
99const environmentIdOf = (environment: string) =>
100 environment.includes("/environments/")
101 ? lastSegment(environment)
102 : environment;
103
104const parentOf = (organization: string, envgroup: string) =>
105 childName(orgNameOf(organization), "envgroups", envgroupIdOf(envgroup));
106
107const toAttrs = (
108 attachment: apigee.GoogleCloudApigeeV1EnvironmentGroupAttachment,
109 organization: string,
110 envgroup: string,
111) => {
112 const name = attachment.name ?? "";
113 return {
114 name,
115 attachmentId: lastSegment(name),
116 envgroup: envgroupIdOf(envgroup),
117 environment: attachment.environment ?? "",
118 organization: orgIdOf(organization),
119 environmentGroupId: attachment.environmentGroupId,
120 createdAt: attachment.createdAt,
121 };
122};
123
124const getByName = (name: string) =>
125 apigee
126 .getOrganizationsEnvgroupsAttachments({ name })
127 .pipe(
128 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
129 Effect.succeed(undefined),
130 ),
131 );
132
133const listAttachments = (parent: string) =>
134 collectPages(
135 apigee.listOrganizationsEnvgroupsAttachments.pages({
136 parent,
137 pageSize: 1000,
138 }),
139 (page) => page.environmentGroupAttachments,
140 ).pipe(
141 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
142 Effect.succeed(
143 [] as apigee.GoogleCloudApigeeV1EnvironmentGroupAttachment[],
144 ),
145 ),
146 );
147
148const findByEnvironment = (
149 parent: string,
150 environment: string,
151 organization: string,
152 envgroup: string,
153) =>
154 Effect.gen(function* () {
155 const attachments = yield* listAttachments(parent);
156 const match = attachments.find(
157 (attachment) => attachment.environment === environment,
158 );
159 return match === undefined
160 ? undefined
161 : toAttrs(match, organization, envgroup);
162 });
163
164export const EnvgroupsAttachmentProvider = () =>
165 Provider.succeed(EnvgroupsAttachment, {
166 stables: [
167 "name",
168 "attachmentId",
169 "envgroup",
170 "environment",
171 "organization",
172 "createdAt",
173 ],
174
175 diff: Effect.fn(function* ({ news, olds, output }) {
176 if (!isResolved(news)) return undefined;
177 const previousGroup = olds?.envgroup ?? output?.envgroup;
178 const previousEnv = olds?.environment ?? output?.environment;
179 const previousOrg = olds?.organization ?? output?.organization;
180 const groupChanged =
181 previousGroup !== undefined &&
182 envgroupIdOf(news.envgroup) !== envgroupIdOf(previousGroup);
183 const envChanged =
184 previousEnv !== undefined &&
185 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
186 const orgChanged =
187 previousOrg !== undefined &&
188 news.organization !== undefined &&
189 orgIdOf(news.organization) !== orgIdOf(previousOrg);
190 if (groupChanged || envChanged || orgChanged) {
191 return { action: "replace" as const, deleteFirst: false };
192 }
193 return undefined;
194 }),
195
196 read: Effect.fn(function* ({ olds, output }) {
197 const env = yield* GcpEnvironment.current;
198 const organization = defaultOrgName(
199 env.project,
200 olds?.organization ?? output?.organization,
201 );
202 const envgroup = olds?.envgroup ?? output?.envgroup;
203 const environment = olds?.environment ?? output?.environment;
204 if (envgroup === undefined || environment === undefined) return undefined;
205 const parent = parentOf(organization, envgroup);
206 if (output?.name !== undefined) {
207 const existing = yield* getByName(output.name);
208 if (existing !== undefined) {
209 return toAttrs(existing, organization, envgroup);
210 }
211 }
212 return yield* findByEnvironment(
213 parent,
214 environmentIdOf(environment),
215 organization,
216 envgroup,
217 );
218 }),
219
220 list: () =>
221 Effect.gen(function* () {
222 const orgs = yield* listOrgNames();
223 const rows: EnvgroupsAttachment["Attributes"][] = [];
224 for (const organization of orgs) {
225 const groups = yield* collectPages(
226 apigee.listOrganizationsEnvgroups.pages({
227 parent: organization,
228 pageSize: 1000,
229 }),
230 (page) => page.environmentGroups,
231 ).pipe(
232 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
233 Effect.succeed(
234 [] as apigee.GoogleCloudApigeeV1EnvironmentGroup[],
235 ),
236 ),
237 );
238 const org = yield* apigee
239 .getOrganizations({ name: organization })
240 .pipe(
241 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
242 Effect.succeed(undefined),
243 ),
244 );
245 const ownedEnvs = new Set<string>();
246 for (const environmentId of org?.environments ?? []) {
247 const environment = yield* apigee
248 .getOrganizationsEnvironments({
249 name: `${organization}/environments/${environmentId}`,
250 })
251 .pipe(
252 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
253 Effect.succeed(undefined),
254 ),
255 );
256 if (hasOwnershipMarker(environment?.description)) {
257 ownedEnvs.add(environmentId);
258 }
259 }
260 for (const group of groups) {
261 const envgroupId = lastSegment(group.name ?? "");
262 if (envgroupId.length === 0) continue;
263 const parent = `${organization}/envgroups/${envgroupId}`;
264 const attachments = yield* listAttachments(parent);
265 for (const attachment of attachments) {
266 const environment = attachment.environment ?? "";
267 if (ownedEnvs.has(environment)) {
268 rows.push(toAttrs(attachment, organization, envgroupId));
269 }
270 }
271 }
272 }
273 return rows;
274 }),
275
276 reconcile: Effect.fn(function* ({ news, output }) {
277 const env = yield* GcpEnvironment.current;
278 const organization = defaultOrgName(env.project, news.organization);
279 const envgroup = envgroupIdOf(news.envgroup);
280 const environment = environmentIdOf(news.environment);
281 const parent = parentOf(organization, envgroup);
282
283 let current =
284 output?.name !== undefined ? yield* getByName(output.name) : undefined;
285 if (current === undefined) {
286 const found = yield* findByEnvironment(
287 parent,
288 environment,
289 organization,
290 envgroup,
291 );
292 if (found !== undefined) {
293 current = yield* getByName(found.name);
294 }
295 }
296
297 if (current === undefined) {
298 const operation = yield* apigee
299 .createOrganizationsEnvgroupsAttachments({
300 parent,
301 body: { environment },
302 })
303 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
304 if (operation !== undefined) {
305 const done = yield* waitForOperation(operation);
306 const createdName = stringField(done.response, "name");
307 if (createdName !== undefined) {
308 current = yield* getByName(createdName);
309 }
310 }
311 if (current === undefined) {
312 const found = yield* findByEnvironment(
313 parent,
314 environment,
315 organization,
316 envgroup,
317 );
318 if (found !== undefined) {
319 current = yield* getByName(found.name);
320 }
321 }
322 }
323
324 if (current === undefined) {
325 return yield* new EnvgroupsAttachmentNotResolved({
326 parent,
327 environment,
328 });
329 }
330
331 return toAttrs(current, organization, envgroup);
332 }),
333
334 delete: Effect.fn(function* ({ output }) {
335 const operation = yield* apigee
336 .deleteOrganizationsEnvgroupsAttachments({ name: output.name })
337 .pipe(
338 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
339 Effect.succeed(undefined),
340 ),
341 );
342 if (operation !== undefined) {
343 yield* waitForOperation(operation, { notFoundOk: true });
344 }
345 }),
346 });