1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as Core from "@/Test/Core";
4import * as privateca from "@distilled.cloud/gcp/privateca_v1";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { dockerAvailable, expectProbe } from "../bindingHost.ts";
8import PrivateCaBindingsHost, { Pool, Root } from "./fixtures/bindings-host.ts";
9
10const testOptions = { providers: GCP.providers() };
11const { test, beforeAll, afterAll } = Test.make(testOptions);
12const sharedStack = Core.scratchStack(testOptions, "PrivateCABindings");
13
14let baseUrl: string;
15let hostAccount: string;
16let poolName: string;
17let caName: string;
18
19/**
20 * Roles the host holds on the CA pool (CAs have no IAM policy of their
21 * own, so both bindings grant on the parent pool).
22 */
23const expectPoolGrants = Effect.gen(function* () {
24 const policy = yield* privateca.getIamPolicyProjectsLocationsCaPools({
25 resource: poolName,
26 });
27 const roles = (policy.bindings ?? [])
28 .filter((binding) =>
29 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
30 )
31 .map((binding) => binding.role)
32 .sort();
33 expect(roles).toEqual([
34 "roles/privateca.auditor",
35 "roles/privateca.poolReader",
36 ]);
37});
38
39describe.skipIf(!dockerAvailable)(
40 "PrivateCA Bindings",
41 {
42 tags: [
43 "provider:gcp",
44 "provider:gcp:privateca",
45 "provider:gcp:run",
46 "live",
47 ],
48 },
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* PrivateCaBindingsHost;
56 return {
57 uri: host.uri,
58 serviceAccount: host.serviceAccount,
59 pool: (yield* Pool).name,
60 ca: (yield* Root).name,
61 };
62 }),
63 );
64 baseUrl = out.uri!;
65 hostAccount = out.serviceAccount!;
66 poolName = out.pool;
67 caName = out.ca;
68 }),
69 { timeout: 900_000 },
70 );
71
72 afterAll(sharedStack.destroy(), { timeout: 600_000 });
73
74 describe("FetchCaCerts", () => {
75 test.provider(
76 "fetches the pool's trust anchors as the host's service account, granted on the pool only",
77 (_stack) =>
78 Effect.gen(function* () {
79 const certs = yield* expectProbe<privateca.FetchCaCertsResponse>(
80 baseUrl,
81 "fetchCaCerts",
82 );
83 const pems = (certs.caCerts ?? []).flatMap(
84 (chain) => chain.certificates ?? [],
85 );
86 // The enabled root CA is the pool's only trust anchor.
87 const ca =
88 yield* privateca.getProjectsLocationsCaPoolsCertificateAuthorities(
89 { name: caName },
90 );
91 expect(pems.map((pem) => pem.trim())).toEqual(
92 (ca.pemCaCertificates ?? []).map((pem) => pem.trim()),
93 );
94 expect(pems[0]).toContain("BEGIN CERTIFICATE");
95 yield* expectPoolGrants;
96 }),
97 {
98 tags: ["provider:gcp", "provider:gcp:privateca", "live"],
99 timeout: 600_000,
100 },
101 );
102 });
103
104 describe("GetCertificateAuthority", () => {
105 test.provider(
106 "reads the CA as the host's service account, granted on the pool only",
107 (_stack) =>
108 Effect.gen(function* () {
109 const live = yield* expectProbe<privateca.CertificateAuthority>(
110 baseUrl,
111 "getCertificateAuthority",
112 );
113 expect(live.name).toEqual(caName);
114 expect(live.type).toEqual("SELF_SIGNED");
115 expect(live.state).toEqual("ENABLED");
116 yield* expectPoolGrants;
117 }),
118 {
119 tags: ["provider:gcp", "provider:gcp:privateca", "live"],
120 timeout: 600_000,
121 },
122 );
123 });
124 },
125);