Skip to content

Commit 009680d

Browse files
authored
fix(ecr-assets): handle Docker 27.4+ output format in TarballImageAsset (#33967)
### Issue # (if applicable) Closes #33428. ### Reason for this change The TarballImageAsset class was failing to properly extract image IDs from Docker 27.4+ output due to a format change from "Loaded image: <digest>" to "Loaded image ID: <digest>". This caused the sed command to fail to properly extract the image ID, resulting in an invalid tag format. After reviewing Docker CLI issue docker/cli#2212 (open since 2019), I found that Docker has actually had two distinct output formats for the `docker load` command for some time: - For images with tags: "Loaded image: <image-name>" - For images without tags: "Loaded image ID: <digest>" See: https://github.com/moby/moby/blob/37f866285af6910f838c762912dfd57396783b72/image/tarexport/load.go#L140-L159 While the issue was triggered by Docker 27.4, the underlying problem is that our code was only handling one of these output formats. We need a solution that handles both formats. ### Description of changes - Introduced a more flexible regex pattern (`s/Loaded image[^:]*: //g`) that handles both output formats - Extracted the pattern to a shared constant `DOCKER_LOAD_OUTPUT_REGEX` for consistency and maintainability - Updated the code in `tarball-asset.ts` to use the new regex pattern - Added unit tests to verify compatibility with both Docker output formats - Ensured backward compatibility with older Docker versions This approach makes our code more robust against variations in Docker's output format. ### Describe any new or updated permissions being added No new or updated IAM permissions are needed for this change. ### Description of how you validated changes - Created unit tests that verify the new regex pattern works with both formats using hardcoded test strings: - "Loaded image: <digest>" (older Docker versions) - "Loaded image ID: <digest>" (Docker 27.4+) - Verified that the old sed expression fails with the new format - Confirmed all existing tests pass with the new implementation ### Checklist - [ ] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent a8edf69 commit 009680d

File tree

2 files changed

+42
-3
lines changed

2 files changed

+42
-3
lines changed

packages/aws-cdk-lib/aws-ecr-assets/lib/tarball-asset.ts

+9-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { IAsset } from '../../assets';
55
import * as ecr from '../../aws-ecr';
66
import { AssetStaging, Names, Stack, Stage, ValidationError } from '../../core';
77

8+
/**
9+
* The sed pattern used to extract the image ID from docker load output
10+
* Works with both formats:
11+
* - "Loaded image: <digest>" (older Docker versions)
12+
* - "Loaded image ID: <digest>" (Docker 27.4+)
13+
*/
14+
export const DOCKER_LOAD_OUTPUT_REGEX = 's/Loaded image[^:]*: //g';
15+
816
/**
917
* Options for TarballImageAsset
1018
*/
@@ -99,7 +107,7 @@ export class TarballImageAsset extends Construct implements IAsset {
99107
executable: [
100108
'sh',
101109
'-c',
102-
`docker load -i ${relativePathInOutDir} | tail -n 1 | sed "s/Loaded image: //g"`,
110+
`docker load -i ${relativePathInOutDir} | tail -n 1 | sed "${DOCKER_LOAD_OUTPUT_REGEX}"`,
103111
],
104112
displayName: props.displayName ?? Names.stackRelativeConstructPath(this),
105113
});

packages/aws-cdk-lib/aws-ecr-assets/test/tarball-asset.test.ts

+33-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
1+
import { spawnSync } from 'child_process';
12
import * as fs from 'fs';
23
import * as path from 'path';
34
import { Template } from '../../assertions';
45
import * as iam from '../../aws-iam';
56
import * as cxschema from '../../cloud-assembly-schema';
67
import { App, Stack, DefaultStackSynthesizer } from '../../core';
78
import * as cxapi from '../../cx-api';
8-
import { TarballImageAsset } from '../lib';
9+
import { TarballImageAsset, DOCKER_LOAD_OUTPUT_REGEX } from '../lib';
910

1011
/* eslint-disable quote-props */
1112

@@ -42,7 +43,7 @@ describe('image asset', () => {
4243
executable: [
4344
'sh',
4445
'-c',
45-
`docker load -i asset.${asset.assetHash}.tar | tail -n 1 | sed "s/Loaded image: //g"`,
46+
`docker load -i asset.${asset.assetHash}.tar | tail -n 1 | sed "${DOCKER_LOAD_OUTPUT_REGEX}"`,
4647
],
4748
},
4849
);
@@ -160,6 +161,36 @@ describe('image asset', () => {
160161
expect(asset2.imageTag).toEqual('banana95c924c84f5d023be4edee540cb2cb401a49f115d01ed403b288f6cb412771df');
161162
});
162163
});
164+
test('docker load output format handling', () => {
165+
// Test that our sed expression can handle both old and new Docker output formats
166+
const oldFormatOutput = 'Loaded image: sha256:4a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a';
167+
const newFormatOutput = 'Loaded image ID: sha256:4a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a';
168+
169+
// Test old format (Loaded image: sha256:...)
170+
const oldFormatResult = spawnSync('sh', [
171+
'-c',
172+
`echo "${oldFormatOutput}" | sed "${DOCKER_LOAD_OUTPUT_REGEX}"`,
173+
]);
174+
expect(oldFormatResult.status).toBe(0);
175+
expect(oldFormatResult.stdout.toString().trim()).toBe('sha256:4a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a');
176+
177+
// Test new format (Loaded image ID: sha256:...)
178+
const newFormatResult = spawnSync('sh', [
179+
'-c',
180+
`echo "${newFormatOutput}" | sed "${DOCKER_LOAD_OUTPUT_REGEX}"`,
181+
]);
182+
expect(newFormatResult.status).toBe(0);
183+
expect(newFormatResult.stdout.toString().trim()).toBe('sha256:4a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a');
184+
185+
// Test that the old sed expression fails with the new format
186+
const oldSedWithNewFormat = spawnSync('sh', [
187+
'-c',
188+
`echo "${newFormatOutput}" | sed "s/Loaded image: //g"`,
189+
]);
190+
expect(oldSedWithNewFormat.status).toBe(0);
191+
expect(oldSedWithNewFormat.stdout.toString().trim()).not.toBe('sha256:4a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a');
192+
expect(oldSedWithNewFormat.stdout.toString().trim()).toBe('Loaded image ID: sha256:4a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a');
193+
});
163194
});
164195

165196
function isAssetManifest(x: cxapi.CloudArtifact): x is cxapi.AssetManifestArtifact {

0 commit comments

Comments
 (0)