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 { Cluster } from "./Cluster.ts";
6
7/**
8 * Shared scaffolding for AWS EKS HTTP bindings.
9 *
10 * NOT exported from `index.ts` — every `{Op}Http.ts` in this service is a
11 * thin `Layer.effect(Cap, makeEKS…HttpBinding({ … }))` over one of the
12 * builders below. Everything except the operation, the request key carrying
13 * the cluster name, and the IAM action list is boilerplate.
14 */
15
16/**
17 * Build the impl Effect for an account-level operation (cluster enumeration,
18 * managed access-policy catalog, Kubernetes/add-on version catalogs). The
19 * deploy-time half grants `actions` on `*` — these read-only catalog and
20 * enumeration actions span the whole account/region and take no resource ARN.
21 */
22export const makeEKSAccountHttpBinding = <I, A, E, R>(options: {
23 /** Fully-qualified binding tag, e.g. `AWS.EKS.ListClusters`. */
24 tag: string;
25 /** The distilled operation, invoked with the caller's request as-is. */
26 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
27 /** IAM actions granted on `*`. */
28 actions: readonly string[];
29}) =>
30 Effect.gen(function* () {
31 const op = yield* options.operation;
32
33 return Effect.fn(function* () {
34 if (!globalThis.__ALCHEMY_RUNTIME__) {
35 const host = yield* Binding.Host;
36 if (isBindingHost(host)) {
37 yield* host.bind`Allow(${host}, ${options.tag}())`({
38 policyStatements: [
39 {
40 Effect: "Allow",
41 Action: [...options.actions],
42 Resource: ["*"],
43 },
44 ],
45 });
46 }
47 }
48 return Effect.fn(options.tag)(function* (request?: I) {
49 return yield* op((request ?? {}) as I);
50 });
51 });
52 });
53
54/**
55 * Which ARNs an action authorizes against, per the EKS service authorization
56 * reference:
57 *
58 * - `cluster` — the cluster ARN itself (`DescribeCluster`, the `List*`
59 * enumerations, insights).
60 * - `subresources` — the cluster's sub-resources. Nodegroups, add-ons,
61 * Fargate profiles, pod identity associations, and access entries all share
62 * the ARN shape `arn:aws:eks:{region}:{acct}:{type}/{clusterName}/…`, so a
63 * single wildcard pattern derived from the cluster ARN (resource type
64 * replaced by a `*` and a trailing `/{star}` appended) covers them without
65 * granting anything on other clusters.
66 * - `both` — actions like `ListUpdates`/`DescribeUpdate` that authorize
67 * against the cluster OR a sub-resource depending on the request.
68 */
69export type EKSIamScope = "cluster" | "subresources" | "both";
70
71const scopeResources = (cluster: Cluster, scope: EKSIamScope) => {
72 const clusterArn = Output.interpolate`${cluster.clusterArn}`;
73 const subresources = Output.map(
74 cluster.clusterArn,
75 (arn: string): string => `${arn.replace(":cluster/", ":*/")}/*`,
76 );
77 return scope === "cluster"
78 ? [clusterArn]
79 : scope === "subresources"
80 ? [subresources]
81 : [clusterArn, subresources];
82};
83
84/**
85 * Build the impl Effect for a cluster-scoped operation: the runtime callable
86 * injects the bound {@link Cluster}'s name under `key` (`clusterName` for the
87 * sub-resource lists and insights, `name` for `DescribeCluster`) and the
88 * deploy-time half grants `actions` on the cluster ARN and/or its
89 * sub-resource ARNs per `scope`.
90 */
91export const makeEKSClusterHttpBinding = <
92 I extends { clusterName?: string } | { name?: string },
93 A,
94 E,
95 R,
96>(options: {
97 /** Fully-qualified binding tag, e.g. `AWS.EKS.ListNodegroups`. */
98 tag: string;
99 /** The distilled operation; the cluster name is injected from the cluster. */
100 operation: Effect.Effect<(input: I) => Effect.Effect<A, E>, never, R>;
101 /** IAM actions granted per `scope`. */
102 actions: readonly string[];
103 /** Request key carrying the cluster name. */
104 key: "clusterName" | "name";
105 /** Which ARNs the actions authorize against. */
106 scope: EKSIamScope;
107}) =>
108 Effect.gen(function* () {
109 const op = yield* options.operation;
110
111 return Effect.fn(function* (cluster: Cluster) {
112 const clusterName = yield* cluster.clusterName;
113 if (!globalThis.__ALCHEMY_RUNTIME__) {
114 const host = yield* Binding.Host;
115 if (isBindingHost(host)) {
116 yield* host.bind`Allow(${host}, ${options.tag}(${cluster}))`({
117 policyStatements: [
118 {
119 Effect: "Allow",
120 Action: [...options.actions],
121 Resource: scopeResources(cluster, options.scope),
122 },
123 ],
124 });
125 }
126 }
127 return Effect.fn(`${options.tag}(${cluster.LogicalId})`)(function* (
128 request?: Omit<I, "clusterName" | "name">,
129 ) {
130 return yield* op({
131 ...request,
132 [options.key]: yield* clusterName,
133 } as I);
134 });
135 });
136 });