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 containeranalysis from "@distilled.cloud/gcp/containeranalysis_v1";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { dockerAvailable, expectProbe } from "../bindingHost.ts";
9import { currentProject, TEST_RESOURCE_URI } from "./common.ts";
10import ContainerAnalysisBindingsHost, {
11 Authority,
12 Signed,
13} from "./fixtures/bindings-host.ts";
14
15const testOptions = { providers: GCP.providers() };
16const { test, beforeAll, afterAll } = Test.make(testOptions);
17const sharedStack = Core.scratchStack(testOptions, "ContainerAnalysisBindings");
18
19let baseUrl: string;
20let noteName: string;
21let occurrenceName: string;
22let hostAccount: string;
23
24/** Roles the host's service account holds on the project policy. */
25const projectRoles = Effect.gen(function* () {
26 const project = yield* currentProject;
27 const policy = yield* crm.getIamPolicyProjects({
28 resource: `projects/${project}`,
29 body: { options: { requestedPolicyVersion: 3 } },
30 });
31 return (policy.bindings ?? [])
32 .filter((binding) =>
33 (binding.members ?? []).includes(`serviceAccount:${hostAccount}`),
34 )
35 .map((binding) => ({
36 role: binding.role,
37 condition: binding.condition?.expression,
38 }));
39});
40
41describe.skipIf(!dockerAvailable)(
42 "ContainerAnalysis Bindings",
43 {
44 tags: [
45 "provider:gcp",
46 "provider:gcp:containeranalysis",
47 "provider:gcp:run",
48 "live",
49 ],
50 },
51 () => {
52 beforeAll(
53 Effect.gen(function* () {
54 yield* sharedStack.destroy();
55 const out = yield* sharedStack.deploy(
56 Effect.gen(function* () {
57 const host = yield* ContainerAnalysisBindingsHost;
58 const note = yield* Authority;
59 const occurrence = yield* Signed;
60 return {
61 uri: host.uri,
62 serviceAccount: host.serviceAccount,
63 note: note.name,
64 occurrence: occurrence.name,
65 };
66 }),
67 );
68 baseUrl = out.uri!;
69 noteName = out.note;
70 occurrenceName = out.occurrence;
71 hostAccount = out.serviceAccount!;
72 }),
73 { timeout: 900_000 },
74 );
75
76 afterAll(sharedStack.destroy(), { timeout: 600_000 });
77
78 describe("GetNote", () => {
79 test.provider(
80 "reads the note as the host's service account, granted on the note only",
81 (_stack) =>
82 Effect.gen(function* () {
83 const note = yield* expectProbe<containeranalysis.Note>(
84 baseUrl,
85 "getNote",
86 );
87 const live = yield* containeranalysis.getProjectsNotes({
88 name: noteName,
89 });
90 expect(note.name).toEqual(noteName);
91 expect(note.kind).toEqual("ATTESTATION");
92 expect(note.shortDescription).toEqual(live.shortDescription);
93
94 const policy = yield* containeranalysis.getIamPolicyProjectsNotes({
95 resource: noteName,
96 });
97 const roles = (policy.bindings ?? [])
98 .filter((binding) =>
99 (binding.members ?? []).includes(
100 `serviceAccount:${hostAccount}`,
101 ),
102 )
103 .map((binding) => binding.role);
104 expect(roles).toEqual(["roles/containeranalysis.notes.viewer"]);
105 expect(
106 (yield* projectRoles).map((grant) => grant.role),
107 ).not.toContain("roles/containeranalysis.notes.viewer");
108 }),
109 {
110 tags: ["provider:gcp", "provider:gcp:containeranalysis", "live"],
111 timeout: 600_000,
112 },
113 );
114 });
115
116 describe("GetOccurrence", () => {
117 test.provider(
118 "reads the occurrence as the host's service account, granted on the project",
119 (_stack) =>
120 Effect.gen(function* () {
121 const occurrence = yield* expectProbe<containeranalysis.Occurrence>(
122 baseUrl,
123 "getOccurrence",
124 );
125 const live = yield* containeranalysis.getProjectsOccurrences({
126 name: occurrenceName,
127 });
128 expect(occurrence.name).toEqual(occurrenceName);
129 expect(occurrence.noteName).toEqual(noteName);
130 expect(occurrence.resourceUri).toEqual(TEST_RESOURCE_URI);
131 expect(occurrence.createTime).toEqual(live.createTime);
132
133 // Occurrence IAM is not modelled as a grant target, so the role is
134 // granted on the project (no condition).
135 expect(yield* projectRoles).toEqual([
136 {
137 role: "roles/containeranalysis.occurrences.viewer",
138 condition: undefined,
139 },
140 ]);
141 }),
142 {
143 tags: ["provider:gcp", "provider:gcp:containeranalysis", "live"],
144 timeout: 600_000,
145 },
146 );
147 });
148 },
149);