Skip to content

Commit 00ef50d

Browse files
authored
fix(core): pressing Ctrl-C when content is bundled leaves broken asset (#33692)
When a bundling command is interrupted with Ctrl-C, the asset output directory has already been created. On the next synthesis, we assume the asset has already successfully been produced, don't do any bundling, and upload it. We will then have produced and uploaded a broken asset. Instead, the common pattern to handle this is: - Do the work into a temporary directory - Rename the temporary directory to the target directory only if the work succeeds. Closes #33201, closes #32869, relates to #14474. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 4ab0b33 commit 00ef50d

File tree

5 files changed

+85
-20
lines changed

5 files changed

+85
-20
lines changed

packages/aws-cdk-lib/core/lib/asset-staging.ts

+12-15
Original file line numberDiff line numberDiff line change
@@ -438,19 +438,23 @@ export class AssetStaging extends Construct {
438438
private bundle(options: BundlingOptions, bundleDir: string) {
439439
if (fs.existsSync(bundleDir)) { return; }
440440

441-
fs.ensureDirSync(bundleDir);
441+
const tempDir = `${bundleDir}-building`;
442+
// Remove the tempDir if it exists, then recreate it
443+
fs.rmSync(tempDir, { recursive: true, force: true });
444+
445+
fs.ensureDirSync(tempDir);
442446
// Chmod the bundleDir to full access.
443-
fs.chmodSync(bundleDir, 0o777);
447+
fs.chmodSync(tempDir, 0o777);
444448

445449
let localBundling: boolean | undefined;
446450
try {
447451
process.stderr.write(`Bundling asset ${this.node.path}...\n`);
448452

449-
localBundling = options.local?.tryBundle(bundleDir, options);
453+
localBundling = options.local?.tryBundle(tempDir, options);
450454
if (!localBundling) {
451455
const assetStagingOptions = {
452456
sourcePath: this.sourcePath,
453-
bundleDir,
457+
bundleDir: tempDir,
454458
...options,
455459
};
456460

@@ -464,18 +468,11 @@ export class AssetStaging extends Construct {
464468
break;
465469
}
466470
}
467-
} catch (err) {
468-
// When bundling fails, keep the bundle output for diagnosability, but
469-
// rename it out of the way so that the next run doesn't assume it has a
470-
// valid bundleDir.
471-
const bundleErrorDir = bundleDir + '-error';
472-
if (fs.existsSync(bundleErrorDir)) {
473-
// Remove the last bundleErrorDir.
474-
fs.removeSync(bundleErrorDir);
475-
}
476471

477-
fs.renameSync(bundleDir, bundleErrorDir);
478-
throw new Error(`Failed to bundle asset ${this.node.path}, bundle output is located at ${bundleErrorDir}: ${err}`);
472+
// Success, rename the tempDir into place
473+
fs.renameSync(tempDir, bundleDir);
474+
} catch (err) {
475+
throw new Error(`Failed to bundle asset ${this.node.path}, bundle output is located at ${tempDir}: ${err}`);
479476
}
480477

481478
if (FileSystem.isEmpty(bundleDir)) {

packages/aws-cdk-lib/core/lib/assets.ts

-2
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,6 @@ export interface AssetOptions {
5353
*
5454
* @default - uploaded as-is to S3 if the asset is a regular file or a .zip file,
5555
* archived into a .zip file and uploaded to S3 otherwise
56-
*
57-
*
5856
*/
5957
readonly bundling?: BundlingOptions;
6058
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* This is a CDK app that is guaranteed to kill itself during bundling
3+
*/
4+
import * as path from 'path';
5+
import { App, AssetStaging, DockerImage, Stack } from '../lib';
6+
7+
const app = new App();
8+
const stack = new Stack(app, 'stack');
9+
const directory = path.join(__dirname, 'fs', 'fixtures', 'test1');
10+
11+
const pid = process.pid;
12+
13+
// WHEN
14+
new AssetStaging(stack, 'Asset', {
15+
sourcePath: directory,
16+
bundling: {
17+
image: DockerImage.fromRegistry('alpine'),
18+
command: ['DOCKER_STUB_EXEC', 'kill', `${pid}`],
19+
},
20+
});
21+
22+
app.synth();

packages/aws-cdk-lib/core/test/docker-stub.sh

+10-1
Original file line numberDiff line numberDiff line change
@@ -49,5 +49,14 @@ if echo "$@" | grep "DOCKER_STUB_SINGLE_FILE"; then
4949
exit 0
5050
fi
5151

52-
echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS,DOCKER_STUB_MULTIPLE_FILES,DOCKER_SINGLE_ARCHIVE"
52+
if echo "$@" | grep "DOCKER_STUB_EXEC"; then
53+
while [[ "$1" != "DOCKER_STUB_EXEC" ]]; do
54+
shift
55+
done
56+
shift
57+
58+
exec "$@" # Execute what's left
59+
fi
60+
61+
echo "Docker mock only supports one of the following commands: DOCKER_STUB_SUCCESS_NO_OUTPUT,DOCKER_STUB_FAIL,DOCKER_STUB_SUCCESS,DOCKER_STUB_MULTIPLE_FILES,DOCKER_SINGLE_ARCHIVE,DOCKER_STUB_EXEC, got '$@'"
5362
exit 1

packages/aws-cdk-lib/core/test/staging.test.ts

+41-2
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { execSync } from 'child_process';
12
import * as os from 'os';
23
import * as path from 'path';
34
import { testDeprecated } from '@aws-cdk/cdk-build-tools';
@@ -610,13 +611,13 @@ describe('staging', () => {
610611
image: DockerImage.fromRegistry('alpine'),
611612
command: [DockerStubCommand.FAIL],
612613
},
613-
})).toThrow(/Failed.*bundl.*asset.*-error/);
614+
})).toThrow(/Failed.*bundl.*asset.*-building/);
614615

615616
// THEN
616617
const assembly = app.synth();
617618

618619
const dir = fs.readdirSync(assembly.directory);
619-
expect(dir.some(entry => entry.match(/asset.*-error/))).toEqual(true);
620+
expect(dir.some(entry => entry.match(/asset.*-building/))).toEqual(true);
620621
});
621622

622623
test('bundler re-uses assets from previous synths', () => {
@@ -675,6 +676,44 @@ describe('staging', () => {
675676
]);
676677
});
677678

679+
test('if bundling is interrupted, target asset directory is not produced', () => {
680+
// GIVEN
681+
const TEST_OUTDIR = path.join(__dirname, 'cdk.out');
682+
if (fs.existsSync(TEST_OUTDIR)) {
683+
fs.removeSync(TEST_OUTDIR);
684+
}
685+
686+
// WHEN
687+
try {
688+
execSync(`npx ts-node ${__dirname}/app-that-is-interrupted-during-staging.ts`, {
689+
env: {
690+
...process.env,
691+
CDK_OUTDIR: TEST_OUTDIR,
692+
},
693+
});
694+
throw new Error('We expected the above command to fail');
695+
} catch (e) {
696+
// We expect the command to be terminated with a signal, which sometimes shows
697+
// as 'signal' is set to SIGTERM, and on some Linuxes as exitCode = 128 + 15 = 143
698+
if (e.signal === 'SIGTERM' || e.status === 143) {
699+
// pass
700+
} else {
701+
throw e;
702+
}
703+
}
704+
705+
// THEN
706+
const generatedFiles = fs.readdirSync(TEST_OUTDIR);
707+
// We expect a 'building' asset directory...
708+
expect(generatedFiles).toContainEqual(
709+
expect.stringMatching(/^asset\.[0-9a-f]+-building$/),
710+
);
711+
// ...not a complete asset directory
712+
expect(generatedFiles).not.toContainEqual(
713+
expect.stringMatching(/^asset\.[0-9a-f]+$/),
714+
);
715+
});
716+
678717
test('bundler re-uses assets from previous synths, ignoring tokens', () => {
679718
// GIVEN
680719
const TEST_OUTDIR = path.join(__dirname, 'cdk.out');

0 commit comments

Comments
 (0)