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 type { NoteProps } from "./Note.ts";
11import {
12 expandNoteName,
13 hasOwnershipMarker,
14 ignoreGone,
15 listLocationNotes,
16 locationParent,
17 missingGet,
18 noteAttrs,
19 noteKind,
20 normalizeLocation,
21 parseDescription,
22 reconcileNote,
23 replaceOnIdentity,
24 retryTransient,
25 toPhysicalId,
26} from "./internal.ts";
27
28export type LocationsNoteProps = NoteProps & {
29 /**
30 * Location (`us-central1`, `us-east1`, …). Immutable — changing it
31 * replaces the note. `US-CENTRAL1` is accepted and normalized to
32 * `us-central1`.
33 * @default the stack's GCP region (`GCP.Region`, profile region, `us-central1`)
34 */
35 location?: string;
36};
37
38export type LocationsNote = Resource<
39 "GCP.ContainerAnalysis.LocationsNote",
40 LocationsNoteProps,
41 {
42 /** Full resource name `projects/{project}/locations/{location}/notes/{note}`. */
43 name: string;
44 /** Note id (last path segment). */
45 noteId: string;
46 /** Project id. */
47 project: string;
48 /** Location id. */
49 location: string;
50 /** One-sentence summary. */
51 shortDescription: string | undefined;
52 /** User description with the Alchemy ownership prefix stripped. */
53 longDescription: string | undefined;
54 /** Server-reported analysis kind. */
55 kind: string | undefined;
56 /** RFC3339 expiration, if any. */
57 expirationTime: string | undefined;
58 /** URLs associated with this note. */
59 relatedUrl: containeranalysis.RelatedUrlList;
60 /** Related note resource names. */
61 relatedNoteNames: string[];
62 /** Attestation authority metadata. */
63 attestation: containeranalysis.AttestationNote | undefined;
64 /** Build provenance metadata. */
65 build: containeranalysis.BuildNote | undefined;
66 /** Discovery metadata. */
67 discovery: containeranalysis.DiscoveryNote | undefined;
68 /** Base image metadata. */
69 image: containeranalysis.ImageNote | undefined;
70 /** Package metadata. */
71 package: containeranalysis.PackageNote | undefined;
72 /** Vulnerability metadata. */
73 vulnerability: containeranalysis.VulnerabilityNote | undefined;
74 /** Compliance metadata. */
75 compliance: containeranalysis.ComplianceNote | undefined;
76 /** Deployment metadata. */
77 deployment: containeranalysis.DeploymentNote | undefined;
78 /** Upgrade metadata. */
79 upgrade: containeranalysis.UpgradeNote | undefined;
80 /** Vulnerability assessment metadata. */
81 vulnerabilityAssessment:
82 | containeranalysis.VulnerabilityAssessmentNote
83 | undefined;
84 /** Secret metadata. */
85 secret: containeranalysis.SecretNote | undefined;
86 /** DSSE attestation metadata. */
87 dsseAttestation: containeranalysis.DSSEAttestationNote | undefined;
88 /** SBOM reference metadata. */
89 sbomReference: containeranalysis.SBOMReferenceNote | undefined;
90 /** AI skill analysis metadata. */
91 aiSkillAnalysis: containeranalysis.AISkillAnalysisNote | undefined;
92 /** RFC3339 creation timestamp. */
93 createTime: string | undefined;
94 /** RFC3339 last-update timestamp. */
95 updateTime: string | undefined;
96 },
97 never,
98 Providers
99>;
100
101/**
102 * A location-scoped Container Analysis (Grafeas) note. Same shape as
103 * {@link Note}, stored at
104 * `projects/{project}/locations/{location}/notes/{note}`.
105 *
106 * Location and note id are identity. Alchemy stamps ownership into
107 * `longDescription` for `list` / nuke.
108 *
109 * ### Creating a Locations Note
110 * **Example:** Regional attestation authority
111 * ```typescript
112 * const note = yield* GCP.ContainerAnalysis.LocationsNote("Authority", {
113 * location: "us-central1",
114 * shortDescription: "qa attestor",
115 * attestation: { hint: { humanReadableName: "QA" } },
116 * });
117 * ```
118 *
119 * @resource
120 * @category ContainerAnalysis
121 */
122export const LocationsNote = Resource<LocationsNote>(
123 "GCP.ContainerAnalysis.LocationsNote",
124);
125
126const resourceName = (project: string, location: string, noteId: string) =>
127 expandNoteName(noteId, project, location);
128
129const getByName = missingGet(containeranalysis.getProjectsLocationsNotes);
130
131const toPublicAttrs = (
132 note: containeranalysis.Note,
133 project: string,
134 region: string,
135): LocationsNote["Attributes"] => {
136 const attrs = noteAttrs(note, project);
137 return {
138 name: attrs.name,
139 noteId: attrs.noteId,
140 project: attrs.project,
141 location: attrs.location ?? region,
142 shortDescription: attrs.shortDescription,
143 longDescription: attrs.longDescription,
144 kind: attrs.kind,
145 expirationTime: attrs.expirationTime,
146 relatedUrl: attrs.relatedUrl,
147 relatedNoteNames: attrs.relatedNoteNames,
148 attestation: attrs.attestation,
149 build: attrs.build,
150 discovery: attrs.discovery,
151 image: attrs.image,
152 package: attrs.package,
153 vulnerability: attrs.vulnerability,
154 compliance: attrs.compliance,
155 deployment: attrs.deployment,
156 upgrade: attrs.upgrade,
157 vulnerabilityAssessment: attrs.vulnerabilityAssessment,
158 secret: attrs.secret,
159 dsseAttestation: attrs.dsseAttestation,
160 sbomReference: attrs.sbomReference,
161 aiSkillAnalysis: attrs.aiSkillAnalysis,
162 createTime: attrs.createTime,
163 updateTime: attrs.updateTime,
164 };
165};
166
167export const LocationsNoteProvider = () =>
168 Provider.succeed(LocationsNote, {
169 stables: ["name", "noteId", "project", "location", "kind", "createTime"],
170
171 diff: Effect.fn(function* ({ news, olds, output }) {
172 if (!isResolved(news)) return undefined;
173 const env = yield* GcpEnvironment.current;
174 const previousKind = noteKind(olds ?? {}) ?? output?.kind;
175 const nextKind = noteKind(news) ?? previousKind;
176 return replaceOnIdentity({
177 previousId: olds?.noteId ?? output?.noteId,
178 nextId: news.noteId,
179 previousParent: locationParent(
180 "x",
181 normalizeLocation(olds?.location ?? output?.location, env.region),
182 ),
183 nextParent: locationParent(
184 "x",
185 normalizeLocation(
186 news.location ?? olds?.location ?? output?.location,
187 env.region,
188 ),
189 ),
190 extra:
191 previousKind !== undefined &&
192 nextKind !== undefined &&
193 previousKind !== nextKind,
194 });
195 }),
196
197 read: Effect.fn(function* ({ id, olds, output }) {
198 const env = yield* GcpEnvironment.current;
199 const location = normalizeLocation(
200 olds?.location ?? output?.location,
201 env.region,
202 );
203 const noteId = yield* toPhysicalId(id, olds?.noteId, output?.noteId);
204 const name = output?.name ?? resourceName(env.project, location, noteId);
205 const existing = yield* getByName(name);
206 if (existing === undefined) return undefined;
207 const attrs = toPublicAttrs(existing, env.project, env.region);
208 const { labels } = parseDescription(existing.longDescription);
209 return (yield* hasAlchemyLabels(id, labels)) ? attrs : Unowned(attrs);
210 }),
211
212 list: () =>
213 Effect.gen(function* () {
214 const env = yield* GcpEnvironment.current;
215 const items = yield* listLocationNotes(env.project, env.region);
216 return items
217 .filter((item) => hasOwnershipMarker(item.longDescription))
218 .map((item) => toPublicAttrs(item, env.project, env.region));
219 }),
220
221 reconcile: Effect.fn(function* ({ id, news, output }) {
222 const env = yield* GcpEnvironment.current;
223 const location = normalizeLocation(
224 news.location ?? output?.location,
225 env.region,
226 );
227 const noteId = yield* toPhysicalId(id, news.noteId, output?.noteId);
228 const parent = locationParent(env.project, location);
229 const name = resourceName(env.project, location, noteId);
230 const current = yield* reconcileNote({
231 id,
232 name: output?.name ?? name,
233 parent,
234 noteId,
235 news,
236 ops: {
237 get: getByName,
238 create: (request) =>
239 retryTransient(
240 containeranalysis.createProjectsLocationsNotes({
241 parent: request.parent,
242 noteId: request.noteId,
243 body: request.body,
244 }),
245 ).pipe(
246 Effect.catchTag("Conflict", () =>
247 getByName(`${request.parent}/notes/${request.noteId}`),
248 ),
249 ),
250 patch: (request) =>
251 retryTransient(
252 containeranalysis.patchProjectsLocationsNotes({
253 name: request.name,
254 updateMask: request.updateMask,
255 body: request.body,
256 }),
257 ),
258 },
259 });
260 return toPublicAttrs(current, env.project, env.region);
261 }),
262
263 delete: Effect.fn(function* ({ output }) {
264 yield* ignoreGone(
265 containeranalysis.deleteProjectsLocationsNotes({ name: output.name }),
266 );
267 }),
268 });