@jcoreio/aws-ecr-utils 1.3.1 → 2.1.0

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 (132) 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 +65 -65
  14. package/checkECRImageAccess.js.map +1 -0
  15. package/checkECRImageAccess.mjs +22 -19
  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 +27 -26
  22. package/checkECRRepositoryPolicy.js.map +1 -0
  23. package/checkECRRepositoryPolicy.mjs +11 -9
  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 +17 -16
  38. package/ecrImageExists.js.map +1 -0
  39. package/ecrImageExists.mjs +6 -5
  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/loginToECR.d.mts +11 -0
  66. package/loginToECR.d.mts.map +1 -0
  67. package/loginToECR.d.ts +11 -7
  68. package/loginToECR.d.ts.map +1 -0
  69. package/loginToECR.js +96 -28
  70. package/loginToECR.js.map +1 -0
  71. package/loginToECR.mjs +46 -8
  72. package/loginToECR.mjs.map +1 -0
  73. package/package.json +17 -6
  74. package/parseECRImageUri.d.mts +7 -0
  75. package/parseECRImageUri.d.mts.map +1 -0
  76. package/parseECRImageUri.d.ts +7 -6
  77. package/parseECRImageUri.d.ts.map +1 -0
  78. package/parseECRImageUri.js +2 -1
  79. package/parseECRImageUri.js.map +1 -0
  80. package/parseECRImageUri.mjs +2 -1
  81. package/parseECRImageUri.mjs.map +1 -0
  82. package/parseECRRepositoryHostname.d.mts +6 -0
  83. package/parseECRRepositoryHostname.d.mts.map +1 -0
  84. package/parseECRRepositoryHostname.d.ts +6 -5
  85. package/parseECRRepositoryHostname.d.ts.map +1 -0
  86. package/parseECRRepositoryHostname.js +2 -1
  87. package/parseECRRepositoryHostname.js.map +1 -0
  88. package/parseECRRepositoryHostname.mjs +2 -1
  89. package/parseECRRepositoryHostname.mjs.map +1 -0
  90. package/src/ImageManifestSchema.ts +19 -0
  91. package/src/checkECRImageAccess.ts +193 -0
  92. package/src/checkECRRepositoryPolicy.ts +153 -0
  93. package/src/copyECRImage.ts +76 -0
  94. package/src/ecrImageExists.ts +48 -0
  95. package/src/formatECRImageUri.ts +19 -0
  96. package/src/formatECRRepositoryHostname.ts +11 -0
  97. package/src/index.ts +11 -0
  98. package/src/loginToECR.ts +96 -0
  99. package/src/parseECRImageUri.ts +13 -0
  100. package/src/parseECRRepositoryHostname.ts +12 -0
  101. package/src/tagECRImage.ts +57 -0
  102. package/src/upsertECRRepository.ts +40 -0
  103. package/tagECRImage.d.mts +16 -0
  104. package/tagECRImage.d.mts.map +1 -0
  105. package/tagECRImage.d.ts +13 -6
  106. package/tagECRImage.d.ts.map +1 -0
  107. package/tagECRImage.js +46 -45
  108. package/tagECRImage.js.map +1 -0
  109. package/tagECRImage.mjs +9 -9
  110. package/tagECRImage.mjs.map +1 -0
  111. package/upsertECRRepository.d.mts +11 -0
  112. package/upsertECRRepository.d.mts.map +1 -0
  113. package/upsertECRRepository.d.ts +12 -6
  114. package/upsertECRRepository.d.ts.map +1 -0
  115. package/upsertECRRepository.js +34 -31
  116. package/upsertECRRepository.js.map +1 -0
  117. package/upsertECRRepository.mjs +11 -8
  118. package/upsertECRRepository.mjs.map +1 -0
  119. package/copyECRImage.js.flow +0 -14
  120. package/copyECRImage.mjs.flow +0 -14
  121. package/ecrImageExists.js.flow +0 -10
  122. package/ecrImageExists.mjs.flow +0 -10
  123. package/index.js.flow +0 -8
  124. package/index.mjs.flow +0 -8
  125. package/loginToECR.js.flow +0 -6
  126. package/loginToECR.mjs.flow +0 -6
  127. package/parseECRImageUri.js.flow +0 -8
  128. package/parseECRImageUri.mjs.flow +0 -8
  129. package/tagECRImage.js.flow +0 -8
  130. package/tagECRImage.mjs.flow +0 -8
  131. package/upsertECRRepository.js.flow +0 -7
  132. package/upsertECRRepository.mjs.flow +0 -7
package/loginToECR.mjs CHANGED
@@ -1,21 +1,50 @@
1
1
  /**
2
2
  * @prettier
3
3
  */
4
- import AWS from 'aws-sdk';
5
4
  import { spawn } from 'promisify-child-process';
6
5
  import base64 from 'base64-js';
6
+ import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr';
7
+ import parseECRImageUri from "./parseECRImageUri.mjs";
8
+ import * as log4jcore from 'log4jcore';
9
+ const log = log4jcore.logger('@jcoreio/aws-ecr-utils/loginToECR');
7
10
  export default async function loginToECR({
8
11
  ecr,
12
+ forImages,
9
13
  awsConfig
10
14
  }) {
11
- if (!ecr) ecr = new AWS.ECR(awsConfig);
15
+ if (forImages) {
16
+ log.debug('logging into AWS ECR for images:', forImages);
17
+ const regions = new Set(forImages.flatMap(image => {
18
+ try {
19
+ return [parseECRImageUri(image).region];
20
+ } catch {
21
+ return [];
22
+ }
23
+ }));
24
+ log.debug('parsed AWS regions from images:', regions);
25
+ await Promise.all([...regions].map(async region => loginToECR({
26
+ awsConfig: {
27
+ ...awsConfig,
28
+ region
29
+ }
30
+ })));
31
+ return;
32
+ }
33
+ if (!ecr) ecr = new ECRClient({
34
+ ...awsConfig
35
+ });
36
+ log.debug('logging into ECR for region:', await ecr.config.region());
12
37
  const {
13
38
  authorizationData
14
- } = await ecr.getAuthorizationToken().promise();
39
+ } = await ecr.send(new GetAuthorizationTokenCommand());
15
40
  const {
16
41
  authorizationToken,
17
42
  proxyEndpoint
18
43
  } = (authorizationData === null || authorizationData === void 0 ? void 0 : authorizationData[0]) || {};
44
+ log.debug('GetAuthorizationToken data:', {
45
+ authorizationToken,
46
+ proxyEndpoint
47
+ });
19
48
  if (!authorizationToken) {
20
49
  throw new Error('failed to get authorizationToken from ECR');
21
50
  }
@@ -23,10 +52,12 @@ export default async function loginToECR({
23
52
  throw new Error('failed to get proxyEndpoint from ECR');
24
53
  }
25
54
  // this is silly...
26
- const decoded = new Buffer(base64.toByteArray(authorizationToken)).toString('utf8');
55
+ const decoded = Buffer.from(base64.toByteArray(authorizationToken)).toString('utf8');
27
56
  const [user, password] = decoded.split(/:/);
28
- const child = spawn('docker', ['login', '-u', user, '--password-stdin', proxyEndpoint], {
29
- stdio: 'pipe',
57
+ const dockerArgs = ['login', '-u', user, '--password-stdin', proxyEndpoint];
58
+ log.debug('running: docker', ...dockerArgs.map(arg => /^[-_a-z0-9]+$/i.test(arg) ? arg : `'${arg.replace(/'/g, `'\\'')}'`)}'`));
59
+ const child = spawn('docker', dockerArgs, {
60
+ stdio: ['pipe', 'pipe', 'inherit'],
30
61
  encoding: 'utf8'
31
62
  });
32
63
  if (!child.stdin) {
@@ -34,5 +65,12 @@ export default async function loginToECR({
34
65
  }
35
66
  child.stdin.write(password);
36
67
  child.stdin.end();
37
- await child;
38
- }
68
+ await child.then(() => log.debug('ECR login succeeded'), error => {
69
+ if (error && typeof error === 'object') {
70
+ if ('code' in error) log.debug('docker login exited with code', error.code);
71
+ if ('signal' in error) log.debug('docker login was killed with signal', error.signal);
72
+ }
73
+ throw error;
74
+ });
75
+ }
76
+ //# sourceMappingURL=loginToECR.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loginToECR.mjs","names":["spawn","base64","ECRClient","GetAuthorizationTokenCommand","parseECRImageUri","log4jcore","log","logger","loginToECR","ecr","forImages","awsConfig","debug","regions","Set","flatMap","image","region","Promise","all","map","config","authorizationData","send","authorizationToken","proxyEndpoint","Error","decoded","Buffer","from","toByteArray","toString","user","password","split","dockerArgs","arg","test","replace","child","stdio","encoding","stdin","write","end","then","error","code","signal"],"sources":["src/loginToECR.ts"],"sourcesContent":[null],"mappings":"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","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jcoreio/aws-ecr-utils",
3
- "version": "1.3.1",
3
+ "version": "2.1.0",
4
4
  "description": "utilities for working with AWS Elastic Container Registry",
5
5
  "sideEffects": false,
6
6
  "repository": {
@@ -19,11 +19,13 @@
19
19
  },
20
20
  "homepage": "https://github.com/jcoreio/aws-ecr-utils#readme",
21
21
  "dependencies": {
22
- "@babel/runtime": "^7.18.6",
23
- "aws-sdk": "^2.1070.0",
22
+ "@aws-sdk/client-ecr": "^3.577.0",
23
+ "@aws-sdk/client-sts": "^3.577.0",
24
+ "@babel/runtime": "^7.26.0",
24
25
  "base64-js": "^1.5.1",
25
26
  "inquirer": "^8.2.6",
26
27
  "is-interactive": "^1.0.0",
28
+ "log4jcore": "^4.8.0",
27
29
  "promisify-child-process": "^4.1.1",
28
30
  "zod": "^3.22.4"
29
31
  },
@@ -32,13 +34,19 @@
32
34
  "types": "index.d.ts",
33
35
  "exports": {
34
36
  ".": {
35
- "types": "./index.d.ts",
37
+ "types": {
38
+ "import": "./index.d.mts",
39
+ "default": "./index.d.ts"
40
+ },
36
41
  "require": "./index.js",
37
42
  "default": "./index.mjs"
38
43
  },
39
44
  "./package.json": "./package.json",
40
45
  "./*": {
41
- "types": "./*.d.ts",
46
+ "types": {
47
+ "import": "./*.d.mts",
48
+ "default": "./*.d.ts"
49
+ },
42
50
  "require": "./*.js",
43
51
  "default": "./*.mjs"
44
52
  }
@@ -46,5 +54,8 @@
46
54
  "engines": {
47
55
  "node": ">=16"
48
56
  },
49
- "packageManager": "pnpm@8.3.1"
57
+ "packageManager": "pnpm@10.6.5",
58
+ "@jcoreio/toolchain": {
59
+ "migratedVersion": "5.8.6"
60
+ }
50
61
  }
@@ -0,0 +1,7 @@
1
+ export default function parseECRImageUri(imageUri: string): {
2
+ registryId: string;
3
+ region: string;
4
+ repositoryName: string;
5
+ imageTag: string;
6
+ };
7
+ //# sourceMappingURL=parseECRImageUri.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseECRImageUri.d.mts","names":["parseECRImageUri","imageUri","registryId","region","repositoryName","imageTag"],"sources":["src/parseECRImageUri.ts"],"sourcesContent":[null],"mappings":"AAAA,eAAc,SAAUA,gBAAgBA,CAACC,QAAQ,EAAE,MAAM,GAAG;EAC1DC,UAAU,EAAE,MAAM;EAClBC,MAAM,EAAE,MAAM;EACdC,cAAc,EAAE,MAAM;EACtBC,QAAQ,EAAE,MAAM;CACjB","ignoreList":[]}
@@ -1,7 +1,8 @@
1
- import AWS from 'aws-sdk';
2
- export default function parseECRImageUri(imageUri: string): {
3
- registryId: AWS.ECR.RegistryId;
4
- region: string;
5
- repositoryName: AWS.ECR.RepositoryName;
6
- imageTag: string;
1
+ declare function parseECRImageUri(imageUri: string): {
2
+ registryId: string;
3
+ region: string;
4
+ repositoryName: string;
5
+ imageTag: string;
7
6
  };
7
+ export = parseECRImageUri;
8
+ //# sourceMappingURL=parseECRImageUri.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parseECRImageUri.d.ts","names":["parseECRImageUri","imageUri","registryId","region","repositoryName","imageTag"],"sources":["src/parseECRImageUri.ts"],"sourcesContent":[null],"mappings":"AAAc,iBAAUA,gBAAgBA,CAACC,QAAQ,EAAE,MAAM,GAAG;EAC1DC,UAAU,EAAE,MAAM;EAClBC,MAAM,EAAE,MAAM;EACdC,cAAc,EAAE,MAAM;EACtBC,QAAQ,EAAE,MAAM;CACjB;AAOA,SAAAL,gBAAA","ignoreList":[]}
@@ -21,4 +21,5 @@ function parseECRImageUri(imageUri) {
21
21
  imageTag: imageTag
22
22
  };
23
23
  }
24
- module.exports = exports.default;
24
+ module.exports = exports.default;
25
+ //# sourceMappingURL=parseECRImageUri.js.map
@@ -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,6 +1,7 @@
1
- import AWS from 'aws-sdk';
2
- export default function parseECRRepositoryHostname(hostname: string): {
3
- registryId: AWS.ECR.RegistryId;
4
- region: string;
5
- repositoryName: AWS.ECR.RepositoryName;
1
+ declare function parseECRRepositoryHostname(hostname: string): {
2
+ registryId: string;
3
+ region: string;
4
+ repositoryName: string;
6
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
+ 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'
12
+ import { ImageManifestSchema } from './ImageManifestSchema'
13
+ import isInteractive from 'is-interactive'
14
+ import inquirer from 'inquirer'
15
+ import formatECRRepositoryHostname from './formatECRRepositoryHostname'
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, '...')
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
+ ECRClientConfig,
4
+ GetRepositoryPolicyCommand,
5
+ SetRepositoryPolicyCommand,
6
+ } from '@aws-sdk/client-ecr'
7
+ import inquirer from 'inquirer'
8
+ import isInteractive from 'is-interactive'
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
+ }