1import * as binaryauthorization from "@distilled.cloud/gcp/binaryauthorization_v1";
2import * as Data from "effect/Data";
3import * as Effect from "effect/Effect";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import * as Provider from "../../Provider.ts";
7import { Resource } from "../../Resource.ts";
8import { GcpEnvironment } from "../Environment.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 attestorName,
12 createOwnership,
13 encodeDescription,
14 expandNoteReference,
15 hasOwnershipMarker,
16 ignoreGone,
17 lastSegment,
18 listAttestors,
19 missingGet,
20 ownedBy,
21 parseAttestorName,
22 parseDescription,
23 publicKeysOf,
24 replaceOnIdentity,
25 ResourceNotResolved,
26 retryTransient,
27 samePublicKeys,
28 sameText,
29 toPhysicalId,
30} from "./internal.ts";
31
32export type AttestorPublicKey = binaryauthorization.AttestorPublicKey;
33
34export type AttestorProps = {
35 /**
36 * Attestor id (the `{attestor}` segment of
37 * `projects/{project}/attestors/{attestor}`). If omitted, a unique RFC1035
38 * id is generated. Immutable — changing it replaces the attestor.
39 */
40 attestorId?: string;
41 /**
42 * Grafeas Attestation Authority note this attestor reads, as
43 * `projects/{project}/notes/{note}` or a note id in the same project.
44 * Immutable — changing it replaces the attestor.
45 */
46 noteReference: string;
47 /**
48 * Human-readable comment. Binary Authorization attestors have no labels
49 * field, so Alchemy stamps ownership into this field for `list` / nuke
50 * and strips the marker from attributes.
51 */
52 description?: string;
53 /**
54 * Public keys that verify attestations signed by this attestor. Empty
55 * means the attestor never reports a valid attestation.
56 */
57 publicKeys?: AttestorPublicKey[];
58};
59
60export type Attestor = Resource<
61 "GCP.BinaryAuthorization.Attestor",
62 AttestorProps,
63 {
64 /** Full resource name `projects/{project}/attestors/{attestor}`. */
65 name: string;
66 /** Attestor id (last path segment). */
67 attestorId: string;
68 /** Project id. */
69 project: string;
70 /** User description with the Alchemy ownership prefix stripped. */
71 description: string | undefined;
72 /** Grafeas note this attestor reads. */
73 noteReference: string;
74 /** Public keys that verify attestations. */
75 publicKeys: AttestorPublicKey[];
76 /**
77 * Service account Binary Authorization uses when querying Container
78 * Analysis for this attestor.
79 */
80 delegationServiceAccountEmail: string | undefined;
81 /** Server-assigned checksum for optimistic concurrency. */
82 etag: string | undefined;
83 /** RFC3339 last-update timestamp. */
84 updateTime: string | undefined;
85 },
86 never,
87 Providers
88>;
89
90/**
91 * A Binary Authorization attestor that verifies container-image
92 * attestations against a Grafeas Attestation Authority note.
93 *
94 * Attestors have no labels field, so Alchemy stamps ownership into
95 * `description` for `list` / nuke. `attestorId` and `noteReference` are
96 * identity — changing either replaces the attestor. Description and
97 * public keys update in place.
98 *
99 * ### Creating an Attestor
100 * **Example:** Generated name
101 * ```typescript
102 * const note = yield* GCP.ContainerAnalysis.Note("Authority", {
103 * attestation: { hint: { humanReadableName: "QA" } },
104 * });
105 * const attestor = yield* GCP.BinaryAuthorization.Attestor("Qa", {
106 * noteReference: note.name,
107 * });
108 * ```
109 *
110 * **Example:** Explicit id, description, and PKIX key
111 * ```typescript
112 * const attestor = yield* GCP.BinaryAuthorization.Attestor("Qa", {
113 * attestorId: "qa-attestor",
114 * noteReference: note.name,
115 * description: "signs production images",
116 * publicKeys: [
117 * {
118 * comment: "ci",
119 * pkixPublicKey: {
120 * publicKeyPem: pem,
121 * signatureAlgorithm: "EC_SIGN_P256_SHA256",
122 * },
123 * },
124 * ],
125 * });
126 * ```
127 *
128 * ### Updating an Attestor
129 * **Example:** Change the description
130 * ```typescript
131 * const attestor = yield* GCP.BinaryAuthorization.Attestor("Qa", {
132 * description: "signs staging and production images",
133 * });
134 * ```
135 *
136 * @resource
137 * @category BinaryAuthorization
138 */
139export const Attestor = Resource<Attestor>("GCP.BinaryAuthorization.Attestor");
140
141export class NoteReferenceRequired extends Data.TaggedError(
142 "GCP.BinaryAuthorization.NoteReferenceRequired",
143)<{
144 attestorId: string;
145}> {}
146
147const getByName = missingGet(binaryauthorization.getProjectsAttestors);
148
149const toAttrs = (
150 attestor: binaryauthorization.Attestor,
151 project: string,
152): Attestor["Attributes"] => {
153 const name = attestor.name ?? "";
154 const parsed = parseAttestorName(name);
155 const { description } = parseDescription(attestor.description);
156 const note = attestor.userOwnedGrafeasNote;
157 return {
158 name,
159 attestorId: parsed.attestorId,
160 project: parsed.project || project,
161 description,
162 noteReference: note?.noteReference ?? "",
163 publicKeys: publicKeysOf(note?.publicKeys),
164 delegationServiceAccountEmail: note?.delegationServiceAccountEmail,
165 etag: attestor.etag,
166 updateTime: attestor.updateTime,
167 };
168};
169
170const toBody = (
171 news: AttestorProps,
172 description: string,
173 noteReference: string,
174): binaryauthorization.Attestor => ({
175 description,
176 userOwnedGrafeasNote: {
177 noteReference,
178 publicKeys: news.publicKeys,
179 },
180});
181
182export const AttestorProvider = () =>
183 Provider.succeed(Attestor, {
184 stables: ["name", "attestorId", "project", "noteReference"],
185
186 diff: Effect.fn(function* ({ news, olds, output }) {
187 if (!isResolved(news)) return undefined;
188 const previousNote = lastSegment(
189 olds?.noteReference ?? output?.noteReference ?? "",
190 );
191 const nextNote = lastSegment(news.noteReference);
192 return replaceOnIdentity({
193 previousId: olds?.attestorId ?? output?.attestorId,
194 nextId: news.attestorId,
195 extra:
196 previousNote.length > 0 &&
197 nextNote.length > 0 &&
198 previousNote !== nextNote,
199 deleteFirst: true,
200 });
201 }),
202
203 read: Effect.fn(function* ({ id, olds, output }) {
204 const env = yield* GcpEnvironment.current;
205 const attestorId = yield* toPhysicalId(
206 id,
207 olds?.attestorId,
208 output?.attestorId,
209 "attestor",
210 );
211 const name = output?.name ?? attestorName(env.project, attestorId);
212 const existing = yield* getByName(name);
213 if (existing === undefined) return undefined;
214 const attrs = toAttrs(existing, env.project);
215 const { labels } = parseDescription(existing.description);
216 return (yield* ownedBy(id, labels)) ? attrs : Unowned(attrs);
217 }),
218
219 list: () =>
220 Effect.gen(function* () {
221 const env = yield* GcpEnvironment.current;
222 const items = yield* listAttestors(env.project);
223 return items
224 .filter((item) => hasOwnershipMarker(item.description))
225 .map((item) => toAttrs(item, env.project));
226 }),
227
228 reconcile: Effect.fn(function* ({ id, news, output }) {
229 const env = yield* GcpEnvironment.current;
230 const attestorId = yield* toPhysicalId(
231 id,
232 news.attestorId,
233 output?.attestorId,
234 "attestor",
235 );
236 const name = attestorName(env.project, attestorId);
237 if (news.noteReference.trim().length === 0) {
238 return yield* new NoteReferenceRequired({ attestorId });
239 }
240 const desiredNote = expandNoteReference(news.noteReference, env.project);
241 const ownership = yield* createOwnership(id);
242 const desiredDescription = encodeDescription(ownership, news.description);
243
244 let current = yield* getByName(output?.name ?? name);
245
246 if (current === undefined) {
247 const created = yield* retryTransient(
248 binaryauthorization.createProjectsAttestors({
249 parent: `projects/${env.project}`,
250 attestorId,
251 body: toBody(news, desiredDescription, desiredNote),
252 }),
253 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
254 current = created ?? undefined;
255 }
256
257 if (current === undefined) {
258 return yield* new ResourceNotResolved({ name });
259 }
260
261 const observedNote = current.userOwnedGrafeasNote?.noteReference ?? "";
262 const descriptionChanged = !sameText(
263 current.description,
264 desiredDescription,
265 );
266 const keysChanged = !samePublicKeys(
267 current.userOwnedGrafeasNote?.publicKeys,
268 news.publicKeys,
269 );
270
271 if (descriptionChanged || keysChanged) {
272 current = yield* retryTransient(
273 binaryauthorization.updateProjectsAttestors({
274 name: current.name ?? name,
275 body: {
276 name: current.name ?? name,
277 description: desiredDescription,
278 etag: current.etag,
279 userOwnedGrafeasNote: {
280 noteReference: observedNote || desiredNote,
281 publicKeys: news.publicKeys,
282 delegationServiceAccountEmail:
283 current.userOwnedGrafeasNote?.delegationServiceAccountEmail,
284 },
285 },
286 }),
287 );
288 }
289
290 return toAttrs(current, env.project);
291 }),
292
293 delete: Effect.fn(function* ({ output }) {
294 yield* ignoreGone(
295 binaryauthorization.deleteProjectsAttestors({ name: output.name }),
296 );
297 }),
298 });