1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as cloudrun from "@distilled.cloud/gcp/run_v2";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as HttpClient from "effect/http/HttpClient";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import RunBindingsHost, {
10 Api,
11 Callee,
12 Migrate,
13 Workers,
14} from "./fixtures/bindings-host.ts";
15
16const testOptions = { providers: GCP.providers() };
17const { test, beforeAll, afterAll } = Test.make(testOptions);
18const sharedStack = Core.scratchStack(testOptions, "RunBindings");
19
20let baseUrl: string;
21let member: string;
22let names: {
23 api: string;
24 callee: string;
25 calleeUri: string;
26 workers: string;
27 job: string;
28};
29
30type Policy = { bindings?: { role?: string; members?: string[] }[] };
31
32const rolesOf = (policy: Policy) =>
33 (policy.bindings ?? [])
34 .filter((binding) => (binding.members ?? []).includes(member))
35 .map((binding) => binding.role)
36 .sort();
37
38const serviceRoles = (name: string) =>
39 cloudrun
40 .getIamPolicyProjectsLocationsServices({
41 resource: name,
42 "options.requestedPolicyVersion": 3,
43 })
44 .pipe(Effect.map(rolesOf));
45
46describe.skipIf(!dockerAvailable)(
47 "Run Bindings",
48 { tags: ["provider:gcp", "provider:gcp:run", "live"] },
49 () => {
50 beforeAll(
51 Effect.gen(function* () {
52 yield* sharedStack.destroy();
53 const out = yield* sharedStack.deploy(
54 Effect.gen(function* () {
55 const host = yield* RunBindingsHost;
56 const callee = yield* Callee;
57 return {
58 uri: host.uri,
59 serviceAccount: host.serviceAccount,
60 api: (yield* Api).name,
61 callee: callee.name,
62 calleeUri: callee.uri,
63 workers: (yield* Workers).name,
64 job: (yield* Migrate).name,
65 };
66 }),
67 );
68 baseUrl = out.uri!;
69 member = `serviceAccount:${out.serviceAccount!}`;
70 names = { ...out, calleeUri: out.calleeUri! };
71 }),
72 { timeout: 900_000 },
73 );
74
75 afterAll(sharedStack.destroy(), { timeout: 600_000 });
76
77 describe("GetService", () => {
78 test.provider(
79 "reads the service, granted viewer on the service only",
80 (_stack) =>
81 Effect.gen(function* () {
82 const live = yield* expectProbe<cloudrun.GoogleCloudRunV2Service>(
83 baseUrl,
84 "getService",
85 );
86 expect(live.name).toEqual(names.api);
87 expect(live.template?.containers?.[0]?.image).toEqual(
88 "us-docker.pkg.dev/cloudrun/container/hello",
89 );
90 expect(yield* serviceRoles(names.api)).toEqual([
91 "roles/run.viewer",
92 ]);
93 }),
94 {
95 tags: ["provider:gcp", "provider:gcp:run", "live"],
96 timeout: 600_000,
97 },
98 );
99 });
100
101 describe("GetWorkerPool", () => {
102 test.provider(
103 "reads the worker pool, granted viewer on the pool only",
104 (_stack) =>
105 Effect.gen(function* () {
106 const live =
107 yield* expectProbe<cloudrun.GoogleCloudRunV2WorkerPool>(
108 baseUrl,
109 "getWorkerPool",
110 );
111 expect(live.name).toEqual(names.workers);
112
113 const policy =
114 yield* cloudrun.getIamPolicyProjectsLocationsWorkerPools({
115 resource: names.workers,
116 "options.requestedPolicyVersion": 3,
117 });
118 expect(rolesOf(policy)).toEqual(["roles/run.viewer"]);
119 }),
120 {
121 tags: ["provider:gcp", "provider:gcp:run", "live"],
122 timeout: 600_000,
123 },
124 );
125 });
126
127 describe("RunJob", () => {
128 test.provider(
129 "starts an execution, granted jobsExecutorWithOverrides on the job only",
130 (_stack) =>
131 Effect.gen(function* () {
132 const operation =
133 yield* expectProbe<cloudrun.GoogleLongrunningOperation>(
134 baseUrl,
135 "runJob",
136 );
137 const executionName = operation.metadata?.name;
138 expect(typeof executionName).toEqual("string");
139 expect(String(executionName)).toContain(`${names.job}/executions/`);
140
141 // Out of band: the execution exists under the bound job.
142 const execution =
143 yield* cloudrun.getProjectsLocationsJobsExecutions({
144 name: String(executionName),
145 });
146 expect(execution.name).toEqual(executionName);
147
148 const policy = yield* cloudrun.getIamPolicyProjectsLocationsJobs({
149 resource: names.job,
150 "options.requestedPolicyVersion": 3,
151 });
152 expect(rolesOf(policy)).toEqual([
153 "roles/run.jobsExecutorWithOverrides",
154 ]);
155 }),
156 {
157 tags: ["provider:gcp", "provider:gcp:run", "live"],
158 timeout: 600_000,
159 },
160 );
161 });
162
163 describe("InvokeService", () => {
164 test.provider(
165 "calls the private service with an ID token, granted invoker on it only",
166 (_stack) =>
167 Effect.gen(function* () {
168 const out = yield* expectProbe<{ status: number; text: string }>(
169 baseUrl,
170 "invokeService",
171 );
172 expect(out.status).toEqual(200);
173 expect(out.text).toContain("It's running!");
174
175 // Without a token Cloud Run rejects the call at its front end.
176 const direct = yield* HttpClient.get(names.calleeUri);
177 expect(direct.status).toEqual(403);
178
179 expect(yield* serviceRoles(names.callee)).toEqual([
180 "roles/run.invoker",
181 ]);
182 }),
183 {
184 tags: ["provider:gcp", "provider:gcp:run", "live"],
185 timeout: 600_000,
186 },
187 );
188 });
189 },
190);