1import * as binaryauthorization from "@distilled.cloud/gcp/binaryauthorization_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 type { Providers } from "../Providers.ts";
9import {
10 createOwnership,
11 DEFAULT_PLATFORM,
12 encodeDescription,
13 gkePolicyOf,
14 hasOwnershipMarker,
15 listPlatformPolicies,
16 missingGet,
17 ownedBy,
18 parseDescription,
19 parsePolicyName,
20 policyName,
21 replaceOnIdentity,
22 ResourceNotResolved,
23 retryTransient,
24 sameJson,
25 sameText,
26 toPhysicalId,
27} from "./internal.ts";
28
29export type GkePolicy = binaryauthorization.GkePolicy;
30
31export type PlatformsPolicyProps = {
32 /**
33 * Policy id (the `{policy}` segment of
34 * `projects/{project}/platforms/{platform}/policies/{policy}`). If omitted,
35 * a unique RFC1035 id is generated. Immutable — changing it replaces the
36 * policy.
37 */
38 policyId?: string;
39 /**
40 * Platform this policy applies to (`gke` or `cloudRun`). Immutable —
41 * changing it replaces the policy.
42 * @default "gke"
43 */
44 platform?: string;
45 /**
46 * Human-readable comment. Platform policies have no labels field, so
47 * Alchemy stamps ownership into this field for `list` / nuke and strips
48 * the marker from attributes.
49 */
50 description?: string;
51 /**
52 * GKE-specific policy. Empty (the default) allows every image. When
53 * `checkSets` is non-empty the last entry must be an unscoped catch-all.
54 */
55 gkePolicy?: GkePolicy;
56};
57
58export type PlatformsPolicy = Resource<
59 "GCP.BinaryAuthorization.PlatformsPolicy",
60 PlatformsPolicyProps,
61 {
62 /** Full name `projects/{project}/platforms/{platform}/policies/{policy}`. */
63 name: string;
64 /** Policy id (last path segment). */
65 policyId: string;
66 /** Platform id (`gke`, `cloudRun`, …). */
67 platform: string;
68 /** Project id. */
69 project: string;
70 /** User description with the Alchemy ownership prefix stripped. */
71 description: string | undefined;
72 /** GKE-specific policy, if set. */
73 gkePolicy: GkePolicy | undefined;
74 /** Server-assigned checksum for optimistic concurrency. */
75 etag: string | undefined;
76 /** RFC3339 last-update timestamp. */
77 updateTime: string | undefined;
78 },
79 never,
80 Providers
81>;
82
83/**
84 * A Binary Authorization platform policy. Platform policies apply to a
85 * specific runtime (`gke` by default) and evaluate images independently of
86 * the project-level admission policy.
87 *
88 * Platform policies have no labels field, so Alchemy stamps ownership into
89 * `description` for `list` / nuke. `policyId` and `platform` are identity —
90 * changing either replaces the policy. Description and `gkePolicy` update
91 * in place via replace.
92 *
93 * ### Creating a Platform Policy
94 * **Example:** Allow all images on GKE
95 * ```typescript
96 * const policy = yield* GCP.BinaryAuthorization.PlatformsPolicy(
97 * "Default",
98 * {},
99 * );
100 * ```
101 *
102 * **Example:** Allowlist Google system images
103 * ```typescript
104 * const policy = yield* GCP.BinaryAuthorization.PlatformsPolicy(
105 * "Default",
106 * {
107 * policyId: "gke-default",
108 * description: "allowlisted system images",
109 * gkePolicy: {
110 * imageAllowlist: {
111 * allowPattern: ["gcr.io/google-containers/*"],
112 * },
113 * },
114 * },
115 * );
116 * ```
117 *
118 * ### Updating a Platform Policy
119 * **Example:** Add a catch-all always-deny check
120 * ```typescript
121 * const policy = yield* GCP.BinaryAuthorization.PlatformsPolicy(
122 * "Default",
123 * {
124 * gkePolicy: {
125 * checkSets: [
126 * {
127 * displayName: "default",
128 * checks: [{ displayName: "deny", alwaysDeny: true }],
129 * },
130 * ],
131 * },
132 * },
133 * );
134 * ```
135 *
136 * @resource
137 * @category BinaryAuthorization
138 */
139export const PlatformsPolicy = Resource<PlatformsPolicy>(
140 "GCP.BinaryAuthorization.PlatformsPolicy",
141);
142
143const getByName = missingGet(binaryauthorization.getProjectsPlatformsPolicies);
144
145const toAttrs = (
146 policy: binaryauthorization.PlatformPolicy,
147 project: string,
148): PlatformsPolicy["Attributes"] => {
149 const name = policy.name ?? "";
150 const parsed = parsePolicyName(name);
151 const { description } = parseDescription(policy.description);
152 return {
153 name,
154 policyId: parsed.policyId,
155 platform: parsed.platform,
156 project: parsed.project || project,
157 description,
158 gkePolicy: policy.gkePolicy,
159 etag: policy.etag,
160 updateTime: policy.updateTime,
161 };
162};
163
164const toBody = (
165 news: PlatformsPolicyProps,
166 description: string,
167 etag: string | undefined,
168): binaryauthorization.PlatformPolicy => ({
169 description,
170 etag,
171 gkePolicy: gkePolicyOf(news.gkePolicy),
172});
173
174export const PlatformsPolicyProvider = () =>
175 Provider.succeed(PlatformsPolicy, {
176 stables: ["name", "policyId", "platform", "project"],
177
178 diff: Effect.fn(function* ({ news, olds, output }) {
179 if (!isResolved(news)) return undefined;
180 return replaceOnIdentity({
181 previousId: olds?.policyId ?? output?.policyId,
182 nextId: news.policyId,
183 previousParent: olds?.platform ?? output?.platform,
184 nextParent: news.platform ?? output?.platform ?? DEFAULT_PLATFORM,
185 });
186 }),
187
188 read: Effect.fn(function* ({ id, olds, output }) {
189 const env = yield* GcpEnvironment.current;
190 const policyId = yield* toPhysicalId(
191 id,
192 olds?.policyId,
193 output?.policyId,
194 "policy",
195 );
196 const platform = olds?.platform ?? output?.platform ?? DEFAULT_PLATFORM;
197 const name = output?.name ?? policyName(env.project, platform, policyId);
198 const existing = yield* getByName(name);
199 if (existing === undefined) return undefined;
200 const attrs = toAttrs(existing, env.project);
201 const { labels } = parseDescription(existing.description);
202 return (yield* ownedBy(id, labels)) ? attrs : Unowned(attrs);
203 }),
204
205 list: () =>
206 Effect.gen(function* () {
207 const env = yield* GcpEnvironment.current;
208 const items = yield* listPlatformPolicies(env.project);
209 return items
210 .filter((item) => hasOwnershipMarker(item.description))
211 .map((item) => toAttrs(item, env.project));
212 }),
213
214 reconcile: Effect.fn(function* ({ id, news, output }) {
215 const env = yield* GcpEnvironment.current;
216 const policyId = yield* toPhysicalId(
217 id,
218 news.policyId,
219 output?.policyId,
220 "policy",
221 );
222 const platform = news.platform ?? output?.platform ?? DEFAULT_PLATFORM;
223 const name = policyName(env.project, platform, policyId);
224 const ownership = yield* createOwnership(id);
225 const desiredDescription = encodeDescription(ownership, news.description);
226 const desiredGke = gkePolicyOf(news.gkePolicy);
227
228 let current = yield* getByName(output?.name ?? name);
229
230 if (current === undefined) {
231 const created = yield* retryTransient(
232 binaryauthorization.createProjectsPlatformsPolicies({
233 parent: `projects/${env.project}/platforms/${platform}`,
234 policyId,
235 body: toBody(news, desiredDescription, undefined),
236 }),
237 ).pipe(Effect.catchTag("Conflict", () => getByName(name)));
238 current = created ?? undefined;
239 }
240
241 if (current === undefined) {
242 return yield* new ResourceNotResolved({ name });
243 }
244
245 const descriptionChanged = !sameText(
246 current.description,
247 desiredDescription,
248 );
249 const gkeChanged = !sameJson(gkePolicyOf(current.gkePolicy), desiredGke);
250
251 if (descriptionChanged || gkeChanged) {
252 current = yield* retryTransient(
253 binaryauthorization.replacePlatformPolicyProjectsPlatformsPolicies({
254 name: current.name ?? name,
255 body: toBody(news, desiredDescription, current.etag),
256 }),
257 );
258 }
259
260 return toAttrs(current, env.project);
261 }),
262
263 delete: Effect.fn(function* ({ output }) {
264 yield* binaryauthorization
265 .deleteProjectsPlatformsPolicies({
266 name: output.name,
267 etag: output.etag,
268 })
269 .pipe(
270 Effect.catchTag("Conflict", () =>
271 binaryauthorization.deleteProjectsPlatformsPolicies({
272 name: output.name,
273 }),
274 ),
275 Effect.catchTag("NotFound", () => Effect.void),
276 );
277 }),
278 });