Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: new synthesizer separates assets out per CDK application #24430

Merged
merged 137 commits into from
May 19, 2023
Merged
Changes from 11 commits
Commits
Show all changes
137 commits
Select commit Hold shift + click to select a range
ae44fc4
initial project structure
kaizencc Mar 2, 2023
b6fd1eb
skeleton implementation
kaizencc Mar 2, 2023
6b59b29
get everything to compile
kaizencc Mar 2, 2023
4ca12fe
delete extraneous files
kaizencc Mar 2, 2023
6ee9d78
more untested progress
kaizencc Mar 4, 2023
696d275
add bedrock for testing
kaizencc Mar 4, 2023
d73f27f
implement staging stack in synth
kaizencc Mar 6, 2023
5153ef5
get basic test to succeed
kaizencc Mar 6, 2023
c3628f4
remove extra lines
kaizencc Mar 7, 2023
a3a4c79
forgot to commit
kaizencc Mar 7, 2023
fc52f21
tests for docker asset and file asset
kaizencc Mar 7, 2023
e292810
add in iam roles
kaizencc Mar 8, 2023
3fd54e1
handle assets
kaizencc Mar 8, 2023
12283bd
assets
kaizencc Mar 8, 2023
8681478
small things
kaizencc Mar 8, 2023
ae2781e
2 tests
kaizencc Mar 8, 2023
143df73
add no tokens
kaizencc Mar 8, 2023
d5a50c3
better error messaging
kaizencc Mar 9, 2023
d088ec8
more work
kaizencc Mar 9, 2023
f3673ca
bootstrap role
kaizencc Mar 10, 2023
4b6944b
basic functionality for file assets
kaizencc Mar 15, 2023
70d6d85
better role
kaizencc Mar 15, 2023
3a96ee0
major clean up
kaizencc Mar 16, 2023
b7e0b6a
use bootstrapless synth
kaizencc Mar 16, 2023
b678df5
Merge branch 'main' into conroy/bootstrap
kaizencc Mar 16, 2023
b3eb0a6
expose appId to user
kaizencc Mar 17, 2023
26985ab
rename folder
kaizencc Mar 17, 2023
f1462dd
rename
kaizencc Mar 17, 2023
5dc7a5f
adr
kaizencc Mar 17, 2023
42b8566
readme start
kaizencc Mar 17, 2023
6e57c29
minor changes
kaizencc Mar 20, 2023
2a5c44e
move appId to the app props
kaizencc Mar 20, 2023
fb50b3d
new tests
kaizencc Mar 20, 2023
5483fe2
IStagingStack
kaizencc Mar 20, 2023
543bafe
lifecylce rules and kms key
kaizencc Mar 20, 2023
4085c79
wip of kms key debacle
kaizencc Mar 22, 2023
f4074b6
kms key works
kaizencc Mar 22, 2023
c3d1fff
update readme for new api
kaizencc Mar 22, 2023
827fea1
remove app chnages
kaizencc Mar 22, 2023
b77eaf9
remove app chnages
kaizencc Mar 22, 2023
b8cd873
appid
kaizencc Mar 22, 2023
7d115ca
massive changes to api but tests succeed
kaizencc Mar 23, 2023
5754712
rename folder
kaizencc Mar 23, 2023
46da1cc
add roles stuff without testing yet
kaizencc Mar 23, 2023
20c8032
minor cleanups
kaizencc Mar 24, 2023
3b55d82
error when mixing agnostic/non-agnostic stacks in an app
kaizencc Mar 24, 2023
e9b1030
additional bootstrap role test
kaizencc Mar 24, 2023
b168308
rename to app-staging-synthesizer
kaizencc Mar 24, 2023
3eee189
add documentation everywhere and comment out docker code
kaizencc Mar 24, 2023
4fbae1b
more docs
kaizencc Mar 24, 2023
d3ec8a7
couple more tests
kaizencc Mar 24, 2023
d8e3af3
Merge branch 'main' of https://github.com/aws/aws-cdk into conroy/boo…
kaizencc Apr 4, 2023
bdd8a71
get package to work under new repo restructure
kaizencc Apr 4, 2023
806221e
rename to -alpha
kaizencc Apr 5, 2023
6fcc13d
update package.json
kaizencc Apr 5, 2023
6f6c9d3
Merge branch 'main' into conroy/bootstrap
kaizencc Apr 5, 2023
477f81f
rename
kaizencc Apr 5, 2023
a9e4b5a
integ test works
kaizencc Apr 5, 2023
9269b9c
resolve qualifier in the synth
kaizencc Apr 6, 2023
9728364
90% of lifecycle rules for ephemeral assets
kaizencc Apr 6, 2023
0487959
grant access to s3 bucket to deploy role
kaizencc Apr 7, 2023
7460def
introduce new translation functions in core
kaizencc Apr 7, 2023
08cd887
use new translation functions
kaizencc Apr 7, 2023
794cc68
revamp bootstrap roles api
kaizencc Apr 10, 2023
2104e68
move tests around
kaizencc Apr 10, 2023
8a12d60
more test stuff
kaizencc Apr 10, 2023
745770f
finish s3 lifecycle rule tests
kaizencc Apr 10, 2023
5db8d0a
expose bucket prefix api
kaizencc Apr 10, 2023
2f5f962
Merge remote-tracking branch 'origin/main' into conroy/bootstrap
rix0rrr Apr 11, 2023
065e8d4
Fix build
rix0rrr Apr 11, 2023
3d83f4a
env-agnostic test
kaizencc Apr 11, 2023
b7bed7e
Move some code around
rix0rrr Apr 12, 2023
7e5ec32
Merge branch 'conroy/bootstrap' of github.com:aws/aws-cdk into conroy…
rix0rrr Apr 12, 2023
9d080b5
Merge remote-tracking branch 'origin/main' into conroy/bootstrap
rix0rrr Apr 12, 2023
6f33075
minor changes
kaizencc Apr 12, 2023
e4b15f4
Reorganize responsibilities between:
rix0rrr Apr 13, 2023
1d836aa
Merge branch 'conroy/bootstrap' of github.com:aws/aws-cdk into conroy…
rix0rrr Apr 13, 2023
cd39826
Fix build issues
rix0rrr Apr 13, 2023
d79aba3
Merge remote-tracking branch 'origin/main' into conroy/bootstrap
rix0rrr Apr 13, 2023
b642943
awslint fixes
rix0rrr Apr 13, 2023
2a84b5c
Fix unit tests
rix0rrr Apr 13, 2023
2331ef4
asset dependencies
kaizencc Apr 13, 2023
4aee77b
Merge branch 'conroy/bootstrap' of https://github.com/aws/aws-cdk int…
kaizencc Apr 13, 2023
b20cb04
wip of workgraph code. does not work yet, many errors
kaizencc Apr 14, 2023
ff3de55
Merge branch 'main' into conroy/bootstrap
kaizencc Apr 17, 2023
ffdfa8f
bigtime changes to the cli that are wip
kaizencc Apr 19, 2023
a0570d2
get to no compilation errors
kaizencc Apr 20, 2023
06dd548
all tests in deploy.test succeed
kaizencc Apr 21, 2023
16e46df
major overhaul of test to make it make sense
kaizencc Apr 21, 2023
c3268f7
fix rest of the tests in display.ts
kaizencc Apr 24, 2023
a0851eb
remove etraneous files
kaizencc Apr 24, 2023
466e3cd
callback interface for deployArtifacts
kaizencc Apr 24, 2023
13795c9
worknode is now an abstract class
kaizencc Apr 24, 2023
9c0183a
Merge branch 'main' into conroy/bootstrap
kaizencc Apr 26, 2023
01fc565
add disable prebuild to workgraph
kaizencc Apr 27, 2023
075ed8b
typo
kaizencc Apr 27, 2023
c4af16e
nested assemblies
kaizencc Apr 27, 2023
925812e
wip of app-staging stuff
kaizencc May 3, 2023
62712b8
Pass deployRoleArn
rix0rrr May 3, 2023
0c5e48c
integ tests r good
kaizencc May 3, 2023
a7b61a4
add assetname to ecr-assets
kaizencc May 3, 2023
5290ad3
update integ test
kaizencc May 4, 2023
0e25c13
more test updates
kaizencc May 4, 2023
53287e0
Merge branch 'main' into conroy/bootstrap
kaizencc May 5, 2023
21564fe
update sto ecr asets
kaizencc May 5, 2023
0cc17e5
Merge branch 'conroy/bootstrap' of https://github.com/aws/aws-cdk int…
kaizencc May 5, 2023
91389b0
grantread
kaizencc May 5, 2023
2d9719e
more unit tests
kaizencc May 8, 2023
475ebe9
unit test for gnostic stuff
kaizencc May 8, 2023
f0867bb
error when staging resource stack too large
kaizencc May 8, 2023
c6fb952
add ecr lifecycle rules
kaizencc May 9, 2023
95a9362
lint
kaizencc May 10, 2023
f679b2c
update testing suite and minor bugs
kaizencc May 10, 2023
39d95e7
readme updates, wip
kaizencc May 10, 2023
505227f
rename IStagingStack to IStagingResources
kaizencc May 11, 2023
01baa7b
more readme
kaizencc May 11, 2023
28e3324
wip integ test
kaizencc May 11, 2023
d38dc36
wip integ test
kaizencc May 11, 2023
f9443f2
Undo some changes that are now on the ot her branch
rix0rrr May 15, 2023
5162dca
Remove some more files
rix0rrr May 15, 2023
34e0cf6
Apply suggestions from code review
kaizencc May 15, 2023
5a910bb
pr feedback wip
kaizencc May 16, 2023
1437915
pr feedback, rosetta passes, tests pass
kaizencc May 16, 2023
ca3cbc6
Merge branch 'main' into conroy/bootstrap
kaizencc May 16, 2023
2ea6d0a
rename app-staging-synthesizer into app-staging-synthesizer-alpha
kaizencc May 16, 2023
d1c873b
mark template assets as ephemeral
kaizencc May 16, 2023
1b71b51
Merge branch 'conroy/bootstrap' of https://github.com/aws/aws-cdk int…
kaizencc May 16, 2023
c91fc26
fix merge
kaizencc May 16, 2023
2c60623
fix tests
kaizencc May 16, 2023
37074c4
rename ephemeral into deployTime
kaizencc May 17, 2023
e0a9089
update readme content
kaizencc May 17, 2023
af5856b
final updates
kaizencc May 17, 2023
800bd89
Merge branch 'main' into conroy/bootstrap
kaizencc May 17, 2023
b1210b3
minor renames
kaizencc May 17, 2023
48270f4
catch errors in isPublished so removePublishedAsets cannot fail
kaizencc May 17, 2023
93dad51
silence warning messages when calling is__Published
kaizencc May 19, 2023
550f50b
Merge branch 'main' into conroy/bootstrap
kaizencc May 19, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
407 changes: 186 additions & 221 deletions packages/@aws-cdk/app-staging-synthesizer/lib/app-staging-synthesizer.ts

Large diffs are not rendered by default.

39 changes: 23 additions & 16 deletions packages/@aws-cdk/app-staging-synthesizer/lib/bootstrap-roles.ts
Original file line number Diff line number Diff line change
@@ -16,36 +16,43 @@ export class BootstrapRole {
* Specify an existing IAM Role to assume
*/
public static fromRoleArn(arn: string) {
StringSpecializer.validateNoTokens(arn, 'BootstrapRole ARN');
return new BootstrapRole(arn);
}

private static CLI_CREDS = 'cli-credentials';

private constructor(private readonly roleArn: string) {}

/**
* Whether or not this is object was created using BootstrapRole.cliCredentials()
*/
public isCliCredentials() {
return this.roleArn === BootstrapRole.CLI_CREDS;
}

public renderRoleArn(options: {
spec?: StringSpecializer,
tokenType?: 'asset' | 'cfn',
} = {}) {
if (this.isCliCredentials()) { return undefined; }
if (!options.spec) { return this.roleArn; }
/**
* @internal
*/
public _arnForCloudFormation() {
return this.isCliCredentials() ? undefined : translateAssetTokenToCfnToken(this.roleArn);
}

/**
* @internal
*/
public _arnForCloudAssembly() {
return this.isCliCredentials() ? undefined : translateCfnTokenToAssetToken(this.roleArn);
}

const arn = options.spec.specialize(this.roleArn);
if (options.tokenType === 'asset') {
return translateCfnTokenToAssetToken(arn);
} else if (options.tokenType === 'cfn') {
return translateAssetTokenToCfnToken(arn);
} else {
return arn;
}
/**
* @internal
*/
public _specialize(spec: StringSpecializer) {
return new BootstrapRole(spec.specialize(this.roleArn));
}
}


/**
* Roles that are bootstrapped to your account.
*/
@@ -62,7 +69,7 @@ export interface BootstrapRoles {
*
* @default - use boostrapped role
*/
readonly deploymentActionRole?: BootstrapRole;
readonly deploymentRole?: BootstrapRole;

/**
* Lookup Role
355 changes: 174 additions & 181 deletions packages/@aws-cdk/app-staging-synthesizer/lib/default-staging-stack.ts

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/@aws-cdk/app-staging-synthesizer/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './default-staging-stack';
export * from './app-staging-synthesizer';
export * from './bootstrap-roles';
export * from './bootstrap-roles';
export * from './staging-stack';
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Stack } from 'aws-cdk-lib';
import { AppScopedGlobal } from './private/app-global';
import { IStagingStack, IStagingStackFactory, ObtainStagingResourcesContext } from './staging-stack';

/**
* Per-environment cache
*
* This is a global because we might have multiple instances of this class
* in the app, but we want to cache across all of them.
*/
const ENVIRONMENT_CACHE = new AppScopedGlobal(() => new Map<string, IStagingStack>);

/**
* Wraps another IStagingStack factory, and caches the result on a per-environment basis
*/
export class PerEnvironmenStagingFactory implements IStagingStackFactory {
constructor(private readonly wrapped: IStagingStackFactory) { }

public obtainStagingResources(stack: Stack, context: ObtainStagingResourcesContext): IStagingStack {
const cacheKey = context.environmentString;

const cache = ENVIRONMENT_CACHE.for(stack);
const existing = cache.get(cacheKey);
if (existing) {
return existing;
}

const result = this.wrapped.obtainStagingResources(stack, context);
cache.set(cacheKey, result);
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { App } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';

/**
* Hold an App-wide global variable
*
* This is a replacement for a `static` variable, but does the right thing in case people
* instantiate multiple Apps in the same process space (for example, in unit tests or
* people using `cli-lib` in advanced configurations).
*
* This class assumes that the global you're going to be storing is a mutable object.
*/
export class AppScopedGlobal<A> {
private readonly map = new WeakMap<App, A>();

constructor(private readonly factory: () => A) {
}

public for(ctr: IConstruct): A {
const app = App.of(ctr);
if (!App.isApp(app)) {
throw new Error(`Construct ${ctr.node.path} must be part of an App`);
}

const existing = this.map.get(app);
if (existing) {
return existing;
}
const instance = this.factory();
this.map.set(app, instance);
return instance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { StringSpecializer } from 'aws-cdk-lib/core/lib/helpers-internal';

export function validateNoTokens<A extends object>(props: A, context: string) {
for (const [key, value] of Object.entries(props)) {
if (typeof value === 'string') {
StringSpecializer.validateNoTokens(value, `${context} property '${key}'`);
}
}
}
120 changes: 120 additions & 0 deletions packages/@aws-cdk/app-staging-synthesizer/lib/staging-stack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { DockerImageAssetSource, FileAssetSource, Stack } from 'aws-cdk-lib';
import { IConstruct } from 'constructs';

/**
* Information returned by the Staging Stack for each file asset.
*/
export interface FileStagingLocation {
/**
* The name of the staging bucket
*/
readonly bucketName: string;

/**
* A prefix to add to the keys
*
* @default ''
*/
readonly prefix?: string;

/**
* The ARN to assume to write files to this bucket
*
* @default - Don't assume a role
*/
readonly assumeRoleArn?: string;

/**
* The stack that creates this bucket (leads to dependencies on it)
*
* @default - Don't add dependencies
*/
readonly dependencyStack?: Stack;
}

/**
* Information returned by the Staging Stack for each image asset
*/
export interface ImageStagingLocation {
/**
* The name of the staging repository
*/
readonly repoName: string;

/**
* The arn to assume to write files to this repository
*
* @default - Don't assume a role
*/
readonly assumeRoleArn?: string;

/**
* The stack that creates this repository (leads to dependencies on it)
*
* @default - Don't add dependencies
*/
readonly dependencyStack?: Stack;
}

/**
* Information on how a Staging Stack should look.
*/
export interface IStagingStack extends IConstruct {
/**
* Return staging resource information for a file asset.
*/
addFile(asset: FileAssetSource): FileStagingLocation;

/**
* Return staging resource information for a docker asset.
*/
addDockerImage(asset: DockerImageAssetSource): ImageStagingLocation;
}

/**
* Staging Stack Factory interface.
*
* The function included in this class will be called by the synthesizer
* to create or reference an IStagingStack that has the necessary
* staging resources for the Stack.
*/
export interface IStagingStackFactory {
/**
* Return an object that will manage staging resources for the given stack
*
* This is called whenever the the `AppStagingSynthesizer` binds to a specific
* stack, and allows selecting where the staging resources go.
*
* This method can choose to either create a new construct (perhaps a stack)
* and return it, or reference an existing construct.
*
* @param stack - stack to return an appropriate IStagingStack for
*/
obtainStagingResources(stack: Stack, context: ObtainStagingResourcesContext): IStagingStack;
}

/**
* Context parameters for the 'obtainStagingResources' function
*/
export interface ObtainStagingResourcesContext {
/**
* A unique string describing the environment that is guaranteed not to have tokens in it
*/
readonly environmentString: string;

/**
* The ARN of the deploy action role, if given
*
* This role will need permissions to read from to the staging resources.
*
* @default - Deploy role ARN is unknown
*/
readonly deployRoleArn?: string;

/**
* The qualifier passed to the synthesizer
*
* The staging stack shouldn't need this, but it might.
*/
readonly qualifier: string;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* eslint-disable jest/no-commented-out-tests */
import * as fs from 'fs';
import { App, Stack, CfnResource, FileAssetPackaging, Token, Lazy, Duration } from 'aws-cdk-lib';
import { Template, Match } from 'aws-cdk-lib/assertions';
import { Match, Template } from 'aws-cdk-lib/assertions';
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
import { evaluateCFN } from 'aws-cdk-lib/core/test/evaluate-cfn';
import { APP_ID, CFN_CONTEXT, TestAppScopedStagingSynthesizer, isAssetManifest, last } from './util';
@@ -38,7 +38,7 @@ describe(AppStagingSynthesizer, () => {
const stackArtifact = asm.getStackArtifact('Stack');

const templateObjectKey = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/'));
expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-000000000000-us-east-1-${APP_ID.toLocaleLowerCase()}/${templateObjectKey}`);
expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-${APP_ID}-staging-000000000000-us-east-1/${templateObjectKey}`);

// THEN - the template is in the asset manifest
const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0];
@@ -51,10 +51,10 @@ describe(AppStagingSynthesizer, () => {
source: { path: 'Stack.template.json', packaging: 'file' },
destinations: {
'000000000000-us-east-1': {
bucketName: `cdk-000000000000-us-east-1-${APP_ID.toLocaleLowerCase()}`,
bucketName: `cdk-${APP_ID}-staging-000000000000-us-east-1`,
objectKey: templateObjectKey,
region: 'us-east-1',
assumeRoleArn: `arn:\${AWS::Partition}:iam::000000000000:role/cdk-file-publishing-role-us-east-1-${APP_ID}`,
assumeRoleArn: `arn:\${AWS::Partition}:iam::000000000000:role/cdk-${APP_ID}-file-publishing-role-us-east-1`,
},
},
});
@@ -85,7 +85,7 @@ describe(AppStagingSynthesizer, () => {
const stackArtifact = asm.getStackArtifact('Stack2');

const templateObjectKey = last(stackArtifact.stackTemplateAssetObjectUrl?.split('/'));
expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-${accountToken}-${regionToken}-${APP_ID.toLocaleLowerCase()}/${templateObjectKey}`);
expect(stackArtifact.stackTemplateAssetObjectUrl).toEqual(`s3://cdk-${APP_ID}-staging-${accountToken}-${regionToken}/${templateObjectKey}`);

// THEN - the template is in the asset manifest
const manifestArtifact = asm.artifacts.filter(isAssetManifest)[0];
@@ -98,10 +98,10 @@ describe(AppStagingSynthesizer, () => {
source: { path: 'Stack2.template.json', packaging: 'file' },
destinations: {
'111111111111-us-east-2': {
bucketName: `cdk-111111111111-us-east-2-${APP_ID.toLocaleLowerCase()}`,
bucketName: `cdk-${APP_ID}-staging-111111111111-us-east-2`,
objectKey: templateObjectKey,
region: 'us-east-2',
assumeRoleArn: `arn:\${AWS::Partition}:iam::111111111111:role/cdk-file-publishing-role-us-east-2-${APP_ID}`,
assumeRoleArn: `arn:\${AWS::Partition}:iam::111111111111:role/cdk-${APP_ID}-file-publishing-role-us-east-2`,
},
},
});
@@ -118,7 +118,7 @@ describe(AppStagingSynthesizer, () => {
// THEN - we have a stack dependency on the staging stack
expect(stack.dependencies.length).toEqual(1);
const depStack = stack.dependencies[0];
expect(depStack.stackName).toEqual(`StagingStack${APP_ID}`);
expect(depStack.stackName).toEqual(`StagingStack-${APP_ID}`);
});

test('add file asset', () => {
@@ -130,8 +130,8 @@ describe(AppStagingSynthesizer, () => {
});

// THEN - we have a fixed asset location
expect(evalCFN(location.bucketName)).toEqual(`cdk-000000000000-us-east-1-${APP_ID.toLocaleLowerCase()}`);
expect(evalCFN(location.httpUrl)).toEqual(`https://s3.us-east-1.domain.aws/cdk-000000000000-us-east-1-${APP_ID.toLocaleLowerCase()}/abcdef.js`);
expect(evalCFN(location.bucketName)).toEqual(`cdk-${APP_ID}-staging-000000000000-us-east-1`);
expect(evalCFN(location.httpUrl)).toEqual(`https://s3.us-east-1.domain.aws/cdk-${APP_ID}-staging-000000000000-us-east-1/abcdef.js`);

// THEN - object key contains source hash somewhere
expect(location.objectKey.indexOf('abcdef')).toBeGreaterThan(-1);
@@ -154,33 +154,8 @@ describe(AppStagingSynthesizer, () => {
expect(evalCFN(location1.bucketName)).toEqual(evalCFN(location2.bucketName));
});

test('can configure bucket prefix', () => {
// GIVEN
app = new App({
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({
bucketPrefix: 'prefix',
}),
});
stack = new Stack(app, 'Stack', {
env: {
account: '000000000000',
region: 'us-west-2',
},
});

// WHEN
const location = stack.synthesizer.addFileAsset({
fileName: __filename,
packaging: FileAssetPackaging.FILE,
sourceHash: 'abcdef',
});

// THEN - assets have the same location
expect(evalCFN(location.objectKey)).toEqual('prefixabcdef.js');
});

describe('ephemeral assets', () => {
test('ephemeral assets have the \'eph-\' prefix', () => {
test('ephemeral assets have the \'handoff/\' prefix', () => {
// WHEN
const location = stack.synthesizer.addFileAsset({
fileName: __filename,
@@ -190,15 +165,13 @@ describe(AppStagingSynthesizer, () => {
});

// THEN - asset has bucket prefix
expect(evalCFN(location.objectKey)).toEqual('eph-abcdef.js');
expect(evalCFN(location.objectKey)).toEqual('handoff/abcdef.js');
});

test('ephemeral assets do not get specified bucketPrefix', () => {
// GIVEN
app = new App({
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({
bucketPrefix: 'prefix',
}),
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({}),
});
stack = new Stack(app, 'Stack', {
env: {
@@ -216,7 +189,7 @@ describe(AppStagingSynthesizer, () => {
});

// THEN - asset has bucket prefix
expect(evalCFN(location.objectKey)).toEqual('eph-abcdef.js');
expect(evalCFN(location.objectKey)).toEqual('handoff/abcdef.js');
});

test('s3 bucket has lifecycle rule on ephemeral assets by default', () => {
@@ -229,15 +202,15 @@ describe(AppStagingSynthesizer, () => {
const asm = app.synth();

// THEN
const stagingStackArtifact = asm.getStackArtifact('StagingStack000000000000us-east-1');
const stagingStackArtifact = asm.getStackArtifact(`StagingStack-${APP_ID}-000000000000-us-east-1`);

Template.fromJSON(stagingStackArtifact.template).hasResourceProperties('AWS::S3::Bucket', {
LifecycleConfiguration: {
Rules: [{
ExpirationInDays: 10,
Prefix: 'eph-',
Rules: Match.arrayWith([{
ExpirationInDays: 30,
Prefix: 'handoff/',
Status: 'Enabled',
}],
}]),
},
});
});
@@ -246,11 +219,7 @@ describe(AppStagingSynthesizer, () => {
// GIVEN
app = new App({
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({
ephemeralFileAssetLifecycleRule: {
prefix: 'eph-',
objectSizeGreaterThan: 10000,
expiration: Duration.days(1),
},
handoffFileAssetLifetime: Duration.days(1),
}),
});
stack = new Stack(app, 'Stack', {
@@ -267,66 +236,17 @@ describe(AppStagingSynthesizer, () => {
const asm = app.synth();

// THEN
const stagingStackArtifact = asm.getStackArtifact('StagingStack000000000000us-west-2');
const stagingStackArtifact = asm.getStackArtifact(`StagingStack-${APP_ID}-000000000000-us-west-2`);

Template.fromJSON(stagingStackArtifact.template).hasResourceProperties('AWS::S3::Bucket', {
LifecycleConfiguration: {
Rules: [{
Rules: Match.arrayWith([{
ExpirationInDays: 1,
Prefix: 'eph-',
Prefix: 'handoff/',
Status: 'Enabled',
ObjectSizeGreaterThan: 10000,
}],
},
});
});

test('customized lifecycle rule must have correct prefix', () => {
// GIVEN
app = new App({
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({
ephemeralFileAssetLifecycleRule: {
objectSizeGreaterThan: 10000,
expiration: Duration.days(1),
},
}),
});
expect(() => {
new Stack(app, 'Stack', {
env: {
account: '000000000000',
region: 'us-west-2',
},
});
}).toThrowError('ephemeralAssetLifecycleRule must contain "prefix: \'eph-\'" but got "prefix: undefined". This prefix is the only way to identify ephemeral assets.');
});

test('lifecycle rule on ephemeral assets can be turned off', () => {
// GIVEN
app = new App({
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({
retainEphemeralFileAssets: true,
}),
});
stack = new Stack(app, 'Stack', {
env: {
account: '000000000000',
region: 'us-west-2',
}]),
},
});
new CfnResource(stack, 'Resource', {
type: 'Some::Resource',
});

// WHEN
const asm = app.synth();

// THEN
const stagingStackArtifact = asm.getStackArtifact('StagingStack000000000000us-west-2');

Template.fromJSON(stagingStackArtifact.template).hasResourceProperties('AWS::S3::Bucket', {
LifecycleConfiguration: Match.absent(),
});
});
});

@@ -394,20 +314,7 @@ describe(AppStagingSynthesizer, () => {
// GIVEN - App with Stack with specific environment

// THEN - Expect environment agnostic stack to fail
expect(() => new Stack(app, 'NoEnvStack')).toThrowError('AppStagingSynthesizer cannot synthesize CDK Apps with BOTH environment-agnostic stacks and set environment stacks.\nPlease either specify environments for all stacks or no stacks in the CDK App.');
});

test('metadata for cdk-env is only added once on the app', () => {
// GIVEN - Additional App with specific environment
new Stack(app, 'EnvStack', {
env: {
account: '000000000000',
region: 'us-west-2',
},
});

// THEN - We only add the env metadata once
expect(app.node.metadata.filter((m) => m.type === 'cdk-env').length).toEqual(1);
expect(() => new Stack(app, 'NoEnvStack')).toThrowError(/It is not safe to use AppStagingSynthesizer/);
});
});

@@ -416,7 +323,7 @@ describe(AppStagingSynthesizer, () => {
defaultStackSynthesizer: TestAppScopedStagingSynthesizer.stackPerEnv({
appId: Lazy.string({ produce: () => 'appId' }),
}),
})).toThrowError(/AppStagingSynthesizer property 'appId' cannot contain tokens;/);
})).toThrowError(/AppStagingSynthesizer property 'appId' may not contain tokens;/);
});

/**
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
import * as fs from 'fs';
import { App, Stack, CfnResource } from 'aws-cdk-lib';
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
import { isAssetManifest } from 'aws-cdk-lib/pipelines/lib/private/cloud-assembly-internals';
import { AppStagingSynthesizer, BootstrapRole } from '../lib';
import { APP_ID, CLOUDFORMATION_EXECUTION_ROLE, DEPLOY_ACTION_ROLE, LOOKUP_ROLE } from './util';
import * as cxschema from 'aws-cdk-lib/cloud-assembly-schema';
import * as fs from 'fs';
import { AppStagingSynthesizer, BootstrapRole } from '../lib';

describe('Boostrap Roles', () => {
test('Can supply existing arns for bootstrapped roles', () => {
// GIVEN
const app = new App({
defaultStackSynthesizer: AppStagingSynthesizer.stackPerEnv({
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
bootstrapRoles: {
deploymentRoles: {
cloudFormationExecutionRole: BootstrapRole.fromRoleArn(CLOUDFORMATION_EXECUTION_ROLE),
lookupRole: BootstrapRole.fromRoleArn(LOOKUP_ROLE),
deploymentActionRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE),
deploymentRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE),
},
}),
});
@@ -43,11 +43,9 @@ describe('Boostrap Roles', () => {
test('can supply existing arns for staging roles', () => {
// GIVEN
const app = new App({
defaultStackSynthesizer: AppStagingSynthesizer.stackPerEnv({
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
stagingRoles: {
fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn'),
},
fileAssetPublishingRole: BootstrapRole.fromRoleArn('arn:aws:iam::123456789012:role/S3Access'),
}),
});
const stack = new Stack(app, 'Stack', {
@@ -69,18 +67,18 @@ describe('Boostrap Roles', () => {
expect(manifestArtifact).toBeDefined();
const manifest: cxschema.AssetManifest = JSON.parse(fs.readFileSync(manifestArtifact.file, { encoding: 'utf-8' }));
const firstFile: any = (manifest.files ? manifest.files[Object.keys(manifest.files)[0]] : undefined) ?? {};
expect(firstFile.destinations['000000000000-us-east-1'].assumeRoleArn).toEqual('arn');
expect(firstFile.destinations['000000000000-us-east-1'].assumeRoleArn).toEqual('arn:aws:iam::123456789012:role/S3Access');
});

test('bootstrap roles can be specified as current cli credentials instead', () => {
// GIVEN
const app = new App({
defaultStackSynthesizer: AppStagingSynthesizer.stackPerEnv({
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
appId: APP_ID,
bootstrapRoles: {
deploymentRoles: {
cloudFormationExecutionRole: BootstrapRole.cliCredentials(),
lookupRole: BootstrapRole.cliCredentials(),
deploymentActionRole: BootstrapRole.cliCredentials(),
deploymentRole: BootstrapRole.cliCredentials(),
},
}),
});
@@ -106,23 +104,10 @@ describe('Boostrap Roles', () => {
expect(stackArtifact.assumeRoleArn).toBeUndefined();
});

test('staging roles cannot be specified as cli credentials', () => {
const app = new App({
defaultStackSynthesizer: AppStagingSynthesizer.stackPerEnv({
appId: APP_ID,
stagingRoles: {
fileAssetPublishingRole: BootstrapRole.cliCredentials(),
},
}),
});

expect(() => new Stack(app, 'Stack')).toThrowError('fileAssetPublishingRole and dockerAssetPublishingRole cannot be specified as cliCredentials(). Please supply an arn to reference an existing IAM role.');
});

test('qualifier is resolved in the synthesizer', () => {
const app = new App({
defaultStackSynthesizer: AppStagingSynthesizer.stackPerEnv({
qualifier: 'abcdef',
defaultStackSynthesizer: AppStagingSynthesizer.defaultResources({
bootstrapQualifier: 'abcdef',
appId: APP_ID,
}),
});
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { AppStagingSynthesizer } from '../lib';
const app = new App();

const stack = new Stack(app, 'app-scoped-staging-test', {
synthesizer: AppStagingSynthesizer.stackPerEnv({
synthesizer: AppStagingSynthesizer.defaultResources({
appId: 'envAgnostic',
}),
});
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { AppStagingSynthesizer } from '../lib';
const app = new App();

const stack = new Stack(app, 'app-scoped-staging-test', {
synthesizer: AppStagingSynthesizer.stackPerEnv({
synthesizer: AppStagingSynthesizer.defaultResources({
appId: 'newId3',
}),
env: {
12 changes: 6 additions & 6 deletions packages/@aws-cdk/app-staging-synthesizer/test/util.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { StackPerEnvProps, AppStagingSynthesizer, BootstrapRole } from '../lib';
import * as cxapi from 'aws-cdk-lib/cx-api';
import { DefaultResourcesOptions, AppStagingSynthesizer, BootstrapRole } from '../lib';

export const CFN_CONTEXT = {
'AWS::Region': 'the_region',
'AWS::AccountId': 'the_account',
'AWS::URLSuffix': 'domain.aws',
};
export const APP_ID = 'appId';
export const APP_ID = 'appid';
export const CLOUDFORMATION_EXECUTION_ROLE = 'role';
export const DEPLOY_ACTION_ROLE = 'role';
export const LOOKUP_ROLE = 'role';
@@ -20,12 +20,12 @@ export function last<A>(xs?: A[]): A | undefined {
}

export class TestAppScopedStagingSynthesizer {
public static stackPerEnv(props: Partial<StackPerEnvProps> = {}): AppStagingSynthesizer {
return AppStagingSynthesizer.stackPerEnv({
public static stackPerEnv(props: Partial<DefaultResourcesOptions> = {}): AppStagingSynthesizer {
return AppStagingSynthesizer.defaultResources({
appId: props.appId ?? APP_ID,
bootstrapRoles: {
deploymentRoles: {
cloudFormationExecutionRole: BootstrapRole.fromRoleArn(CLOUDFORMATION_EXECUTION_ROLE),
deploymentActionRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE),
deploymentRole: BootstrapRole.fromRoleArn(DEPLOY_ACTION_ROLE),
lookupRole: BootstrapRole.fromRoleArn(LOOKUP_ROLE),
},
...props,
Original file line number Diff line number Diff line change
@@ -11,9 +11,22 @@ function replaceAll(s: string, search: string, replace: string) {
}

export class StringSpecializer {
constructor(private readonly stack: Stack, private readonly qualifier: string) {
/**
* Validate that the given string does not contain tokens
*/
public static validateNoTokens(s: string, what: string) {
if (Token.isUnresolved(s)) {
throw new Error(`${what} may not contain tokens; only the following literal placeholder strings are allowed: ` + [
'${Qualifier}',
cxapi.EnvironmentPlaceholders.CURRENT_REGION,
cxapi.EnvironmentPlaceholders.CURRENT_ACCOUNT,
cxapi.EnvironmentPlaceholders.CURRENT_PARTITION,
].join(', ') + `. Got: ${s}`);
}
}

constructor(private readonly stack: Stack, private readonly qualifier: string) { }

/**
* Function to replace placeholders in the input string as much as possible
*
@@ -31,6 +44,14 @@ export class StringSpecializer {
});
}

/**
* Specialize the given string, make sure it doesn't contain tokens
*/
public specializeNoTokens(s: string, what: string): string {
StringSpecializer.validateNoTokens(s, what);
return this.specialize(s);
}

/**
* Specialize only the qualifier
*/
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import * as fs from 'fs';
import * as path from 'path';
import * as cxschema from '../../../cloud-assembly-schema';
import * as cxapi from '../../../cx-api';
import { resolvedOr } from '../helpers-internal/string-specializer';
import { addStackArtifactToAssembly, contentHash } from './_shared';
import { IStackSynthesizer, ISynthesisSession } from './types';
import * as cxschema from '../../../cloud-assembly-schema';
import * as cxapi from '../../../cx-api';
import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource, FileAssetPackaging } from '../assets';
import { Fn } from '../cfn-fn';
import { CfnParameter } from '../cfn-parameter';
import { CfnRule } from '../cfn-rule';
import { resolvedOr } from '../helpers-internal/string-specializer';
import { Stack } from '../stack';

/**