import * as medialive from "@distilled.cloud/aws/medialive"; import * as Effect from "effect/Effect"; import * as Schedule from "effect/Schedule"; import * as Stream from "effect/Stream"; import { Unowned } from "../../AdoptPolicy.ts"; import * as Provider from "../../Provider.ts"; import { Resource } from "../../Resource.ts"; import { createInternalTags, hasAlchemyTags } from "../../Tags.ts"; import type { Providers } from "../Providers.ts"; import { ensureIdentified, syncMlTags, toTagRecord } from "./internal.ts"; // Explicitly-typed pipeable retry helper. Inlining `Effect.retry` in a // provider lifecycle op leaks `Retry.Return`'s conditional into declaration // emit and widens the provider layer to `unknown` R for every consumer of // `AWS.providers()`. // // An Input Security Group attached to a still-deleting Input rejects // deletion with a `BadRequestException` until the Input is fully gone. const retryWhileInUse = ( self: Effect.Effect, ): Effect.Effect => Effect.retry(self, { while: (e) => e._tag === "BadRequestException" || e._tag === "ConflictException", schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(10)]), }); /** An input security group whose server-assigned identity is present. */ type IdentifiedGroup = medialive.InputSecurityGroup & { Id: string; Arn: string; }; const hasIdentity = ( isg: medialive.InputSecurityGroup, ): isg is IdentifiedGroup => isg.Id !== undefined && isg.Arn !== undefined; export interface InputSecurityGroupProps { /** * IPv4 CIDR ranges allowed to push content to inputs attached to this * security group (e.g. `"10.0.0.0/16"`, `"0.0.0.0/0"`). * @default ["0.0.0.0/0"] */ whitelistRules?: string[]; /** * User-defined tags for the input security group. */ tags?: Record; } export interface InputSecurityGroup extends Resource< "AWS.MediaLive.InputSecurityGroup", InputSecurityGroupProps, { /** Server-assigned unique id of the input security group. */ inputSecurityGroupId: string; /** ARN of the input security group. */ inputSecurityGroupArn: string; /** Current state (e.g. `IDLE`, `IN_USE`). */ state: medialive.InputSecurityGroupState | undefined; /** Allowlisted source CIDR blocks. */ whitelistRules: string[]; }, never, Providers > {} /** * An AWS Elemental MediaLive input security group — an IP allowlist that * gates which source networks may push content to attached PUSH inputs * (RTMP_PUSH, RTP_PUSH, UDP_PUSH). * * ### Creating an Input Security Group * **Example:** Allow a single network * ```typescript * const isg = yield* MediaLive.InputSecurityGroup("Allowlist", { * whitelistRules: ["10.0.0.0/16"], * }); * ``` * * **Example:** Open to the world (test-only) * ```typescript * const isg = yield* MediaLive.InputSecurityGroup("Open", { * whitelistRules: ["0.0.0.0/0"], * tags: { team: "media" }, * }); * ``` * * ### Attaching to an Input * **Example:** Gate an RTMP push input * ```typescript * const input = yield* MediaLive.Input("Stream", { * type: "RTMP_PUSH", * inputSecurityGroups: [isg.inputSecurityGroupId], * destinations: [{ StreamName: "live/stream" }], * }); * ``` * * @resource */ export const InputSecurityGroup = Resource( "AWS.MediaLive.InputSecurityGroup", ); export const InputSecurityGroupProvider = () => Provider.effect( InputSecurityGroup, Effect.gen(function* () { const toAttrs = (isg: IdentifiedGroup) => ({ inputSecurityGroupId: isg.Id, inputSecurityGroupArn: isg.Arn, state: isg.State, whitelistRules: (isg.WhitelistRules ?? []) .map((r) => r.Cidr) .filter((cidr): cidr is string => cidr !== undefined), }); /** Describe by id; typed not-found (or tombstone state) → undefined. */ const getGroup = Effect.fn(function* (id: string) { const isg = yield* medialive .describeInputSecurityGroup({ InputSecurityGroupId: id }) .pipe( Effect.catchTag("NotFoundException", () => Effect.succeed(undefined), ), ); if (isg === undefined || isg.State === "DELETED") return undefined; if (!hasIdentity(isg)) return undefined; return isg; }); /** * Input security groups have server-assigned ids and no name, so a * read without cached output scans the account list for the group * carrying this logical id's Alchemy tags. */ const findByTags = Effect.fn(function* (id: string) { const groups = yield* medialive.listInputSecurityGroups.items({}).pipe( Stream.runCollect, Effect.map((chunk) => Array.from(chunk)), ); for (const isg of groups) { if (isg.State === "DELETED" || !hasIdentity(isg)) continue; if (yield* hasAlchemyTags(id, toTagRecord(isg.Tags))) { return isg; } } return undefined; }); const desiredRules = (props: InputSecurityGroupProps) => (props.whitelistRules ?? ["0.0.0.0/0"]).map((cidr) => ({ Cidr: cidr, })); return InputSecurityGroup.Provider.of({ stables: ["inputSecurityGroupId", "inputSecurityGroupArn"], list: () => medialive.listInputSecurityGroups.items({}).pipe( Stream.runCollect, Effect.map((chunk) => Array.from(chunk) .filter( (isg): isg is IdentifiedGroup => hasIdentity(isg) && isg.State !== "DELETED", ) .map(toAttrs), ), ), read: Effect.fn(function* ({ id, output }) { const isg = output?.inputSecurityGroupId !== undefined ? yield* getGroup(output.inputSecurityGroupId) : yield* findByTags(id); if (isg === undefined) return undefined; const attrs = toAttrs(isg); return (yield* hasAlchemyTags(id, toTagRecord(isg.Tags))) ? attrs : Unowned(attrs); }), // No immutable props — every drift is an in-place update. diff: Effect.fn(function* () {}), reconcile: Effect.fn(function* ({ id, news, output, session }) { const internalTags = yield* createInternalTags(id); const desiredTags = { ...internalTags, ...news.tags }; const rules = desiredRules(news); // 1. Observe — cloud state is authoritative; output is an id cache. let isg = output?.inputSecurityGroupId !== undefined ? yield* getGroup(output.inputSecurityGroupId) : yield* findByTags(id); // 2. Ensure — create if missing. if (isg === undefined) { const created = yield* medialive.createInputSecurityGroup({ WhitelistRules: rules, Tags: desiredTags, }); isg = yield* ensureIdentified( created.SecurityGroup, "CreateInputSecurityGroup SecurityGroup Id/Arn", ); } else { // 3. Sync — apply whitelist rules only when they drift. const observed = (isg.WhitelistRules ?? []) .map((r) => r.Cidr) .filter((cidr): cidr is string => cidr !== undefined) .sort(); const wanted = rules .map((r) => r.Cidr) .slice() .sort(); if (JSON.stringify(observed) !== JSON.stringify(wanted)) { const updated = yield* medialive.updateInputSecurityGroup({ InputSecurityGroupId: isg.Id, WhitelistRules: rules, }); isg = yield* ensureIdentified( updated.SecurityGroup, "UpdateInputSecurityGroup SecurityGroup Id/Arn", ); } } // 3b. Sync tags — diff against OBSERVED cloud tags. yield* syncMlTags(isg.Arn, desiredTags); yield* session.note(isg.Id); return toAttrs(isg); }), delete: Effect.fn(function* ({ output }) { yield* medialive .deleteInputSecurityGroup({ InputSecurityGroupId: output.inputSecurityGroupId, }) .pipe( retryWhileInUse, Effect.catchTag("NotFoundException", () => Effect.void), ); }), }); }), );