@jcoreio/aws-ecr-utils 2.0.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ImageManifestSchema.d.mts +59 -0
- package/ImageManifestSchema.d.mts.map +1 -0
- package/ImageManifestSchema.d.ts +50 -49
- package/ImageManifestSchema.d.ts.map +1 -0
- package/ImageManifestSchema.js +2 -1
- package/ImageManifestSchema.js.map +1 -0
- package/ImageManifestSchema.mjs +2 -1
- package/ImageManifestSchema.mjs.map +1 -0
- package/checkECRImageAccess.d.mts +25 -0
- package/checkECRImageAccess.d.mts.map +1 -0
- package/checkECRImageAccess.d.ts +25 -17
- package/checkECRImageAccess.d.ts.map +1 -0
- package/checkECRImageAccess.js +46 -47
- package/checkECRImageAccess.js.map +1 -0
- package/checkECRImageAccess.mjs +9 -7
- package/checkECRImageAccess.mjs.map +1 -0
- package/checkECRRepositoryPolicy.d.mts +28 -0
- package/checkECRRepositoryPolicy.d.mts.map +1 -0
- package/checkECRRepositoryPolicy.d.ts +21 -12
- package/checkECRRepositoryPolicy.d.ts.map +1 -0
- package/checkECRRepositoryPolicy.js +20 -19
- package/checkECRRepositoryPolicy.js.map +1 -0
- package/checkECRRepositoryPolicy.mjs +5 -5
- package/checkECRRepositoryPolicy.mjs.map +1 -0
- package/copyECRImage.d.mts +17 -0
- package/copyECRImage.d.mts.map +1 -0
- package/copyECRImage.d.ts +17 -12
- package/copyECRImage.d.ts.map +1 -0
- package/copyECRImage.js +21 -20
- package/copyECRImage.js.map +1 -0
- package/copyECRImage.mjs +2 -1
- package/copyECRImage.mjs.map +1 -0
- package/ecrImageExists.d.mts +17 -0
- package/ecrImageExists.d.mts.map +1 -0
- package/ecrImageExists.d.ts +17 -8
- package/ecrImageExists.d.ts.map +1 -0
- package/ecrImageExists.js +13 -12
- package/ecrImageExists.js.map +1 -0
- package/ecrImageExists.mjs +2 -1
- package/ecrImageExists.mjs.map +1 -0
- package/formatECRImageUri.d.mts +12 -0
- package/formatECRImageUri.d.mts.map +1 -0
- package/formatECRImageUri.d.ts +12 -5
- package/formatECRImageUri.d.ts.map +1 -0
- package/formatECRImageUri.js +2 -1
- package/formatECRImageUri.js.map +1 -0
- package/formatECRImageUri.mjs +2 -1
- package/formatECRImageUri.mjs.map +1 -0
- package/formatECRRepositoryHostname.d.mts +10 -0
- package/formatECRRepositoryHostname.d.mts.map +1 -0
- package/formatECRRepositoryHostname.d.ts +10 -4
- package/formatECRRepositoryHostname.d.ts.map +1 -0
- package/formatECRRepositoryHostname.js +2 -1
- package/formatECRRepositoryHostname.js.map +1 -0
- package/formatECRRepositoryHostname.mjs +2 -1
- package/formatECRRepositoryHostname.mjs.map +1 -0
- package/index.d.mts +12 -0
- package/index.d.mts.map +1 -0
- package/index.d.ts +12 -11
- package/index.d.ts.map +1 -0
- package/index.js +2 -1
- package/index.js.map +1 -0
- package/index.mjs +2 -1
- package/index.mjs.map +1 -0
- package/isInteractive.d.mts +2 -0
- package/isInteractive.d.mts.map +1 -0
- package/isInteractive.d.ts +2 -0
- package/isInteractive.d.ts.map +1 -0
- package/isInteractive.js +8 -0
- package/isInteractive.js.map +1 -0
- package/isInteractive.mjs +2 -0
- package/isInteractive.mjs.map +1 -0
- package/loginToECR.d.mts +11 -0
- package/loginToECR.d.mts.map +1 -0
- package/loginToECR.d.ts +11 -4
- package/loginToECR.d.ts.map +1 -0
- package/loginToECR.js +106 -28
- package/loginToECR.js.map +1 -0
- package/loginToECR.mjs +42 -9
- package/loginToECR.mjs.map +1 -0
- package/package.json +17 -8
- package/parseECRImageUri.d.mts +7 -0
- package/parseECRImageUri.d.mts.map +1 -0
- package/parseECRImageUri.d.ts +7 -5
- package/parseECRImageUri.d.ts.map +1 -0
- package/parseECRImageUri.js +2 -1
- package/parseECRImageUri.js.map +1 -0
- package/parseECRImageUri.mjs +2 -1
- package/parseECRImageUri.mjs.map +1 -0
- package/parseECRRepositoryHostname.d.mts +6 -0
- package/parseECRRepositoryHostname.d.mts.map +1 -0
- package/parseECRRepositoryHostname.d.ts +6 -4
- package/parseECRRepositoryHostname.d.ts.map +1 -0
- package/parseECRRepositoryHostname.js +2 -1
- package/parseECRRepositoryHostname.js.map +1 -0
- package/parseECRRepositoryHostname.mjs +2 -1
- package/parseECRRepositoryHostname.mjs.map +1 -0
- package/src/ImageManifestSchema.ts +19 -0
- package/src/checkECRImageAccess.ts +193 -0
- package/src/checkECRRepositoryPolicy.ts +153 -0
- package/src/copyECRImage.ts +76 -0
- package/src/ecrImageExists.ts +48 -0
- package/src/formatECRImageUri.ts +19 -0
- package/src/formatECRRepositoryHostname.ts +11 -0
- package/src/index.ts +11 -0
- package/src/isInteractive.ts +2 -0
- package/src/loginToECR.ts +93 -0
- package/src/parseECRImageUri.ts +13 -0
- package/src/parseECRRepositoryHostname.ts +12 -0
- package/src/tagECRImage.ts +57 -0
- package/src/upsertECRRepository.ts +40 -0
- package/tagECRImage.d.mts +16 -0
- package/tagECRImage.d.mts.map +1 -0
- package/tagECRImage.d.ts +13 -6
- package/tagECRImage.d.ts.map +1 -0
- package/tagECRImage.js +40 -39
- package/tagECRImage.js.map +1 -0
- package/tagECRImage.mjs +3 -3
- package/tagECRImage.mjs.map +1 -0
- package/upsertECRRepository.d.mts +11 -0
- package/upsertECRRepository.d.mts.map +1 -0
- package/upsertECRRepository.d.ts +11 -5
- package/upsertECRRepository.d.ts.map +1 -0
- package/upsertECRRepository.js +21 -20
- package/upsertECRRepository.js.map +1 -0
- package/upsertECRRepository.mjs +3 -2
- package/upsertECRRepository.mjs.map +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseECRImageUri.js","names":["parseECRImageUri","imageUri","match","exec","Error","concat","_match","_slicedToArray2","registryId","region","repositoryName","imageTag","module","exports","default"],"sources":["src/parseECRImageUri.ts"],"sourcesContent":[null],"mappings":";;;;;;;;AAAe,SAASA,gBAAgBA,CAACC,QAAgB,EAKvD;EACA,IAAMC,KAAK,GAAG,wDAAwD,CAACC,IAAI,CACzEF,QACF,CAAC;EACD,IAAI,CAACC,KAAK,EAAE,MAAM,IAAIE,KAAK,sBAAAC,MAAA,CAAsBJ,QAAQ,CAAE,CAAC;EAC5D,IAAAK,MAAA,OAAAC,eAAA,aAAyDL,KAAK;IAArDM,UAAU,GAAAF,MAAA;IAAEG,MAAM,GAAAH,MAAA;IAAEI,cAAc,GAAAJ,MAAA;IAAEK,QAAQ,GAAAL,MAAA;EACrD,OAAO;IAAEE,UAAU,EAAVA,UAAU;IAAEC,MAAM,EAANA,MAAM;IAAEC,cAAc,EAAdA,cAAc;IAAEC,QAAQ,EAARA;EAAS,CAAC;AACzD;AAACC,MAAA,CAAAC,OAAA,GAAAA,OAAA,CAAAC,OAAA","ignoreList":[]}
|
package/parseECRImageUri.mjs
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseECRImageUri.mjs","names":["parseECRImageUri","imageUri","match","exec","Error","registryId","region","repositoryName","imageTag"],"sources":["src/parseECRImageUri.ts"],"sourcesContent":[null],"mappings":"AAAA,eAAe,SAASA,gBAAgBA,CAACC,QAAgB,EAKvD;EACA,MAAMC,KAAK,GAAG,wDAAwD,CAACC,IAAI,CACzEF,QACF,CAAC;EACD,IAAI,CAACC,KAAK,EAAE,MAAM,IAAIE,KAAK,CAAC,qBAAqBH,QAAQ,EAAE,CAAC;EAC5D,MAAM,GAAGI,UAAU,EAAEC,MAAM,EAAEC,cAAc,EAAEC,QAAQ,CAAC,GAAGN,KAAK;EAC9D,OAAO;IAAEG,UAAU;IAAEC,MAAM;IAAEC,cAAc;IAAEC;EAAS,CAAC;AACzD","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseECRRepositoryHostname.d.mts","names":["parseECRRepositoryHostname","hostname","registryId","region","repositoryName"],"sources":["src/parseECRRepositoryHostname.ts"],"sourcesContent":[null],"mappings":"AAAA,eAAc,SAAUA,0BAA0BA,CAACC,QAAQ,EAAE,MAAM,GAAG;EACpEC,UAAU,EAAE,MAAM;EAClBC,MAAM,EAAE,MAAM;EACdC,cAAc,EAAE,MAAM;CACvB","ignoreList":[]}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
declare function parseECRRepositoryHostname(hostname: string): {
|
|
2
|
+
registryId: string;
|
|
3
|
+
region: string;
|
|
4
|
+
repositoryName: string;
|
|
5
5
|
};
|
|
6
|
+
export = parseECRRepositoryHostname;
|
|
7
|
+
//# sourceMappingURL=parseECRRepositoryHostname.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseECRRepositoryHostname.d.ts","names":["parseECRRepositoryHostname","hostname","registryId","region","repositoryName"],"sources":["src/parseECRRepositoryHostname.ts"],"sourcesContent":[null],"mappings":"AAAc,iBAAUA,0BAA0BA,CAACC,QAAQ,EAAE,MAAM,GAAG;EACpEC,UAAU,EAAE,MAAM;EAClBC,MAAM,EAAE,MAAM;EACdC,cAAc,EAAE,MAAM;CACvB;AAOA,SAAAJ,0BAAA","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseECRRepositoryHostname.js","names":["parseECRRepositoryHostname","hostname","match","exec","Error","concat","_match","_slicedToArray2","registryId","region","repositoryName","module","exports","default"],"sources":["src/parseECRRepositoryHostname.ts"],"sourcesContent":[null],"mappings":";;;;;;;;AAAe,SAASA,0BAA0BA,CAACC,QAAgB,EAIjE;EACA,IAAMC,KAAK,GAAG,sDAAsD,CAACC,IAAI,CACvEF,QACF,CAAC;EACD,IAAI,CAACC,KAAK,EAAE,MAAM,IAAIE,KAAK,qCAAAC,MAAA,CAAqCJ,QAAQ,CAAE,CAAC;EAC3E,IAAAK,MAAA,OAAAC,eAAA,aAA+CL,KAAK;IAA3CM,UAAU,GAAAF,MAAA;IAAEG,MAAM,GAAAH,MAAA;IAAEI,cAAc,GAAAJ,MAAA;EAC3C,OAAO;IAAEE,UAAU,EAAVA,UAAU;IAAEC,MAAM,EAANA,MAAM;IAAEC,cAAc,EAAdA;EAAe,CAAC;AAC/C;AAACC,MAAA,CAAAC,OAAA,GAAAA,OAAA,CAAAC,OAAA","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseECRRepositoryHostname.mjs","names":["parseECRRepositoryHostname","hostname","match","exec","Error","registryId","region","repositoryName"],"sources":["src/parseECRRepositoryHostname.ts"],"sourcesContent":[null],"mappings":"AAAA,eAAe,SAASA,0BAA0BA,CAACC,QAAgB,EAIjE;EACA,MAAMC,KAAK,GAAG,sDAAsD,CAACC,IAAI,CACvEF,QACF,CAAC;EACD,IAAI,CAACC,KAAK,EAAE,MAAM,IAAIE,KAAK,CAAC,oCAAoCH,QAAQ,EAAE,CAAC;EAC3E,MAAM,GAAGI,UAAU,EAAEC,MAAM,EAAEC,cAAc,CAAC,GAAGL,KAAK;EACpD,OAAO;IAAEG,UAAU;IAAEC,MAAM;IAAEC;EAAe,CAAC;AAC/C","ignoreList":[]}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import z from 'zod'
|
|
2
|
+
|
|
3
|
+
const MediaType = z.string().min(1)
|
|
4
|
+
const Size = z.number().int().nonnegative()
|
|
5
|
+
const Digest = z.string().min(32)
|
|
6
|
+
|
|
7
|
+
const LayerSchema = z.object({
|
|
8
|
+
mediaType: MediaType,
|
|
9
|
+
size: Size,
|
|
10
|
+
digest: Digest,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
export const ImageManifestSchema = z.object({
|
|
14
|
+
schemaVersion: z.literal(2),
|
|
15
|
+
mediaType: z.string(),
|
|
16
|
+
config: LayerSchema,
|
|
17
|
+
layers: z.array(LayerSchema),
|
|
18
|
+
})
|
|
19
|
+
export type ImageManifestSchema = z.infer<typeof ImageManifestSchema>
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BatchCheckLayerAvailabilityCommand,
|
|
3
|
+
BatchGetImageCommand,
|
|
4
|
+
ECRClient,
|
|
5
|
+
type ECRClientConfig,
|
|
6
|
+
GetDownloadUrlForLayerCommand,
|
|
7
|
+
GetRepositoryPolicyCommand,
|
|
8
|
+
SetRepositoryPolicyCommand,
|
|
9
|
+
} from '@aws-sdk/client-ecr'
|
|
10
|
+
import { STSClient, GetCallerIdentityCommand } from '@aws-sdk/client-sts'
|
|
11
|
+
import parseECRImageUri from './parseECRImageUri.ts'
|
|
12
|
+
import { ImageManifestSchema } from './ImageManifestSchema.ts'
|
|
13
|
+
import { isInteractive } from './isInteractive.ts'
|
|
14
|
+
import inquirer from 'inquirer'
|
|
15
|
+
import formatECRRepositoryHostname from './formatECRRepositoryHostname.ts'
|
|
16
|
+
|
|
17
|
+
export default async function checkECRImageAccess({
|
|
18
|
+
ecr,
|
|
19
|
+
awsConfig,
|
|
20
|
+
repoAccountAwsConfig,
|
|
21
|
+
imageUri,
|
|
22
|
+
log = console,
|
|
23
|
+
}: {
|
|
24
|
+
ecr?: ECRClient
|
|
25
|
+
awsConfig?: ECRClientConfig
|
|
26
|
+
/**
|
|
27
|
+
* Config for the AWS account containing the ECR repository.
|
|
28
|
+
* Optional; if given, will prompt to add/update the policy on the
|
|
29
|
+
* ECR repository, if access checks failed and the terminal is
|
|
30
|
+
* interactive.
|
|
31
|
+
*/
|
|
32
|
+
repoAccountAwsConfig?: ECRClientConfig
|
|
33
|
+
imageUri: string
|
|
34
|
+
log?: {
|
|
35
|
+
info: (...args: any[]) => void
|
|
36
|
+
warn: (...args: any[]) => void
|
|
37
|
+
error: (...args: any[]) => void
|
|
38
|
+
}
|
|
39
|
+
}): Promise<boolean> {
|
|
40
|
+
log.error('checking access to ECR image:', imageUri, '....ts')
|
|
41
|
+
|
|
42
|
+
const { registryId, region, repositoryName, imageTag } =
|
|
43
|
+
parseECRImageUri(imageUri)
|
|
44
|
+
if (!ecr) ecr = new ECRClient({ ...awsConfig, region })
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const { images = [] } = await ecr.send(
|
|
48
|
+
new BatchGetImageCommand({
|
|
49
|
+
registryId,
|
|
50
|
+
repositoryName,
|
|
51
|
+
imageIds: [{ imageTag }],
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const imageManifest = images[0]?.imageManifest
|
|
56
|
+
|
|
57
|
+
if (!imageManifest) {
|
|
58
|
+
throw new Error(`imageManifest not found for: ${imageUri}`)
|
|
59
|
+
}
|
|
60
|
+
const { config, layers } = ImageManifestSchema.parse(
|
|
61
|
+
JSON.parse(imageManifest)
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
await ecr.send(
|
|
65
|
+
new BatchCheckLayerAvailabilityCommand({
|
|
66
|
+
registryId,
|
|
67
|
+
repositoryName,
|
|
68
|
+
layerDigests: [config.digest, ...layers.map((l) => l.digest)],
|
|
69
|
+
})
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
await ecr.send(
|
|
73
|
+
new GetDownloadUrlForLayerCommand({
|
|
74
|
+
registryId,
|
|
75
|
+
repositoryName,
|
|
76
|
+
layerDigest: layers[0].digest,
|
|
77
|
+
})
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
log.error(`ECR image is accessible: ${imageUri}`)
|
|
81
|
+
return true
|
|
82
|
+
} catch (error) {
|
|
83
|
+
if (!(error instanceof Error) || error.name !== 'AccessDeniedException') {
|
|
84
|
+
throw error
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
log.error(`Unable to access ECR image: ${imageUri}`)
|
|
88
|
+
|
|
89
|
+
const Action = [
|
|
90
|
+
'ecr:GetDownloadUrlForLayer',
|
|
91
|
+
'ecr:BatchCheckLayerAvailability',
|
|
92
|
+
'ecr:BatchGetImage',
|
|
93
|
+
]
|
|
94
|
+
|
|
95
|
+
log.error(`You may need to add a policy to the ECR repository to allow this account.
|
|
96
|
+
|
|
97
|
+
The policy should include:
|
|
98
|
+
|
|
99
|
+
${JSON.stringify(
|
|
100
|
+
{
|
|
101
|
+
Version: '2012-10-17',
|
|
102
|
+
Statement: [
|
|
103
|
+
{
|
|
104
|
+
Effect: 'Allow',
|
|
105
|
+
Principal: {
|
|
106
|
+
AWS: ['XXXXXXXXXXXX'],
|
|
107
|
+
},
|
|
108
|
+
Action,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
null,
|
|
113
|
+
2
|
|
114
|
+
).replace(/\n/gm, '\n ')}
|
|
115
|
+
`)
|
|
116
|
+
|
|
117
|
+
if (repoAccountAwsConfig && isInteractive) {
|
|
118
|
+
const { Account } = await new STSClient({
|
|
119
|
+
credentials: ecr.config.credentials,
|
|
120
|
+
region,
|
|
121
|
+
}).send(new GetCallerIdentityCommand())
|
|
122
|
+
if (!Account) {
|
|
123
|
+
log.error(`failed to determine AWS account`)
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const { update } = await inquirer.prompt([
|
|
128
|
+
{
|
|
129
|
+
name: 'update',
|
|
130
|
+
message: 'Do you want to add/update the policy?',
|
|
131
|
+
type: 'confirm',
|
|
132
|
+
default: false,
|
|
133
|
+
},
|
|
134
|
+
])
|
|
135
|
+
if (!update) return false
|
|
136
|
+
|
|
137
|
+
const srcEcr = new ECRClient({
|
|
138
|
+
...repoAccountAwsConfig,
|
|
139
|
+
region,
|
|
140
|
+
})
|
|
141
|
+
const { policyText } = await srcEcr
|
|
142
|
+
.send(
|
|
143
|
+
new GetRepositoryPolicyCommand({
|
|
144
|
+
registryId,
|
|
145
|
+
repositoryName,
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
.catch((error: unknown): { policyText?: string } => {
|
|
149
|
+
if (
|
|
150
|
+
error &&
|
|
151
|
+
typeof error === 'object' &&
|
|
152
|
+
'name' in error &&
|
|
153
|
+
error.name === 'RepositoryPolicyNotFoundException'
|
|
154
|
+
)
|
|
155
|
+
return {}
|
|
156
|
+
throw error
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
const policy: any = JSON.parse(policyText || '{}')
|
|
160
|
+
await srcEcr.send(
|
|
161
|
+
new SetRepositoryPolicyCommand({
|
|
162
|
+
repositoryName,
|
|
163
|
+
policyText: JSON.stringify(
|
|
164
|
+
{
|
|
165
|
+
Version: '2012-10-17',
|
|
166
|
+
...policy,
|
|
167
|
+
Statement: [
|
|
168
|
+
...(policy.Statement || []),
|
|
169
|
+
{
|
|
170
|
+
Effect: 'Allow',
|
|
171
|
+
Principal: {
|
|
172
|
+
AWS: [Account],
|
|
173
|
+
},
|
|
174
|
+
Action,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
},
|
|
178
|
+
null,
|
|
179
|
+
2
|
|
180
|
+
),
|
|
181
|
+
})
|
|
182
|
+
)
|
|
183
|
+
log.info(
|
|
184
|
+
`updated policy on ECR repository ${formatECRRepositoryHostname({
|
|
185
|
+
registryId,
|
|
186
|
+
region,
|
|
187
|
+
repositoryName,
|
|
188
|
+
})}`
|
|
189
|
+
)
|
|
190
|
+
return await checkECRImageAccess({ awsConfig, imageUri, log, ecr })
|
|
191
|
+
}
|
|
192
|
+
return false
|
|
193
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ECRClient,
|
|
3
|
+
type ECRClientConfig,
|
|
4
|
+
GetRepositoryPolicyCommand,
|
|
5
|
+
SetRepositoryPolicyCommand,
|
|
6
|
+
} from '@aws-sdk/client-ecr'
|
|
7
|
+
import inquirer from 'inquirer'
|
|
8
|
+
import { isInteractive } from './isInteractive.ts'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Checks if the given ECR repository has a sufficient repository
|
|
12
|
+
* policy to allow the given AWS principal to access docker images.
|
|
13
|
+
*
|
|
14
|
+
* If not, prints a warning, and if the terminal is interactive, asks the user if they
|
|
15
|
+
* would like to add/update the repository policy.
|
|
16
|
+
*/
|
|
17
|
+
export default async function checkECRRepositoryPolicy({
|
|
18
|
+
ecr,
|
|
19
|
+
awsConfig,
|
|
20
|
+
repositoryName,
|
|
21
|
+
awsPrincipal,
|
|
22
|
+
Action = [
|
|
23
|
+
'ecr:GetDownloadUrlForLayer',
|
|
24
|
+
'ecr:BatchCheckLayerAvailability',
|
|
25
|
+
'ecr:BatchGetImage',
|
|
26
|
+
],
|
|
27
|
+
log = console,
|
|
28
|
+
}: {
|
|
29
|
+
ecr?: ECRClient
|
|
30
|
+
awsConfig?: ECRClientConfig
|
|
31
|
+
repositoryName: string
|
|
32
|
+
awsPrincipal: string
|
|
33
|
+
Action?: string[]
|
|
34
|
+
log?: {
|
|
35
|
+
info: (...args: any[]) => void
|
|
36
|
+
warn: (...args: any[]) => void
|
|
37
|
+
error: (...args: any[]) => void
|
|
38
|
+
}
|
|
39
|
+
}): Promise<boolean> {
|
|
40
|
+
const rootUserMatch = /^arn:aws:iam::(\d+):root$/.exec(awsPrincipal)
|
|
41
|
+
if (rootUserMatch) awsPrincipal = rootUserMatch[1]
|
|
42
|
+
|
|
43
|
+
const principalAliases =
|
|
44
|
+
/^(\d+)$/.test(awsPrincipal) ?
|
|
45
|
+
[awsPrincipal, `arn:aws:iam::${awsPrincipal}:root`]
|
|
46
|
+
: [awsPrincipal]
|
|
47
|
+
|
|
48
|
+
if (!ecr) ecr = new ECRClient({ ...awsConfig })
|
|
49
|
+
const { policyText } = await ecr
|
|
50
|
+
.send(new GetRepositoryPolicyCommand({ repositoryName }))
|
|
51
|
+
.catch((error: unknown): { policyText?: string } => {
|
|
52
|
+
if (
|
|
53
|
+
error &&
|
|
54
|
+
typeof error === 'object' &&
|
|
55
|
+
'name' in error &&
|
|
56
|
+
error.name === 'RepositoryPolicyNotFoundException'
|
|
57
|
+
)
|
|
58
|
+
return {}
|
|
59
|
+
throw error
|
|
60
|
+
})
|
|
61
|
+
const policy: any = JSON.parse(policyText || '{}')
|
|
62
|
+
const statementForAction = policy.Statement?.find(
|
|
63
|
+
(s: any) =>
|
|
64
|
+
s.Effect === 'Allow' &&
|
|
65
|
+
Array.isArray(Action) &&
|
|
66
|
+
Action.every((a) => s.Action?.includes(a))
|
|
67
|
+
)
|
|
68
|
+
const statementPrincipal = statementForAction?.Principal?.AWS
|
|
69
|
+
|
|
70
|
+
if (
|
|
71
|
+
(statementPrincipal &&
|
|
72
|
+
typeof statementPrincipal === 'string' &&
|
|
73
|
+
principalAliases.includes(statementPrincipal)) ||
|
|
74
|
+
(Array.isArray(statementPrincipal) &&
|
|
75
|
+
statementPrincipal.some((s) => principalAliases.includes(s)))
|
|
76
|
+
) {
|
|
77
|
+
log.info(
|
|
78
|
+
`Found policy on ECR repository ${repositoryName} to allow access for AWS Principal ${awsPrincipal}.`
|
|
79
|
+
)
|
|
80
|
+
return true
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.warn(`Missing policy on ECR repository ${repositoryName} to allow access for AWS Principal ${awsPrincipal}.
|
|
85
|
+
|
|
86
|
+
The policy should include:
|
|
87
|
+
|
|
88
|
+
${JSON.stringify(
|
|
89
|
+
{
|
|
90
|
+
Version: '2012-10-17',
|
|
91
|
+
Statement: [
|
|
92
|
+
{
|
|
93
|
+
Effect: 'Allow',
|
|
94
|
+
Principal: {
|
|
95
|
+
AWS: [awsPrincipal],
|
|
96
|
+
},
|
|
97
|
+
Action,
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
null,
|
|
102
|
+
2
|
|
103
|
+
).replace(/\n/gm, '\n ')}
|
|
104
|
+
`)
|
|
105
|
+
if (isInteractive) {
|
|
106
|
+
const { update } = await inquirer.prompt([
|
|
107
|
+
{
|
|
108
|
+
name: 'update',
|
|
109
|
+
message: 'Do you want to add/update the policy?',
|
|
110
|
+
type: 'confirm',
|
|
111
|
+
default: false,
|
|
112
|
+
},
|
|
113
|
+
])
|
|
114
|
+
if (update) {
|
|
115
|
+
let finalPolicy = policy
|
|
116
|
+
if (statementForAction?.Action?.length === Action.length) {
|
|
117
|
+
statementForAction.Principal = {
|
|
118
|
+
...statementForAction.Principal,
|
|
119
|
+
AWS: [
|
|
120
|
+
...(typeof statementPrincipal === 'string' ? [statementPrincipal]
|
|
121
|
+
: Array.isArray(statementPrincipal) ? statementPrincipal
|
|
122
|
+
: []),
|
|
123
|
+
awsPrincipal,
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
finalPolicy = {
|
|
128
|
+
Version: '2012-10-17',
|
|
129
|
+
...policy,
|
|
130
|
+
Statement: [
|
|
131
|
+
...(policy.Statement || []),
|
|
132
|
+
{
|
|
133
|
+
Effect: 'Allow',
|
|
134
|
+
Principal: {
|
|
135
|
+
AWS: [awsPrincipal],
|
|
136
|
+
},
|
|
137
|
+
Action,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
await ecr.send(
|
|
143
|
+
new SetRepositoryPolicyCommand({
|
|
144
|
+
repositoryName,
|
|
145
|
+
policyText: JSON.stringify(finalPolicy, null, 2),
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
log.info(`updated policy on ECR repository ${repositoryName}`)
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return false
|
|
153
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { spawn } from 'promisify-child-process'
|
|
2
|
+
import loginToECR from './loginToECR.ts'
|
|
3
|
+
import ecrImageExists from './ecrImageExists.ts'
|
|
4
|
+
import parseECRImageUri from './parseECRImageUri.ts'
|
|
5
|
+
import { ECRClient, type ECRClientConfig } from '@aws-sdk/client-ecr'
|
|
6
|
+
|
|
7
|
+
export default async function copyECRImage({
|
|
8
|
+
from,
|
|
9
|
+
to,
|
|
10
|
+
}: {
|
|
11
|
+
from: {
|
|
12
|
+
imageUri: string
|
|
13
|
+
ecr?: ECRClient
|
|
14
|
+
awsConfig?: ECRClientConfig
|
|
15
|
+
}
|
|
16
|
+
to: {
|
|
17
|
+
imageUri: string
|
|
18
|
+
ecr?: ECRClient
|
|
19
|
+
awsConfig?: ECRClientConfig
|
|
20
|
+
}
|
|
21
|
+
}): Promise<void> {
|
|
22
|
+
if (from.imageUri === to.imageUri) return
|
|
23
|
+
const srcRepositoryUri = from.imageUri.replace(/:.+/, '')
|
|
24
|
+
const repositoryUri = to.imageUri.replace(/:.+/, '')
|
|
25
|
+
|
|
26
|
+
const { region: fromRegion } = parseECRImageUri(from.imageUri)
|
|
27
|
+
const {
|
|
28
|
+
region: toRegion,
|
|
29
|
+
repositoryName,
|
|
30
|
+
imageTag,
|
|
31
|
+
} = parseECRImageUri(to.imageUri)
|
|
32
|
+
|
|
33
|
+
if (
|
|
34
|
+
await ecrImageExists({
|
|
35
|
+
awsConfig: { ...to.awsConfig, region: toRegion },
|
|
36
|
+
ecr: to.ecr,
|
|
37
|
+
repositoryName,
|
|
38
|
+
imageTag,
|
|
39
|
+
})
|
|
40
|
+
) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.error(
|
|
43
|
+
`Clarity image already exists in your ECR: ${repositoryName}:${imageTag}`
|
|
44
|
+
)
|
|
45
|
+
} else {
|
|
46
|
+
// eslint-disable-next-line no-console
|
|
47
|
+
console.error(
|
|
48
|
+
`Logging into source ECR: ${srcRepositoryUri.replace(/\/.*/, '')}...`
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await loginToECR({
|
|
52
|
+
ecr: from.ecr,
|
|
53
|
+
awsConfig: { ...from.awsConfig, region: fromRegion },
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.error(`Pulling ${from.imageUri}...`)
|
|
58
|
+
await spawn('docker', ['pull', from.imageUri], { stdio: 'inherit' })
|
|
59
|
+
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.error(
|
|
62
|
+
`Logging into dest ECR: ${repositoryUri.replace(/\/.*/, '')}...`
|
|
63
|
+
)
|
|
64
|
+
await loginToECR({
|
|
65
|
+
ecr: to.ecr,
|
|
66
|
+
awsConfig: { ...to.awsConfig, region: toRegion },
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
// eslint-disable-next-line no-console
|
|
70
|
+
console.error(`Pushing ${to.imageUri}...`)
|
|
71
|
+
await spawn('docker', ['tag', from.imageUri, to.imageUri], {
|
|
72
|
+
stdio: 'inherit',
|
|
73
|
+
})
|
|
74
|
+
await spawn('docker', ['push', to.imageUri], { stdio: 'inherit' })
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DescribeImagesCommand,
|
|
3
|
+
ECRClient,
|
|
4
|
+
type ECRClientConfig,
|
|
5
|
+
} from '@aws-sdk/client-ecr'
|
|
6
|
+
|
|
7
|
+
export default async function ecrImageExists({
|
|
8
|
+
ecr,
|
|
9
|
+
awsConfig,
|
|
10
|
+
imageUri,
|
|
11
|
+
registryId,
|
|
12
|
+
repositoryName,
|
|
13
|
+
imageTag,
|
|
14
|
+
}: {
|
|
15
|
+
ecr?: ECRClient
|
|
16
|
+
awsConfig?: ECRClientConfig
|
|
17
|
+
imageUri?: string
|
|
18
|
+
registryId?: string
|
|
19
|
+
repositoryName?: string
|
|
20
|
+
imageTag?: string
|
|
21
|
+
}): Promise<boolean> {
|
|
22
|
+
let region
|
|
23
|
+
if (imageUri) {
|
|
24
|
+
const match = /(\d+)\.dkr\.ecr\.(.+?)\.amazonaws\.com\/(.+?):(.+)/.exec(
|
|
25
|
+
imageUri
|
|
26
|
+
)
|
|
27
|
+
if (!match) throw new Error(`failed to parse imageUri: ${imageUri}`)
|
|
28
|
+
;[, registryId, region, repositoryName, imageTag] = match
|
|
29
|
+
}
|
|
30
|
+
if (!region) region = awsConfig?.region
|
|
31
|
+
if (!ecr) ecr = new ECRClient({ ...awsConfig, region })
|
|
32
|
+
|
|
33
|
+
if (!repositoryName || !imageTag) {
|
|
34
|
+
throw new Error(`missing repositoryName/imageTag or imageUri`)
|
|
35
|
+
}
|
|
36
|
+
return await ecr
|
|
37
|
+
.send(
|
|
38
|
+
new DescribeImagesCommand({
|
|
39
|
+
registryId,
|
|
40
|
+
repositoryName,
|
|
41
|
+
imageIds: [{ imageTag }],
|
|
42
|
+
})
|
|
43
|
+
)
|
|
44
|
+
.then(
|
|
45
|
+
() => true,
|
|
46
|
+
() => false
|
|
47
|
+
)
|
|
48
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import formatECRRepositoryHostname from './formatECRRepositoryHostname.ts'
|
|
2
|
+
|
|
3
|
+
export default function formatECRImageUri({
|
|
4
|
+
registryId,
|
|
5
|
+
region,
|
|
6
|
+
repositoryName,
|
|
7
|
+
imageTag,
|
|
8
|
+
}: {
|
|
9
|
+
registryId: string
|
|
10
|
+
region: string
|
|
11
|
+
repositoryName: string
|
|
12
|
+
imageTag: string
|
|
13
|
+
}): string {
|
|
14
|
+
return `${formatECRRepositoryHostname({
|
|
15
|
+
registryId,
|
|
16
|
+
region,
|
|
17
|
+
repositoryName,
|
|
18
|
+
})}:${imageTag}`
|
|
19
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export default function formatECRRepositoryHostname({
|
|
2
|
+
registryId,
|
|
3
|
+
region,
|
|
4
|
+
repositoryName,
|
|
5
|
+
}: {
|
|
6
|
+
registryId: string
|
|
7
|
+
region: string
|
|
8
|
+
repositoryName: string
|
|
9
|
+
}): string {
|
|
10
|
+
return `${registryId}.dkr.ecr.${region}.amazonaws.com/${repositoryName}`
|
|
11
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { default as copyECRImage } from './copyECRImage.ts'
|
|
2
|
+
export { default as ecrImageExists } from './ecrImageExists.ts'
|
|
3
|
+
export { default as loginToECR } from './loginToECR.ts'
|
|
4
|
+
export { default as tagECRImage } from './tagECRImage.ts'
|
|
5
|
+
export { default as parseECRImageUri } from './parseECRImageUri.ts'
|
|
6
|
+
export { default as parseECRRepositoryHostname } from './parseECRRepositoryHostname.ts'
|
|
7
|
+
export { default as upsertECRRepository } from './upsertECRRepository.ts'
|
|
8
|
+
export { default as checkECRRepositoryPolicy } from './checkECRRepositoryPolicy.ts'
|
|
9
|
+
export { default as checkECRImageAccess } from './checkECRImageAccess.ts'
|
|
10
|
+
export { default as formatECRRepositoryHostname } from './formatECRRepositoryHostname.ts'
|
|
11
|
+
export { default as formatECRImageUri } from './formatECRImageUri.ts'
|