@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.
Files changed (127) hide show
  1. package/ImageManifestSchema.d.mts +59 -0
  2. package/ImageManifestSchema.d.mts.map +1 -0
  3. package/ImageManifestSchema.d.ts +50 -49
  4. package/ImageManifestSchema.d.ts.map +1 -0
  5. package/ImageManifestSchema.js +2 -1
  6. package/ImageManifestSchema.js.map +1 -0
  7. package/ImageManifestSchema.mjs +2 -1
  8. package/ImageManifestSchema.mjs.map +1 -0
  9. package/checkECRImageAccess.d.mts +25 -0
  10. package/checkECRImageAccess.d.mts.map +1 -0
  11. package/checkECRImageAccess.d.ts +25 -17
  12. package/checkECRImageAccess.d.ts.map +1 -0
  13. package/checkECRImageAccess.js +46 -47
  14. package/checkECRImageAccess.js.map +1 -0
  15. package/checkECRImageAccess.mjs +9 -7
  16. package/checkECRImageAccess.mjs.map +1 -0
  17. package/checkECRRepositoryPolicy.d.mts +28 -0
  18. package/checkECRRepositoryPolicy.d.mts.map +1 -0
  19. package/checkECRRepositoryPolicy.d.ts +21 -12
  20. package/checkECRRepositoryPolicy.d.ts.map +1 -0
  21. package/checkECRRepositoryPolicy.js +20 -19
  22. package/checkECRRepositoryPolicy.js.map +1 -0
  23. package/checkECRRepositoryPolicy.mjs +5 -5
  24. package/checkECRRepositoryPolicy.mjs.map +1 -0
  25. package/copyECRImage.d.mts +17 -0
  26. package/copyECRImage.d.mts.map +1 -0
  27. package/copyECRImage.d.ts +17 -12
  28. package/copyECRImage.d.ts.map +1 -0
  29. package/copyECRImage.js +21 -20
  30. package/copyECRImage.js.map +1 -0
  31. package/copyECRImage.mjs +2 -1
  32. package/copyECRImage.mjs.map +1 -0
  33. package/ecrImageExists.d.mts +17 -0
  34. package/ecrImageExists.d.mts.map +1 -0
  35. package/ecrImageExists.d.ts +17 -8
  36. package/ecrImageExists.d.ts.map +1 -0
  37. package/ecrImageExists.js +13 -12
  38. package/ecrImageExists.js.map +1 -0
  39. package/ecrImageExists.mjs +2 -1
  40. package/ecrImageExists.mjs.map +1 -0
  41. package/formatECRImageUri.d.mts +12 -0
  42. package/formatECRImageUri.d.mts.map +1 -0
  43. package/formatECRImageUri.d.ts +12 -5
  44. package/formatECRImageUri.d.ts.map +1 -0
  45. package/formatECRImageUri.js +2 -1
  46. package/formatECRImageUri.js.map +1 -0
  47. package/formatECRImageUri.mjs +2 -1
  48. package/formatECRImageUri.mjs.map +1 -0
  49. package/formatECRRepositoryHostname.d.mts +10 -0
  50. package/formatECRRepositoryHostname.d.mts.map +1 -0
  51. package/formatECRRepositoryHostname.d.ts +10 -4
  52. package/formatECRRepositoryHostname.d.ts.map +1 -0
  53. package/formatECRRepositoryHostname.js +2 -1
  54. package/formatECRRepositoryHostname.js.map +1 -0
  55. package/formatECRRepositoryHostname.mjs +2 -1
  56. package/formatECRRepositoryHostname.mjs.map +1 -0
  57. package/index.d.mts +12 -0
  58. package/index.d.mts.map +1 -0
  59. package/index.d.ts +12 -11
  60. package/index.d.ts.map +1 -0
  61. package/index.js +2 -1
  62. package/index.js.map +1 -0
  63. package/index.mjs +2 -1
  64. package/index.mjs.map +1 -0
  65. package/isInteractive.d.mts +2 -0
  66. package/isInteractive.d.mts.map +1 -0
  67. package/isInteractive.d.ts +2 -0
  68. package/isInteractive.d.ts.map +1 -0
  69. package/isInteractive.js +8 -0
  70. package/isInteractive.js.map +1 -0
  71. package/isInteractive.mjs +2 -0
  72. package/isInteractive.mjs.map +1 -0
  73. package/loginToECR.d.mts +11 -0
  74. package/loginToECR.d.mts.map +1 -0
  75. package/loginToECR.d.ts +11 -4
  76. package/loginToECR.d.ts.map +1 -0
  77. package/loginToECR.js +106 -28
  78. package/loginToECR.js.map +1 -0
  79. package/loginToECR.mjs +42 -9
  80. package/loginToECR.mjs.map +1 -0
  81. package/package.json +17 -8
  82. package/parseECRImageUri.d.mts +7 -0
  83. package/parseECRImageUri.d.mts.map +1 -0
  84. package/parseECRImageUri.d.ts +7 -5
  85. package/parseECRImageUri.d.ts.map +1 -0
  86. package/parseECRImageUri.js +2 -1
  87. package/parseECRImageUri.js.map +1 -0
  88. package/parseECRImageUri.mjs +2 -1
  89. package/parseECRImageUri.mjs.map +1 -0
  90. package/parseECRRepositoryHostname.d.mts +6 -0
  91. package/parseECRRepositoryHostname.d.mts.map +1 -0
  92. package/parseECRRepositoryHostname.d.ts +6 -4
  93. package/parseECRRepositoryHostname.d.ts.map +1 -0
  94. package/parseECRRepositoryHostname.js +2 -1
  95. package/parseECRRepositoryHostname.js.map +1 -0
  96. package/parseECRRepositoryHostname.mjs +2 -1
  97. package/parseECRRepositoryHostname.mjs.map +1 -0
  98. package/src/ImageManifestSchema.ts +19 -0
  99. package/src/checkECRImageAccess.ts +193 -0
  100. package/src/checkECRRepositoryPolicy.ts +153 -0
  101. package/src/copyECRImage.ts +76 -0
  102. package/src/ecrImageExists.ts +48 -0
  103. package/src/formatECRImageUri.ts +19 -0
  104. package/src/formatECRRepositoryHostname.ts +11 -0
  105. package/src/index.ts +11 -0
  106. package/src/isInteractive.ts +2 -0
  107. package/src/loginToECR.ts +93 -0
  108. package/src/parseECRImageUri.ts +13 -0
  109. package/src/parseECRRepositoryHostname.ts +12 -0
  110. package/src/tagECRImage.ts +57 -0
  111. package/src/upsertECRRepository.ts +40 -0
  112. package/tagECRImage.d.mts +16 -0
  113. package/tagECRImage.d.mts.map +1 -0
  114. package/tagECRImage.d.ts +13 -6
  115. package/tagECRImage.d.ts.map +1 -0
  116. package/tagECRImage.js +40 -39
  117. package/tagECRImage.js.map +1 -0
  118. package/tagECRImage.mjs +3 -3
  119. package/tagECRImage.mjs.map +1 -0
  120. package/upsertECRRepository.d.mts +11 -0
  121. package/upsertECRRepository.d.mts.map +1 -0
  122. package/upsertECRRepository.d.ts +11 -5
  123. package/upsertECRRepository.d.ts.map +1 -0
  124. package/upsertECRRepository.js +21 -20
  125. package/upsertECRRepository.js.map +1 -0
  126. package/upsertECRRepository.mjs +3 -2
  127. 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":[]}
@@ -8,4 +8,5 @@ export default function parseECRImageUri(imageUri) {
8
8
  repositoryName,
9
9
  imageTag
10
10
  };
11
- }
11
+ }
12
+ //# sourceMappingURL=parseECRImageUri.mjs.map
@@ -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,6 @@
1
+ export default function parseECRRepositoryHostname(hostname: string): {
2
+ registryId: string;
3
+ region: string;
4
+ repositoryName: string;
5
+ };
6
+ //# sourceMappingURL=parseECRRepositoryHostname.d.mts.map
@@ -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
- export default function parseECRRepositoryHostname(hostname: string): {
2
- registryId: string;
3
- region: string;
4
- repositoryName: string;
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":[]}
@@ -19,4 +19,5 @@ function parseECRRepositoryHostname(hostname) {
19
19
  repositoryName: repositoryName
20
20
  };
21
21
  }
22
- module.exports = exports.default;
22
+ module.exports = exports.default;
23
+ //# sourceMappingURL=parseECRRepositoryHostname.js.map
@@ -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":[]}
@@ -7,4 +7,5 @@ export default function parseECRRepositoryHostname(hostname) {
7
7
  region,
8
8
  repositoryName
9
9
  };
10
- }
10
+ }
11
+ //# sourceMappingURL=parseECRRepositoryHostname.mjs.map
@@ -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'
@@ -0,0 +1,2 @@
1
+ export const isInteractive =
2
+ process.stdout.isTTY && process.env.TERM !== 'dumb' && !('CI' in process.env)