1import * as apihub from "@distilled.cloud/gcp/apihub_v1";
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 ApihubNotResolved,
12 locationParent,
13 normalizeLocation,
14 parseName,
15 projectIdOf,
16 projectNameOf,
17 replaceOnIdentity,
18} from "./internal.ts";
19
20export type RuntimeProjectAttachmentProps = {
21 /**
22 * Runtime project to attach, as `projects/{project}` or a bare project
23 * id. Immutable — changing it replaces the attachment. The attachment
24 * id must equal this project's id. Defaults to the stack project.
25 */
26 runtimeProject?: string;
27 /**
28 * Attachment id. Must equal the project id of `runtimeProject`. If
29 * omitted, that project id is used. Immutable — changing it replaces
30 * the attachment.
31 */
32 runtimeProjectAttachmentId?: string;
33 /**
34 * Location of the API Hub host (`us-central1`, …). Immutable —
35 * changing it replaces the attachment.
36 * @default the stack's GCP region (`GCP.Region`, else the profile region, else `us-central1`)
37 */
38 location?: string;
39};
40
41export type RuntimeProjectAttachment = Resource<
42 "GCP.ApiHub.RuntimeProjectAttachment",
43 RuntimeProjectAttachmentProps,
44 {
45 /** Full resource name. */
46 name: string;
47 /** Attachment id (last path segment, equal to the runtime project id). */
48 runtimeProjectAttachmentId: string;
49 /** Host project id. */
50 project: string;
51 /** Location id. */
52 location: string;
53 /** Runtime project resource name (`projects/{number}` on output). */
54 runtimeProject: string | undefined;
55 /** RFC3339 creation timestamp. */
56 createTime: string | undefined;
57 },
58 never,
59 Providers
60>;
61
62/**
63 * An API Hub runtime project attachment. Attaching a runtime project
64 * lets API Hub discover deployments in that project.
65 *
66 * Attachments have no labels, description, or display name, and the id
67 * is forced to the runtime project id, so `list` cannot stamp ownership.
68 * The resource is existence-only: runtime project and location are
69 * identity, and there is nothing mutable to sync.
70 *
71 * ### Creating a Runtime Project Attachment
72 * **Example:** Attach the stack project
73 * ```typescript
74 * const attachment = yield* GCP.ApiHub.RuntimeProjectAttachment(
75 * "Runtime",
76 * {},
77 * );
78 * ```
79 *
80 * **Example:** Attach a specific runtime project
81 * ```typescript
82 * const attachment = yield* GCP.ApiHub.RuntimeProjectAttachment(
83 * "Runtime",
84 * {
85 * runtimeProject: "projects/my-runtime",
86 * location: "us-central1",
87 * },
88 * );
89 * ```
90 *
91 * @resource
92 * @category ApiHub
93 */
94export const RuntimeProjectAttachment = Resource<RuntimeProjectAttachment>(
95 "GCP.ApiHub.RuntimeProjectAttachment",
96);
97
98const resourceName = (
99 project: string,
100 location: string,
101 attachmentId: string,
102) =>
103 `${locationParent(project, location)}/runtimeProjectAttachments/${attachmentId}`;
104
105const toAttrs = (
106 attachment: apihub.GoogleCloudApihubV1RuntimeProjectAttachment,
107 project: string,
108 region: string,
109) => {
110 const name = attachment.name ?? "";
111 const parsed = parseName(name, "runtimeProjectAttachments", region);
112 return {
113 name,
114 runtimeProjectAttachmentId: parsed.id,
115 project: parsed.project || project,
116 location: parsed.location,
117 runtimeProject: attachment.runtimeProject,
118 createTime: attachment.createTime,
119 };
120};
121
122const getByName = (name: string) =>
123 name.length === 0
124 ? Effect.succeed(undefined)
125 : apihub
126 .getProjectsLocationsRuntimeProjectAttachments({ name })
127 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
128
129export const RuntimeProjectAttachmentProvider = () =>
130 Provider.succeed(RuntimeProjectAttachment, {
131 stables: [
132 "name",
133 "runtimeProjectAttachmentId",
134 "project",
135 "location",
136 "runtimeProject",
137 "createTime",
138 ],
139
140 diff: Effect.fn(function* ({ news, olds, output }) {
141 if (!isResolved(news)) return undefined;
142 const env = yield* GcpEnvironment.current;
143 const previousProject = projectIdOf(
144 olds?.runtimeProject ?? output?.runtimeProject,
145 "",
146 );
147 const nextProject = projectIdOf(
148 news.runtimeProject ?? olds?.runtimeProject ?? output?.runtimeProject,
149 previousProject,
150 );
151 return replaceOnIdentity({
152 previousId:
153 olds?.runtimeProjectAttachmentId ??
154 output?.runtimeProjectAttachmentId,
155 nextId:
156 news.runtimeProjectAttachmentId ??
157 olds?.runtimeProjectAttachmentId ??
158 output?.runtimeProjectAttachmentId,
159 previousLocation: normalizeLocation(
160 olds?.location ?? output?.location,
161 env.region,
162 ),
163 nextLocation: normalizeLocation(
164 news.location ?? olds?.location ?? output?.location,
165 env.region,
166 ),
167 extra:
168 previousProject.length > 0 &&
169 nextProject.length > 0 &&
170 previousProject !== nextProject,
171 });
172 }),
173
174 read: Effect.fn(function* ({ olds, output }) {
175 const env = yield* GcpEnvironment.current;
176 const runtimeProjectId = projectIdOf(
177 olds?.runtimeProject ?? output?.runtimeProject,
178 env.project,
179 );
180 const attachmentId =
181 olds?.runtimeProjectAttachmentId ??
182 output?.runtimeProjectAttachmentId ??
183 runtimeProjectId;
184 const location = normalizeLocation(
185 olds?.location ?? output?.location,
186 env.region,
187 );
188 const name =
189 output?.name ?? resourceName(env.project, location, attachmentId);
190 const existing = yield* getByName(name);
191 if (existing === undefined) return undefined;
192 const attrs = toAttrs(existing, env.project, env.region);
193 // Attachments cannot carry ownership metadata. Adopt only when the
194 // observed id matches the deterministic runtime project id we would
195 // have created; anything else is foreign.
196 return attrs.runtimeProjectAttachmentId === attachmentId
197 ? attrs
198 : Unowned(attrs);
199 }),
200
201 list: () =>
202 Effect.gen(function* () {
203 const env = yield* GcpEnvironment.current;
204 // No labels or description — return nothing so nuke does not
205 // delete foreign attachments.
206 return yield* apihub.listProjectsLocationsRuntimeProjectAttachments
207 .pages({
208 parent: locationParent(env.project, env.region),
209 pageSize: 1000,
210 })
211 .pipe(
212 Stream.flatMap((page) =>
213 Stream.fromIterable(page.runtimeProjectAttachments ?? []),
214 ),
215 Stream.filter(
216 (item) =>
217 projectIdOf(item.runtimeProject, "") === env.project ||
218 parseName(
219 item.name ?? "",
220 "runtimeProjectAttachments",
221 env.region,
222 ).id === env.project,
223 ),
224 Stream.map((item) => toAttrs(item, env.project, env.region)),
225 Stream.runCollect,
226 Effect.map((chunk) => Array.from(chunk)),
227 Effect.catchTag("NotFound", () => Effect.succeed([])),
228 );
229 }),
230
231 reconcile: Effect.fn(function* ({ news, output }) {
232 const env = yield* GcpEnvironment.current;
233 const location = normalizeLocation(
234 news.location ?? output?.location,
235 env.region,
236 );
237 const runtimeProject = projectNameOf(news.runtimeProject, env.project);
238 const attachmentId =
239 news.runtimeProjectAttachmentId ??
240 output?.runtimeProjectAttachmentId ??
241 projectIdOf(runtimeProject, env.project);
242 const name = resourceName(env.project, location, attachmentId);
243 const parent = locationParent(env.project, location);
244
245 let current = yield* getByName(output?.name ?? name);
246
247 if (current === undefined) {
248 const created = yield* apihub
249 .createProjectsLocationsRuntimeProjectAttachments({
250 parent,
251 runtimeProjectAttachmentId: attachmentId,
252 body: {
253 runtimeProject,
254 },
255 })
256 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
257 current = created ?? (yield* getByName(name));
258 }
259
260 if (current === undefined) {
261 return yield* new ApihubNotResolved({ name });
262 }
263
264 return toAttrs(current, env.project, env.region);
265 }),
266
267 delete: Effect.fn(function* ({ output }) {
268 yield* apihub
269 .deleteProjectsLocationsRuntimeProjectAttachments({
270 name: output.name,
271 })
272 .pipe(Effect.catchTag("NotFound", () => Effect.void));
273 }),
274 });