1import * as apigee from "@distilled.cloud/gcp/apigee_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { tagRecord } from "../../Tags.ts";
9import { GcpEnvironment } from "../Environment.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 deployedConfig,
13 encodeDescription,
14 environmentIdOf,
15 environmentNameOf,
16 hasOwnershipMarker,
17 lastSegment,
18 listProjectEnvironments,
19 missingToUndefined,
20 namesFromConfig,
21 organizationIdOf,
22 ownedById,
23 parseDescription,
24 parseOrgEnv,
25 sameText,
26 toResourceId,
27} from "./common.ts";
28import { createInternalLabels } from "../Labels.ts";
29
30const MAX_NAME_LENGTH = 255;
31const DEFAULT_RESOURCE_TYPE = "KeyStore";
32
33export type EnvironmentsReferenceProps = {
34 /**
35 * Apigee organization id or `organizations/{org}`. Defaults to the
36 * current GCP project id. Immutable — changing it replaces the reference.
37 */
38 organization?: string;
39 /**
40 * Environment id or `organizations/{org}/environments/{env}`. Immutable —
41 * changing it replaces the reference.
42 */
43 environment: string;
44 /**
45 * Reference id. If omitted, a unique name is generated from the stack,
46 * stage, and logical id. Immutable — changing it replaces the reference.
47 */
48 referenceId?: string;
49 /**
50 * Id of the keystore or truststore this reference points at. Must exist
51 * in the same environment.
52 */
53 refers: string;
54 /**
55 * Resource type referred to. Valid values are `KeyStore` or `TrustStore`.
56 * @default "KeyStore"
57 */
58 resourceType?: string;
59 /**
60 * Human-readable description. Alchemy stamps ownership into a
61 * `[alchemy …]` prefix because references have no labels field.
62 */
63 description?: string;
64};
65
66export type EnvironmentsReference = Resource<
67 "GCP.Apigee.EnvironmentsReference",
68 EnvironmentsReferenceProps,
69 {
70 /** Full resource name `organizations/{org}/environments/{env}/references/{ref}`. */
71 name: string;
72 /** Reference id. */
73 referenceId: string;
74 /** Apigee organization id. */
75 organizationId: string;
76 /** Environment id. */
77 environmentId: string;
78 /** Id of the referred keystore or truststore. */
79 refers: string;
80 /** Resource type (`KeyStore` or `TrustStore`). */
81 resourceType: string | undefined;
82 /** User description with the Alchemy ownership prefix stripped. */
83 description: string | undefined;
84 },
85 never,
86 Providers
87>;
88
89/**
90 * An Apigee environment reference to a keystore or truststore.
91 *
92 * References have no labels, so Alchemy stamps ownership into
93 * `description` for `list` / nuke. Name is identity; `refers`,
94 * `resourceType`, and description update in place.
95 *
96 * ### Creating a Reference
97 * **Example:** Point at a keystore
98 * ```typescript
99 * const keystore = yield* GCP.Apigee.EnvironmentsKeystore("Tls", {
100 * environment: "eval",
101 * });
102 * const reference = yield* GCP.Apigee.EnvironmentsReference("TlsRef", {
103 * environment: "eval",
104 * refers: keystore.keystoreId,
105 * resourceType: "KeyStore",
106 * });
107 * ```
108 *
109 * @resource
110 * @category Apigee
111 */
112export const EnvironmentsReference = Resource<EnvironmentsReference>(
113 "GCP.Apigee.EnvironmentsReference",
114);
115
116export class EnvironmentsReferenceNotResolved extends Data.TaggedError(
117 "GCP.Apigee.EnvironmentsReferenceNotResolved",
118)<{
119 name: string;
120}> {}
121
122const resourceName = (
123 organizationId: string,
124 environmentId: string,
125 referenceId: string,
126) =>
127 `${environmentNameOf(organizationId, environmentId)}/references/${referenceId}`;
128
129const toAttrs = (
130 reference: apigee.GoogleCloudApigeeV1Reference,
131 organizationId: string,
132 environmentId: string,
133) => {
134 const raw = reference.name ?? "";
135 const parsed = parseOrgEnv(raw);
136 const referenceId = lastSegment(raw);
137 const description = parseDescription(reference.description);
138 return {
139 name: raw.includes("/")
140 ? raw
141 : resourceName(organizationId, environmentId, referenceId || raw),
142 referenceId: referenceId || raw,
143 organizationId: parsed.organizationId || organizationId,
144 environmentId: parsed.environmentId || environmentId,
145 refers: reference.refers ?? "",
146 resourceType: reference.resourceType,
147 description: description.description,
148 };
149};
150
151const getByName = (name: string) =>
152 missingToUndefined(apigee.getOrganizationsEnvironmentsReferences({ name }));
153
154export const EnvironmentsReferenceProvider = () =>
155 Provider.succeed(EnvironmentsReference, {
156 stables: ["name", "referenceId", "organizationId", "environmentId"],
157
158 diff: Effect.fn(function* ({ news, olds, output }) {
159 if (!isResolved(news)) return undefined;
160 const previousId = olds?.referenceId ?? output?.referenceId;
161 const previousOrg = olds?.organization ?? output?.organizationId;
162 const previousEnv = olds?.environment ?? output?.environmentId;
163 const idChanged =
164 previousId !== undefined &&
165 news.referenceId !== undefined &&
166 news.referenceId !== previousId;
167 const orgChanged =
168 previousOrg !== undefined &&
169 news.organization !== undefined &&
170 organizationIdOf(news.organization, "") !==
171 organizationIdOf(previousOrg, "");
172 const envChanged =
173 previousEnv !== undefined &&
174 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
175 if (idChanged || orgChanged || envChanged) {
176 return { action: "replace" as const, deleteFirst: false };
177 }
178 return undefined;
179 }),
180
181 read: Effect.fn(function* ({ id, olds, output }) {
182 const { project } = yield* GcpEnvironment.current;
183 const organizationId = organizationIdOf(
184 olds?.organization ?? output?.organizationId,
185 project,
186 );
187 const environmentId = environmentIdOf(
188 olds?.environment ?? output?.environmentId ?? "",
189 );
190 const referenceId = yield* toResourceId(
191 id,
192 olds?.referenceId,
193 output?.referenceId,
194 { maxLength: MAX_NAME_LENGTH },
195 );
196 const name =
197 output?.name ??
198 resourceName(organizationId, environmentId, referenceId);
199 const existing = yield* getByName(name);
200 if (existing === undefined) return undefined;
201 const attrs = toAttrs(existing, organizationId, environmentId);
202 const { labels } = parseDescription(existing.description);
203 return (yield* ownedById(id, tagRecord(labels))) ? attrs : Unowned(attrs);
204 }),
205
206 list: () =>
207 Effect.gen(function* () {
208 const environments = yield* listProjectEnvironments();
209 const found: EnvironmentsReference["Attributes"][] = [];
210 for (const item of environments) {
211 const ids = namesFromConfig(
212 (yield* deployedConfig(item.parent))?.resourceReferences,
213 );
214 for (const raw of ids) {
215 const name = raw.includes("/")
216 ? raw
217 : resourceName(item.organizationId, item.environmentId, raw);
218 const reference = yield* getByName(name);
219 if (reference === undefined) continue;
220 if (!hasOwnershipMarker(reference.description)) continue;
221 found.push(
222 toAttrs(reference, item.organizationId, item.environmentId),
223 );
224 }
225 }
226 return found;
227 }),
228
229 reconcile: Effect.fn(function* ({ id, news, output }) {
230 const { project } = yield* GcpEnvironment.current;
231 const organizationId = organizationIdOf(news.organization, project);
232 const environmentId = environmentIdOf(news.environment);
233 const referenceId = yield* toResourceId(
234 id,
235 news.referenceId,
236 output?.referenceId,
237 { maxLength: MAX_NAME_LENGTH },
238 );
239 const parent = environmentNameOf(organizationId, environmentId);
240 const name = resourceName(organizationId, environmentId, referenceId);
241 const ownership = yield* createInternalLabels(id);
242 const desiredDescription = encodeDescription(ownership, news.description);
243 const desiredType = news.resourceType ?? DEFAULT_RESOURCE_TYPE;
244
245 let current = yield* getByName(output?.name ?? name);
246
247 if (current === undefined) {
248 const created = yield* apigee
249 .createOrganizationsEnvironmentsReferences({
250 parent,
251 body: {
252 name: referenceId,
253 refers: news.refers,
254 resourceType: desiredType,
255 description: desiredDescription,
256 },
257 })
258 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
259 current = created ?? undefined;
260 }
261
262 if (current === undefined) {
263 return yield* new EnvironmentsReferenceNotResolved({ name });
264 }
265
266 if (
267 !sameText(current.refers, news.refers) ||
268 !sameText(current.resourceType, desiredType) ||
269 !sameText(current.description, desiredDescription)
270 ) {
271 current = yield* apigee.updateOrganizationsEnvironmentsReferences({
272 name,
273 body: {
274 name: referenceId,
275 refers: news.refers,
276 resourceType: desiredType,
277 description: desiredDescription,
278 },
279 });
280 }
281
282 return toAttrs(current, organizationId, environmentId);
283 }),
284
285 delete: Effect.fn(function* ({ output }) {
286 yield* apigee
287 .deleteOrganizationsEnvironmentsReferences({ name: output.name })
288 .pipe(
289 Effect.catchTag(
290 ["NotFound", "ApigeeResourceNotFound"],
291 () => Effect.void,
292 ),
293 );
294 }),
295 });