import * as ecrpublic from "@distilled.cloud/aws/ecr-public"; import * as Effect from "effect/Effect"; import * as Stream from "effect/Stream"; import { Unowned } from "../../AdoptPolicy.ts"; import { isResolved } from "../../Diff.ts"; import { createPhysicalName } from "../../PhysicalName.ts"; import * as Provider from "../../Provider.ts"; import { Resource } from "../../Resource.ts"; import { createInternalTags, diffTags, hasAlchemyTags } from "../../Tags.ts"; import type { AccountID } from "../Environment.ts"; import type { Providers } from "../Providers.ts"; import { Region } from "../Region.ts"; // Amazon ECR Public is a single global registry hosted only in us-east-1. Every // control-plane call must target that region regardless of the ambient stack // region, so we pin it on every distilled operation. const US_EAST_1 = "us-east-1"; const pin = (effect: Effect.Effect) => effect.pipe(Effect.provideService(Region, Effect.succeed(US_EAST_1))); export type RepositoryName = string; export type RepositoryArn = `arn:aws:ecr-public::${AccountID}:repository/${RepositoryName}`; /** * Catalog display metadata shown on the public.ecr.aws gallery page for a * repository. All fields are optional and free-form. */ export interface PublicRepositoryCatalogData { /** Short description of the contents of the repository. */ description?: string; /** CPU architectures the images support (e.g. `["x86-64", "ARM 64"]`). */ architectures?: string[]; /** Operating systems the images support (e.g. `["Linux"]`). */ operatingSystems?: string[]; /** Detailed "about" markdown shown on the gallery page. */ aboutText?: string; /** Usage instructions markdown shown on the gallery page. */ usageText?: string; } export interface PublicRepositoryProps { /** * Name of the public repository. If omitted, a unique name is generated. * Must be lowercase. */ repositoryName?: string; /** * Catalog display metadata for the public gallery page. */ catalogData?: PublicRepositoryCatalogData; /** * Repository permission policy JSON granting push/pull access to other * principals. Omit for a private-push, public-pull repository. */ policyText?: string; /** * User-defined tags to apply to the repository. */ tags?: Record; } export interface PublicRepository extends Resource< "AWS.ECRPublic.Repository", PublicRepositoryProps, { /** The name of the public repository. */ repositoryName: RepositoryName; /** The ARN of the public repository. */ repositoryArn: RepositoryArn; /** The public pull URI, e.g. `public.ecr.aws//`. */ repositoryUri: string; /** The AWS account ID of the registry. */ registryId: string; /** The JSON repository permissions policy, if any. */ policyText?: string; /** The tags attached to the repository. */ tags: Record; }, never, Providers > {} /** * An Amazon ECR Public repository on the public.ecr.aws registry. Images pushed * here are pullable by anyone. ECR Public is a global service hosted only in * `us-east-1`; this resource pins every control-plane call there regardless of * the stack region. * * ### Creating Public Repositories * **Example:** Basic Public Repository * ```typescript * const repo = yield* PublicRepository("MyPublicRepo", {}); * ``` * * **Example:** With Catalog Metadata * ```typescript * const repo = yield* PublicRepository("MyPublicRepo", { * catalogData: { * description: "My awesome container image", * architectures: ["x86-64", "ARM 64"], * operatingSystems: ["Linux"], * aboutText: "# About\nThis image does X.", * usageText: "docker pull public.ecr.aws/...", * }, * }); * ``` * * ### Access Policies * **Example:** Grant Cross-Account Push * ```typescript * const repo = yield* PublicRepository("MyPublicRepo", { * policyText: JSON.stringify({ * Version: "2012-10-17", * Statement: [ * { * Sid: "AllowPush", * Effect: "Allow", * Principal: { AWS: "arn:aws:iam::123456789012:root" }, * Action: ["ecr-public:BatchCheckLayerAvailability", "ecr-public:PutImage"], * }, * ], * }), * }); * ``` * * @resource */ export const PublicRepository = Resource( "AWS.ECRPublic.Repository", ); const toCatalogInput = ( catalogData: PublicRepositoryCatalogData | undefined, ): ecrpublic.RepositoryCatalogDataInput | undefined => catalogData ? { description: catalogData.description, architectures: catalogData.architectures, operatingSystems: catalogData.operatingSystems, aboutText: catalogData.aboutText, usageText: catalogData.usageText, } : undefined; const toTagRecord = ( tags: ecrpublic.Tag[] | undefined, ): Record => Object.fromEntries( (tags ?? []) .filter( (t): t is { Key: string; Value: string } => typeof t.Key === "string" && typeof t.Value === "string", ) .map((t) => [t.Key, t.Value]), ); export const PublicRepositoryProvider = () => Provider.effect( PublicRepository, Effect.gen(function* () { const toRepositoryName = ( id: string, props: { repositoryName?: string } = {}, ) => props.repositoryName ? Effect.succeed(props.repositoryName) : createPhysicalName({ id, maxLength: 205, lowercase: true }); const observe = (repositoryName: string) => pin( ecrpublic.describeRepositories({ repositoryNames: [repositoryName], }), ).pipe( Effect.map((r) => r.repositories?.[0]), Effect.catchTag("RepositoryNotFoundException", () => Effect.succeed(undefined), ), ); const readPolicy = (repositoryName: string) => pin(ecrpublic.getRepositoryPolicy({ repositoryName })).pipe( Effect.map((r) => r.policyText), Effect.catchTag("RepositoryPolicyNotFoundException", () => Effect.succeed(undefined), ), ); const syncPolicy = Effect.fn(function* ( repositoryName: string, desired: string | undefined, ) { const observed = yield* readPolicy(repositoryName); if (desired) { if (observed !== desired) { yield* pin( ecrpublic.setRepositoryPolicy({ repositoryName, policyText: desired, }), ); } } else if (observed !== undefined) { yield* pin(ecrpublic.deleteRepositoryPolicy({ repositoryName })).pipe( Effect.catchTag( "RepositoryPolicyNotFoundException", () => Effect.void, ), ); } }); const syncTags = Effect.fn(function* ( repositoryArn: string, desiredTags: Record, ) { const listed = yield* pin( ecrpublic.listTagsForResource({ resourceArn: repositoryArn }), ); const observedTags = toTagRecord(listed.tags); const { removed, upsert } = diffTags(observedTags, desiredTags); if (upsert.length > 0) { yield* pin( ecrpublic.tagResource({ resourceArn: repositoryArn, tags: upsert }), ); } if (removed.length > 0) { yield* pin( ecrpublic.untagResource({ resourceArn: repositoryArn, tagKeys: removed, }), ); } }); return { stables: [ "repositoryArn", "repositoryName", "repositoryUri", "registryId", ], diff: Effect.fn(function* ({ id, olds, news }) { if (!isResolved(news)) return; if ( (yield* toRepositoryName(id, olds ?? {})) !== (yield* toRepositoryName(id, news ?? {})) ) { return { action: "replace" } as const; } }), read: Effect.fn(function* ({ id, olds, output }) { const repositoryName = output?.repositoryName ?? (yield* toRepositoryName(id, olds ?? {})); const repository = yield* observe(repositoryName); if (!repository?.repositoryArn || !repository.repositoryUri) { return undefined; } const listed = yield* pin( ecrpublic.listTagsForResource({ resourceArn: repository.repositoryArn, }), ); const attrs = { repositoryName, repositoryArn: repository.repositoryArn as RepositoryArn, repositoryUri: repository.repositoryUri, registryId: repository.registryId!, policyText: yield* readPolicy(repositoryName), tags: toTagRecord(listed.tags), }; return (yield* hasAlchemyTags(id, toTagRecord(listed.tags))) ? attrs : Unowned(attrs); }), reconcile: Effect.fn(function* ({ id, news, session }) { const repositoryName = yield* toRepositoryName(id, news); const internalTags = yield* createInternalTags(id); const desiredTags = { ...internalTags, ...news.tags }; // Observe — cloud state is authoritative. let repository = yield* observe(repositoryName); // Ensure — create if missing. Tolerate the AlreadyExists race. if (!repository?.repositoryArn || !repository.repositoryUri) { const created = yield* pin( ecrpublic.createRepository({ repositoryName, catalogData: toCatalogInput(news.catalogData), tags: Object.entries(desiredTags).map(([Key, Value]) => ({ Key, Value, })), }), ).pipe( Effect.map((r) => r.repository), Effect.catchTag("RepositoryAlreadyExistsException", () => observe(repositoryName), ), ); repository = created; if (!repository?.repositoryArn || !repository.repositoryUri) { return yield* Effect.fail( new Error( `Failed to create or read public repository ${repositoryName}`, ), ); } } else if (news.catalogData) { // Sync catalog data on updates (create already set it). yield* pin( ecrpublic.putRepositoryCatalogData({ repositoryName, catalogData: toCatalogInput(news.catalogData)!, }), ); } const repositoryArn = repository.repositoryArn as RepositoryArn; yield* syncPolicy(repositoryName, news.policyText); yield* syncTags(repositoryArn, desiredTags); yield* session.note(repositoryArn); return { repositoryName, repositoryArn, repositoryUri: repository.repositoryUri, registryId: repository.registryId!, policyText: news.policyText, tags: desiredTags, }; }), // Enumerate every public repository in the account. `describeRepositories` // is paginated (items under `repositories`). list: () => Effect.gen(function* () { const repositories = yield* pin( ecrpublic.describeRepositories.pages({}).pipe( Stream.runCollect, Effect.map((chunk) => Array.from(chunk).flatMap((page) => page.repositories ?? []), ), ), ); return yield* Effect.forEach( repositories.filter( ( r, ): r is ecrpublic.Repository & { repositoryName: string; repositoryArn: string; repositoryUri: string; } => r.repositoryName != null && r.repositoryArn != null && r.repositoryUri != null, ), (repository) => Effect.gen(function* () { const listed = yield* pin( ecrpublic.listTagsForResource({ resourceArn: repository.repositoryArn, }), ); return { repositoryName: repository.repositoryName, repositoryArn: repository.repositoryArn as RepositoryArn, repositoryUri: repository.repositoryUri, registryId: repository.registryId!, policyText: yield* readPolicy(repository.repositoryName), tags: toTagRecord(listed.tags), }; }), { concurrency: 10 }, ); }), delete: Effect.fn(function* ({ output }) { yield* pin( ecrpublic.deleteRepository({ repositoryName: output.repositoryName, force: true, }), ).pipe( Effect.catchTag("RepositoryNotFoundException", () => Effect.void), ); }), }; }), );