1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as Core from "@/Test/Core";
5import * as resourcemanager from "@distilled.cloud/gcp/cloudresourcemanager_v3";
6import * as parametermanager from "@distilled.cloud/gcp/parametermanager_v1";
7import { describe, expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { dockerAvailable, expectProbe } from "../bindingHost.ts";
10import ParameterManagerBindingsHost, {
11 AppConfig,
12 PAYLOAD,
13 V1,
14} from "./fixtures/bindings-host.ts";
15
16const testOptions = { providers: GCP.providers() };
17const { test, beforeAll, afterAll } = Test.make(testOptions);
18const sharedStack = Core.scratchStack(testOptions, "ParameterManagerBindings");
19
20let baseUrl: string;
21let member: string;
22let parameterName: string;
23let versionName: string;
24
25const scoped = (name: string) =>
26 `resource.name == "${name}" || resource.name.startsWith("${name}/")`;
27
28/** `[role, condition expression]` the host holds on the project. */
29const hostProjectGrants = Effect.gen(function* () {
30 const { project } = yield* GcpEnvironment.current;
31 const policy = yield* resourcemanager.getIamPolicyProjects({
32 resource: `projects/${project}`,
33 body: { options: { requestedPolicyVersion: 3 } },
34 });
35 return (policy.bindings ?? [])
36 .filter((binding) => (binding.members ?? []).includes(member))
37 .map((binding) => [binding.role, binding.condition?.expression]);
38});
39
40const base64 = (text: string) => Buffer.from(text, "utf8").toString("base64");
41
42describe.skipIf(!dockerAvailable)(
43 "ParameterManager Bindings",
44 {
45 tags: [
46 "provider:gcp",
47 "provider:gcp:parametermanager",
48 "provider:gcp:run",
49 "live",
50 ],
51 },
52 () => {
53 beforeAll(
54 Effect.gen(function* () {
55 yield* sharedStack.destroy();
56 const out = yield* sharedStack.deploy(
57 Effect.gen(function* () {
58 const host = yield* ParameterManagerBindingsHost;
59 return {
60 uri: host.uri,
61 serviceAccount: host.serviceAccount,
62 parameter: (yield* AppConfig).name,
63 version: (yield* V1).name,
64 };
65 }),
66 );
67 baseUrl = out.uri!;
68 member = `serviceAccount:${out.serviceAccount!}`;
69 parameterName = out.parameter;
70 versionName = out.version;
71 }),
72 { timeout: 900_000 },
73 );
74
75 afterAll(sharedStack.destroy(), { timeout: 600_000 });
76
77 describe("GetParameter", () => {
78 test.provider(
79 "reads the parameter, granted parameterViewer scoped to it",
80 (_stack) =>
81 Effect.gen(function* () {
82 const live = yield* expectProbe<parametermanager.Parameter>(
83 baseUrl,
84 "getParameter",
85 );
86 expect(live.name).toEqual(parameterName);
87 expect(live.format).toEqual("JSON");
88
89 expect(yield* hostProjectGrants).toContainEqual([
90 "roles/parametermanager.parameterViewer",
91 scoped(parameterName),
92 ]);
93 }),
94 {
95 tags: ["provider:gcp", "provider:gcp:parametermanager", "live"],
96 timeout: 600_000,
97 },
98 );
99 });
100
101 describe("GetParameterVersion", () => {
102 test.provider(
103 "reads the version payload, granted parameterViewer scoped to it",
104 (_stack) =>
105 Effect.gen(function* () {
106 const live = yield* expectProbe<parametermanager.ParameterVersion>(
107 baseUrl,
108 "getParameterVersion",
109 );
110 expect(live.name).toEqual(versionName);
111 expect(live.payload?.data).toEqual(base64(PAYLOAD));
112
113 expect(yield* hostProjectGrants).toContainEqual([
114 "roles/parametermanager.parameterViewer",
115 scoped(versionName),
116 ]);
117 }),
118 {
119 tags: ["provider:gcp", "provider:gcp:parametermanager", "live"],
120 timeout: 600_000,
121 },
122 );
123 });
124
125 describe("RenderParameterVersion", () => {
126 test.provider(
127 "renders the version, granted parameterAccessor scoped to it",
128 (_stack) =>
129 Effect.gen(function* () {
130 const rendered =
131 yield* expectProbe<parametermanager.RenderParameterVersionResponse>(
132 baseUrl,
133 "renderParameterVersion",
134 );
135 expect(rendered.parameterVersion).toEqual(versionName);
136 expect(rendered.renderedPayload).toEqual(base64(PAYLOAD));
137
138 // Out of band: the deployer renders the same payload.
139 const direct =
140 yield* parametermanager.renderProjectsLocationsParametersVersions(
141 { name: versionName },
142 );
143 expect(direct.renderedPayload).toEqual(rendered.renderedPayload);
144
145 const grants = yield* hostProjectGrants;
146 expect(grants).toContainEqual([
147 "roles/parametermanager.parameterAccessor",
148 scoped(versionName),
149 ]);
150 // Every grant the host holds is condition-scoped.
151 expect(grants.filter(([, expression]) => !expression)).toEqual([]);
152 expect(grants).toHaveLength(3);
153 }),
154 {
155 tags: ["provider:gcp", "provider:gcp:parametermanager", "live"],
156 timeout: 600_000,
157 },
158 );
159 });
160 },
161);