1import * as apigee from "@distilled.cloud/gcp/apigee_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import * as Stream from "effect/Stream";
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 { lastSegment, orgParent, organizationFromName } from "./names.ts";
11import { waitForOperation } from "./operations.ts";
12import { listOwnedInstances } from "./org.ts";
13
14export type InstancesAttachmentProps = {
15 /**
16 * Apigee organization id. Defaults to the current GCP project id.
17 * Immutable — changing it replaces the attachment.
18 */
19 organization?: string;
20 /**
21 * Parent instance id or full name
22 * (`organizations/{org}/instances/{instance}`). Immutable — changing it
23 * replaces the attachment.
24 */
25 instance: string;
26 /**
27 * Environment id to attach. Immutable — changing it replaces the
28 * attachment.
29 */
30 environment: string;
31};
32
33export type InstancesAttachment = Resource<
34 "GCP.Apigee.InstancesAttachment",
35 InstancesAttachmentProps,
36 {
37 /** Full resource name `organizations/{org}/instances/{instance}/attachments/{attachment}`. */
38 name: string;
39 /** Attachment id (last path segment). */
40 attachmentId: string;
41 /** Parent instance id. */
42 instanceId: string;
43 /** Attached environment id. */
44 environment: string;
45 /** Apigee organization id. */
46 organization: string;
47 /** Creation time in milliseconds since epoch. */
48 createdAt: string | undefined;
49 },
50 never,
51 Providers
52>;
53
54/**
55 * An attachment of an Apigee environment onto a runtime instance.
56 *
57 * Attachments have no labels or description. `list` enumerates
58 * attachments on alchemy-owned instances so `pnpm nuke:gcp` can find
59 * leaked rows. Organization, instance, and environment are identity —
60 * changing them replaces the attachment. There is nothing else to
61 * update in place.
62 *
63 * ### Creating an Attachment
64 * **Example:** Attach an environment to a runtime instance
65 * ```typescript
66 * const runtime = yield* GCP.Apigee.Instance("Runtime", {});
67 * const attachment = yield* GCP.Apigee.InstancesAttachment("Eval", {
68 * instance: runtime.name,
69 * environment: "eval",
70 * });
71 * ```
72 *
73 * @resource
74 * @category Apigee
75 */
76export const InstancesAttachment = Resource<InstancesAttachment>(
77 "GCP.Apigee.InstancesAttachment",
78);
79
80export class InstancesAttachmentNotResolved extends Data.TaggedError(
81 "GCP.Apigee.InstancesAttachmentNotResolved",
82)<{
83 name: string;
84}> {}
85
86const instanceIdOf = (instance: string) => lastSegment(instance);
87
88const instanceName = (organization: string, instance: string) =>
89 instance.includes("/")
90 ? instance
91 : `${orgParent(organization)}/instances/${instance}`;
92
93const resourceName = (
94 organization: string,
95 instance: string,
96 attachmentId: string,
97) => `${instanceName(organization, instance)}/attachments/${attachmentId}`;
98
99const toAttrs = (
100 attachment: apigee.GoogleCloudApigeeV1InstanceAttachment,
101 organization: string,
102 instanceId: string,
103) => {
104 const raw = attachment.name ?? "";
105 const name = raw.includes("/")
106 ? raw
107 : resourceName(organization, instanceId, raw);
108 return {
109 name,
110 attachmentId: lastSegment(name),
111 instanceId,
112 environment: attachment.environment ?? "",
113 organization: organizationFromName(name) ?? organization,
114 createdAt: attachment.createdAt,
115 };
116};
117
118const getByName = (name: string) =>
119 apigee
120 .getOrganizationsInstancesAttachments({ name })
121 .pipe(
122 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
123 Effect.succeed(undefined),
124 ),
125 );
126
127const listByInstance = (parent: string) =>
128 apigee.listOrganizationsInstancesAttachments
129 .pages({
130 parent,
131 pageSize: 1000,
132 })
133 .pipe(
134 Stream.flatMap((page) => Stream.fromIterable(page.attachments ?? [])),
135 Stream.runCollect,
136 Effect.map((chunk) => Array.from(chunk)),
137 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
138 Effect.succeed([] as apigee.GoogleCloudApigeeV1InstanceAttachment[]),
139 ),
140 );
141
142const findByEnvironment = (
143 parent: string,
144 environment: string,
145 organization: string,
146 instanceId: string,
147) =>
148 listByInstance(parent).pipe(
149 Effect.map((attachments) =>
150 attachments.find((attachment) => attachment.environment === environment),
151 ),
152 Effect.map((attachment) =>
153 attachment === undefined
154 ? undefined
155 : toAttrs(attachment, organization, instanceId),
156 ),
157 );
158
159export const InstancesAttachmentProvider = () =>
160 Provider.succeed(InstancesAttachment, {
161 stables: [
162 "name",
163 "attachmentId",
164 "instanceId",
165 "organization",
166 "createdAt",
167 ],
168
169 diff: Effect.fn(function* ({ news, olds, output }) {
170 if (!isResolved(news)) return undefined;
171 const previousInstance = olds?.instance ?? output?.instanceId;
172 const previousEnv = olds?.environment ?? output?.environment;
173 const previousOrg = olds?.organization ?? output?.organization;
174 if (
175 (previousInstance !== undefined &&
176 instanceIdOf(news.instance) !== instanceIdOf(previousInstance)) ||
177 (previousEnv !== undefined && news.environment !== previousEnv) ||
178 (previousOrg !== undefined &&
179 news.organization !== undefined &&
180 news.organization !== previousOrg)
181 ) {
182 return { action: "replace" as const, deleteFirst: true };
183 }
184 return undefined;
185 }),
186
187 read: Effect.fn(function* ({ olds, output }) {
188 const env = yield* GcpEnvironment.current;
189 const organization =
190 organizationFromName(output?.name) ?? olds?.organization ?? env.project;
191 const instanceId = instanceIdOf(
192 olds?.instance ?? output?.instanceId ?? "",
193 );
194 if (instanceId.length === 0) return undefined;
195 const parent = instanceName(organization, instanceId);
196 if (output?.name !== undefined) {
197 const existing = yield* getByName(output.name);
198 if (existing === undefined) return undefined;
199 return toAttrs(existing, organization, instanceId);
200 }
201 const environment = olds?.environment ?? output?.environment;
202 if (environment === undefined) return undefined;
203 const found = yield* findByEnvironment(
204 parent,
205 environment,
206 organization,
207 instanceId,
208 );
209 return found;
210 }),
211
212 list: () =>
213 Effect.gen(function* () {
214 const env = yield* GcpEnvironment.current;
215 const instances = yield* listOwnedInstances(env.project);
216 const pages = yield* Effect.forEach(
217 instances,
218 (instance) => {
219 const parent = instance.name?.includes("/")
220 ? instance.name
221 : `${orgParent(env.project)}/instances/${lastSegment(instance.name ?? "")}`;
222 const instanceId = lastSegment(parent);
223 return listByInstance(parent).pipe(
224 Effect.map((attachments) =>
225 attachments.map((attachment) =>
226 toAttrs(attachment, env.project, instanceId),
227 ),
228 ),
229 );
230 },
231 { concurrency: 4 },
232 );
233 return pages.flat();
234 }),
235
236 reconcile: Effect.fn(function* ({ news, output }) {
237 const env = yield* GcpEnvironment.current;
238 const organization =
239 news.organization ?? output?.organization ?? env.project;
240 const instanceId = instanceIdOf(news.instance);
241 const parent = instanceName(organization, instanceId);
242 const existingName = output?.name;
243
244 let current =
245 existingName !== undefined ? yield* getByName(existingName) : undefined;
246 if (current === undefined) {
247 const found = yield* findByEnvironment(
248 parent,
249 news.environment,
250 organization,
251 instanceId,
252 );
253 if (found !== undefined) {
254 current = yield* getByName(found.name);
255 }
256 }
257
258 if (current === undefined) {
259 const created = yield* apigee
260 .createOrganizationsInstancesAttachments({
261 parent,
262 body: { environment: news.environment },
263 })
264 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
265 if (created !== undefined) {
266 yield* waitForOperation(created, { alreadyExistsOk: true });
267 }
268 const found = yield* findByEnvironment(
269 parent,
270 news.environment,
271 organization,
272 instanceId,
273 );
274 if (found === undefined) {
275 return yield* new InstancesAttachmentNotResolved({
276 name: `${parent}/attachments/${news.environment}`,
277 });
278 }
279 return found;
280 }
281
282 return toAttrs(current, organization, instanceId);
283 }),
284
285 delete: Effect.fn(function* ({ output }) {
286 const deleted = yield* apigee
287 .deleteOrganizationsInstancesAttachments({ name: output.name })
288 .pipe(
289 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
290 Effect.succeed(undefined),
291 ),
292 );
293 if (deleted !== undefined) {
294 yield* waitForOperation(deleted, { notFoundOk: true });
295 }
296 }),
297 });