1import * as containeranalysis from "@distilled.cloud/gcp/containeranalysis_v1";
2import * as Effect from "effect/Effect";
3import { Unowned } from "../../AdoptPolicy.ts";
4import { isResolved } from "../../Diff.ts";
5import * as Provider from "../../Provider.ts";
6import { Resource } from "../../Resource.ts";
7import { GcpEnvironment } from "../Environment.ts";
8import { hasAlchemyLabels } from "../Labels.ts";
9import type { Providers } from "../Providers.ts";
10import {
11 expandNoteName,
12 hasOwnershipMarker,
13 ignoreGone,
14 listProjectNotes,
15 missingGet,
16 noteAttrs,
17 noteKind,
18 parseDescription,
19 projectParent,
20 reconcileNote,
21 replaceOnIdentity,
22 retryTransient,
23 toPhysicalId,
24} from "./internal.ts";
25
26export type NoteProps = {
27 /**
28 * Note id (the `{note}` segment of
29 * `projects/{project}/notes/{note}`). If omitted, a unique RFC1035 id is
30 * generated. Immutable — changing it replaces the note.
31 */
32 noteId?: string;
33 /**
34 * One-sentence summary of the analysis this note describes.
35 */
36 shortDescription?: string;
37 /**
38 * Detailed description. Alchemy stamps ownership into this field for
39 * `list` / nuke because notes have no labels API.
40 */
41 longDescription?: string;
42 /**
43 * RFC3339 expiration. Empty means the note does not expire.
44 */
45 expirationTime?: string;
46 /**
47 * URLs associated with this note.
48 */
49 relatedUrl?: containeranalysis.RelatedUrlList;
50 /**
51 * Other note resource names related to this note.
52 */
53 relatedNoteNames?: string[];
54 /**
55 * Attestation authority note. Used when no other kind is set.
56 */
57 attestation?: containeranalysis.AttestationNote;
58 /**
59 * Build provenance note.
60 */
61 build?: containeranalysis.BuildNote;
62 /**
63 * Discovery note.
64 */
65 discovery?: containeranalysis.DiscoveryNote;
66 /**
67 * Base image note.
68 */
69 image?: containeranalysis.ImageNote;
70 /**
71 * Package note.
72 */
73 package?: containeranalysis.PackageNote;
74 /**
75 * Vulnerability note.
76 */
77 vulnerability?: containeranalysis.VulnerabilityNote;
78 /**
79 * Compliance note.
80 */
81 compliance?: containeranalysis.ComplianceNote;
82 /**
83 * Deployment note.
84 */
85 deployment?: containeranalysis.DeploymentNote;
86 /**
87 * Upgrade note.
88 */
89 upgrade?: containeranalysis.UpgradeNote;
90 /**
91 * Vulnerability assessment note.
92 */
93 vulnerabilityAssessment?: containeranalysis.VulnerabilityAssessmentNote;
94 /**
95 * Secret note.
96 */
97 secret?: containeranalysis.SecretNote;
98 /**
99 * DSSE attestation note.
100 */
101 dsseAttestation?: containeranalysis.DSSEAttestationNote;
102 /**
103 * SBOM reference note.
104 */
105 sbomReference?: containeranalysis.SBOMReferenceNote;
106 /**
107 * AI skill analysis note.
108 */
109 aiSkillAnalysis?: containeranalysis.AISkillAnalysisNote;
110};
111
112export type Note = Resource<
113 "GCP.ContainerAnalysis.Note",
114 NoteProps,
115 {
116 /** Full resource name `projects/{project}/notes/{note}`. */
117 name: string;
118 /** Note id (last path segment). */
119 noteId: string;
120 /** Project id. */
121 project: string;
122 /** One-sentence summary. */
123 shortDescription: string | undefined;
124 /** User description with the Alchemy ownership prefix stripped. */
125 longDescription: string | undefined;
126 /** Server-reported analysis kind. */
127 kind: string | undefined;
128 /** RFC3339 expiration, if any. */
129 expirationTime: string | undefined;
130 /** URLs associated with this note. */
131 relatedUrl: containeranalysis.RelatedUrlList;
132 /** Related note resource names. */
133 relatedNoteNames: string[];
134 /** Attestation authority metadata. */
135 attestation: containeranalysis.AttestationNote | undefined;
136 /** Build provenance metadata. */
137 build: containeranalysis.BuildNote | undefined;
138 /** Discovery metadata. */
139 discovery: containeranalysis.DiscoveryNote | undefined;
140 /** Base image metadata. */
141 image: containeranalysis.ImageNote | undefined;
142 /** Package metadata. */
143 package: containeranalysis.PackageNote | undefined;
144 /** Vulnerability metadata. */
145 vulnerability: containeranalysis.VulnerabilityNote | undefined;
146 /** Compliance metadata. */
147 compliance: containeranalysis.ComplianceNote | undefined;
148 /** Deployment metadata. */
149 deployment: containeranalysis.DeploymentNote | undefined;
150 /** Upgrade metadata. */
151 upgrade: containeranalysis.UpgradeNote | undefined;
152 /** Vulnerability assessment metadata. */
153 vulnerabilityAssessment:
154 | containeranalysis.VulnerabilityAssessmentNote
155 | undefined;
156 /** Secret metadata. */
157 secret: containeranalysis.SecretNote | undefined;
158 /** DSSE attestation metadata. */
159 dsseAttestation: containeranalysis.DSSEAttestationNote | undefined;
160 /** SBOM reference metadata. */
161 sbomReference: containeranalysis.SBOMReferenceNote | undefined;
162 /** AI skill analysis metadata. */
163 aiSkillAnalysis: containeranalysis.AISkillAnalysisNote | undefined;
164 /** RFC3339 creation timestamp. */
165 createTime: string | undefined;
166 /** RFC3339 last-update timestamp. */
167 updateTime: string | undefined;
168 },
169 never,
170 Providers
171>;
172
173/**
174 * A project-scoped Container Analysis (Grafeas) note. Notes describe a
175 * type of analysis; occurrences attach a note to a resource such as a
176 * container image.
177 *
178 * Notes have no labels field, so Alchemy stamps ownership into
179 * `longDescription` for `list` / nuke. `noteId` is identity — changing it
180 * replaces the note. Descriptions, related URLs, and kind payloads update
181 * in place. Switching the analysis kind replaces the note.
182 *
183 * ### Creating a Note
184 * **Example:** Attestation authority
185 * ```typescript
186 * const note = yield* GCP.ContainerAnalysis.Note("Authority", {
187 * shortDescription: "qa attestor",
188 * attestation: { hint: { humanReadableName: "QA" } },
189 * });
190 * ```
191 *
192 * **Example:** Explicit id and related URL
193 * ```typescript
194 * const note = yield* GCP.ContainerAnalysis.Note("Authority", {
195 * noteId: "qa-attestor",
196 * shortDescription: "qa attestor",
197 * longDescription: "signs production images",
198 * relatedUrl: [{ url: "https://example.com/policy", label: "policy" }],
199 * attestation: { hint: { humanReadableName: "QA" } },
200 * });
201 * ```
202 *
203 * ### Updating a Note
204 * Re-declare the same logical id with changed props; the engine keeps the
205 * physical resource and updates it in place.
206 *
207 * **Example:** Change the summary
208 * ```typescript
209 * const note = yield* GCP.ContainerAnalysis.Note("Authority", {
210 * shortDescription: "qa and staging attestor",
211 * attestation: { hint: { humanReadableName: "QA" } },
212 * });
213 * ```
214 *
215 * @resource
216 * @category ContainerAnalysis
217 */
218export const Note = Resource<Note>("GCP.ContainerAnalysis.Note");
219
220const resourceName = (project: string, noteId: string) =>
221 expandNoteName(noteId, project);
222
223const getByName = missingGet(containeranalysis.getProjectsNotes);
224
225const toPublicAttrs = (
226 note: containeranalysis.Note,
227 project: string,
228): Note["Attributes"] => {
229 const attrs = noteAttrs(note, project);
230 return {
231 name: attrs.name,
232 noteId: attrs.noteId,
233 project: attrs.project,
234 shortDescription: attrs.shortDescription,
235 longDescription: attrs.longDescription,
236 kind: attrs.kind,
237 expirationTime: attrs.expirationTime,
238 relatedUrl: attrs.relatedUrl,
239 relatedNoteNames: attrs.relatedNoteNames,
240 attestation: attrs.attestation,
241 build: attrs.build,
242 discovery: attrs.discovery,
243 image: attrs.image,
244 package: attrs.package,
245 vulnerability: attrs.vulnerability,
246 compliance: attrs.compliance,
247 deployment: attrs.deployment,
248 upgrade: attrs.upgrade,
249 vulnerabilityAssessment: attrs.vulnerabilityAssessment,
250 secret: attrs.secret,
251 dsseAttestation: attrs.dsseAttestation,
252 sbomReference: attrs.sbomReference,
253 aiSkillAnalysis: attrs.aiSkillAnalysis,
254 createTime: attrs.createTime,
255 updateTime: attrs.updateTime,
256 };
257};
258
259export const NoteProvider = () =>
260 Provider.succeed(Note, {
261 stables: ["name", "noteId", "project", "kind", "createTime"],
262
263 diff: Effect.fn(function* ({ news, olds, output }) {
264 if (!isResolved(news)) return undefined;
265 const previousKind = noteKind(olds ?? {}) ?? output?.kind;
266 const nextKind = noteKind(news) ?? previousKind;
267 return replaceOnIdentity({
268 previousId: olds?.noteId ?? output?.noteId,
269 nextId: news.noteId,
270 extra:
271 previousKind !== undefined &&
272 nextKind !== undefined &&
273 previousKind !== nextKind,
274 });
275 }),
276
277 read: Effect.fn(function* ({ id, olds, output }) {
278 const env = yield* GcpEnvironment.current;
279 const noteId = yield* toPhysicalId(id, olds?.noteId, output?.noteId);
280 const name = output?.name ?? resourceName(env.project, noteId);
281 const existing = yield* getByName(name);
282 if (existing === undefined) return undefined;
283 const attrs = toPublicAttrs(existing, env.project);
284 const { labels } = parseDescription(existing.longDescription);
285 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
286 }),
287
288 list: () =>
289 Effect.gen(function* () {
290 const env = yield* GcpEnvironment.current;
291 const items = yield* listProjectNotes(env.project);
292 return items
293 .filter((item) => hasOwnershipMarker(item.longDescription))
294 .map((item) => toPublicAttrs(item, env.project));
295 }),
296
297 reconcile: Effect.fn(function* ({ id, news, output }) {
298 const env = yield* GcpEnvironment.current;
299 const noteId = yield* toPhysicalId(id, news.noteId, output?.noteId);
300 const parent = projectParent(env.project);
301 const name = resourceName(env.project, noteId);
302 const current = yield* reconcileNote({
303 id,
304 name: output?.name ?? name,
305 parent,
306 noteId,
307 news,
308 ops: {
309 get: getByName,
310 create: (request) =>
311 retryTransient(
312 containeranalysis.createProjectsNotes({
313 parent: request.parent,
314 noteId: request.noteId,
315 body: request.body,
316 }),
317 ).pipe(
318 Effect.catchTag("Conflict", () =>
319 getByName(`${request.parent}/notes/${request.noteId}`),
320 ),
321 ),
322 patch: (request) =>
323 retryTransient(
324 containeranalysis.patchProjectsNotes({
325 name: request.name,
326 updateMask: request.updateMask,
327 body: request.body,
328 }),
329 ),
330 },
331 });
332 return toPublicAttrs(current, env.project);
333 }),
334
335 delete: Effect.fn(function* ({ output }) {
336 yield* ignoreGone(
337 containeranalysis.deleteProjectsNotes({ name: output.name }),
338 );
339 }),
340 });