Skip to content

Commit

Permalink
feat: add checkECRImageAccess function
Browse files Browse the repository at this point in the history
  • Loading branch information
jedwards1211 committed Jan 30, 2024
1 parent 818ff0b commit 4b5da39
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 2 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,8 @@
"base64-js": "^1.5.1",
"inquirer": "^8.2.6",
"is-interactive": "^1.0.0",
"promisify-child-process": "^4.1.1"
"promisify-child-process": "^4.1.1",
"zod": "^3.22.4"
},
"main": "dist/index.js",
"module": "dist/index.mjs",
Expand Down
4 changes: 3 additions & 1 deletion pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 19 additions & 0 deletions src/ImageManifestSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import z from 'zod'

const MediaType = z.string().min(1)
const Size = z.number().int().nonnegative()
const Digest = z.string().min(32)

const LayerSchema = z.object({
mediaType: MediaType,
size: Size,
digest: Digest,
})

export const ImageManifestSchema = z.object({
schemaVersion: z.literal(2),
mediaType: z.string(),
config: LayerSchema,
layers: z.array(LayerSchema),
})
export type ImageManifestSchema = z.infer<typeof ImageManifestSchema>
179 changes: 179 additions & 0 deletions src/checkECRImageAccess.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
import AWS from 'aws-sdk'
import parseECRImageUri from './parseECRImageUri'
import { ImageManifestSchema } from './ImageManifestSchema'
import isInteractive from 'is-interactive'
import inquirer from 'inquirer'
import formatECRRepositoryHostname from './formatECRRepositoryHostname'

export default async function checkECRImageAccess({
ecr,
awsConfig,
repoAccountAwsConfig,
imageUri,
log = console,
}: {
ecr?: AWS.ECR
awsConfig?: AWS.ConfigurationOptions
/**
* Config for the AWS account containing the ECR repository.
* Optional; if given, will prompt to add/update the policy on the
* ECR repository, if access checks failed and the terminal is
* interactive.
*/
repoAccountAwsConfig?: AWS.ConfigurationOptions
imageUri: string
log?: {
info: (...args: any[]) => void
warn: (...args: any[]) => void
error: (...args: any[]) => void
}
}): Promise<boolean> {
log.error('checking access to ECR image:', imageUri, '...')

const { registryId, region, repositoryName, imageTag } =
parseECRImageUri(imageUri)
if (!ecr) ecr = new AWS.ECR({ ...awsConfig, region })

try {
const { images: [image] = [] } = await ecr
.batchGetImage({
registryId,
repositoryName,
imageIds: [{ imageTag }],
})
.promise()

const imageManifest = image?.imageManifest

if (!imageManifest) {
throw new Error(`imageManifest not found for: ${imageUri}`)
}
const { config, layers } = ImageManifestSchema.parse(
JSON.parse(imageManifest)
)

await ecr
.batchCheckLayerAvailability({
registryId,
repositoryName,
layerDigests: [config.digest, ...layers.map((l) => l.digest)],
})
.promise()

await ecr
.getDownloadUrlForLayer({
registryId,
repositoryName,
layerDigest: layers[0].digest,
})
.promise()

log.error(`ECR image is accessible: ${imageUri}`)
return true
} catch (error) {
if (!(error instanceof Error) || error.name !== 'AccessDeniedException') {
throw error
}
}
log.error(`Unable to access ECR image: ${imageUri}`)

const Action = [
'ecr:GetDownloadUrlForLayer',
'ecr:BatchCheckLayerAvailability',
'ecr:BatchGetImage',
]

log.error(`You may need to add a policy to the ECR repository to allow this account.
The policy should include:
${JSON.stringify(
{
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Principal: {
AWS: ['XXXXXXXXXXXX'],
},
Action,
},
],
},
null,
2
).replace(/\n/gm, '\n ')}
`)

if (repoAccountAwsConfig && isInteractive()) {
const { Account } = await new AWS.STS({
credentials: ecr.config.credentials,
region,
})
.getCallerIdentity()
.promise()
if (!Account) {
log.error(`failed to determine AWS account`)
return false
}

const { update } = await inquirer.prompt([
{
name: 'update',
message: 'Do you want to add/update the policy?',
type: 'confirm',
default: false,
},
])
if (!update) return false

const srcEcr = new AWS.ECR({
...repoAccountAwsConfig,
region,
})
const { policyText } = await srcEcr
.getRepositoryPolicy({
registryId,
repositoryName,
})
.promise()
.catch((error): AWS.ECR.GetRepositoryPolicyResponse => {
if (error.name === 'RepositoryPolicyNotFoundException') return {}
throw error
})

const policy: any = JSON.parse(policyText || '{}')
await srcEcr
.setRepositoryPolicy({
repositoryName,
policyText: JSON.stringify(
{
Version: '2012-10-17',
...policy,
Statement: [
...(policy.Statement || []),
{
Effect: 'Allow',
Principal: {
AWS: [Account],
},
Action,
},
],
},
null,
2
),
})
.promise()
log.info(
`updated policy on ECR repository ${formatECRRepositoryHostname({
registryId,
region,
repositoryName,
})}`
)
return true
}
return false
}
19 changes: 19 additions & 0 deletions src/formatECRImageUri.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import formatECRRepositoryHostname from './formatECRRepositoryHostname'

export default function formatECRImageUri({
registryId,
region,
repositoryName,
imageTag,
}: {
registryId: AWS.ECR.RegistryId
region: string
repositoryName: AWS.ECR.RepositoryName
imageTag: AWS.ECR.ImageTag
}): string {
return `${formatECRRepositoryHostname({
registryId,
region,
repositoryName,
})}:${imageTag}`
}
11 changes: 11 additions & 0 deletions src/formatECRRepositoryHostname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export default function formatECRRepositoryHostname({
registryId,
region,
repositoryName,
}: {
registryId: AWS.ECR.RegistryId
region: string
repositoryName: AWS.ECR.RepositoryName
}): string {
return `${registryId}.dkr.ecr.${region}.amazonaws.com/${repositoryName}`
}
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ export { default as parseECRImageUri } from './parseECRImageUri'
export { default as parseECRRepositoryHostname } from './parseECRRepositoryHostname'
export { default as upsertECRRepository } from './upsertECRRepository'
export { default as checkECRRepositoryPolicy } from './checkECRRepositoryPolicy'
export { default as checkECRImageAccess } from './checkECRImageAccess'
export { default as formatECRRepositoryHostname } from './formatECRRepositoryHostname'
export { default as formatECRImageUri } from './formatECRImageUri'

0 comments on commit 4b5da39

Please sign in to comment.