1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as crm from "@distilled.cloud/gcp/cloudresourcemanager_v3";
5import * as workflowexecutions from "@distilled.cloud/gcp/workflowexecutions_v1";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import WorkflowsBindingsHost, { Greet } from "./fixtures/bindings-host.ts";
11
12const testOptions = { providers: GCP.providers() };
13const { test, beforeAll, afterAll } = Test.make(testOptions);
14const sharedStack = Core.scratchStack(testOptions, "WorkflowsBindings");
15
16let baseUrl: string;
17let hostAccount: string;
18let project: string;
19let workflowName: string;
20
21describe.skipIf(!dockerAvailable)(
22 "Workflows Bindings",
23 {
24 tags: [
25 "provider:gcp",
26 "provider:gcp:workflows",
27 "provider:gcp:run",
28 "live",
29 ],
30 },
31 () => {
32 beforeAll(
33 Effect.gen(function* () {
34 yield* sharedStack.destroy();
35 const out = yield* sharedStack.deploy(
36 Effect.gen(function* () {
37 const host = yield* WorkflowsBindingsHost;
38 const workflow = yield* Greet;
39 return {
40 uri: host.uri,
41 serviceAccount: host.serviceAccount,
42 name: workflow.name,
43 project: workflow.project,
44 };
45 }),
46 );
47 baseUrl = out.uri!;
48 hostAccount = out.serviceAccount!;
49 workflowName = out.name;
50 project = out.project;
51 }),
52 { timeout: 900_000 },
53 );
54
55 afterAll(sharedStack.destroy(), { timeout: 600_000 });
56
57 describe("CreateExecution", () => {
58 test.provider(
59 "starts a run as the host's service account, granted workflows.invoker on the project",
60 (_stack) =>
61 Effect.gen(function* () {
62 const started = yield* expectProbe<workflowexecutions.Execution>(
63 baseUrl,
64 "createExecution",
65 );
66 // Execution names carry the project number, not its id.
67 const workflowPath = workflowName.slice(
68 workflowName.indexOf("/locations/"),
69 );
70 expect(started.name).toEqual(
71 expect.stringContaining(`${workflowPath}/executions/`),
72 );
73
74 const finished = yield* workflowexecutions
75 .getProjectsLocationsWorkflowsExecutions({
76 name: started.name ?? "",
77 })
78 .pipe(
79 Effect.repeat({
80 schedule: Schedule.spaced("2 seconds"),
81 until: (execution) => execution.state !== "ACTIVE",
82 times: 30,
83 }),
84 );
85 expect(finished.state).toEqual("SUCCEEDED");
86 expect(finished.result).toEqual(JSON.stringify("hello alchemy"));
87
88 // Workflows has no per-workflow IAM policy: the binding grants
89 // roles/workflows.invoker on the project.
90 const policy = yield* crm.getIamPolicyProjects({
91 resource: `projects/${project}`,
92 body: { options: { requestedPolicyVersion: 3 } },
93 });
94 const roles = (policy.bindings ?? [])
95 .filter((binding) =>
96 (binding.members ?? []).includes(
97 `serviceAccount:${hostAccount}`,
98 ),
99 )
100 .map((binding) => ({
101 role: binding.role,
102 condition: binding.condition,
103 }));
104 expect(roles).toEqual([
105 { role: "roles/workflows.invoker", condition: undefined },
106 ]);
107 }),
108 {
109 tags: ["provider:gcp", "provider:gcp:workflows", "live"],
110 timeout: 600_000,
111 },
112 );
113 });
114 },
115);