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 deployedConfig,
11 environmentIdOf,
12 environmentNameOf,
13 lastSegment,
14 listProjectEnvironments,
15 missingToUndefined,
16 namesFromConfig,
17 organizationIdOf,
18 parseOrgEnv,
19 toResourceId,
20} from "./common.ts";
21
22const MAX_NAME_LENGTH = 255;
23
24export type EnvironmentsKeystoreProps = {
25 /**
26 * Apigee organization id or `organizations/{org}`. Defaults to the
27 * current GCP project id. Immutable — changing it replaces the keystore.
28 */
29 organization?: string;
30 /**
31 * Environment id or `organizations/{org}/environments/{env}`. Immutable —
32 * changing it replaces the keystore.
33 */
34 environment: string;
35 /**
36 * Keystore id (last path segment). If omitted, a unique name is generated
37 * from the stack, stage, and logical id. Must match
38 * `[\w[:space:].-]{1,255}`. Immutable — changing it replaces the keystore.
39 */
40 keystoreId?: string;
41};
42
43export type EnvironmentsKeystore = Resource<
44 "GCP.Apigee.EnvironmentsKeystore",
45 EnvironmentsKeystoreProps,
46 {
47 /** Full resource name `organizations/{org}/environments/{env}/keystores/{keystore}`. */
48 name: string;
49 /** Keystore id (last path segment). */
50 keystoreId: string;
51 /** Apigee organization id. */
52 organizationId: string;
53 /** Environment id. */
54 environmentId: string;
55 /** Alias ids stored in this keystore. */
56 aliases: string[];
57 },
58 never,
59 Providers
60>;
61
62/**
63 * An Apigee environment keystore or truststore for TLS certificates.
64 *
65 * Keystores have no labels or description, so `list` enumerates every
66 * keystore in Apigee environments mapped to this GCP project for
67 * `pnpm nuke:gcp`. Name is identity — changing `keystoreId`,
68 * `organization`, or `environment` replaces the keystore. Aliases are
69 * managed by `EnvironmentsKeystoresAlias`.
70 *
71 * ### Creating a Keystore
72 * **Example:** Generated name
73 * ```typescript
74 * const keystore = yield* GCP.Apigee.EnvironmentsKeystore("Tls", {
75 * environment: "eval",
76 * });
77 * ```
78 *
79 * **Example:** Named keystore
80 * ```typescript
81 * const keystore = yield* GCP.Apigee.EnvironmentsKeystore("Tls", {
82 * environment: "eval",
83 * keystoreId: "app-tls",
84 * });
85 * ```
86 *
87 * @resource
88 * @category Apigee
89 */
90export const EnvironmentsKeystore = Resource<EnvironmentsKeystore>(
91 "GCP.Apigee.EnvironmentsKeystore",
92);
93
94export class EnvironmentsKeystoreNotResolved extends Data.TaggedError(
95 "GCP.Apigee.EnvironmentsKeystoreNotResolved",
96)<{
97 name: string;
98}> {}
99
100const resourceName = (
101 organizationId: string,
102 environmentId: string,
103 keystoreId: string,
104) =>
105 `${environmentNameOf(organizationId, environmentId)}/keystores/${keystoreId}`;
106
107const toAttrs = (
108 keystore: apigee.GoogleCloudApigeeV1Keystore,
109 organizationId: string,
110 environmentId: string,
111) => {
112 const raw = keystore.name ?? "";
113 const parsed = parseOrgEnv(raw);
114 const keystoreId = lastSegment(raw);
115 return {
116 name: raw.includes("/")
117 ? raw
118 : resourceName(organizationId, environmentId, keystoreId || raw),
119 keystoreId: keystoreId || raw,
120 organizationId: parsed.organizationId || organizationId,
121 environmentId: parsed.environmentId || environmentId,
122 aliases: keystore.aliases ?? [],
123 };
124};
125
126const getByName = (name: string) =>
127 missingToUndefined(apigee.getOrganizationsEnvironmentsKeystores({ name }));
128
129const listIds = (parent: string) =>
130 deployedConfig(parent).pipe(
131 Effect.map((config) => namesFromConfig(config?.keystores)),
132 );
133
134export const EnvironmentsKeystoreProvider = () =>
135 Provider.succeed(EnvironmentsKeystore, {
136 stables: ["name", "keystoreId", "organizationId", "environmentId"],
137
138 diff: Effect.fn(function* ({ news, olds, output }) {
139 if (!isResolved(news)) return undefined;
140 const previousId = olds?.keystoreId ?? output?.keystoreId;
141 const previousOrg = olds?.organization ?? output?.organizationId;
142 const previousEnv = olds?.environment ?? output?.environmentId;
143 const idChanged =
144 previousId !== undefined &&
145 news.keystoreId !== undefined &&
146 news.keystoreId !== previousId;
147 const orgChanged =
148 previousOrg !== undefined &&
149 news.organization !== undefined &&
150 organizationIdOf(news.organization, "") !==
151 organizationIdOf(previousOrg, "");
152 const envChanged =
153 previousEnv !== undefined &&
154 environmentIdOf(news.environment) !== environmentIdOf(previousEnv);
155 if (idChanged || orgChanged || envChanged) {
156 return { action: "replace" as const, deleteFirst: false };
157 }
158 return undefined;
159 }),
160
161 read: Effect.fn(function* ({ id, olds, output }) {
162 const { project } = yield* GcpEnvironment.current;
163 const organizationId = organizationIdOf(
164 olds?.organization ?? output?.organizationId,
165 project,
166 );
167 const environmentId = environmentIdOf(
168 olds?.environment ?? output?.environmentId ?? "",
169 );
170 const keystoreId = yield* toResourceId(
171 id,
172 olds?.keystoreId,
173 output?.keystoreId,
174 { maxLength: MAX_NAME_LENGTH },
175 );
176 const name =
177 output?.name ?? resourceName(organizationId, environmentId, keystoreId);
178 const existing = yield* getByName(name);
179 if (existing === undefined) return undefined;
180 return toAttrs(existing, organizationId, environmentId);
181 }),
182
183 list: () =>
184 Effect.gen(function* () {
185 const environments = yield* listProjectEnvironments();
186 const found: EnvironmentsKeystore["Attributes"][] = [];
187 for (const item of environments) {
188 const ids = yield* listIds(item.parent);
189 for (const raw of ids) {
190 const name = raw.includes("/")
191 ? raw
192 : resourceName(item.organizationId, item.environmentId, raw);
193 const keystore = yield* getByName(name);
194 if (keystore === undefined) {
195 found.push(
196 toAttrs({ name }, item.organizationId, item.environmentId),
197 );
198 continue;
199 }
200 found.push(
201 toAttrs(keystore, item.organizationId, item.environmentId),
202 );
203 }
204 }
205 return found;
206 }),
207
208 reconcile: Effect.fn(function* ({ id, news, output }) {
209 const { project } = yield* GcpEnvironment.current;
210 const organizationId = organizationIdOf(news.organization, project);
211 const environmentId = environmentIdOf(news.environment);
212 const keystoreId = yield* toResourceId(
213 id,
214 news.keystoreId,
215 output?.keystoreId,
216 { maxLength: MAX_NAME_LENGTH },
217 );
218 const parent = environmentNameOf(organizationId, environmentId);
219 const name = resourceName(organizationId, environmentId, keystoreId);
220
221 let current = yield* getByName(output?.name ?? name);
222
223 if (current === undefined) {
224 const created = yield* apigee
225 .createOrganizationsEnvironmentsKeystores({
226 parent,
227 name: keystoreId,
228 body: { name: keystoreId },
229 })
230 .pipe(Effect.catchTag("Conflict", () => getByName(name)));
231 current = created ?? undefined;
232 }
233
234 if (current === undefined) {
235 return yield* new EnvironmentsKeystoreNotResolved({ name });
236 }
237
238 return toAttrs(current, organizationId, environmentId);
239 }),
240
241 delete: Effect.fn(function* ({ output }) {
242 yield* apigee
243 .deleteOrganizationsEnvironmentsKeystores({ name: output.name })
244 .pipe(
245 Effect.catchTag(
246 ["NotFound", "ApigeeResourceNotFound"],
247 () => Effect.void,
248 ),
249 );
250 }),
251 });