| 1 | import type { GcpOpContext } from "@distilled.cloud/gcp/Protocol"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import type { Input } from "../Input.ts"; |
| 4 | import * as Output from "../Output.ts"; |
| 5 | import { bindGcpHost, type GcpIamGrant } from "./Host.ts"; |
| 6 | import type { GcpIamResourceKind } from "./IamPolicy.ts"; |
| 7 | |
| 8 | /** |
| 9 | * Distilled ops are `OperationMethod`s: yield them once at Layer |
| 10 | * construction so the inner runtime Effect is `Effect<A, E>` and does |
| 11 | * not leak `GcpOpContext`. Do not `provideService` Credentials/HttpClient |
| 12 | * here — the host Layer already provides them. |
| 13 | */ |
| 14 | export type GcpHttpOp<I, A, E> = Effect.Effect< |
| 15 | (input: I) => Effect.Effect<A, E>, |
| 16 | never, |
| 17 | GcpOpContext |
| 18 | > & |
| 19 | ((input: I) => Effect.Effect<A, E, GcpOpContext>); |
| 20 | |
| 21 | /** |
| 22 | * IAM a binding grants the host's runtime service account on the bound |
| 23 | * resource: `role` is the narrowest predefined role covering the |
| 24 | * operation. With `on`, the role is granted on the bound resource's own |
| 25 | * IAM policy (resource-scoped, like an AWS statement's `Resource: [arn]`); |
| 26 | * without it — only for services that have no resource-level IAM — the |
| 27 | * role is granted on the project. |
| 28 | */ |
| 29 | export interface BindingIam { |
| 30 | role: string; |
| 31 | on?: Exclude<GcpIamResourceKind, "project">; |
| 32 | /** |
| 33 | * For services with no per-resource IAM policy but IAM Conditions |
| 34 | * support (Firestore, Cloud SQL, Managed Kafka): grant on the project |
| 35 | * under a condition matching only the bound resource and its children. |
| 36 | */ |
| 37 | scopeByCondition?: boolean; |
| 38 | } |
| 39 | |
| 40 | const nameExpression = (value: string) => |
| 41 | `resource.name == "${value}" || resource.name.startsWith("${value}/")`; |
| 42 | |
| 43 | /** IAM Condition matching one resource (by full name) and its children. */ |
| 44 | export const resourceNameCondition = (name: Input<string>) => ({ |
| 45 | title: "alchemy-scoped", |
| 46 | expression: |
| 47 | typeof name === "string" |
| 48 | ? nameExpression(name) |
| 49 | : Output.map(name as Output.Output<string>, nameExpression), |
| 50 | }); |
| 51 | |
| 52 | /** Build the grant for `iam` against a bound resource's full name. */ |
| 53 | export const grantFor = ( |
| 54 | iam: BindingIam, |
| 55 | name: Input<string>, |
| 56 | ): Input<GcpIamGrant> => |
| 57 | iam.on !== undefined |
| 58 | ? // The engine resolves the name Output before the host reconciles. |
| 59 | { role: iam.role, resource: { kind: iam.on, name } } |
| 60 | : iam.scopeByCondition |
| 61 | ? { role: iam.role, condition: resourceNameCondition(name) } |
| 62 | : { role: iam.role }; |
| 63 | |
| 64 | /** |
| 65 | * Shared HTTP scaffolding for GCP named-resource bindings. |
| 66 | * |
| 67 | * Yields the distilled operation (no double-provide) and, at deploy |
| 68 | * time, grants `iam` on the ambient Cloud Run / Function host the way |
| 69 | * AWS bindings attach IAM policy statements. |
| 70 | */ |
| 71 | export const makeNamedHttpBinding = < |
| 72 | Resource extends { LogicalId: string }, |
| 73 | I extends { name?: string }, |
| 74 | A, |
| 75 | E, |
| 76 | >(options: { |
| 77 | tag: string; |
| 78 | operation: GcpHttpOp<I, A, E>; |
| 79 | iam: BindingIam; |
| 80 | resourceName: (resource: Resource) => Output.Output<string, never>; |
| 81 | }) => |
| 82 | Effect.gen(function* () { |
| 83 | const run = yield* options.operation; |
| 84 | return Effect.fn(function* (resource: Resource) { |
| 85 | const resourceName = options.resourceName(resource); |
| 86 | yield* bindGcpHost({ |
| 87 | tag: options.tag, |
| 88 | resource, |
| 89 | iam: [grantFor(options.iam, resourceName)], |
| 90 | }); |
| 91 | const name = yield* resourceName; |
| 92 | return Effect.fn(`${options.tag}(${resource.LogicalId})`)(function* ( |
| 93 | request?: Omit<I, "name">, |
| 94 | ) { |
| 95 | return yield* run({ |
| 96 | ...(request ?? {}), |
| 97 | name: yield* name, |
| 98 | } as I); |
| 99 | }); |
| 100 | }); |
| 101 | }); |