1import * as redshift from "@distilled.cloud/aws/redshift";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts";
10import { AWSEnvironment } from "../Environment.ts";
11import type { Providers } from "../Providers.ts";
12import { applyRedshiftTagDelta, redshiftArn, toTagRecord } from "./internal.ts";
13
14export interface ClusterParameterGroupProps {
15 /**
16 * Name of the cluster parameter group. Must be 1-255 lowercase
17 * alphanumeric characters or hyphens, starting with a letter, and must
18 * not be prefixed `default`. If omitted, a deterministic physical name is
19 * generated. Changing the name replaces the parameter group.
20 */
21 clusterParameterGroupName?: string;
22 /**
23 * Parameter group family the group applies to, e.g. `redshift-2.0` (or `redshift-1.0`).
24 * Changing the family replaces the parameter group.
25 */
26 family: string;
27 /**
28 * Human-readable description. Changing the description replaces the
29 * parameter group (Redshift descriptions are create-only).
30 * @default "Managed by Alchemy"
31 */
32 description?: string;
33 /**
34 * Cluster database parameters to override from the family defaults, e.g.
35 * `{ enable_user_activity_logging: "true" }`. Parameters removed from
36 * this map are reset to their engine defaults.
37 */
38 parameters?: Record<string, string>;
39 /**
40 * User-defined tags for the parameter group.
41 */
42 tags?: Record<string, string>;
43}
44
45export interface ClusterParameterGroup extends Resource<
46 "AWS.Redshift.ClusterParameterGroup",
47 ClusterParameterGroupProps,
48 {
49 /**
50 * Name of the parameter group.
51 */
52 clusterParameterGroupName: string;
53 /**
54 * ARN of the parameter group.
55 */
56 clusterParameterGroupArn: string;
57 /**
58 * Parameter group family (e.g. `"redshift-2.0"`).
59 */
60 family: string;
61 /**
62 * Description of the parameter group.
63 */
64 description: string | undefined;
65 /**
66 * Non-default parameter values applied to the group.
67 */
68 parameters: Record<string, string>;
69 /**
70 * Tags on the parameter group (including internal Alchemy tags).
71 */
72 tags: Record<string, string>;
73 },
74 never,
75 Providers
76> {}
77
78/**
79 * An Amazon Redshift cluster parameter group — a named set of database
80 * parameters applied to provisioned Redshift clusters.
81 *
82 * Parameter groups are free and provision instantly. A {@link Cluster}
83 * references one by name via `clusterParameterGroupName`; parameter changes
84 * take effect after the cluster reboots.
85 * ### Creating a Parameter Group
86 * **Example:** Default Parameter Group
87 * ```typescript
88 * const params = yield* Redshift.ClusterParameterGroup("WarehouseParams", {
89 * family: "redshift-2.0",
90 * });
91 * ```
92 * **Example:** Overriding Parameters
93 * ```typescript
94 * const params = yield* Redshift.ClusterParameterGroup("WarehouseParams", {
95 * family: "redshift-2.0",
96 * parameters: {
97 * enable_user_activity_logging: "true",
98 * statement_timeout: "60000",
99 * },
100 * });
101 * ```
102 *
103 * @resource
104 */
105export const ClusterParameterGroup = Resource<ClusterParameterGroup>(
106 "AWS.Redshift.ClusterParameterGroup",
107);
108
109export const ClusterParameterGroupProvider = () =>
110 Provider.effect(
111 ClusterParameterGroup,
112 Effect.gen(function* () {
113 const toName = (
114 id: string,
115 props: Partial<ClusterParameterGroupProps>,
116 ) =>
117 props.clusterParameterGroupName
118 ? Effect.succeed(props.clusterParameterGroupName)
119 : createPhysicalName({ id, maxLength: 255, lowercase: true });
120
121 const readGroup = Effect.fn(function* (name: string) {
122 const response = yield* redshift
123 .describeClusterParameterGroups({ ParameterGroupName: name })
124 .pipe(
125 Effect.catchTag("ClusterParameterGroupNotFoundFault", () =>
126 Effect.succeed(undefined),
127 ),
128 );
129 return response?.ParameterGroups?.[0];
130 });
131
132 // User-sourced parameter overrides currently applied to the group.
133 const readUserParameters = Effect.fn(function* (name: string) {
134 const parameters = yield* redshift.describeClusterParameters
135 .items({ ParameterGroupName: name, Source: "user" })
136 .pipe(Stream.runCollect, Effect.map(Array.from<redshift.Parameter>));
137 return Object.fromEntries(
138 parameters.flatMap((parameter) =>
139 parameter.ParameterName !== undefined &&
140 parameter.ParameterValue !== undefined
141 ? [[parameter.ParameterName, parameter.ParameterValue]]
142 : [],
143 ),
144 );
145 });
146
147 const toAttrs = Effect.fn(function* (
148 group: redshift.ClusterParameterGroup,
149 ) {
150 const { accountId, region } = yield* AWSEnvironment.current;
151 if (!group.ParameterGroupName) {
152 return yield* Effect.fail(
153 new Error("Cluster parameter group is missing its name"),
154 );
155 }
156 return {
157 clusterParameterGroupName: group.ParameterGroupName,
158 clusterParameterGroupArn: redshiftArn(
159 region,
160 accountId,
161 "parametergroup",
162 group.ParameterGroupName,
163 ),
164 family: group.ParameterGroupFamily ?? "",
165 description: group.Description,
166 parameters: yield* readUserParameters(group.ParameterGroupName),
167 tags: toTagRecord(group.Tags),
168 };
169 });
170
171 return {
172 stables: ["clusterParameterGroupName", "clusterParameterGroupArn"],
173
174 diff: Effect.fn(function* ({ id, olds, news }) {
175 if (!isResolved(news)) return undefined;
176 if (
177 (yield* toName(id, olds ?? {})) !== (yield* toName(id, news ?? {}))
178 ) {
179 return { action: "replace" } as const;
180 }
181 // Family and description are create-only.
182 if (
183 olds?.family !== news.family ||
184 olds?.description !== news.description
185 ) {
186 return { action: "replace" } as const;
187 }
188 }),
189
190 read: Effect.fn(function* ({ id, olds, output }) {
191 const name =
192 output?.clusterParameterGroupName ??
193 (yield* toName(id, olds ?? {}));
194 const group = yield* readGroup(name);
195 if (!group?.ParameterGroupName) return undefined;
196 const attrs = yield* toAttrs(group);
197 return (yield* hasAlchemyTags(id, attrs.tags))
198 ? attrs
199 : Unowned(attrs);
200 }),
201
202 reconcile: Effect.fn(function* ({ id, news, output, session }) {
203 const name =
204 output?.clusterParameterGroupName ?? (yield* toName(id, news));
205 const internalTags = yield* createInternalTags(id);
206 const desiredTags = { ...internalTags, ...news.tags };
207
208 // 1. Observe — cloud state is authoritative.
209 let observed = yield* readGroup(name);
210
211 // 2. Ensure — create if missing; tolerate AlreadyExists as a race
212 // with a peer reconciler by re-reading.
213 if (!observed?.ParameterGroupName) {
214 yield* redshift
215 .createClusterParameterGroup({
216 ParameterGroupName: name,
217 ParameterGroupFamily: news.family,
218 Description: news.description ?? "Managed by Alchemy",
219 Tags: Object.entries(desiredTags).map(([Key, Value]) => ({
220 Key,
221 Value,
222 })),
223 })
224 .pipe(
225 Effect.catchTag(
226 "ClusterParameterGroupAlreadyExistsFault",
227 () => Effect.void,
228 ),
229 );
230 observed = yield* readGroup(name);
231 if (!observed?.ParameterGroupName) {
232 return yield* Effect.fail(
233 new Error(`Failed to create cluster parameter group '${name}'`),
234 );
235 }
236 }
237
238 // 3. Sync parameters — diff OBSERVED user-sourced overrides
239 // against desired; changed/added values are applied, removed
240 // ones are reset to the engine defaults.
241 const observedParameters = yield* readUserParameters(name);
242 const desiredParameters = news.parameters ?? {};
243 const upsertParameters = Object.entries(desiredParameters)
244 .filter(([key, value]) => observedParameters[key] !== value)
245 .map(([ParameterName, ParameterValue]) => ({
246 ParameterName,
247 ParameterValue,
248 }));
249 const resetParameters = Object.keys(observedParameters)
250 .filter((key) => !(key in desiredParameters))
251 .map((ParameterName) => ({ ParameterName }));
252 if (upsertParameters.length > 0) {
253 yield* redshift.modifyClusterParameterGroup({
254 ParameterGroupName: name,
255 Parameters: upsertParameters,
256 });
257 }
258 if (resetParameters.length > 0) {
259 yield* redshift.resetClusterParameterGroup({
260 ParameterGroupName: name,
261 ResetAllParameters: false,
262 Parameters: resetParameters,
263 });
264 }
265
266 // 3b. Sync tags — diff against OBSERVED cloud tags (describe
267 // surfaces them inline).
268 const { accountId, region } = yield* AWSEnvironment.current;
269 const arn = redshiftArn(region, accountId, "parametergroup", name);
270 const { removed, upsert } = diffTags(
271 toTagRecord(observed.Tags),
272 desiredTags,
273 );
274 yield* applyRedshiftTagDelta({ arn, upsert, removed });
275
276 yield* session.note(arn);
277 return {
278 clusterParameterGroupName: name,
279 clusterParameterGroupArn: arn,
280 family: observed.ParameterGroupFamily ?? news.family,
281 description: observed.Description,
282 parameters: desiredParameters,
283 tags: desiredTags,
284 };
285 }),
286
287 delete: Effect.fn(function* ({ output }) {
288 yield* redshift
289 .deleteClusterParameterGroup({
290 ParameterGroupName: output.clusterParameterGroupName,
291 })
292 .pipe(
293 Effect.catchTag(
294 "ClusterParameterGroupNotFoundFault",
295 () => Effect.void,
296 ),
297 );
298 }),
299
300 list: () =>
301 // Top-level account/region collection: exhaustively paginate
302 // describeClusterParameterGroups. AWS-managed `default.*` groups
303 // cannot be deleted — don't enumerate them.
304 redshift.describeClusterParameterGroups.pages({}).pipe(
305 Stream.runCollect,
306 Effect.map((chunk) =>
307 Array.from(chunk).flatMap((page) =>
308 (page.ParameterGroups ?? []).filter(
309 (group) =>
310 group.ParameterGroupName !== undefined &&
311 !group.ParameterGroupName.startsWith("default."),
312 ),
313 ),
314 ),
315 Effect.flatMap(
316 Effect.forEach((group) => toAttrs(group), { concurrency: 4 }),
317 ),
318 ),
319 };
320 }),
321 );