1import * as ce from "@distilled.cloud/aws/cost-explorer";
2import * as Effect from "effect/Effect";
3import * as Schedule from "effect/Schedule";
4import { diffTags } from "../../Tags.ts";
5import { Region } from "../Region.ts";
6
7// Cost Explorer is a global service served exclusively from the us-east-1
8// endpoint. Every control-plane call must target that region regardless of
9// the ambient stack region, so we pin it on every distilled operation. The
10// distilled Region service value is `Effect<RegionName>`, not a raw string —
11// providing a bare string makes the client `yield*` a string and crash
12// (same pattern as CloudFront KVS / ECR Public / WAFv2 / GlobalAccelerator).
13export const CE_REGION = "us-east-1" as const;
14
15// Cost Explorer's control-plane rate limits are extremely low and shared
16// account-wide, so concurrent deploys readily trip `LimitExceededException:
17// Rate limit exceeded`. Retry it with capped exponential backoff, bounded
18// (~47s total) so a genuine quota LimitExceeded still surfaces quickly.
19const ceThrottleRetrySchedule = Schedule.max([
20 Schedule.min([
21 Schedule.exponential("1 second"),
22 Schedule.spaced("8 seconds"),
23 ]),
24 Schedule.recurs(8),
25]);
26
27export const pinCe = <A, E extends { _tag: string }, R>(
28 effect: Effect.Effect<A, E, R>,
29): Effect.Effect<A, E, R> =>
30 effect.pipe(
31 Effect.provideService(Region, Effect.succeed(CE_REGION)),
32 Effect.retry({
33 while: (e) => e._tag === "LimitExceededException",
34 schedule: ceThrottleRetrySchedule,
35 }),
36 );
37
38/** Convert a plain tag record to Cost Explorer's `ResourceTag` list shape. */
39export const toResourceTags = (
40 tags: Record<string, string>,
41): ce.ResourceTag[] =>
42 Object.entries(tags).map(([Key, Value]) => ({ Key, Value }));
43
44/** Convert a Cost Explorer `ResourceTag` list to a plain tag record. */
45export const toTagRecord = (
46 tags: readonly ce.ResourceTag[] | undefined,
47): Record<string, string> =>
48 Object.fromEntries((tags ?? []).map((t) => [t.Key, t.Value]));
49
50/**
51 * Fetch the observed tags for a Cost Explorer resource ARN. Tolerates the
52 * resource disappearing mid-read (`ResourceNotFoundException` → `{}`).
53 */
54export const fetchCeTags = Effect.fn(function* (resourceArn: string) {
55 const listed = yield* pinCe(
56 ce.listTagsForResource({ ResourceArn: resourceArn }),
57 ).pipe(
58 Effect.catchTag("ResourceNotFoundException", () =>
59 Effect.succeed({ ResourceTags: [] }),
60 ),
61 );
62 return toTagRecord(listed.ResourceTags);
63});
64
65/**
66 * Sync the tags on a Cost Explorer resource: diff the OBSERVED cloud tags
67 * against the desired set and apply only the delta.
68 */
69export const syncCeTags = Effect.fn(function* (
70 resourceArn: string,
71 desiredTags: Record<string, string>,
72) {
73 const observedTags = yield* fetchCeTags(resourceArn);
74 const { upsert, removed } = diffTags(observedTags, desiredTags);
75 if (upsert.length > 0) {
76 yield* pinCe(
77 ce.tagResource({ ResourceArn: resourceArn, ResourceTags: upsert }),
78 );
79 }
80 if (removed.length > 0) {
81 yield* pinCe(
82 ce.untagResource({ ResourceArn: resourceArn, ResourceTagKeys: removed }),
83 );
84 }
85});