Skip to content

Commit 69981ac

Browse files
authored
feat(cli): allow disabling parallel asset publishing (#22579)
Once again, any change to anything anywhere broke someone. In this particular case, parallel asset publishing (#19367, and in particular building) broke a use case where someone uses a tool to build assets that is not concurrency-safe. So -- now we need to make that configurable. Command line: ``` cdk deploy --no-asset-parallelism ``` cdk.json: ``` { "assetParallelism": false } ``` Environment variables: ``` export CDK_ASSET_PARALLELISM=false ``` ---- ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing guide?](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) ### Adding new Unconventional Dependencies: * [ ] This PR adds new unconventional dependencies following the process described [here](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md/#adding-new-unconventional-dependencies) ### New Features * [ ] Have you added the new feature to an [integration test](https://github.com/aws/aws-cdk/blob/main/INTEGRATION_TESTS.md)? * [ ] Did you use `yarn integ` to deploy the infrastructure and generate the snapshot (i.e. `yarn integ` without `--dry-run`)? *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
1 parent 42160fc commit 69981ac

9 files changed

+158
-14
lines changed

Diff for: packages/aws-cdk/lib/api/cloudformation-deployments.ts

+24-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as cxapi from '@aws-cdk/cx-api';
22
import { AssetManifest } from 'cdk-assets';
33
import { Tag } from '../cdk-toolkit';
44
import { debug, warning } from '../logging';
5-
import { buildAssets, publishAssets } from '../util/asset-publishing';
5+
import { buildAssets, publishAssets, BuildAssetsOptions, PublishAssetsOptions } from '../util/asset-publishing';
66
import { Mode } from './aws-auth/credentials';
77
import { ISDK } from './aws-auth/sdk';
88
import { SdkProvider } from './aws-auth/sdk-provider';
@@ -253,6 +253,13 @@ export interface DeployStackOptions {
253253
* @default true To remain backward compatible.
254254
*/
255255
readonly buildAssets?: boolean;
256+
257+
/**
258+
* Whether to build/publish assets in parallel
259+
*
260+
* @default true To remain backward compatible.
261+
*/
262+
readonly assetParallelism?: boolean;
256263
}
257264

258265
export interface BuildStackAssetsOptions {
@@ -274,6 +281,11 @@ export interface BuildStackAssetsOptions {
274281
* @default - Current role
275282
*/
276283
readonly roleArn?: string;
284+
285+
/**
286+
* Options to pass on to `buildAsests()` function
287+
*/
288+
readonly buildOptions?: BuildAssetsOptions;
277289
}
278290

279291
interface PublishStackAssetsOptions {
@@ -283,6 +295,11 @@ interface PublishStackAssetsOptions {
283295
* @default true To remain backward compatible.
284296
*/
285297
readonly buildAssets?: boolean;
298+
299+
/**
300+
* Options to pass on to `publishAsests()` function
301+
*/
302+
readonly publishOptions?: Omit<PublishAssetsOptions, 'buildAssets'>;
286303
}
287304

288305
export interface DestroyStackOptions {
@@ -401,6 +418,9 @@ export class CloudFormationDeployments {
401418
if (options.resourcesToImport === undefined) {
402419
await this.publishStackAssets(options.stack, toolkitInfo, {
403420
buildAssets: options.buildAssets ?? true,
421+
publishOptions: {
422+
parallel: options.assetParallelism,
423+
},
404424
});
405425
}
406426

@@ -434,6 +454,7 @@ export class CloudFormationDeployments {
434454
extraUserAgent: options.extraUserAgent,
435455
resourcesToImport: options.resourcesToImport,
436456
overrideTemplate: options.overrideTemplate,
457+
assetParallelism: options.assetParallelism,
437458
});
438459
}
439460

@@ -529,7 +550,7 @@ export class CloudFormationDeployments {
529550
toolkitInfo);
530551

531552
const manifest = AssetManifest.fromFile(assetArtifact.file);
532-
await buildAssets(manifest, this.sdkProvider, stackEnv);
553+
await buildAssets(manifest, this.sdkProvider, stackEnv, options.buildOptions);
533554
}
534555
}
535556

@@ -549,6 +570,7 @@ export class CloudFormationDeployments {
549570

550571
const manifest = AssetManifest.fromFile(assetArtifact.file);
551572
await publishAssets(manifest, this.sdkProvider, stackEnv, {
573+
...options.publishOptions,
552574
buildAssets: options.buildAssets ?? true,
553575
});
554576
}

Diff for: packages/aws-cdk/lib/api/deploy-stack.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,13 @@ export interface DeployStackOptions {
198198
* @default - Use the stored template
199199
*/
200200
readonly overrideTemplate?: any;
201+
202+
/**
203+
* Whether to build/publish assets in parallel
204+
*
205+
* @default true To remain backward compatible.
206+
*/
207+
readonly assetParallelism?: boolean;
201208
}
202209

203210
export type DeploymentMethod =
@@ -287,7 +294,9 @@ export async function deployStack(options: DeployStackOptions): Promise<DeploySt
287294
options.toolkitInfo,
288295
options.sdk,
289296
options.overrideTemplate);
290-
await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv);
297+
await publishAssets(legacyAssets.toManifest(stackArtifact.assembly.directory), options.sdkProvider, stackEnv, {
298+
parallel: options.assetParallelism,
299+
});
291300

292301
if (options.hotswap) {
293302
// attempt to short-circuit the deployment if possible

Diff for: packages/aws-cdk/lib/cdk-toolkit.ts

+14-1
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,7 @@ export class CdkToolkit {
258258
hotswap: options.hotswap,
259259
extraUserAgent: options.extraUserAgent,
260260
buildAssets: false,
261+
assetParallelism: options.assetParallelism,
261262
});
262263

263264
const message = result.noOp
@@ -764,7 +765,7 @@ export class CdkToolkit {
764765
}
765766
}
766767

767-
private async buildAllAssetsForSingleStack(stack: cxapi.CloudFormationStackArtifact, options: Pick<DeployOptions, 'roleArn' | 'toolkitStackName'>): Promise<void> {
768+
private async buildAllAssetsForSingleStack(stack: cxapi.CloudFormationStackArtifact, options: Pick<DeployOptions, 'roleArn' | 'toolkitStackName' | 'assetParallelism'>): Promise<void> {
768769
// Check whether the stack has an asset manifest before trying to build and publish.
769770
if (!stack.dependencies.some(cxapi.AssetManifestArtifact.isAssetManifestArtifact)) {
770771
return;
@@ -775,6 +776,9 @@ export class CdkToolkit {
775776
stack,
776777
roleArn: options.roleArn,
777778
toolkitStackName: options.toolkitStackName,
779+
buildOptions: {
780+
parallel: options.assetParallelism,
781+
},
778782
});
779783
print('\n%s: assets built\n', chalk.bold(stack.displayName));
780784
}
@@ -1029,6 +1033,15 @@ export interface DeployOptions extends CfnDeployOptions, WatchOptions {
10291033
* @default 1
10301034
*/
10311035
readonly concurrency?: number;
1036+
1037+
/**
1038+
* Build/publish assets for a single stack in parallel
1039+
*
1040+
* Independent of whether stacks are being done in parallel or no.
1041+
*
1042+
* @default true
1043+
*/
1044+
readonly assetParallelism?: boolean;
10321045
}
10331046

10341047
export interface ImportOptions extends CfnDeployOptions {

Diff for: packages/aws-cdk/lib/cli.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,8 @@ async function parseCommandLineArguments() {
155155
"'true' by default, use --no-logs to turn off. " +
156156
"Only in effect if specified alongside the '--watch' option",
157157
})
158-
.option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }),
158+
.option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true })
159+
.option('asset-parallelism', { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }),
159160
)
160161
.command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs
161162
.option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true })
@@ -514,6 +515,7 @@ async function initCommandLine() {
514515
watch: args.watch,
515516
traceLogs: args.logs,
516517
concurrency: args.concurrency,
518+
assetParallelism: args.assetParallelism,
517519
});
518520

519521
case 'import':

Diff for: packages/aws-cdk/lib/settings.ts

+1
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,7 @@ export class Settings {
289289
lookups: argv.lookups,
290290
rollback: argv.rollback,
291291
notices: argv.notices,
292+
assetParallelism: argv['asset-parallelism'],
292293
});
293294
}
294295

Diff for: packages/aws-cdk/lib/util/asset-publishing.ts

+16-2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,13 @@ export interface PublishAssetsOptions {
1818
* @default true To remain backward compatible.
1919
*/
2020
readonly buildAssets?: boolean;
21+
22+
/**
23+
* Whether to build/publish assets in parallel
24+
*
25+
* @default true To remain backward compatible.
26+
*/
27+
readonly parallel?: boolean;
2128
}
2229

2330
/**
@@ -44,7 +51,7 @@ export async function publishAssets(
4451
aws: new PublishingAws(sdk, targetEnv),
4552
progressListener: new PublishingProgressListener(options.quiet ?? false),
4653
throwOnError: false,
47-
publishInParallel: true,
54+
publishInParallel: options.parallel ?? true,
4855
buildAssets: options.buildAssets ?? true,
4956
publishAssets: true,
5057
});
@@ -59,6 +66,13 @@ export interface BuildAssetsOptions {
5966
* Print progress at 'debug' level
6067
*/
6168
readonly quiet?: boolean;
69+
70+
/**
71+
* Build assets in parallel
72+
*
73+
* @default true
74+
*/
75+
readonly parallel?: boolean;
6276
}
6377

6478
/**
@@ -85,7 +99,7 @@ export async function buildAssets(
8599
aws: new PublishingAws(sdk, targetEnv),
86100
progressListener: new PublishingProgressListener(options.quiet ?? false),
87101
throwOnError: false,
88-
publishInParallel: true,
102+
publishInParallel: options.parallel ?? true,
89103
buildAssets: true,
90104
publishAssets: false,
91105
});

Diff for: packages/aws-cdk/test/api/cloudformation-deployments.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -904,7 +904,7 @@ test('building assets', async () => {
904904
name: 'aws://account/region',
905905
region: 'region',
906906
});
907-
expect(buildAssets).toBeCalledWith(expectedAssetManifest, sdkProvider, expectedEnvironment);
907+
expect(buildAssets).toBeCalledWith(expectedAssetManifest, sdkProvider, expectedEnvironment, undefined);
908908
});
909909

910910
function pushStackResourceSummaries(stackName: string, ...items: CloudFormation.StackResourceSummary[]) {

Diff for: packages/aws-cdk/test/cdk-toolkit.test.ts

+66-5
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ jest.mock('../lib/logging', () => ({
5252
...jest.requireActual('../lib/logging'),
5353
data: mockData,
5454
}));
55+
jest.setTimeout(30_000);
5556

57+
import * as path from 'path';
5658
import * as cxschema from '@aws-cdk/cloud-assembly-schema';
59+
import { Manifest } from '@aws-cdk/cloud-assembly-schema';
5760
import * as cxapi from '@aws-cdk/cx-api';
5861
import { Bootstrapper } from '../lib/api/bootstrap';
5962
import { CloudFormationDeployments, DeployStackOptions, DestroyStackOptions } from '../lib/api/cloudformation-deployments';
@@ -62,7 +65,8 @@ import { Template } from '../lib/api/util/cloudformation';
6265
import { CdkToolkit, Tag } from '../lib/cdk-toolkit';
6366
import { RequireApproval } from '../lib/diff';
6467
import { flatten } from '../lib/util';
65-
import { instanceMockFrom, MockCloudExecutable, TestStackArtifact } from './util';
68+
import { instanceMockFrom, MockCloudExecutable, TestStackArtifact, withMocked } from './util';
69+
import { MockSdkProvider } from './util/mock-sdk';
6670

6771
let cloudExecutable: MockCloudExecutable;
6872
let bootstrapper: jest.Mocked<Bootstrapper>;
@@ -555,6 +559,36 @@ describe('deploy', () => {
555559
expect(cloudExecutable.hasApp).toEqual(false);
556560
expect(mockSynthesize).not.toHaveBeenCalled();
557561
});
562+
563+
test('can disable asset parallelism', async () => {
564+
// GIVEN
565+
cloudExecutable = new MockCloudExecutable({
566+
stacks: [MockStack.MOCK_STACK_WITH_ASSET],
567+
});
568+
const fakeCloudFormation = new FakeCloudFormation({});
569+
570+
const toolkit = new CdkToolkit({
571+
cloudExecutable,
572+
configuration: cloudExecutable.configuration,
573+
sdkProvider: cloudExecutable.sdkProvider,
574+
cloudFormation: fakeCloudFormation,
575+
});
576+
577+
// WHEN
578+
// Not the best test but following this through to the asset publishing library fails
579+
await withMocked(fakeCloudFormation, 'buildStackAssets', async (mockBuildStackAssets) => {
580+
await toolkit.deploy({
581+
selector: { patterns: ['Test-Stack-Asset'] },
582+
assetParallelism: false,
583+
});
584+
585+
expect(mockBuildStackAssets).toHaveBeenCalledWith(expect.objectContaining({
586+
buildOptions: expect.objectContaining({
587+
parallel: false,
588+
}),
589+
}));
590+
});
591+
});
558592
});
559593
});
560594

@@ -911,6 +945,23 @@ class MockStack {
911945
},
912946
displayName: 'Test-Stack-A/witherrors',
913947
}
948+
public static readonly MOCK_STACK_WITH_ASSET: TestStackArtifact = {
949+
stackName: 'Test-Stack-Asset',
950+
template: { Resources: { TemplateName: 'Test-Stack-Asset' } },
951+
env: 'aws://123456789012/bermuda-triangle-1',
952+
assetManifest: {
953+
version: Manifest.version(),
954+
files: {
955+
xyz: {
956+
source: {
957+
path: path.resolve(__dirname, '..', 'LICENSE'),
958+
},
959+
destinations: {
960+
},
961+
},
962+
},
963+
},
964+
}
914965
}
915966

916967
class FakeCloudFormation extends CloudFormationDeployments {
@@ -921,7 +972,7 @@ class FakeCloudFormation extends CloudFormationDeployments {
921972
expectedTags: { [stackName: string]: { [key: string]: string } } = {},
922973
expectedNotificationArns?: string[],
923974
) {
924-
super({ sdkProvider: undefined as any });
975+
super({ sdkProvider: new MockSdkProvider() });
925976

926977
for (const [stackName, tags] of Object.entries(expectedTags)) {
927978
this.expectedTags[stackName] =
@@ -934,9 +985,17 @@ class FakeCloudFormation extends CloudFormationDeployments {
934985
}
935986

936987
public deployStack(options: DeployStackOptions): Promise<DeployStackResult> {
937-
expect([MockStack.MOCK_STACK_A.stackName, MockStack.MOCK_STACK_B.stackName, MockStack.MOCK_STACK_C.stackName])
938-
.toContain(options.stack.stackName);
939-
expect(options.tags).toEqual(this.expectedTags[options.stack.stackName]);
988+
expect([
989+
MockStack.MOCK_STACK_A.stackName,
990+
MockStack.MOCK_STACK_B.stackName,
991+
MockStack.MOCK_STACK_C.stackName,
992+
MockStack.MOCK_STACK_WITH_ASSET.stackName,
993+
]).toContain(options.stack.stackName);
994+
995+
if (this.expectedTags[options.stack.stackName]) {
996+
expect(options.tags).toEqual(this.expectedTags[options.stack.stackName]);
997+
}
998+
940999
expect(options.notificationArns).toEqual(this.expectedNotificationArns);
9411000
return Promise.resolve({
9421001
stackArn: `arn:aws:cloudformation:::stack/${options.stack.stackName}/MockedOut`,
@@ -959,6 +1018,8 @@ class FakeCloudFormation extends CloudFormationDeployments {
9591018
return Promise.resolve({});
9601019
case MockStack.MOCK_STACK_C.stackName:
9611020
return Promise.resolve({});
1021+
case MockStack.MOCK_STACK_WITH_ASSET.stackName:
1022+
return Promise.resolve({});
9621023
default:
9631024
return Promise.reject(`Not an expected mock stack: ${stack.stackName}`);
9641025
}

0 commit comments

Comments
 (0)