1import * as deadline from "@distilled.cloud/aws/deadline";
2import * as Effect from "effect/Effect";
3import * as EffectStream 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, hasAlchemyTags, type Tags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import {
12 deadlineArnOf,
13 fetchDeadlineTags,
14 retryThroughIamPropagation,
15 syncDeadlineTags,
16} from "./internal.ts";
17
18export interface MonitorProps {
19 /**
20 * Display name of the monitor, shown on its web page.
21 * @default ${app}-${stage}-${id}
22 */
23 displayName?: string;
24 /**
25 * Subdomain the monitor URL is served under
26 * (`https://{subdomain}.{region}.deadlinecloud.amazonaws.com`).
27 */
28 subdomain: string;
29 /**
30 * ARN of the IAM Identity Center instance responsible for authenticating
31 * monitor users. Changing it replaces the monitor.
32 */
33 identityCenterInstanceArn: string;
34 /**
35 * Region of the IAM Identity Center instance, when it differs from the
36 * monitor's region.
37 */
38 identityCenterRegion?: string;
39 /**
40 * ARN of the IAM role the monitor assumes on behalf of signed-in users.
41 */
42 roleArn: string;
43 /**
44 * Tags to associate with the monitor.
45 */
46 tags?: Record<string, string>;
47}
48
49export interface Monitor extends Resource<
50 "AWS.Deadline.Monitor",
51 MonitorProps,
52 {
53 /**
54 * Service-assigned unique identifier of the monitor (`monitor-...`).
55 */
56 monitorId: string;
57 /**
58 * ARN of the monitor.
59 */
60 monitorArn: string;
61 /**
62 * The monitor's display name.
63 */
64 displayName: string;
65 /**
66 * The configured subdomain.
67 */
68 subdomain: string;
69 /**
70 * Full URL of the monitor's web page.
71 */
72 url: string;
73 /**
74 * ARN of the monitor's user role.
75 */
76 roleArn: string;
77 /**
78 * ARN of the IAM Identity Center instance backing sign-in.
79 */
80 identityCenterInstanceArn: string;
81 /**
82 * ARN of the Identity Center application the monitor registered.
83 */
84 identityCenterApplicationArn: string;
85 /**
86 * Current tags reported for the monitor.
87 */
88 tags: Record<string, string>;
89 },
90 never,
91 Providers
92> {}
93
94/**
95 * An AWS Deadline Cloud monitor — the hosted web console where artists and
96 * administrators view farms, queues, and jobs, authenticated through IAM
97 * Identity Center.
98 *
99 * ### Creating Monitors
100 * **Example:** Basic Monitor
101 * ```typescript
102 * import * as AWS from "alchemy/AWS";
103 *
104 * const monitor = yield* AWS.Deadline.Monitor("StudioMonitor", {
105 * subdomain: "studio-renders",
106 * identityCenterInstanceArn: "arn:aws:sso:::instance/ssoins-1234567890abcdef",
107 * roleArn: monitorRole.roleArn,
108 * });
109 * ```
110 *
111 * **Example:** Export the Monitor URL
112 * ```typescript
113 * // The monitor's web console URL is available as an output attribute —
114 * // return it from the stack so users know where to sign in.
115 * const monitor = yield* AWS.Deadline.Monitor("StudioMonitor", {
116 * subdomain: "studio-renders",
117 * identityCenterInstanceArn: identityCenterArn,
118 * roleArn: monitorRole.roleArn,
119 * });
120 * return { monitorUrl: monitor.url };
121 * ```
122 *
123 * @resource
124 */
125export const Monitor = Resource<Monitor>("AWS.Deadline.Monitor");
126
127const createMonitorName = (
128 id: string,
129 props: { displayName?: string | undefined },
130) =>
131 props.displayName
132 ? Effect.succeed(props.displayName)
133 : createPhysicalName({ id, maxLength: 100 });
134
135interface MonitorState {
136 attrs: Monitor["Attributes"];
137 described: deadline.GetMonitorResponse;
138}
139
140const readMonitorById = Effect.fn(function* (
141 monitorId: string,
142 arnOf: (path: string) => string,
143) {
144 const described = yield* deadline
145 .getMonitor({ monitorId })
146 .pipe(
147 Effect.catchTag("ResourceNotFoundException", () =>
148 Effect.succeed(undefined),
149 ),
150 );
151 if (!described) return undefined;
152 const monitorArn = arnOf(`monitor/${described.monitorId}`);
153 const state: MonitorState = {
154 described,
155 attrs: {
156 monitorId: described.monitorId,
157 monitorArn,
158 displayName: described.displayName,
159 subdomain: described.subdomain,
160 url: described.url,
161 roleArn: described.roleArn,
162 identityCenterInstanceArn: described.identityCenterInstanceArn,
163 identityCenterApplicationArn: described.identityCenterApplicationArn,
164 tags: yield* fetchDeadlineTags(monitorArn),
165 },
166 };
167 return state;
168});
169
170const findMonitorBySubdomain = Effect.fn(function* (
171 subdomain: string,
172 arnOf: (path: string) => string,
173) {
174 const summaries = yield* deadline.listMonitors.items({}).pipe(
175 EffectStream.runCollect,
176 Effect.map((chunk) => Array.from(chunk)),
177 );
178 const match = summaries.find((summary) => summary.subdomain === subdomain);
179 if (!match) return undefined;
180 return yield* readMonitorById(match.monitorId, arnOf);
181});
182
183export const MonitorProvider = () =>
184 Provider.effect(
185 Monitor,
186 Effect.gen(function* () {
187 return {
188 stables: [
189 "monitorId",
190 "monitorArn",
191 "identityCenterInstanceArn",
192 "identityCenterApplicationArn",
193 ],
194 list: () =>
195 Effect.gen(function* () {
196 const arnOf = yield* deadlineArnOf;
197 const summaries = yield* deadline.listMonitors.items({}).pipe(
198 EffectStream.runCollect,
199 Effect.map((chunk) => Array.from(chunk)),
200 );
201 const states = yield* Effect.forEach(
202 summaries,
203 (summary) => readMonitorById(summary.monitorId, arnOf),
204 { concurrency: 4 },
205 );
206 return states
207 .filter((state): state is MonitorState => state !== undefined)
208 .map((state) => state.attrs);
209 }),
210 read: Effect.fn(function* ({ id, olds, output }) {
211 const arnOf = yield* deadlineArnOf;
212 const subdomain = output?.subdomain ?? olds?.subdomain;
213 const state = output?.monitorId
214 ? yield* readMonitorById(output.monitorId, arnOf)
215 : subdomain !== undefined
216 ? yield* findMonitorBySubdomain(subdomain, arnOf)
217 : undefined;
218 if (!state) return undefined;
219 return (yield* hasAlchemyTags(id, state.attrs.tags as Tags))
220 ? state.attrs
221 : Unowned(state.attrs);
222 }),
223 diff: Effect.fn(function* ({ news, olds }) {
224 if (!isResolved(news)) return;
225 if (olds === undefined) return;
226 // The Identity Center instance is fixed at creation.
227 if (
228 olds.identityCenterInstanceArn !== news.identityCenterInstanceArn
229 ) {
230 return { action: "replace" } as const;
231 }
232 }),
233 reconcile: Effect.fn(function* ({ id, news, output, session }) {
234 if (news === undefined) {
235 return yield* Effect.fail(
236 new Error("AWS.Deadline.Monitor requires props"),
237 );
238 }
239 const arnOf = yield* deadlineArnOf;
240 const displayName = yield* createMonitorName(id, news);
241 const internalTags = yield* createInternalTags(id);
242 const desiredTags = { ...internalTags, ...news.tags };
243
244 // Observe.
245 let state = output?.monitorId
246 ? yield* readMonitorById(output.monitorId, arnOf)
247 : yield* findMonitorBySubdomain(news.subdomain, arnOf);
248
249 // Ensure.
250 if (state === undefined) {
251 const created = yield* retryThroughIamPropagation(
252 deadline.createMonitor({
253 displayName,
254 subdomain: news.subdomain,
255 identityCenterInstanceArn: news.identityCenterInstanceArn,
256 identityCenterRegion: news.identityCenterRegion,
257 roleArn: news.roleArn,
258 tags: desiredTags,
259 }),
260 );
261 yield* session.note(
262 `Created monitor ${displayName} (${created.monitorId})`,
263 );
264 state = yield* readMonitorById(created.monitorId, arnOf);
265 if (state === undefined) {
266 return yield* Effect.fail(
267 new Error(`failed to read created monitor ${displayName}`),
268 );
269 }
270 }
271
272 // Sync mutable settings — only when drifted from OBSERVED state.
273 const described = state.described;
274 const needsUpdate =
275 displayName !== described.displayName ||
276 news.subdomain !== described.subdomain ||
277 news.roleArn !== described.roleArn;
278 if (needsUpdate) {
279 yield* deadline.updateMonitor({
280 monitorId: state.attrs.monitorId,
281 displayName,
282 subdomain: news.subdomain,
283 roleArn: news.roleArn,
284 });
285 yield* session.note(`Updated monitor ${displayName}`);
286 }
287
288 // Sync tags — diff against observed cloud tags.
289 yield* syncDeadlineTags(state.attrs.monitorArn, desiredTags);
290
291 yield* session.note(state.attrs.monitorArn);
292 const final = yield* readMonitorById(state.attrs.monitorId, arnOf);
293 if (!final) {
294 return yield* Effect.fail(
295 new Error(`failed to read reconciled monitor ${displayName}`),
296 );
297 }
298 return final.attrs;
299 }),
300 delete: Effect.fn(function* ({ output }) {
301 yield* deadline
302 .deleteMonitor({ monitorId: output.monitorId })
303 .pipe(
304 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
305 );
306 }),
307 };
308 }),
309 );