1import * as ecr from "@distilled.cloud/aws/ecr";
2import * as Effect from "effect/Effect";
3import * as Provider from "../../Provider.ts";
4import { Resource } from "../../Resource.ts";
5import { AWSEnvironment } from "../Environment.ts";
6import type { PolicyDocument } from "../IAM/Policy.ts";
7import {
8 normalizePolicyDocument,
9 stringifyPolicyDocument,
10} from "../IAM/Policy.ts";
11import type { Providers } from "../Providers.ts";
12
13export interface RegistryPolicyProps {
14 /**
15 * The registry permissions policy — grants other AWS accounts registry
16 * level permissions such as `ecr:ReplicateImage` (cross-account
17 * replication) or `ecr:CreateRepository`. Accepts either a structured IAM
18 * {@link PolicyDocument} or a raw JSON string (escape hatch / adoption of
19 * an existing policy).
20 */
21 policy: PolicyDocument | string;
22}
23
24export interface RegistryPolicy extends Resource<
25 "AWS.ECR.RegistryPolicy",
26 RegistryPolicyProps,
27 {
28 /** The AWS account ID of the registry. */
29 registryId: string;
30 /** The JSON registry permissions policy as stored by ECR. */
31 policy: string;
32 },
33 never,
34 Providers
35> {}
36
37/**
38 * The permissions policy for a private Amazon ECR registry — an
39 * account/region **singleton** used to grant other AWS accounts
40 * registry-level permissions (most commonly `ecr:ReplicateImage` when
41 * configuring cross-account replication).
42 * ### Managing the Registry Policy
43 * **Example:** Allow Cross-Account Replication
44 * ```typescript
45 * const policy = yield* RegistryPolicy("ReplicationPolicy", {
46 * policy: {
47 * Version: "2012-10-17",
48 * Statement: [
49 * {
50 * Sid: "AllowReplication",
51 * Effect: "Allow",
52 * Principal: { AWS: `arn:aws:iam::${sourceAccountId}:root` },
53 * Action: ["ecr:ReplicateImage"],
54 * Resource: `arn:aws:ecr:us-east-1:${accountId}:repository/*`,
55 * },
56 * ],
57 * },
58 * });
59 * ```
60 *
61 * @resource
62 */
63export const RegistryPolicy = Resource<RegistryPolicy>(
64 "AWS.ECR.RegistryPolicy",
65);
66
67export const RegistryPolicyProvider = () =>
68 Provider.effect(
69 RegistryPolicy,
70 Effect.gen(function* () {
71 const readPolicy = ecr
72 .getRegistryPolicy({})
73 .pipe(
74 Effect.catchTag("RegistryPolicyNotFoundException", () =>
75 Effect.succeed(undefined),
76 ),
77 );
78
79 const toPolicyText = (policy: PolicyDocument | string) =>
80 typeof policy === "string" ? policy : stringifyPolicyDocument(policy);
81
82 return {
83 stables: ["registryId"],
84 read: Effect.fn(function* () {
85 const observed = yield* readPolicy;
86 if (observed?.policyText === undefined) return undefined;
87 return {
88 registryId:
89 observed.registryId ?? (yield* AWSEnvironment.current).accountId,
90 policy: observed.policyText,
91 };
92 }),
93 // The registry policy is an account/region singleton: at most one
94 // exists, keyed on the caller's registry.
95 list: () =>
96 Effect.gen(function* () {
97 const observed = yield* readPolicy;
98 if (observed?.policyText === undefined) return [];
99 return [
100 {
101 registryId:
102 observed.registryId ??
103 (yield* AWSEnvironment.current).accountId,
104 policy: observed.policyText,
105 },
106 ];
107 }),
108 reconcile: Effect.fn(function* ({ news, session }) {
109 const desired = toPolicyText(news.policy);
110
111 // Observe — compare the live policy against the desired one via
112 // `normalizePolicyDocument` (key order / whitespace insensitive)
113 // so a re-deploy of an equivalent document is a no-op.
114 const observed = yield* readPolicy;
115 let registryId = observed?.registryId;
116 if (
117 observed?.policyText === undefined ||
118 normalizePolicyDocument(observed.policyText) !==
119 normalizePolicyDocument(desired)
120 ) {
121 const put = yield* ecr.putRegistryPolicy({ policyText: desired });
122 registryId = put.registryId ?? registryId;
123 }
124 registryId ??= (yield* AWSEnvironment.current).accountId;
125
126 yield* session.note(registryId);
127 return { registryId, policy: desired };
128 }),
129 delete: Effect.fn(function* () {
130 yield* ecr
131 .deleteRegistryPolicy({})
132 .pipe(
133 Effect.catchTag(
134 "RegistryPolicyNotFoundException",
135 () => Effect.void,
136 ),
137 );
138 }),
139 };
140 }),
141 );