1import * as Effect from "effect/Effect";
2import * as Binding from "../../Binding.ts";
3import * as Output from "../../Output.ts";
4import { isBindingHost } from "../Lambda/Function.ts";
5import type { JobTemplate } from "./JobTemplate.ts";
6import type { VirtualCluster } from "./VirtualCluster.ts";
7
8/**
9 * Shared scaffolding for Amazon EMR on EKS (`emr-containers`) HTTP bindings.
10 *
11 * NOT exported from `index.ts` — every thin `{Op}Http.ts` in this service is
12 * a `Layer.effect(Cap, make…HttpBinding({ … }))` over one of the builders
13 * below. Everything except the operation, the identifier injection, and the
14 * IAM action list is boilerplate.
15 *
16 * EMR on EKS authorizes virtual-cluster-addressed actions against the
17 * virtual cluster ARN (`arn:…:/virtualclusters/vc-id`) and sub-resource
18 * actions (job runs, managed endpoints) against child ARNs
19 * (`…/virtualclusters/vc-id/jobruns/id`, `…/endpoints/id`), so the
20 * virtual-cluster builders grant on both the bound cluster's ARN and its
21 * sub-resource pattern.
22 */
23export const virtualClusterPolicyStatement = (
24 virtualCluster: VirtualCluster,
25 actions: readonly string[],
26) => ({
27 Effect: "Allow" as const,
28 Action: [...actions],
29 Resource: [
30 Output.interpolate`${virtualCluster.virtualClusterArn}`,
31 Output.map(virtualCluster.virtualClusterArn, (arn) => `${arn}/*`),
32 ],
33});
34
35/**
36 * Build the impl Effect for an operation whose input carries a
37 * `virtualClusterId` field: the runtime callable injects the bound
38 * {@link VirtualCluster}'s ID and the deploy-time half grants `actions` on
39 * the virtual cluster ARN (and its sub-resource pattern).
40 */
41export const makeEMRContainersVirtualClusterHttpBinding = <
42 I extends { virtualClusterId?: string },
43 A,
44 E,
45 R,
46>(options: {
47 /** Fully-qualified binding tag, e.g. `AWS.EMRContainers.StartJobRun`. */
48 tag: string;
49 /** The distilled operation; `virtualClusterId` is injected. */
50 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
51 /** IAM actions granted on the virtual cluster ARN + sub-resource pattern. */
52 actions: readonly string[];
53}) =>
54 Effect.gen(function* () {
55 const op = yield* options.operation;
56
57 return Effect.fn(function* (virtualCluster: VirtualCluster) {
58 // Outputs yield a DEFERRED effect — resolve again per invocation below.
59 const VirtualClusterId = yield* virtualCluster.virtualClusterId;
60 if (!globalThis.__ALCHEMY_RUNTIME__) {
61 const host = yield* Binding.Host;
62 if (isBindingHost(host)) {
63 yield* host.bind`Allow(${host}, ${options.tag}(${virtualCluster}))`({
64 policyStatements: [
65 virtualClusterPolicyStatement(virtualCluster, options.actions),
66 ],
67 });
68 }
69 }
70 return Effect.fn(`${options.tag}(${virtualCluster.LogicalId})`)(
71 function* (request?: Omit<I, "virtualClusterId">) {
72 return yield* op({
73 ...request,
74 virtualClusterId: yield* VirtualClusterId,
75 } as I);
76 },
77 );
78 });
79 });
80
81/**
82 * Build the impl Effect for an operation whose input carries an `id` field
83 * addressing a {@link JobTemplate}: the runtime callable injects the bound
84 * template's ID and the deploy-time half grants `actions` on the template
85 * ARN.
86 */
87export const makeEMRContainersJobTemplateHttpBinding = <
88 I extends { id?: string },
89 A,
90 E,
91 R,
92>(options: {
93 /** Fully-qualified binding tag, e.g. `AWS.EMRContainers.DescribeJobTemplate`. */
94 tag: string;
95 /** The distilled operation; the template `id` is injected. */
96 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
97 /** IAM actions granted on the job template ARN. */
98 actions: readonly string[];
99}) =>
100 Effect.gen(function* () {
101 const op = yield* options.operation;
102
103 return Effect.fn(function* (template: JobTemplate) {
104 const JobTemplateId = yield* template.jobTemplateId;
105 if (!globalThis.__ALCHEMY_RUNTIME__) {
106 const host = yield* Binding.Host;
107 if (isBindingHost(host)) {
108 yield* host.bind`Allow(${host}, ${options.tag}(${template}))`({
109 policyStatements: [
110 {
111 Effect: "Allow",
112 Action: [...options.actions],
113 Resource: [template.jobTemplateArn],
114 },
115 ],
116 });
117 }
118 }
119 return Effect.fn(`${options.tag}(${template.LogicalId})`)(function* (
120 request?: Omit<I, "id">,
121 ) {
122 return yield* op({ ...request, id: yield* JobTemplateId } as I);
123 });
124 });
125 });
126
127/**
128 * Build the impl Effect for an account-level operation (e.g. enumerating the
129 * account's job templates). The deploy-time half grants `actions` on `*` —
130 * account-level list actions are not scoped to a single resource.
131 */
132export const makeEMRContainersAccountHttpBinding = <I, A, E, R>(options: {
133 /** Fully-qualified binding tag, e.g. `AWS.EMRContainers.ListJobTemplates`. */
134 tag: string;
135 /** The distilled operation, invoked with the caller's request as-is. */
136 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
137 /** IAM actions granted on `*`. */
138 actions: readonly string[];
139}) =>
140 Effect.gen(function* () {
141 const op = yield* options.operation;
142
143 return Effect.fn(function* () {
144 if (!globalThis.__ALCHEMY_RUNTIME__) {
145 const host = yield* Binding.Host;
146 if (isBindingHost(host)) {
147 yield* host.bind`Allow(${host}, ${options.tag}())`({
148 policyStatements: [
149 {
150 Effect: "Allow",
151 Action: [...options.actions],
152 Resource: ["*"],
153 },
154 ],
155 });
156 }
157 }
158 return Effect.fn(options.tag)(function* (request?: I) {
159 return yield* op((request ?? {}) as I);
160 });
161 });
162 });