1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as cloudbuild from "@distilled.cloud/gcp/cloudbuild_v2";
6import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import CloudBuildBindingsHost, { Source } from "./fixtures/bindings-host.ts";
11
12const testOptions = { providers: GCP.providers() };
13const { test, beforeAll, afterAll } = Test.make(testOptions);
14const sharedStack = Core.scratchStack(testOptions, "CloudBuildBindings");
15
16// Needs a GitHub connection that has completed the OAuth handshake.
17const runLifecycle =
18 !!process.env.GCP_TEST_CLOUDBUILD_REPO && !process.env.FAST;
19
20let baseUrl: string;
21let hostAccount: string;
22let repositoryName: string;
23
24/** Every project-level role (and its IAM Condition) `account` holds. */
25const projectGrantsOf = (account: string) =>
26 Effect.gen(function* () {
27 const { project } = yield* GcpEnvironment.current;
28 const policy = yield* resourcemanager.getIamPolicyProjects({
29 resource: `projects/${project}`,
30 body: { options: { requestedPolicyVersion: 3 } },
31 });
32 return (policy.bindings ?? [])
33 .filter((binding) =>
34 (binding.members ?? []).includes(`serviceAccount:${account}`),
35 )
36 .map((binding) => ({
37 role: binding.role,
38 condition: binding.condition?.expression,
39 }))
40 .sort((left, right) => (left.role ?? "").localeCompare(right.role ?? ""));
41 });
42
43/** Cloud Build repository bindings grant on the project. */
44const PROJECT_GRANTS = [
45 { role: "roles/cloudbuild.connectionViewer", condition: undefined },
46 { role: "roles/cloudbuild.readTokenAccessor", condition: undefined },
47 { role: "roles/cloudbuild.tokenAccessor", condition: undefined },
48];
49
50const expectProjectGrants = Effect.gen(function* () {
51 expect(yield* projectGrantsOf(hostAccount)).toEqual(PROJECT_GRANTS);
52});
53
54describe.skipIf(!dockerAvailable || !runLifecycle)(
55 "CloudBuild Bindings",
56 {
57 tags: [
58 "provider:gcp",
59 "provider:gcp:cloudbuild",
60 "provider:gcp:run",
61 "live",
62 ],
63 },
64 () => {
65 beforeAll(
66 Effect.gen(function* () {
67 yield* sharedStack.destroy();
68 const out = yield* sharedStack.deploy(
69 Effect.gen(function* () {
70 const host = yield* CloudBuildBindingsHost;
71 const repository = yield* Source;
72 return {
73 uri: host.uri,
74 serviceAccount: host.serviceAccount,
75 repository: repository.name,
76 };
77 }),
78 );
79 baseUrl = out.uri!;
80 hostAccount = out.serviceAccount!;
81 repositoryName = out.repository;
82 }),
83 { timeout: 900_000 },
84 );
85
86 afterAll(sharedStack.destroy(), { timeout: 600_000 });
87
88 describe("AccessReadToken", () => {
89 test.provider(
90 "mints a read token as the host's service account",
91 (_stack) =>
92 Effect.gen(function* () {
93 const out = yield* expectProbe<{
94 token?: string;
95 expirationTime?: string;
96 }>(baseUrl, "accessReadToken");
97 expect(out.token?.length ?? 0).toBeGreaterThan(0);
98 expect(out.expirationTime).toEqual(expect.any(String));
99 yield* expectProjectGrants;
100 }),
101 {
102 tags: ["provider:gcp", "provider:gcp:cloudbuild", "live"],
103 timeout: 600_000,
104 },
105 );
106 });
107
108 describe("AccessReadWriteToken", () => {
109 test.provider(
110 "mints a read/write token as the host's service account",
111 (_stack) =>
112 Effect.gen(function* () {
113 const out = yield* expectProbe<{
114 token?: string;
115 expirationTime?: string;
116 }>(baseUrl, "accessReadWriteToken");
117 expect(out.token?.length ?? 0).toBeGreaterThan(0);
118 expect(out.expirationTime).toEqual(expect.any(String));
119 yield* expectProjectGrants;
120 }),
121 {
122 tags: ["provider:gcp", "provider:gcp:cloudbuild", "live"],
123 timeout: 600_000,
124 },
125 );
126 });
127
128 describe("FetchGitRefs", () => {
129 test.provider(
130 "lists the repository's branches as the host's service account",
131 (_stack) =>
132 Effect.gen(function* () {
133 const out = yield* expectProbe<{ refNames?: string[] }>(
134 baseUrl,
135 "fetchGitRefs",
136 );
137 const expected =
138 yield* cloudbuild.fetchGitRefsProjectsLocationsConnectionsRepositories(
139 { repository: repositoryName, refType: "BRANCH" },
140 );
141 // Every repository has at least its default branch.
142 expect(out.refNames?.length ?? 0).toBeGreaterThan(0);
143 expect(out.refNames).toEqual(expected.refNames);
144 yield* expectProjectGrants;
145 }),
146 {
147 tags: ["provider:gcp", "provider:gcp:cloudbuild", "live"],
148 timeout: 600_000,
149 },
150 );
151 });
152 },
153);