feat: fully functioning ALB
This commit is contained in:
@@ -0,0 +1,155 @@
|
||||
import {
|
||||
BatchDeleteImageCommand,
|
||||
CreateRepositoryCommand,
|
||||
DeleteRepositoryCommand,
|
||||
DescribeImagesCommand,
|
||||
DescribeRepositoriesCommand,
|
||||
type ECRClient,
|
||||
type ImageDetail,
|
||||
type Repository,
|
||||
} from '@aws-sdk/client-ecr'
|
||||
import type {ChildItem, CollectionPage, ItemStoreAdapter, PageQuery} from '../cloud-spi/childCollections'
|
||||
import {RuntimeError, ValidationError} from '../cloud-spi/errors'
|
||||
import {ecr as defaultEcr} from '../aws'
|
||||
import {awsEcrSchema} from '../cloud-spi/ecrSchema'
|
||||
import type {CloudResource, CloudServiceAdapter, CreateResourceInput, ResourceQuery, ServiceSchema} from '../cloud-spi/types'
|
||||
|
||||
const ECR_REPOSITORY_NAME = /^(?:[a-z0-9]+(?:[._-][a-z0-9]+)*\/)*[a-z0-9]+(?:[._-][a-z0-9]+)*$/
|
||||
|
||||
export class AwsEcrAdapter implements CloudServiceAdapter {
|
||||
readonly cloud = 'aws' as const
|
||||
readonly service = 'ecr' as const
|
||||
readonly items: ItemStoreAdapter
|
||||
|
||||
constructor(private readonly ecr: ECRClient = defaultEcr) {
|
||||
this.items = new EcrImageStore(ecr)
|
||||
}
|
||||
|
||||
schema(): ServiceSchema {
|
||||
return awsEcrSchema()
|
||||
}
|
||||
|
||||
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
|
||||
const repositories: Repository[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const response = await this.ecr.send(new DescribeRepositoriesCommand({nextToken}))
|
||||
repositories.push(...(response.repositories ?? []))
|
||||
nextToken = response.nextToken
|
||||
} while (nextToken)
|
||||
|
||||
const search = query.search?.trim().toLowerCase()
|
||||
return repositories
|
||||
.filter((repository) => !search || repository.repositoryName?.toLowerCase().includes(search))
|
||||
.map(toResource)
|
||||
}
|
||||
|
||||
async get(id: string): Promise<CloudResource | null> {
|
||||
try {
|
||||
const response = await this.ecr.send(new DescribeRepositoriesCommand({repositoryNames: [id]}))
|
||||
const repository = response.repositories?.[0]
|
||||
return repository ? toResource(repository) : null
|
||||
} catch (error) {
|
||||
if (isRepositoryNotFound(error)) return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
async create(input: CreateResourceInput): Promise<CloudResource> {
|
||||
const repositoryName = stringValue(input.values.repositoryName)
|
||||
if (!repositoryName) throw new ValidationError('repositoryName is required')
|
||||
if (repositoryName.length < 2 || repositoryName.length > 256 || !ECR_REPOSITORY_NAME.test(repositoryName)) {
|
||||
throw new ValidationError('Use a valid ECR repository name.')
|
||||
}
|
||||
|
||||
const response = await this.ecr.send(new CreateRepositoryCommand({repositoryName}))
|
||||
return toResource(response.repository ?? {repositoryName})
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.ecr.send(new DeleteRepositoryCommand({repositoryName: id, force: false}))
|
||||
}
|
||||
}
|
||||
|
||||
class EcrImageStore implements ItemStoreAdapter {
|
||||
constructor(private readonly ecr: ECRClient) {}
|
||||
|
||||
async listItems(resourceId: string, page: PageQuery = {}): Promise<CollectionPage<ChildItem>> {
|
||||
const response = await this.ecr.send(new DescribeImagesCommand({
|
||||
repositoryName: resourceId,
|
||||
nextToken: page.cursor,
|
||||
maxResults: page.limit,
|
||||
}))
|
||||
return {
|
||||
items: (response.imageDetails ?? []).flatMap(toItem),
|
||||
nextCursor: response.nextToken ?? null,
|
||||
}
|
||||
}
|
||||
|
||||
async deleteItem(resourceId: string, itemId: string): Promise<void> {
|
||||
const response = await this.ecr.send(new BatchDeleteImageCommand({
|
||||
repositoryName: resourceId,
|
||||
imageIds: [{imageDigest: itemId}],
|
||||
}))
|
||||
const failure = response.failures?.[0]
|
||||
if (failure) throw new RuntimeError(failure.failureReason ?? `Unable to delete image ${itemId}`)
|
||||
}
|
||||
}
|
||||
|
||||
function toResource(repository: Repository): CloudResource {
|
||||
const name = repository.repositoryName ?? ''
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
cloud: 'aws',
|
||||
service: 'ecr',
|
||||
type: 'repository',
|
||||
region: regionFromArn(repository.repositoryArn),
|
||||
createdAt: repository.createdAt?.toISOString() ?? null,
|
||||
metadata: {
|
||||
arn: repository.repositoryArn,
|
||||
registryId: repository.registryId,
|
||||
repositoryUri: repository.repositoryUri,
|
||||
imageTagMutability: repository.imageTagMutability,
|
||||
imageScanningConfiguration: repository.imageScanningConfiguration,
|
||||
encryptionConfiguration: repository.encryptionConfiguration,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function toItem(image: ImageDetail): ChildItem[] {
|
||||
if (!image.imageDigest) return []
|
||||
return [{
|
||||
id: image.imageDigest,
|
||||
collectionId: null,
|
||||
timestamp: image.imagePushedAt?.toISOString() ?? null,
|
||||
body: {
|
||||
imageDigest: image.imageDigest,
|
||||
imageTags: image.imageTags ?? [],
|
||||
imageSizeInBytes: image.imageSizeInBytes ?? null,
|
||||
imagePushedAt: image.imagePushedAt?.toISOString() ?? null,
|
||||
imageManifestMediaType: image.imageManifestMediaType ?? null,
|
||||
artifactMediaType: image.artifactMediaType ?? null,
|
||||
},
|
||||
metadata: {
|
||||
imageScanStatus: image.imageScanStatus,
|
||||
imageScanFindingsSummary: image.imageScanFindingsSummary,
|
||||
registryId: image.registryId,
|
||||
repositoryName: image.repositoryName,
|
||||
},
|
||||
}]
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function regionFromArn(arn?: string): string | null {
|
||||
return arn?.split(':')[3] || null
|
||||
}
|
||||
|
||||
function isRepositoryNotFound(error: unknown): boolean {
|
||||
return typeof error === 'object' && error !== null
|
||||
&& 'name' in error
|
||||
&& (error as {name?: string}).name === 'RepositoryNotFoundException'
|
||||
}
|
||||
Reference in New Issue
Block a user