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 { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 defaultOrgName,
12 lastSegment,
13 letterPrefixedId,
14 orgIdOf,
15 orgNameOf,
16 sameStringList,
17 waitForOperation,
18} from "./operations.ts";
19
20export type EnvgroupProps = {
21 /**
22 * Apigee organization id or `organizations/{org}`. Defaults to the
23 * current GCP project id. Immutable — changing it replaces the group.
24 */
25 organization?: string;
26 /**
27 * Environment group id (the `{envgroup}` segment of
28 * `organizations/{org}/envgroups/{envgroup}`). If omitted, a unique
29 * name is generated. Immutable — changing it replaces the group.
30 */
31 envgroupId?: string;
32 /**
33 * Host names for this environment group.
34 */
35 hostnames: string[];
36};
37
38export type Envgroup = Resource<
39 "GCP.Apigee.Envgroup",
40 EnvgroupProps,
41 {
42 /** Full resource name `organizations/{org}/envgroups/{envgroup}`. */
43 name: string;
44 /** Environment group id (last path segment). */
45 envgroupId: string;
46 /** Apigee organization id. */
47 organization: string;
48 /** Host names routed to this group. */
49 hostnames: string[];
50 /** Server-reported state (`CREATING`, `ACTIVE`, …). */
51 state: string | undefined;
52 /** Creation time in milliseconds since epoch. */
53 createdAt: string | undefined;
54 /** Last modification time in milliseconds since epoch. */
55 lastModifiedAt: string | undefined;
56 },
57 never,
58 Providers
59>;
60
61/**
62 * An Apigee environment group that maps hostnames to one or more
63 * environments.
64 *
65 * The API has no labels or description, so Alchemy identifies its groups
66 * by the generated `envgroupId`: `read` reports a group with an explicit
67 * `envgroupId` as unowned (adopt it with `--adopt`), and nuke cannot
68 * discover groups. Name is identity — changing `envgroupId` replaces the
69 * group. Hostnames update in place.
70 *
71 * ### Creating an Environment Group
72 * **Example:** Group with a hostname
73 * ```typescript
74 * const group = yield* GCP.Apigee.Envgroup("Api", {
75 * hostnames: ["api.example.com"],
76 * });
77 * ```
78 *
79 * **Example:** Named group
80 * ```typescript
81 * const group = yield* GCP.Apigee.Envgroup("Api", {
82 * envgroupId: "prod-group",
83 * hostnames: ["api.example.com", "api.example.net"],
84 * });
85 * ```
86 *
87 * @resource
88 * @category Apigee
89 */
90export const Envgroup = Resource<Envgroup>("GCP.Apigee.Envgroup");
91
92export class EnvgroupNotResolved extends Data.TaggedError(
93 "GCP.Apigee.EnvgroupNotResolved",
94)<{
95 name: string;
96}> {}
97
98const resourceName = (organization: string, envgroupId: string) =>
99 `${orgNameOf(organization)}/envgroups/${envgroupId}`;
100
101const toAttrs = (
102 group: apigee.GoogleCloudApigeeV1EnvironmentGroup,
103 organization: string,
104) => {
105 const name = group.name ?? "";
106 return {
107 name,
108 envgroupId: lastSegment(name),
109 organization: orgIdOf(organization),
110 hostnames: [...(group.hostnames ?? [])],
111 state: group.state,
112 createdAt: group.createdAt,
113 lastModifiedAt: group.lastModifiedAt,
114 };
115};
116
117const getByName = (name: string) =>
118 apigee
119 .getOrganizationsEnvgroups({ name })
120 .pipe(
121 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
122 Effect.succeed(undefined),
123 ),
124 );
125
126export const EnvgroupProvider = () =>
127 Provider.succeed(Envgroup, {
128 stables: ["name", "envgroupId", "organization", "createdAt"],
129
130 diff: Effect.fn(function* ({ news, olds, output }) {
131 if (!isResolved(news)) return undefined;
132 const previousId = olds?.envgroupId ?? output?.envgroupId;
133 const previousOrg = olds?.organization ?? output?.organization;
134 const idChanged =
135 previousId !== undefined &&
136 news.envgroupId !== undefined &&
137 news.envgroupId !== previousId;
138 const orgChanged =
139 previousOrg !== undefined &&
140 news.organization !== undefined &&
141 orgIdOf(news.organization) !== orgIdOf(previousOrg);
142 if (idChanged || orgChanged) {
143 return {
144 action: "replace" as const,
145 deleteFirst: idChanged && !orgChanged,
146 };
147 }
148 return undefined;
149 }),
150
151 read: Effect.fn(function* ({ id, olds, output }) {
152 const env = yield* GcpEnvironment.current;
153 const organization = defaultOrgName(
154 env.project,
155 olds?.organization ?? output?.organization,
156 );
157 const envgroupId = yield* letterPrefixedId(
158 id,
159 olds?.envgroupId,
160 output?.envgroupId,
161 63,
162 );
163 const name = output?.name ?? resourceName(organization, envgroupId);
164 const existing = yield* getByName(name);
165 if (existing === undefined) return undefined;
166 const attrs = toAttrs(existing, organization);
167 const generated = yield* letterPrefixedId(id, undefined, undefined, 63);
168 return attrs.envgroupId === generated ? attrs : Unowned(attrs);
169 }),
170
171 reconcile: Effect.fn(function* ({ id, news, output }) {
172 const env = yield* GcpEnvironment.current;
173 const organization = defaultOrgName(env.project, news.organization);
174 const envgroupId = yield* letterPrefixedId(
175 id,
176 news.envgroupId,
177 output?.envgroupId,
178 63,
179 );
180 const name = resourceName(organization, envgroupId);
181 const desiredHostnames = news.hostnames;
182
183 let current = yield* getByName(output?.name ?? name);
184
185 if (current === undefined) {
186 const operation = yield* apigee
187 .createOrganizationsEnvgroups({
188 parent: organization,
189 name: envgroupId,
190 body: {
191 name: envgroupId,
192 hostnames: desiredHostnames,
193 },
194 })
195 .pipe(Effect.catchTag("Conflict", () => Effect.succeed(undefined)));
196 if (operation !== undefined) {
197 yield* waitForOperation(operation);
198 }
199 current = yield* getByName(name);
200 }
201
202 if (current === undefined) {
203 return yield* new EnvgroupNotResolved({ name });
204 }
205
206 if (!sameStringList(current.hostnames, desiredHostnames)) {
207 const operation = yield* apigee.patchOrganizationsEnvgroups({
208 name,
209 updateMask: "hostnames",
210 body: { hostnames: desiredHostnames },
211 });
212 yield* waitForOperation(operation);
213 current = (yield* getByName(name)) ?? current;
214 }
215
216 return toAttrs(current, organization);
217 }),
218
219 delete: Effect.fn(function* ({ output }) {
220 const operation = yield* apigee
221 .deleteOrganizationsEnvgroups({ name: output.name })
222 .pipe(
223 Effect.catchTag(["NotFound", "ApigeeResourceNotFound"], () =>
224 Effect.succeed(undefined),
225 ),
226 );
227 if (operation !== undefined) {
228 yield* waitForOperation(operation, { notFoundOk: true });
229 }
230 }),
231 });