diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index ef8f8536a1b32..17ef1024519e5 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -282,5 +282,7 @@ env: {"area":"@aws-cdk/yaml-cfn","keywords":["aws-yaml-cfn","yaml-cfn"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["kaizencc"]}, {"area":"@aws-cdk/aws-lightsail","keywords":["lightsail","aws-lightsail"],"labels":["@aws-cdk/aws-lightsail"],"assignees":["corymhall"]}, {"area":"@aws-cdk/aws-aps","keywords":["aps","aws-aps","prometheus"],"labels":["@aws-cdk/aws-aps"],"assignees":["corymhall"]}, - {"area":"@aws-cdk/triggers","keywords":["trigger","triggers"],"labels":["@aws-cdk/triggers"],"assignees":["otaviomacedo"]} + {"area":"@aws-cdk/triggers","keywords":["trigger","triggers"],"labels":["@aws-cdk/triggers"],"assignees":["otaviomacedo"]}, + {"area":"@aws-cdk/integ-tests","keywords":["integ-tests", "integ"],"labels":["@aws-cdk/integ-tests"],"assignees":["corymhall"]}, + {"area":"@aws-cdk/integ-runner","keywords":["integ-runner"],"labels":["@aws-cdk/integ-runner"],"assignees":["corymhall"]} ] diff --git a/.mergify.yml b/.mergify.yml index f626f6fcf428d..0869261bf3472 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -95,6 +95,7 @@ pull_request_rules: - base=main - -merged - -closed + - changes-requested-reviews-by!=aws-cdk-automation - name: if fails conventional commits actions: comment: diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 83f86fe6e2f95..62ae9b8ae0aaa 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -34,6 +34,11 @@ const templateJson = '{ "Resources": ... }'; /* The CloudFormation template as J const template = Template.fromString(templateJson); ``` +**Cyclical Resources Note** + +If allowing cyclical references is desired, for example in the case of unprocessed Transform templates, supply TemplateParsingOptions and +set skipCyclicalDependenciesCheck to true. In all other cases, will fail on detecting cyclical dependencies. + ## Full Template Match The simplest assertion would be to assert that the template matches a given diff --git a/packages/@aws-cdk/assertions/lib/template.ts b/packages/@aws-cdk/assertions/lib/template.ts index 0dffd428da27a..ccc77ad555ec7 100644 --- a/packages/@aws-cdk/assertions/lib/template.ts +++ b/packages/@aws-cdk/assertions/lib/template.ts @@ -21,34 +21,42 @@ export class Template { /** * Base your assertions on the CloudFormation template synthesized by a CDK `Stack`. * @param stack the CDK Stack to run assertions on + * @param templateParsingOptions Optional param to configure template parsing behavior, such as disregarding circular + * dependencies. */ - public static fromStack(stack: Stack): Template { - return new Template(toTemplate(stack)); + public static fromStack(stack: Stack, templateParsingOptions?: TemplateParsingOptions): Template { + return new Template(toTemplate(stack), templateParsingOptions); } /** * Base your assertions from an existing CloudFormation template formatted as an in-memory * JSON object. * @param template the CloudFormation template formatted as a nested set of records + * @param templateParsingOptions Optional param to configure template parsing behavior, such as disregarding circular + * dependencies. */ - public static fromJSON(template: { [key: string] : any }): Template { - return new Template(template); + public static fromJSON(template: { [key: string] : any }, templateParsingOptions?: TemplateParsingOptions): Template { + return new Template(template, templateParsingOptions); } /** * Base your assertions from an existing CloudFormation template formatted as a * JSON string. * @param template the CloudFormation template in + * @param templateParsingOptions Optional param to configure template parsing behavior, such as disregarding circular + * dependencies. */ - public static fromString(template: string): Template { - return new Template(JSON.parse(template)); + public static fromString(template: string, templateParsingOptions?: TemplateParsingOptions): Template { + return new Template(JSON.parse(template), templateParsingOptions); } private readonly template: TemplateType; - private constructor(template: { [key: string]: any }) { + private constructor(template: { [key: string]: any }, templateParsingOptions: TemplateParsingOptions = {}) { this.template = template as TemplateType; - checkTemplateForCyclicDependencies(this.template); + if (!templateParsingOptions?.skipCyclicalDependenciesCheck ?? true) { + checkTemplateForCyclicDependencies(this.template); + } } /** @@ -243,6 +251,20 @@ export class Template { } } +/** + * Options to configure template parsing behavior, such as disregarding circular + * dependencies. + */ +export interface TemplateParsingOptions { + /** + * If set to true, will skip checking for cyclical / circular dependencies. Should be set to false other than for + * templates that are valid despite containing cycles, such as unprocessed transform stacks. + * + * @default false + */ + readonly skipCyclicalDependenciesCheck?: boolean; +} + function toTemplate(stack: Stack): any { const root = stack.node.root; if (!Stage.isStage(root)) { @@ -255,4 +277,4 @@ function toTemplate(stack: Stack): any { return JSON.parse(fs.readFileSync(path.join(assembly.directory, stack.templateFile)).toString('utf-8')); } return assembly.getStackArtifact(stack.artifactId).template; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/assertions/test/template.test.ts b/packages/@aws-cdk/assertions/test/template.test.ts index bae0a27ce2b0d..13354ca3614eb 100644 --- a/packages/@aws-cdk/assertions/test/template.test.ts +++ b/packages/@aws-cdk/assertions/test/template.test.ts @@ -1194,6 +1194,27 @@ describe('Template', () => { }); }).toThrow(/dependency cycle/); }); + + test('does not throw when given a template with cyclic dependencies if check is skipped', () => { + expect(() => { + Template.fromJSON({ + Resources: { + Res1: { + Type: 'Foo', + Properties: { + Thing: { Ref: 'Res2' }, + }, + }, + Res2: { + Type: 'Foo', + DependsOn: ['Res1'], + }, + }, + }, { + skipCyclicalDependenciesCheck: true, + }); + }).not.toThrow(/dependency cycle/); + }); }); function expectToThrow(fn: () => void, msgs: (RegExp | string)[], done: jest.DoneCallback): void { diff --git a/packages/@aws-cdk/aws-cloudwatch/README.md b/packages/@aws-cdk/aws-cloudwatch/README.md index 1f87ad8b169a1..c62002d5343c2 100644 --- a/packages/@aws-cdk/aws-cloudwatch/README.md +++ b/packages/@aws-cdk/aws-cloudwatch/README.md @@ -288,6 +288,30 @@ new cloudwatch.CompositeAlarm(this, 'MyAwesomeCompositeAlarm', { }); ``` +#### Actions Suppressor + +If you want to disable actions of a Composite Alarm based on a certain condition, you can use [Actions Suppression](https://www.amazonaws.cn/en/new/2022/amazon-cloudwatch-supports-composite-alarm-actions-suppression/). + +```ts +declare const childAlarm1: cloudwatch.Alarm; +declare const childAlarm2: cloudwatch.Alarm; +declare const onAlarmAction: cloudwatch.IAlarmAction; +declare const onOkAction: cloudwatch.IAlarmAction; +declare const actionsSuppressor: cloudwatch.Alarm; + +const alarmRule = cloudwatch.AlarmRule.anyOf(alarm1, alarm2); + +const myCompositeAlarm = new cloudwatch.CompositeAlarm(this, 'MyAwesomeCompositeAlarm', { + alarmRule, + actionsSuppressor, +}); +myCompositeAlarm.addAlarmActions(onAlarmAction); +myComposireAlarm.addOkAction(onOkAction); +``` + +In the provided example, if `actionsSuppressor` is in `ALARM` state, `onAlarmAction` won't be triggered even if `myCompositeAlarm` goes into `ALARM` state. +Similar, if `actionsSuppressor` is in `ALARM` state and `myCompositeAlarm` goes from `ALARM` into `OK` state, `onOkAction` won't be triggered. + ### A note on units In CloudWatch, Metrics datums are emitted with units, such as `seconds` or diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts index 08f0db1b0880c..e2a412209e5c4 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts @@ -1,4 +1,4 @@ -import { ArnFormat, Lazy, Names, Stack } from '@aws-cdk/core'; +import { ArnFormat, Lazy, Names, Stack, Duration } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base'; import { CfnCompositeAlarm } from './cloudwatch.generated'; @@ -18,14 +18,14 @@ export interface CompositeAlarmProps { /** * Description for the alarm * - * @default No description + * @default - No description. */ readonly alarmDescription?: string; /** * Name of the alarm * - * @default Automatically generated name + * @default - Automatically generated name. */ readonly compositeAlarmName?: string; @@ -34,6 +34,28 @@ export interface CompositeAlarmProps { */ readonly alarmRule: IAlarmRule; + /** + * Actions will be suppressed if the suppressor alarm is in the ALARM state. + * + * @default - alarm will not be suppressed. + */ + readonly actionsSuppressor?: IAlarm; + + /** + * The maximum duration that the composite alarm waits after suppressor alarm goes out of the ALARM state. + * After this time, the composite alarm performs its actions. + * + * @default - 1 minute extension period will be set. + */ + readonly actionsSuppressorExtensionPeriod?: Duration; + + /** + * The maximum duration that the composite alarm waits for the suppressor alarm to go into the ALARM state. + * After this time, the composite alarm performs its actions. + * + * @default - 1 minute wait period will be set. + */ + readonly actionsSuppressorWaitPeriod?: Duration; } /** @@ -98,6 +120,17 @@ export class CompositeAlarm extends AlarmBase { throw new Error('Alarm Rule expression cannot be greater than 10240 characters, please reduce the conditions in the Alarm Rule'); } + let extensionPeriod = props.actionsSuppressorExtensionPeriod; + let waitPeriod = props.actionsSuppressorWaitPeriod; + if (props.actionsSuppressor === undefined) { + if (extensionPeriod !== undefined || waitPeriod !== undefined) { + throw new Error('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.'); + } + } else { + extensionPeriod = extensionPeriod ?? Duration.minutes(1); + waitPeriod = waitPeriod ?? Duration.minutes(1); + } + this.alarmRule = props.alarmRule.renderAlarmRule(); const alarm = new CfnCompositeAlarm(this, 'Resource', { @@ -108,6 +141,9 @@ export class CompositeAlarm extends AlarmBase { alarmActions: Lazy.list({ produce: () => this.alarmActionArns }), insufficientDataActions: Lazy.list({ produce: (() => this.insufficientDataActionArns) }), okActions: Lazy.list({ produce: () => this.okActionArns }), + actionsSuppressor: props.actionsSuppressor?.alarmArn, + actionsSuppressorExtensionPeriod: extensionPeriod?.toSeconds(), + actionsSuppressorWaitPeriod: waitPeriod?.toSeconds(), }); this.alarmName = this.getResourceNameAttribute(alarm.ref); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.assets.json b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.assets.json index 26cab6f7698f3..6f57ae7b8f1e6 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.assets.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.assets.json @@ -1,7 +1,7 @@ { - "version": "20.0.0", + "version": "21.0.0", "files": { - "1f1d7f1c425488b9245a0ff851dae7650c25e5558781cc88a972edb6a36be237": { + "ad8a5012407e26a8fc0b1b169b0ab2373b8466d955070ee91a90193c5c70d1a4": { "source": { "path": "CompositeAlarmIntegrationTest.template.json", "packaging": "file" @@ -9,7 +9,7 @@ "destinations": { "current_account-current_region": { "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", - "objectKey": "1f1d7f1c425488b9245a0ff851dae7650c25e5558781cc88a972edb6a36be237.json", + "objectKey": "ad8a5012407e26a8fc0b1b169b0ab2373b8466d955070ee91a90193c5c70d1a4.json", "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" } } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.template.json b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.template.json index ac9522e52679c..c03f0bcf32b15 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.template.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/CompositeAlarmIntegrationTest.template.json @@ -107,7 +107,15 @@ "\")))) OR FALSE)" ] ] - } + }, + "ActionsSuppressor": { + "Fn::GetAtt": [ + "Alarm548383B2F", + "Arn" + ] + }, + "ActionsSuppressorExtensionPeriod": 60, + "ActionsSuppressorWaitPeriod": 60 } } }, diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/cdk.out index 588d7b269d34f..8ecc185e9dbee 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/cdk.out +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/cdk.out @@ -1 +1 @@ -{"version":"20.0.0"} \ No newline at end of file +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/integ.json b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/integ.json index 923f46cb8791c..62f015a9b23aa 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/integ.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/integ.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "21.0.0", "testCases": { "integ.composite-alarm": { "stacks": [ diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/manifest.json index 0f228a394fe33..28faccf6ea43f 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/manifest.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/manifest.json @@ -1,5 +1,5 @@ { - "version": "20.0.0", + "version": "21.0.0", "artifacts": { "Tree": { "type": "cdk:tree", @@ -23,7 +23,7 @@ "validateOnSynth": false, "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", - "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/1f1d7f1c425488b9245a0ff851dae7650c25e5558781cc88a972edb6a36be237.json", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/ad8a5012407e26a8fc0b1b169b0ab2373b8466d955070ee91a90193c5c70d1a4.json", "requiresBootstrapStackVersion": 6, "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", "additionalDependencies": [ diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/tree.json b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/tree.json index a9869726361c3..55ac27b6607a0 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/tree.json +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.integ.snapshot/tree.json @@ -9,7 +9,7 @@ "path": "Tree", "constructInfo": { "fqn": "constructs.Construct", - "version": "10.1.85" + "version": "10.1.108" } }, "CompositeAlarmIntegrationTest": { @@ -220,7 +220,15 @@ "\")))) OR FALSE)" ] ] - } + }, + "actionsSuppressor": { + "Fn::GetAtt": [ + "Alarm548383B2F", + "Arn" + ] + }, + "actionsSuppressorExtensionPeriod": 60, + "actionsSuppressorWaitPeriod": 60 } }, "constructInfo": { @@ -236,14 +244,14 @@ } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" } } }, "constructInfo": { - "fqn": "constructs.Construct", - "version": "10.1.85" + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts index 0633f455eadbf..ad229dbbc5605 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/composite-alarm.test.ts @@ -1,5 +1,5 @@ import { Template } from '@aws-cdk/assertions'; -import { Stack } from '@aws-cdk/core'; +import { Duration, Stack } from '@aws-cdk/core'; import { Alarm, AlarmRule, AlarmState, CompositeAlarm, Metric } from '../lib'; describe('CompositeAlarm', () => { @@ -109,4 +109,90 @@ describe('CompositeAlarm', () => { }); + test('test action suppressor translates to a correct CFN properties', () => { + const stack = new Stack(); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + }); + + const actionsSuppressor = new Alarm(stack, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + + const alarmRule = AlarmRule.fromBoolean(true); + + new CompositeAlarm(stack, 'CompositeAlarm', { + alarmRule, + actionsSuppressor, + actionsSuppressorExtensionPeriod: Duration.minutes(2), + actionsSuppressorWaitPeriod: Duration.minutes(5), + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::CompositeAlarm', { + AlarmName: 'CompositeAlarm', + ActionsSuppressor: { + 'Fn::GetAtt': [ + 'Alarm1F9009D71', + 'Arn', + ], + }, + ActionsSuppressorExtensionPeriod: 120, + ActionsSuppressorWaitPeriod: 300, + }); + }); + + test('test wait and extension periods set without action suppressor', () => { + const stack = new Stack(); + + const alarmRule = AlarmRule.fromBoolean(true); + + var createAlarm = () => new CompositeAlarm(stack, 'CompositeAlarm', { + alarmRule, + actionsSuppressorExtensionPeriod: Duration.minutes(2), + actionsSuppressorWaitPeriod: Duration.minutes(5), + }); + + expect(createAlarm).toThrow('ActionsSuppressor Extension/Wait Periods require an ActionsSuppressor to be set.'); + }); + + test('test action suppressor has correct defaults set', () => { + const stack = new Stack(); + + const testMetric = new Metric({ + namespace: 'CDK/Test', + metricName: 'Metric', + }); + + const actionsSuppressor = new Alarm(stack, 'Alarm1', { + metric: testMetric, + threshold: 100, + evaluationPeriods: 3, + }); + + + const alarmRule = AlarmRule.fromBoolean(true); + + new CompositeAlarm(stack, 'CompositeAlarm', { + alarmRule, + actionsSuppressor, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::CompositeAlarm', { + AlarmName: 'CompositeAlarm', + ActionsSuppressor: { + 'Fn::GetAtt': [ + 'Alarm1F9009D71', + 'Arn', + ], + }, + ActionsSuppressorExtensionPeriod: 60, + ActionsSuppressorWaitPeriod: 60, + }); + }); + }); diff --git a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts index e4ed35c19c17f..a668b775ee8f5 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/integ.composite-alarm.ts @@ -57,6 +57,7 @@ class CompositeAlarmIntegrationTest extends Stack { new CompositeAlarm(this, 'CompositeAlarm', { alarmRule, + actionsSuppressor: alarm5, }); } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/README.md b/packages/@aws-cdk/aws-codepipeline-actions/README.md index 0d04e0b241187..3a3c211e46ae7 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/README.md +++ b/packages/@aws-cdk/aws-codepipeline-actions/README.md @@ -945,6 +945,28 @@ new codepipeline.Pipeline(this, 'Pipeline', { }); ``` +### Elastic Beanstalk Deployment + +To deploy an Elastic Beanstalk Application in CodePipeline: + +```ts +const sourceOutput = new codepipeline.Artifact(); +const targetBucket = new s3.Bucket(this, 'MyBucket'); + +const pipeline = new codepipeline.Pipeline(this, 'MyPipeline'); +const deployAction = new codepipeline_actions.ElasticBeanstalkDeployAction({ + actionName: 'ElasticBeanstalkDeploy', + input: sourceOutput, + environmentName: 'envName', + applicationName: 'appName', +}); + +const deployStage = pipeline.addStage({ + stageName: 'Deploy', + actions: [deployAction], +}); +``` + ### Alexa Skill You can deploy to Alexa using CodePipeline with the following Action: diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts new file mode 100644 index 0000000000000..a356078c74a69 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/elastic-beanstalk/deploy-action.ts @@ -0,0 +1,66 @@ +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { Construct } from 'constructs'; +import { Action } from '../action'; +import { deployArtifactBounds } from '../common'; + +/** + * Construction properties of the {@link ElasticBeanstalkDeployAction Elastic Beanstalk deploy CodePipeline Action}. + */ +export interface ElasticBeanstalkDeployActionProps extends codepipeline.CommonAwsActionProps { + /** + * The source to use as input for deployment. + */ + readonly input: codepipeline.Artifact; + + /** + * The name of the AWS Elastic Beanstalk application to deploy. + */ + readonly applicationName: string; + + /** + * The name of the AWS Elastic Beanstalk environment to deploy to. + */ + readonly environmentName: string; +} + +/** + * CodePipeline action to deploy an AWS ElasticBeanstalk Application. + */ +export class ElasticBeanstalkDeployAction extends Action { + private readonly applicationName: string; + private readonly environmentName: string; + + constructor(props: ElasticBeanstalkDeployActionProps) { + super({ + ...props, + category: codepipeline.ActionCategory.DEPLOY, + provider: 'ElasticBeanstalk', + artifactBounds: deployArtifactBounds(), + inputs: [props.input], + }); + + this.applicationName = props.applicationName; + this.environmentName = props.environmentName; + } + + protected bound( + _scope: Construct, + _stage: codepipeline.IStage, + options: codepipeline.ActionBindOptions, + ): codepipeline.ActionConfig { + + // Per https://docs.aws.amazon.com/elasticbeanstalk/latest/dg/AWSHowTo.iam.managed-policies.html + // it doesn't seem we can scope this down further for the codepipeline action. + options.role.addManagedPolicy({ managedPolicyArn: 'arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk' }); + + // the Action's Role needs to read from the Bucket to get artifacts + options.bucket.grantRead(options.role); + + return { + configuration: { + ApplicationName: this.applicationName, + EnvironmentName: this.environmentName, + }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts index a0d665eddd886..e82e34232e931 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/index.ts @@ -8,6 +8,7 @@ export * from './codedeploy/ecs-deploy-action'; export * from './codedeploy/server-deploy-action'; export * from './ecr/source-action'; export * from './ecs/deploy-action'; +export * from './elastic-beanstalk/deploy-action'; export * from './github/source-action'; export * from './jenkins/jenkins-action'; export * from './jenkins/jenkins-provider'; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index c8e37b02fd3ec..ab49ad77e0451 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -80,6 +80,7 @@ "@aws-cdk/aws-cloudtrail": "0.0.0", "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-s3-deployment": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/integ-tests": "0.0.0", @@ -99,6 +100,7 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", + "@aws-cdk/aws-elasticbeanstalk": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", @@ -122,6 +124,7 @@ "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", + "@aws-cdk/aws-elasticbeanstalk": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-events-targets": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/assets/nodejs.zip b/packages/@aws-cdk/aws-codepipeline-actions/test/assets/nodejs.zip new file mode 100644 index 0000000000000..4810a50d131d9 Binary files /dev/null and b/packages/@aws-cdk/aws-codepipeline-actions/test/assets/nodejs.zip differ diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/elastic-beanstalk/elastic-beanstalk-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/elastic-beanstalk/elastic-beanstalk-deploy-action.test.ts new file mode 100644 index 0000000000000..16855d3342f11 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/elastic-beanstalk/elastic-beanstalk-deploy-action.test.ts @@ -0,0 +1,126 @@ +import { Template } from '@aws-cdk/assertions'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import { Bucket } from '@aws-cdk/aws-s3'; +import { App, Stack } from '@aws-cdk/core'; +import { ElasticBeanstalkDeployAction, S3SourceAction, S3Trigger } from '../../lib'; + +describe('elastic beanstalk deploy action tests', () => { + test('region and account are action region and account when set', () => { + const stack = buildPipelineStack(); + Template.fromStack(stack).hasResourceProperties('AWS::CodePipeline::Pipeline', { + Stages: [ + { + Actions: [ + { + ActionTypeId: { + Category: 'Source', + Owner: 'AWS', + Provider: 'S3', + Version: '1', + }, + Configuration: { + S3Bucket: { + Ref: 'MyBucketF68F3FF0', + }, + S3ObjectKey: 'some/path/to', + PollForSourceChanges: true, + }, + Name: 'Source', + OutputArtifacts: [ + { + Name: 'Artifact_Source_Source', + }, + ], + RoleArn: { + 'Fn::GetAtt': [ + 'MyPipelineSourceCodePipelineActionRoleAA05D76F', + 'Arn', + ], + }, + RunOrder: 1, + }, + ], + Name: 'Source', + }, + { + Actions: [ + { + ActionTypeId: { + Category: 'Deploy', + Owner: 'AWS', + Provider: 'ElasticBeanstalk', + Version: '1', + }, + Configuration: { + ApplicationName: 'testApplication', + EnvironmentName: 'env-testApplication', + }, + InputArtifacts: [ + { + Name: 'Artifact_Source_Source', + }, + ], + Name: 'Deploy', + RoleArn: { + 'Fn::GetAtt': [ + 'MyPipelineDeployCodePipelineActionRole742BD48A', + 'Arn', + ], + }, + RunOrder: 1, + }, + ], + Name: 'Deploy', + }, + ], + ArtifactStore: { + EncryptionKey: { + Id: { + 'Fn::GetAtt': [ + 'MyPipelineArtifactsBucketEncryptionKey8BF0A7F3', + 'Arn', + ], + }, + Type: 'KMS', + }, + Location: { + Ref: 'MyPipelineArtifactsBucket727923DD', + }, + Type: 'S3', + }, + }); + }); +}); + +function buildPipelineStack(): Stack { + const app = new App(); + const stack = new Stack(app); + const sourceOutput = new codepipeline.Artifact(); + const pipeline = new codepipeline.Pipeline(stack, 'MyPipeline'); + pipeline.addStage({ + stageName: 'Source', + actions: [ + new S3SourceAction({ + actionName: 'Source', + bucket: new Bucket(stack, 'MyBucket'), + bucketKey: 'some/path/to', + output: sourceOutput, + trigger: S3Trigger.POLL, + }), + ], + }); + + pipeline.addStage({ + stageName: 'Deploy', + actions: [ + new ElasticBeanstalkDeployAction({ + actionName: 'Deploy', + applicationName: 'testApplication', + environmentName: 'env-testApplication', + input: sourceOutput, + }), + ], + }); + + return stack; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeilne-elastic-beanstalk-deploy.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeilne-elastic-beanstalk-deploy.ts new file mode 100644 index 0000000000000..631c3d6f84c26 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeilne-elastic-beanstalk-deploy.ts @@ -0,0 +1,147 @@ +import * as path from 'path'; +import * as codepipeline from '@aws-cdk/aws-codepipeline'; +import * as elasticbeanstalk from '@aws-cdk/aws-elasticbeanstalk'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as deploy from '@aws-cdk/aws-s3-deployment'; +import { App, Fn, RemovalPolicy, Stack } from '@aws-cdk/core'; +import * as integ from '@aws-cdk/integ-tests'; +import * as cpactions from '../lib'; + +/** + * To validate that the deployment actually succeeds, perform the following actions: + * + * 1. Delete the snapshot + * 2. Run `yarn integ --update-on-failed --no-clean` + * 3. Navigate to CodePipeline in the console and click 'Release change' + * - Before releasing the change, the pipeline will show a failure because it + * attempts to run on creation but the elastic beanstalk environment is not yet ready + * 4. Navigate to Elastic Beanstalk and click on the URL for the application just deployed + * - You should see 'Congratulations' message + * 5. Manually delete the 'aws-cdk-codepipeline-elastic-beanstalk-deploy' stack + */ + +const app = new App(); + +const stack = new Stack(app, 'aws-cdk-codepipeline-elastic-beanstalk-deploy'); + +const bucket = new s3.Bucket(stack, 'PipelineBucket', { + versioned: true, + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, +}); + +const artifact = new deploy.BucketDeployment(stack, 'DeployApp', { + sources: [deploy.Source.asset(path.join(__dirname, 'assets/nodejs.zip'))], + destinationBucket: bucket, + extract: false, +}); + +const serviceRole = new iam.Role(stack, 'service-role', { + roleName: 'codepipeline-elasticbeanstalk-action-test-serivce-role', + assumedBy: new iam.ServicePrincipal('elasticbeanstalk.amazonaws.com'), + managedPolicies: [ + { + managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth', + }, + { + managedPolicyArn: 'arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy', + }, + ], +}); + +const instanceProfileRole = new iam.Role(stack, 'instance-profile-role', { + roleName: 'codepipeline-elasticbeanstalk-action-test-instance-profile-role', + assumedBy: new iam.ServicePrincipal('ec2.amazonaws.com'), + managedPolicies: [ + { + managedPolicyArn: 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier', + }, + { + managedPolicyArn: 'arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker', + }, + { + managedPolicyArn: 'arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier', + }, + ], +}); + +const instanceProfile = new iam.CfnInstanceProfile(stack, 'instance-profile', { + roles: [instanceProfileRole.roleName], + instanceProfileName: instanceProfileRole.roleName, +}); + +const beanstalkApp = new elasticbeanstalk.CfnApplication(stack, 'beastalk-app', { + applicationName: 'codepipeline-test-app', +}); + +const beanstalkEnv = new elasticbeanstalk.CfnEnvironment(stack, 'beanstlk-env', { + applicationName: beanstalkApp.applicationName!, + environmentName: 'codepipeline-test-env', + solutionStackName: '64bit Amazon Linux 2 v5.5.6 running Node.js 16', + optionSettings: [ + { + namespace: 'aws:autoscaling:launchconfiguration', + optionName: 'IamInstanceProfile', + value: instanceProfile.instanceProfileName, + }, + { + namespace: 'aws:elasticbeanstalk:environment', + optionName: 'ServiceRole', + value: serviceRole.roleName, + }, + { + namespace: 'aws:elasticbeanstalk:environment', + optionName: 'LoadBalancerType', + value: 'application', + }, + { + namespace: 'aws:elasticbeanstalk:managedactions', + optionName: 'ServiceRoleForManagedUpdates', + value: 'AWSServiceRoleForElasticBeanstalkManagedUpdates', + }, + ], +}); + +beanstalkEnv.addDependsOn(instanceProfile); +beanstalkEnv.addDependsOn(beanstalkApp); + +const pipeline = new codepipeline.Pipeline(stack, 'Pipeline', { + artifactBucket: bucket, +}); + +const sourceOutput = new codepipeline.Artifact('SourceArtifact'); +const sourceAction = new cpactions.S3SourceAction({ + actionName: 'Source', + output: sourceOutput, + bucket, + bucketKey: Fn.select(0, artifact.objectKeys), +}); + +pipeline.addStage({ + stageName: 'Source', + actions: [ + sourceAction, + ], +}); + +const deployAction = new cpactions.ElasticBeanstalkDeployAction({ + actionName: 'Deploy', + input: sourceOutput, + environmentName: beanstalkEnv.environmentName!, + applicationName: beanstalkApp.applicationName!, +}); + +pipeline.addStage({ + stageName: 'Deploy', + actions: [ + deployAction, + ], +}); + +new integ.IntegTest(app, 'codepipeline-elastic-beanstalk-deploy', { + testCases: [stack], + stackUpdateWorkflow: false, +}); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/aws-cdk-codepipeline-elastic-beanstalk-deploy.assets.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/aws-cdk-codepipeline-elastic-beanstalk-deploy.assets.json new file mode 100644 index 0000000000000..9de2d1dbcc771 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/aws-cdk-codepipeline-elastic-beanstalk-deploy.assets.json @@ -0,0 +1,71 @@ +{ + "version": "21.0.0", + "files": { + "60767da3831353fede3cfe92efef10580a600592dec8ccbb06c051e95b9c1b26": { + "source": { + "path": "asset.60767da3831353fede3cfe92efef10580a600592dec8ccbb06c051e95b9c1b26", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "60767da3831353fede3cfe92efef10580a600592dec8ccbb06c051e95b9c1b26.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "c409e6c5845f1f349df8cd84e160bf6f1c35d2b060b63e1f032f9bd39d4542cc": { + "source": { + "path": "asset.c409e6c5845f1f349df8cd84e160bf6f1c35d2b060b63e1f032f9bd39d4542cc.zip", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "c409e6c5845f1f349df8cd84e160bf6f1c35d2b060b63e1f032f9bd39d4542cc.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "6ddcf10002539818a9256eff3fb2b22aa09298d8f946e26ba121c175a600c44e": { + "source": { + "path": "asset.6ddcf10002539818a9256eff3fb2b22aa09298d8f946e26ba121c175a600c44e", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6ddcf10002539818a9256eff3fb2b22aa09298d8f946e26ba121c175a600c44e.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "6a9a0507c3e2bfac9749f257bdd3d9f313c01ee3984ad7bf373597c341863d6f": { + "source": { + "path": "asset.6a9a0507c3e2bfac9749f257bdd3d9f313c01ee3984ad7bf373597c341863d6f.zip", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6a9a0507c3e2bfac9749f257bdd3d9f313c01ee3984ad7bf373597c341863d6f.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "41fa0558ddcbfbd77d585fbccca6c3d2435c64c4949b651ff2c2e22ca3c5690e": { + "source": { + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "41fa0558ddcbfbd77d585fbccca6c3d2435c64c4949b651ff2c2e22ca3c5690e.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/aws-cdk-codepipeline-elastic-beanstalk-deploy.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/aws-cdk-codepipeline-elastic-beanstalk-deploy.template.json new file mode 100644 index 0000000000000..d8c77c41abaff --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/aws-cdk-codepipeline-elastic-beanstalk-deploy.template.json @@ -0,0 +1,877 @@ +{ + "Resources": { + "PipelineBucketB967BD35": { + "Type": "AWS::S3::Bucket", + "Properties": { + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + }, + { + "Key": "aws-cdk:cr-owned:46838d62", + "Value": "true" + } + ], + "VersioningConfiguration": { + "Status": "Enabled" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "PipelineBucketPolicyD65CDEF5": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineBucketAutoDeleteObjectsCustomResource5F37E165": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "PipelineBucketB967BD35" + } + }, + "DependsOn": [ + "PipelineBucketPolicyD65CDEF5" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "60767da3831353fede3cfe92efef10580a600592dec8ccbb06c051e95b9c1b26.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": "nodejs14.x", + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "PipelineBucketB967BD35" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "DeployAppAwsCliLayerAEF99B2F": { + "Type": "AWS::Lambda::LayerVersion", + "Properties": { + "Content": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "c409e6c5845f1f349df8cd84e160bf6f1c35d2b060b63e1f032f9bd39d4542cc.zip" + }, + "Description": "/opt/awscli/aws" + } + }, + "DeployAppCustomResourceDEE6DDD6": { + "Type": "Custom::CDKBucketDeployment", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536", + "Arn" + ] + }, + "SourceBucketNames": [ + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ], + "SourceObjectKeys": [ + "6a9a0507c3e2bfac9749f257bdd3d9f313c01ee3984ad7bf373597c341863d6f.zip" + ], + "DestinationBucketName": { + "Ref": "PipelineBucketB967BD35" + }, + "Extract": false, + "Prune": true + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "Roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "6ddcf10002539818a9256eff3fb2b22aa09298d8f946e26ba121c175a600c44e.zip" + }, + "Role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "Handler": "index.handler", + "Layers": [ + { + "Ref": "DeployAppAwsCliLayerAEF99B2F" + } + ], + "Runtime": "python3.9", + "Timeout": 900 + }, + "DependsOn": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + ] + }, + "servicerole2C3CD20E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "elasticbeanstalk.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth", + "arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy" + ], + "RoleName": "codepipeline-elasticbeanstalk-action-test-serivce-role" + } + }, + "instanceprofilerole786BCBC7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier", + "arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker", + "arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier" + ], + "RoleName": "codepipeline-elasticbeanstalk-action-test-instance-profile-role" + } + }, + "instanceprofile": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "instanceprofilerole786BCBC7" + } + ], + "InstanceProfileName": { + "Ref": "instanceprofilerole786BCBC7" + } + } + }, + "beastalkapp": { + "Type": "AWS::ElasticBeanstalk::Application", + "Properties": { + "ApplicationName": "codepipeline-test-app" + } + }, + "beanstlkenv": { + "Type": "AWS::ElasticBeanstalk::Environment", + "Properties": { + "ApplicationName": "codepipeline-test-app", + "EnvironmentName": "codepipeline-test-env", + "OptionSettings": [ + { + "Namespace": "aws:autoscaling:launchconfiguration", + "OptionName": "IamInstanceProfile", + "Value": { + "Ref": "instanceprofilerole786BCBC7" + } + }, + { + "Namespace": "aws:elasticbeanstalk:environment", + "OptionName": "ServiceRole", + "Value": { + "Ref": "servicerole2C3CD20E" + } + }, + { + "Namespace": "aws:elasticbeanstalk:environment", + "OptionName": "LoadBalancerType", + "Value": "application" + }, + { + "Namespace": "aws:elasticbeanstalk:managedactions", + "OptionName": "ServiceRoleForManagedUpdates", + "Value": "AWSServiceRoleForElasticBeanstalkManagedUpdates" + } + ], + "SolutionStackName": "64bit Amazon Linux 2 v5.5.6 running Node.js 16" + }, + "DependsOn": [ + "beastalkapp", + "instanceprofile" + ] + }, + "PipelineRoleD68726F7": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineRoleDefaultPolicyC7A05455": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineDeployCodePipelineActionRole8B83082E", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineRoleDefaultPolicyC7A05455", + "Roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "PipelineC660917D": { + "Type": "AWS::CodePipeline::Pipeline", + "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "Stages": [ + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Source", + "Owner": "AWS", + "Provider": "S3", + "Version": "1" + }, + "Configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": { + "Fn::Select": [ + 0, + { + "Fn::GetAtt": [ + "DeployAppCustomResourceDEE6DDD6", + "SourceObjectKeys" + ] + } + ] + } + }, + "Name": "Source", + "OutputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "RoleArn": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Source" + }, + { + "Actions": [ + { + "ActionTypeId": { + "Category": "Deploy", + "Owner": "AWS", + "Provider": "ElasticBeanstalk", + "Version": "1" + }, + "Configuration": { + "ApplicationName": "codepipeline-test-app", + "EnvironmentName": "codepipeline-test-env" + }, + "InputArtifacts": [ + { + "Name": "SourceArtifact" + } + ], + "Name": "Deploy", + "RoleArn": { + "Fn::GetAtt": [ + "PipelineDeployCodePipelineActionRole8B83082E", + "Arn" + ] + }, + "RunOrder": 1 + } + ], + "Name": "Deploy" + } + ], + "ArtifactStore": { + "Location": { + "Ref": "PipelineBucketB967BD35" + }, + "Type": "S3" + } + }, + "DependsOn": [ + "PipelineRoleDefaultPolicyC7A05455", + "PipelineRoleD68726F7" + ] + }, + "PipelineSourceCodePipelineActionRoleC6F9E7F5": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::GetAtt": [ + "DeployAppCustomResourceDEE6DDD6", + "SourceObjectKeys" + ] + } + ] + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925", + "Roles": [ + { + "Ref": "PipelineSourceCodePipelineActionRoleC6F9E7F5" + } + ] + } + }, + "PipelineDeployCodePipelineActionRole8B83082E": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + "arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk" + ] + } + }, + "PipelineDeployCodePipelineActionRoleDefaultPolicyEE6D615B": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PipelineDeployCodePipelineActionRoleDefaultPolicyEE6D615B", + "Roles": [ + { + "Ref": "PipelineDeployCodePipelineActionRole8B83082E" + } + ] + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets.json new file mode 100644 index 0000000000000..9ace40765b682 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets.json @@ -0,0 +1,19 @@ +{ + "version": "21.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.template.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/integ.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/integ.json new file mode 100644 index 0000000000000..f6b5be49154e4 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/integ.json @@ -0,0 +1,13 @@ +{ + "version": "21.0.0", + "testCases": { + "codepipeline-elastic-beanstalk-deploy/DefaultTest": { + "stacks": [ + "aws-cdk-codepipeline-elastic-beanstalk-deploy" + ], + "stackUpdateWorkflow": false, + "assertionStack": "codepipeline-elastic-beanstalk-deploy/DefaultTest/DeployAssert", + "assertionStackName": "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..7f3de22166c07 --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/manifest.json @@ -0,0 +1,237 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-cdk-codepipeline-elastic-beanstalk-deploy.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-codepipeline-elastic-beanstalk-deploy.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-codepipeline-elastic-beanstalk-deploy": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-codepipeline-elastic-beanstalk-deploy.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/41fa0558ddcbfbd77d585fbccca6c3d2435c64c4949b651ff2c2e22ca3c5690e.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-codepipeline-elastic-beanstalk-deploy.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-codepipeline-elastic-beanstalk-deploy.assets" + ], + "metadata": { + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineBucketB967BD35" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineBucketPolicyD65CDEF5" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/AutoDeleteObjectsCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineBucketAutoDeleteObjectsCustomResource5F37E165" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/AwsCliLayer/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "DeployAppAwsCliLayerAEF99B2F" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/CustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "DeployAppCustomResourceDEE6DDD6" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C81C01536" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/service-role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "servicerole2C3CD20E" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/instance-profile-role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "instanceprofilerole786BCBC7" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/instance-profile": [ + { + "type": "aws:cdk:logicalId", + "data": "instanceprofile" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/beastalk-app": [ + { + "type": "aws:cdk:logicalId", + "data": "beastalkapp" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/beanstlk-env": [ + { + "type": "aws:cdk:logicalId", + "data": "beanstlkenv" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Role/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineRoleD68726F7" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Role/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineRoleDefaultPolicyC7A05455" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineC660917D" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source/CodePipelineActionRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineSourceCodePipelineActionRoleC6F9E7F5" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source/CodePipelineActionRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy/CodePipelineActionRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineDeployCodePipelineActionRole8B83082E" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy/CodePipelineActionRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "PipelineDeployCodePipelineActionRoleDefaultPolicyEE6D615B" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-codepipeline-elastic-beanstalk-deploy/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-codepipeline-elastic-beanstalk-deploy" + }, + "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "codepipelineelasticbeanstalkdeployDefaultTestDeployAssert785E452B.assets" + ], + "metadata": { + "/codepipeline-elastic-beanstalk-deploy/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/codepipeline-elastic-beanstalk-deploy/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "codepipeline-elastic-beanstalk-deploy/DefaultTest/DeployAssert" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/tree.json b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/tree.json new file mode 100644 index 0000000000000..634f9bcc6feab --- /dev/null +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeilne-elastic-beanstalk-deploy.integ.snapshot/tree.json @@ -0,0 +1,1289 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + }, + "aws-cdk-codepipeline-elastic-beanstalk-deploy": { + "id": "aws-cdk-codepipeline-elastic-beanstalk-deploy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy", + "children": { + "PipelineBucket": { + "id": "PipelineBucket", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "aws-cdk:auto-delete-objects", + "value": "true" + }, + { + "key": "aws-cdk:cr-owned:46838d62", + "value": "true" + } + ], + "versioningConfiguration": { + "status": "Enabled" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketPolicy", + "version": "0.0.0" + } + }, + "AutoDeleteObjectsCustomResource": { + "id": "AutoDeleteObjectsCustomResource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/AutoDeleteObjectsCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/PipelineBucket/AutoDeleteObjectsCustomResource/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.Bucket", + "version": "0.0.0" + } + }, + "Custom::S3AutoDeleteObjectsCustomResourceProvider": { + "id": "Custom::S3AutoDeleteObjectsCustomResourceProvider", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::S3AutoDeleteObjectsCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::S3AutoDeleteObjectsCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResourceProvider", + "version": "0.0.0" + } + }, + "DeployApp": { + "id": "DeployApp", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp", + "children": { + "AwsCliLayer": { + "id": "AwsCliLayer", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/AwsCliLayer", + "children": { + "Code": { + "id": "Code", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/AwsCliLayer/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/AwsCliLayer/Code/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/AwsCliLayer/Code/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/AwsCliLayer/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::LayerVersion", + "aws:cdk:cloudformation:props": { + "content": { + "s3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "s3Key": "c409e6c5845f1f349df8cd84e160bf6f1c35d2b060b63e1f032f9bd39d4542cc.zip" + }, + "description": "/opt/awscli/aws" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnLayerVersion", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/lambda-layer-awscli.AwsCliLayer", + "version": "0.0.0" + } + }, + "CustomResourceHandler": { + "id": "CustomResourceHandler", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/CustomResourceHandler", + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.SingletonFunction", + "version": "0.0.0" + } + }, + "Asset1": { + "id": "Asset1", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/Asset1", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/Asset1/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/Asset1/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "CustomResource": { + "id": "CustomResource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/CustomResource", + "children": { + "Default": { + "id": "Default", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/DeployApp/CustomResource/Default", + "constructInfo": { + "fqn": "@aws-cdk/core.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-deployment.BucketDeployment", + "version": "0.0.0" + } + }, + "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C": { + "id": "Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + ] + ] + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRoleDefaultPolicy88902FDF", + "roles": [ + { + "Ref": "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Code": { + "id": "Code", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code", + "children": { + "Stage": { + "id": "Stage", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Code/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Custom::CDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756C/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Lambda::Function", + "aws:cdk:cloudformation:props": { + "code": { + "s3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "s3Key": "6ddcf10002539818a9256eff3fb2b22aa09298d8f946e26ba121c175a600c44e.zip" + }, + "role": { + "Fn::GetAtt": [ + "CustomCDKBucketDeployment8693BB64968944B69AAFB0CC9EB8756CServiceRole89A01265", + "Arn" + ] + }, + "handler": "index.handler", + "layers": [ + { + "Ref": "DeployAppAwsCliLayerAEF99B2F" + } + ], + "runtime": "python3.9", + "timeout": 900 + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.CfnFunction", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-lambda.Function", + "version": "0.0.0" + } + }, + "service-role": { + "id": "service-role", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/service-role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/service-role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "elasticbeanstalk.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + "arn:aws:iam::aws:policy/service-role/AWSElasticBeanstalkEnhancedHealth", + "arn:aws:iam::aws:policy/AWSElasticBeanstalkManagedUpdatesCustomerRolePolicy" + ], + "roleName": "codepipeline-elasticbeanstalk-action-test-serivce-role" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "instance-profile-role": { + "id": "instance-profile-role", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/instance-profile-role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/instance-profile-role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + "arn:aws:iam::aws:policy/AWSElasticBeanstalkWebTier", + "arn:aws:iam::aws:policy/AWSElasticBeanstalkMulticontainerDocker", + "arn:aws:iam::aws:policy/AWSElasticBeanstalkWorkerTier" + ], + "roleName": "codepipeline-elasticbeanstalk-action-test-instance-profile-role" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "instance-profile": { + "id": "instance-profile", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/instance-profile", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::InstanceProfile", + "aws:cdk:cloudformation:props": { + "roles": [ + { + "Ref": "instanceprofilerole786BCBC7" + } + ], + "instanceProfileName": { + "Ref": "instanceprofilerole786BCBC7" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnInstanceProfile", + "version": "0.0.0" + } + }, + "beastalk-app": { + "id": "beastalk-app", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/beastalk-app", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ElasticBeanstalk::Application", + "aws:cdk:cloudformation:props": { + "applicationName": "codepipeline-test-app" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticbeanstalk.CfnApplication", + "version": "0.0.0" + } + }, + "beanstlk-env": { + "id": "beanstlk-env", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/beanstlk-env", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::ElasticBeanstalk::Environment", + "aws:cdk:cloudformation:props": { + "applicationName": "codepipeline-test-app", + "environmentName": "codepipeline-test-env", + "optionSettings": [ + { + "namespace": "aws:autoscaling:launchconfiguration", + "optionName": "IamInstanceProfile", + "value": { + "Ref": "instanceprofilerole786BCBC7" + } + }, + { + "namespace": "aws:elasticbeanstalk:environment", + "optionName": "ServiceRole", + "value": { + "Ref": "servicerole2C3CD20E" + } + }, + { + "namespace": "aws:elasticbeanstalk:environment", + "optionName": "LoadBalancerType", + "value": "application" + }, + { + "namespace": "aws:elasticbeanstalk:managedactions", + "optionName": "ServiceRoleForManagedUpdates", + "value": "AWSServiceRoleForElasticBeanstalkManagedUpdates" + } + ], + "solutionStackName": "64bit Amazon Linux 2 v5.5.6 running Node.js 16" + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-elasticbeanstalk.CfnEnvironment", + "version": "0.0.0" + } + }, + "Pipeline": { + "id": "Pipeline", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline", + "children": { + "Role": { + "id": "Role", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Role", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "codepipeline.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + }, + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineDeployCodePipelineActionRole8B83082E", + "Arn" + ] + }, + { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "PipelineRoleDefaultPolicyC7A05455", + "roles": [ + { + "Ref": "PipelineRoleD68726F7" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::CodePipeline::Pipeline", + "aws:cdk:cloudformation:props": { + "roleArn": { + "Fn::GetAtt": [ + "PipelineRoleD68726F7", + "Arn" + ] + }, + "stages": [ + { + "name": "Source", + "actions": [ + { + "name": "Source", + "outputArtifacts": [ + { + "name": "SourceArtifact" + } + ], + "actionTypeId": { + "category": "Source", + "version": "1", + "owner": "AWS", + "provider": "S3" + }, + "configuration": { + "S3Bucket": { + "Ref": "PipelineBucketB967BD35" + }, + "S3ObjectKey": { + "Fn::Select": [ + 0, + { + "Fn::GetAtt": [ + "DeployAppCustomResourceDEE6DDD6", + "SourceObjectKeys" + ] + } + ] + } + }, + "runOrder": 1, + "roleArn": { + "Fn::GetAtt": [ + "PipelineSourceCodePipelineActionRoleC6F9E7F5", + "Arn" + ] + } + } + ] + }, + { + "name": "Deploy", + "actions": [ + { + "name": "Deploy", + "inputArtifacts": [ + { + "name": "SourceArtifact" + } + ], + "actionTypeId": { + "category": "Deploy", + "version": "1", + "owner": "AWS", + "provider": "ElasticBeanstalk" + }, + "configuration": { + "ApplicationName": "codepipeline-test-app", + "EnvironmentName": "codepipeline-test-env" + }, + "runOrder": 1, + "roleArn": { + "Fn::GetAtt": [ + "PipelineDeployCodePipelineActionRole8B83082E", + "Arn" + ] + } + } + ] + } + ], + "artifactStore": { + "type": "S3", + "location": { + "Ref": "PipelineBucketB967BD35" + } + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-codepipeline.CfnPipeline", + "version": "0.0.0" + } + }, + "Source": { + "id": "Source", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source", + "children": { + "Source": { + "id": "Source", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source", + "children": { + "CodePipelineActionRole": { + "id": "CodePipelineActionRole", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source/CodePipelineActionRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source/CodePipelineActionRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source/CodePipelineActionRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Source/Source/CodePipelineActionRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::GetAtt": [ + "DeployAppCustomResourceDEE6DDD6", + "SourceObjectKeys" + ] + } + ] + } + ] + ] + } + ] + }, + { + "Action": [ + "s3:Abort*", + "s3:DeleteObject*", + "s3:PutObject", + "s3:PutObjectLegalHold", + "s3:PutObjectRetention", + "s3:PutObjectTagging", + "s3:PutObjectVersionTagging" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "PipelineSourceCodePipelineActionRoleDefaultPolicy2D565925", + "roles": [ + { + "Ref": "PipelineSourceCodePipelineActionRoleC6F9E7F5" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + }, + "Deploy": { + "id": "Deploy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy", + "children": { + "Deploy": { + "id": "Deploy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy", + "children": { + "CodePipelineActionRole": { + "id": "CodePipelineActionRole", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy/CodePipelineActionRole", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy/CodePipelineActionRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "managedPolicyArns": [ + "arn:aws:iam::aws:policy/AdministratorAccess-AWSElasticBeanstalk" + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy/CodePipelineActionRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-codepipeline-elastic-beanstalk-deploy/Pipeline/Deploy/Deploy/CodePipelineActionRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "PipelineBucketB967BD35", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "PipelineDeployCodePipelineActionRoleDefaultPolicyEE6D615B", + "roles": [ + { + "Ref": "PipelineDeployCodePipelineActionRole8B83082E" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + } + }, + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-codepipeline.Pipeline", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + }, + "codepipeline-elastic-beanstalk-deploy": { + "id": "codepipeline-elastic-beanstalk-deploy", + "path": "codepipeline-elastic-beanstalk-deploy", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "codepipeline-elastic-beanstalk-deploy/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "codepipeline-elastic-beanstalk-deploy/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.108" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "codepipeline-elastic-beanstalk-deploy/DefaultTest/DeployAssert", + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests.IntegTest", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/.gitignore b/packages/@aws-cdk/aws-gamelift/.gitignore index 6d05bba61dfa7..da70d0d9db790 100644 --- a/packages/@aws-cdk/aws-gamelift/.gitignore +++ b/packages/@aws-cdk/aws-gamelift/.gitignore @@ -21,3 +21,7 @@ junit.xml !**/*.integ.snapshot/**/asset.*/*.d.ts !**/*.integ.snapshot/**/asset.*/** + +#include game build js file +!test/my-game-build/*.js +!test/my-game-script/*.js diff --git a/packages/@aws-cdk/aws-gamelift/README.md b/packages/@aws-cdk/aws-gamelift/README.md index 5cc87f2e9eafc..48a572920d223 100644 --- a/packages/@aws-cdk/aws-gamelift/README.md +++ b/packages/@aws-cdk/aws-gamelift/README.md @@ -9,31 +9,103 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +[Amazon GameLift](https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-intro.html) is a service used +to deploy, operate, and scale dedicated, low-cost servers in the cloud for session-based multiplayer games. Built +on AWS global computing infrastructure, GameLift helps deliver high-performance, high-reliability game servers +while dynamically scaling your resource usage to meet worldwide player demand. -```ts nofixture -import * as gamelift from '@aws-cdk/aws-gamelift'; -``` +GameLift is composed of three main components: + +* GameLift FlexMatch which is a customizable matchmaking service for +multiplayer games. With FlexMatch, you can +build a custom set of rules that defines what a multiplayer match looks like +for your game, and determines how to +evaluate and select compatible players for each match. You can also customize +key aspects of the matchmaking +process to fit your game, including fine-tuning the matching algorithm. + +* GameLift hosting for custom or realtime servers which helps you deploy, +operate, and scale dedicated game servers. It regulates the resources needed to +host games, finds available game servers to host new game sessions, and puts +players into games. + +* GameLift FleetIQ to optimize the use of low-cost Amazon Elastic Compute Cloud +(Amazon EC2) Spot Instances for cloud-based game hosting. With GameLift +FleetIQ, you can work directly with your hosting resources in Amazon EC2 and +Amazon EC2 Auto Scaling while taking advantage of GameLift optimizations to +deliver inexpensive, resilient game hosting for your players + +This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. It allows you to define components for your matchmaking +configuration or game server fleet management system. + +## GameLift Hosting + +### Defining a GameLift Fleet + +GameLift helps you deploy, operate, and scale dedicated game servers for +session-based multiplayer games. It helps you regulate the resources needed to +host your games, finds available game servers to host new game sessions, and +puts players into games. - +### Uploading builds and scripts to GameLift -There are no official hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. Here are some suggestions on how to proceed: +Before deploying your GameLift-enabled multiplayer game servers for hosting with the GameLift service, you need to upload +your game server files. This section provides guidance on preparing and uploading custom game server build +files or Realtime Servers server script files. When you upload files, you create a GameLift build or script resource, which +you then deploy on fleets of hosting resources. -- Search [Construct Hub for GameLift construct libraries](https://constructs.dev/search?q=gamelift) -- Use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, in the same way you would use [the CloudFormation AWS::GameLift resources](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_GameLift.html) directly. +To troubleshoot fleet activation problems related to the server script, see [Debug GameLift fleet issues](https://docs.aws.amazon.com/gamelift/latest/developerguide/fleets-creating-debug.html). +#### Upload a custom server build to GameLift - +Before uploading your configured game server to GameLift for hosting, package the game build files into a build directory. +This directory must include all components required to run your game servers and host game sessions, including the following: -There are no hand-written ([L2](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) constructs for this service yet. -However, you can still use the automatically generated [L1](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_l1_using) constructs, and use this service exactly as you would using CloudFormation directly. +* Game server binaries – The binary files required to run the game server. A build can include binaries for multiple game +servers built to run on the same platform. For a list of supported platforms, see [Download Amazon GameLift SDKs](https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-supported.html). -For more information on the resources and properties available for this service, see the [CloudFormation documentation for AWS::GameLift](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/AWS_GameLift.html). +* Dependencies – Any dependent files that your game server executables require to run. Examples include assets, configuration +files, and dependent libraries. -(Read the [CDK Contributing Guide](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and submit an RFC if you are interested in contributing to this construct library.) +* Install script – A script file to handle tasks that are required to fully install your game build on GameLift hosting +servers. Place this file at the root of the build directory. GameLift runs the install script as part of fleet creation. - +You can set up any application in your build, including your install script, to access your resources securely on other AWS +services. + +```ts +declare const bucket: s3.Bucket; +new gamelift.Build(this, 'Build', { + content: gamelift.Content.fromBucket(bucket, "sample-asset-key") +}); +``` + +#### Upload a realtime server Script + +Your server script can include one or more files combined into a single .zip file for uploading. The .zip file must contain +all files that your script needs to run. + +You can store your zipped script files in either a local file directory or in an Amazon Simple Storage Service (Amazon S3) +bucket or defines a directory asset which is archived as a .zip file and uploaded to S3 during deployment. + +After you create the script resource, GameLift deploys the script with a new Realtime Servers fleet. GameLift installs your +server script onto each instance in the fleet, placing the script files in `/local/game`. + +```ts +declare const bucket: s3.Bucket; +new gamelift.Script(this, 'Script', { + content: gamelift.Content.fromBucket(bucket, "sample-asset-key") +}); +``` diff --git a/packages/@aws-cdk/aws-gamelift/lib/build.ts b/packages/@aws-cdk/aws-gamelift/lib/build.ts new file mode 100644 index 0000000000000..a2d4422e47453 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/lib/build.ts @@ -0,0 +1,213 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { Content } from './content'; +import { CfnBuild } from './gamelift.generated'; + +/** + * Your custom-built game server software that runs on GameLift and hosts game sessions for your players. + * A game build represents the set of files that run your game server on a particular operating system. + * You can have many different builds, such as for different flavors of your game. + * The game build must be integrated with the GameLift service. + * You upload game build files to the GameLift service in the Regions where you plan to set up fleets. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-build-cli-uploading.html + */ +export interface IBuild extends cdk.IResource, iam.IGrantable { + + /** + * The Identifier of the build. + * + * @attribute + */ + readonly buildId: string; +} + +/** + * Base class for new and imported GameLift server build. + */ +export abstract class BuildBase extends cdk.Resource implements IBuild { + /** + * The Identifier of the build. + */ + public abstract readonly buildId: string; + + public abstract readonly grantPrincipal: iam.IPrincipal; +} + +/** + * The operating system that the game server binaries are built to run on. + */ +export enum OperatingSystem { + AMAZON_LINUX = 'AMAZON_LINUX', + AMAZON_LINUX_2 = 'AMAZON_LINUX_2', + WINDOWS_2012 = 'WINDOWS_2012' +} + + +/** + * Represents a Build content defined outside of this stack. + */ +export interface BuildAttributes { + /** + * The identifier of the build + */ + readonly buildId: string; + /** + * The IAM role assumed by GameLift to access server build in S3. + * @default - undefined + */ + readonly role?: iam.IRole; +} + +/** + * Properties for a new build + */ +export interface BuildProps { + /** + * Name of this build + * + * @default No name + */ + readonly buildName?: string; + + /** + * Version of this build + * + * @default No version + */ + readonly buildVersion?: string; + + /** + * The operating system that the game server binaries are built to run on. + * + * @default No version + */ + readonly operatingSystem?: OperatingSystem; + + /** + * The game build file storage + */ + readonly content: Content; + + /** + * The IAM role assumed by GameLift to access server build in S3. + * If providing a custom role, it needs to trust the GameLift service principal (gamelift.amazonaws.com) and be granted sufficient permissions + * to have Read access to a specific key content into a specific S3 bucket. + * Below an example of required permission: + * { + * "Version": "2012-10-17", + * "Statement": [{ + * "Effect": "Allow", + * "Action": [ + * "s3:GetObject", + * "s3:GetObjectVersion" + * ], + * "Resource": "arn:aws:s3:::bucket-name/object-name" + * }] + *} + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-access-storage-loc + * + * @default - a role will be created with default permissions. + */ + readonly role?: iam.IRole; +} + +/** + * A GameLift build, that is installed and runs on instances in an Amazon GameLift fleet. It consists of + * a zip file with all of the components of the game server build. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/gamelift-build-cli-uploading.html + * + * @resource AWS::GameLift::Build + */ +export class Build extends BuildBase { + + /** + * Create a new Build from s3 content + */ + static fromBucket(scope: Construct, id: string, bucket: s3.IBucket, key: string, objectVersion?: string) { + return new Build(scope, id, { + content: Content.fromBucket(bucket, key, objectVersion), + }); + } + + /** + * Create a new Build from asset content + */ + static fromAsset(scope: Construct, id: string, path: string, options?: s3_assets.AssetOptions) { + return new Build(scope, id, { + content: Content.fromAsset(path, options), + }); + } + + /** + * Import a build into CDK using its identifier + */ + static fromBuildId(scope: Construct, id: string, buildId: string): IBuild { + return this.fromBuildAttributes(scope, id, { buildId }); + } + + /** + * Import an existing build from its attributes. + */ + static fromBuildAttributes(scope: Construct, id: string, attrs: BuildAttributes): IBuild { + class Import extends BuildBase { + public readonly buildId = attrs.buildId; + public readonly grantPrincipal = attrs.role ?? new iam.UnknownPrincipal({ resource: this }); + } + + return new Import(scope, id); + } + + /** + * The Identifier of the build. + */ + public readonly buildId: string; + + /** + * The IAM role GameLift assumes to acccess server build content. + */ + public readonly role: iam.IRole; + + /** + * The principal this GameLift Build is using. + */ + public readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: Construct, id: string, props: BuildProps) { + super(scope, id, { + physicalName: props.buildName, + }); + + if (props.buildName && !cdk.Token.isUnresolved(props.buildName)) { + if (props.buildName.length > 1024) { + throw new Error(`Build name can not be longer than 1024 characters but has ${props.buildName.length} characters.`); + } + } + this.role = props.role ?? new iam.Role(this, 'ServiceRole', { + assumedBy: new iam.ServicePrincipal('gamelift.amazonaws.com'), + }); + this.grantPrincipal = this.role; + const content = props.content.bind(this, this.role); + + const resource = new CfnBuild(this, 'Resource', { + name: props.buildName, + version: props.buildVersion, + operatingSystem: props.operatingSystem, + storageLocation: { + bucket: content.s3Location && content.s3Location.bucketName, + key: content.s3Location && content.s3Location.objectKey, + objectVersion: content.s3Location && content.s3Location.objectVersion, + roleArn: this.role.roleArn, + }, + }); + + this.buildId = resource.ref; + } + + +} diff --git a/packages/@aws-cdk/aws-gamelift/lib/content.ts b/packages/@aws-cdk/aws-gamelift/lib/content.ts new file mode 100644 index 0000000000000..3c510757b3880 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/lib/content.ts @@ -0,0 +1,110 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; + +/** + * Before deploying your GameLift-enabled multiplayer game servers for hosting with the GameLift service, you need to upload your game server files. + * The class helps you on preparing and uploading custom game server build files or Realtime Servers server script files. + */ +export abstract class Content { + /** + * Game content as an S3 object. + * @param bucket The S3 bucket + * @param key The object key + * @param objectVersion Optional S3 ob ject version + */ + public static fromBucket(bucket: s3.IBucket, key: string, objectVersion?: string): S3Content { + return new S3Content(bucket, key, objectVersion); + } + + + /** + * Loads the game content from a local disk path. + * + * @param path Either a directory with the game content bundle or a .zip file + */ + public static fromAsset(path: string, options?: s3_assets.AssetOptions): AssetContent { + return new AssetContent(path, options); + } + + /** + * Called when the Build is initialized to allow this object to bind + */ + public abstract bind(scope: Construct, grantable: iam.IGrantable): ContentConfig; + +} + +/** + * Result of binding `Content` into a `Build`. + */ +export interface ContentConfig { + /** + * The location of the content in S3. + */ + readonly s3Location: s3.Location; +} + +/** + * Game content from an S3 archive. + */ +export class S3Content extends Content { + + constructor(private readonly bucket: s3.IBucket, private key: string, private objectVersion?: string) { + super(); + if (!bucket.bucketName) { + throw new Error('bucketName is undefined for the provided bucket'); + } + } + + public bind(_scope: Construct, grantable: iam.IGrantable): ContentConfig { + this.bucket.grantRead(grantable, this.key); + return { + s3Location: { + bucketName: this.bucket.bucketName, + objectKey: this.key, + objectVersion: this.objectVersion, + }, + }; + } +} + +/** + * Game content from a local directory. + */ +export class AssetContent extends Content { + private asset?: s3_assets.Asset; + + /** + * @param path The path to the asset file or directory. + */ + constructor(public readonly path: string, private readonly options: s3_assets.AssetOptions = { }) { + super(); + } + + public bind(scope: Construct, grantable: iam.IGrantable): ContentConfig { + // If the same AssetContent is used multiple times, retain only the first instantiation. + if (!this.asset) { + this.asset = new s3_assets.Asset(scope, 'Content', { + path: this.path, + ...this.options, + }); + } else if (cdk.Stack.of(this.asset) !== cdk.Stack.of(scope)) { + throw new Error(`Asset is already associated with another stack '${cdk.Stack.of(this.asset).stackName}'. ` + + 'Create a new Content instance for every stack.'); + } + this.asset.grantRead(grantable); + + if (!this.asset.isZipArchive) { + throw new Error(`Asset must be a .zip file or a directory (${this.path})`); + } + + return { + s3Location: { + bucketName: this.asset.s3BucketName, + objectKey: this.asset.s3ObjectKey, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-gamelift/lib/index.ts b/packages/@aws-cdk/aws-gamelift/lib/index.ts index 843bf2236645e..13cc0c8eada9c 100644 --- a/packages/@aws-cdk/aws-gamelift/lib/index.ts +++ b/packages/@aws-cdk/aws-gamelift/lib/index.ts @@ -1,2 +1,6 @@ +export * from './content'; +export * from './build'; +export * from './script'; + // AWS::GameLift CloudFormation Resources: export * from './gamelift.generated'; diff --git a/packages/@aws-cdk/aws-gamelift/lib/script.ts b/packages/@aws-cdk/aws-gamelift/lib/script.ts new file mode 100644 index 0000000000000..f387388732486 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/lib/script.ts @@ -0,0 +1,243 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as s3_assets from '@aws-cdk/aws-s3-assets'; +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { Content } from './content'; +import { CfnScript } from './gamelift.generated'; + +/** + * Your configuration and custom game logic for use with Realtime Servers. + * Realtime Servers are provided by GameLift to use instead of a custom-built game server. + * You configure Realtime Servers for your game clients by creating a script using JavaScript, + * and add custom game logic as appropriate to host game sessions for your players. + * You upload the Realtime script to the GameLift service in the Regions where you plan to set up fleets. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script-uploading.html + */ +export interface IScript extends cdk.IResource, iam.IGrantable { + + /** + * The Identifier of the realtime server script. + * + * @attribute + */ + readonly scriptId: string; + + /** + * The ARN of the realtime server script. + * + * @attribute + */ + readonly scriptArn: string; +} + +/** + * Base class for new and imported GameLift realtime server script. + */ +export abstract class ScriptBase extends cdk.Resource implements IScript { + /** + * The Identifier of the realtime server script. + */ + public abstract readonly scriptId: string; + public abstract readonly scriptArn: string; + + public abstract readonly grantPrincipal: iam.IPrincipal; +} + +/** + * Represents a Script content defined outside of this stack. + */ +export interface ScriptAttributes { + /** + * The ARN of the realtime server script + */ + readonly scriptArn: string; + /** + * The IAM role assumed by GameLift to access server script in S3. + * @default - undefined + */ + readonly role?: iam.IRole; +} + +/** + * Properties for a new realtime server script + */ +export interface ScriptProps { + /** + * Name of this realtime server script + * + * @default No name + */ + readonly scriptName?: string; + + /** + * Version of this realtime server script + * + * @default No version + */ + readonly scriptVersion?: string; + + /** + * The game content + */ + readonly content: Content; + + /** + * The IAM role assumed by GameLift to access server script in S3. + * If providing a custom role, it needs to trust the GameLift service principal (gamelift.amazonaws.com) and be granted sufficient permissions + * to have Read access to a specific key content into a specific S3 bucket. + * Below an example of required permission: + * { + * "Version": "2012-10-17", + * "Statement": [{ + * "Effect": "Allow", + * "Action": [ + * "s3:GetObject", + * "s3:GetObjectVersion" + * ], + * "Resource": "arn:aws:s3:::bucket-name/object-name" + * }] + *} + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/security_iam_id-based-policy-examples.html#security_iam_id-based-policy-examples-access-storage-loc + * + * @default - a role will be created with default permissions. + */ + readonly role?: iam.IRole; +} + +/** + * A GameLift script, that is installed and runs on instances in an Amazon GameLift fleet. It consists of + * a zip file with all of the components of the realtime game server script. + * + * @see https://docs.aws.amazon.com/gamelift/latest/developerguide/realtime-script-uploading.html + * + * @resource AWS::GameLift::Script + */ +export class Script extends ScriptBase { + + /** + * Create a new realtime server script from s3 content + */ + static fromBucket(scope: Construct, id: string, bucket: s3.IBucket, key: string, objectVersion?: string) { + return new Script(scope, id, { + content: Content.fromBucket(bucket, key, objectVersion), + }); + } + + /** + * Create a new realtime server script from asset content + */ + static fromAsset(scope: Construct, id: string, path: string, options?: s3_assets.AssetOptions) { + return new Script(scope, id, { + content: Content.fromAsset(path, options), + }); + } + + /** + * Import a script into CDK using its ARN + */ + static fromScriptArn(scope: Construct, id: string, scriptArn: string): IScript { + return this.fromScriptAttributes(scope, id, { scriptArn }); + } + + /** + * Import an existing realtime server script from its attributes. + */ + static fromScriptAttributes(scope: Construct, id: string, attrs: ScriptAttributes): IScript { + const scriptArn = attrs.scriptArn; + const scriptId = extractIdFromArn(attrs.scriptArn); + const role = attrs.role; + + class Import extends ScriptBase { + public readonly scriptArn = scriptArn; + public readonly scriptId = scriptId; + public readonly grantPrincipal:iam.IPrincipal; + public readonly role = role + + constructor(s: Construct, i: string) { + super(s, i, { + environmentFromArn: scriptArn, + }); + + this.grantPrincipal = this.role || new iam.UnknownPrincipal({ resource: this }); + } + } + + return new Import(scope, id); + } + + /** + * The Identifier of the realtime server script. + */ + public readonly scriptId: string; + + /** + * The ARN of the realtime server script. + */ + public readonly scriptArn: string; + + /** + * The IAM role GameLift assumes to acccess server script content. + */ + public readonly role: iam.IRole; + + /** + * The principal this GameLift script is using. + */ + public readonly grantPrincipal: iam.IPrincipal; + + constructor(scope: Construct, id: string, props: ScriptProps) { + super(scope, id, { + physicalName: props.scriptName, + }); + + if (props.scriptName && !cdk.Token.isUnresolved(props.scriptName)) { + if (props.scriptName.length > 1024) { + throw new Error(`Script name can not be longer than 1024 characters but has ${props.scriptName.length} characters.`); + } + } + this.role = props.role ?? new iam.Role(this, 'ServiceRole', { + assumedBy: new iam.ServicePrincipal('gamelift.amazonaws.com'), + }); + this.grantPrincipal = this.role; + const content = props.content.bind(this, this.role); + + const resource = new CfnScript(this, 'Resource', { + name: props.scriptName, + version: props.scriptVersion, + storageLocation: { + bucket: content.s3Location && content.s3Location.bucketName, + key: content.s3Location && content.s3Location.objectKey, + objectVersion: content.s3Location && content.s3Location.objectVersion, + roleArn: this.role.roleArn, + }, + }); + + this.scriptId = this.getResourceNameAttribute(resource.ref); + this.scriptArn = this.getResourceArnAttribute(resource.attrArn, { + service: 'gamelift', + resource: `script/${this.physicalName}`, + arnFormat: cdk.ArnFormat.COLON_RESOURCE_NAME, + }); + } +} + +/** + * Given an opaque (token) ARN, returns a CloudFormation expression that extracts the script + * identifier from the ARN. + * + * Script ARNs look like this: + * + * arn:aws:gamelift:region:account-id:script/script-identifier + * + * ..which means that in order to extract the `script-identifier` component from the ARN, we can + * split the ARN using ":" and select the component in index 5 then split using "/" and select the component in index 1. + * + * @returns the script identifier from his ARN + */ +function extractIdFromArn(arn: string) { + const splitValue = cdk.Fn.select(5, cdk.Fn.split(':', arn)); + return cdk.Fn.select(1, cdk.Fn.split('/', splitValue)); +} diff --git a/packages/@aws-cdk/aws-gamelift/package.json b/packages/@aws-cdk/aws-gamelift/package.json index 19f96730ef99e..6f133644d1d2b 100644 --- a/packages/@aws-cdk/aws-gamelift/package.json +++ b/packages/@aws-cdk/aws-gamelift/package.json @@ -82,24 +82,54 @@ "devDependencies": { "@aws-cdk/assertions": "0.0.0", "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/integ-runner": "0.0.0", "@aws-cdk/cfn2ts": "0.0.0", "@aws-cdk/pkglint": "0.0.0", - "@types/jest": "^27.5.2" + "@aws-cdk/cx-api": "0.0.0", + "@types/jest": "^27.5.2", + "jest": "^27.5.1" }, "dependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-events": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/region-info": "0.0.0", "constructs": "^10.0.0" }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-events": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", + "@aws-cdk/aws-logs": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", + "@aws-cdk/aws-sns": "0.0.0", + "@aws-cdk/region-info": "0.0.0", "constructs": "^10.0.0" }, "engines": { "node": ">= 14.15.0" }, + "awslint": { + "exclude": [ + "docs-public-apis:@aws-cdk/aws-gamelift.OperatingSystem.AMAZON_LINUX", + "docs-public-apis:@aws-cdk/aws-gamelift.OperatingSystem.AMAZON_LINUX_2", + "docs-public-apis:@aws-cdk/aws-gamelift.OperatingSystem.WINDOWS_2012" + ] + }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-gamelift/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-gamelift/rosetta/default.ts-fixture new file mode 100644 index 0000000000000..4a4c332becb50 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/rosetta/default.ts-fixture @@ -0,0 +1,16 @@ +// Fixture with packages imported, but nothing else +import { Construct } from 'constructs'; +import { Duration, Size, Stack } from '@aws-cdk/core'; +import * as gamelift from '@aws-cdk/aws-gamelift'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as kms from '@aws-cdk/aws-kms'; +import * as iam from '@aws-cdk/aws-iam'; +import * as path from 'path'; + +class Fixture extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + + /// here + } +} diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/aws-gamelift-build.assets.json b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/aws-gamelift-build.assets.json new file mode 100644 index 0000000000000..d27cd073bc49a --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/aws-gamelift-build.assets.json @@ -0,0 +1,32 @@ +{ + "version": "21.0.0", + "files": { + "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7": { + "source": { + "path": "asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222": { + "source": { + "path": "aws-gamelift-build.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/aws-gamelift-build.template.json b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/aws-gamelift-build.template.json new file mode 100644 index 0000000000000..394b49a1b66ff --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/aws-gamelift-build.template.json @@ -0,0 +1,129 @@ +{ + "Resources": { + "BuildServiceRole1F57E904": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "gamelift.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "BuildServiceRoleDefaultPolicyCB7101C6": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "BuildServiceRoleDefaultPolicyCB7101C6", + "Roles": [ + { + "Ref": "BuildServiceRole1F57E904" + } + ] + } + }, + "Build45A36621": { + "Type": "AWS::GameLift::Build", + "Properties": { + "StorageLocation": { + "Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "Key": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "RoleArn": { + "Fn::GetAtt": [ + "BuildServiceRole1F57E904", + "Arn" + ] + } + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/integ.json b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/integ.json new file mode 100644 index 0000000000000..f646149706bea --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "21.0.0", + "testCases": { + "integ.build": { + "stacks": [ + "aws-gamelift-build" + ], + "diffAssets": false, + "stackUpdateWorkflow": true + } + }, + "synthContext": {}, + "enableLookups": false +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..43fe5eb776640 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/manifest.json @@ -0,0 +1,64 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-gamelift-build.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-gamelift-build.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-gamelift-build": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-gamelift-build.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-gamelift-build.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-gamelift-build.assets" + ], + "metadata": { + "/aws-gamelift-build/Build/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BuildServiceRole1F57E904" + } + ], + "/aws-gamelift-build/Build/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BuildServiceRoleDefaultPolicyCB7101C6" + } + ], + "/aws-gamelift-build/Build/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Build45A36621" + } + ] + }, + "displayName": "aws-gamelift-build" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/tree.json b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/tree.json new file mode 100644 index 0000000000000..96d14a552e490 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.integ.snapshot/tree.json @@ -0,0 +1,202 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.33" + } + }, + "build-test-assets": { + "id": "build-test-assets", + "path": "build-test-assets", + "children": { + "Build": { + "id": "Build", + "path": "build-test-assets/Build", + "children": { + "Service Role": { + "id": "Service Role", + "path": "build-test-assets/Build/Service Role", + "children": { + "Resource": { + "id": "Resource", + "path": "build-test-assets/Build/Service Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "gamelift.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "build-test-assets/Build/Service Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "build-test-assets/Build/Service Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "BuildServiceRoleDefaultPolicy90803718", + "roles": [ + { + "Ref": "BuildServiceRole4643E19E" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Content": { + "id": "Content", + "path": "build-test-assets/Build/Content", + "children": { + "Stage": { + "id": "Stage", + "path": "build-test-assets/Build/Content/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "build-test-assets/Build/Content/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "build-test-assets/Build/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::GameLift::Build", + "aws:cdk:cloudformation:props": { + "storageLocation": { + "bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "key": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "roleArn": { + "Fn::GetAtt": [ + "BuildServiceRole4643E19E", + "Arn" + ] + } + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.CfnBuild", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.Build", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/build.test.ts b/packages/@aws-cdk/aws-gamelift/test/build.test.ts new file mode 100644 index 0000000000000..9e5b6bd6855db --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/build.test.ts @@ -0,0 +1,235 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as gamelift from '../lib'; + +describe('build', () => { + const buildId = 'test-identifier'; + const buildName = 'test-build'; + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + stack = new cdk.Stack(app); + }); + + describe('.fromBuildId()', () => { + test('with required fields', () => { + const build = gamelift.Build.fromBuildId(stack, 'ImportedBuild', buildId); + + expect(build.buildId).toEqual(buildId); + expect(build.grantPrincipal).toEqual(new iam.UnknownPrincipal({ resource: build })); + }); + }); + + describe('.fromBuildAttributes()', () => { + test('with required attrs only', () => { + const build = gamelift.Build.fromBuildAttributes(stack, 'ImportedBuild', { buildId }); + + expect(build.buildId).toEqual(buildId); + expect(build.grantPrincipal).toEqual(new iam.UnknownPrincipal({ resource: build })); + }); + + test('with all attrs', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/TestRole'); + const build = gamelift.Build.fromBuildAttributes(stack, 'ImportedBuild', { buildId, role }); + + expect(buildId).toEqual(buildId); + expect(build.grantPrincipal).toEqual(role); + }); + }); + + describe('new', () => { + const localAsset = path.join(__dirname, 'my-game-build'); + const contentBucketName = 'bucketname'; + const contentBucketAccessStatement = { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:s3:::${contentBucketName}`, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:s3:::${contentBucketName}/content`, + ], + ], + }, + ], + }; + let contentBucket: s3.IBucket; + let content: gamelift.Content; + let build: gamelift.Build; + let defaultProps: gamelift.BuildProps; + + beforeEach(() => { + contentBucket = s3.Bucket.fromBucketName(stack, 'ContentBucket', contentBucketName); + content = gamelift.Content.fromBucket(contentBucket, 'content'); + defaultProps = { + content, + }; + }); + + describe('.fromAsset()', () => { + test('should create a new build from asset', () => { + build = gamelift.Build.fromAsset(stack, 'ImportedBuild', localAsset); + + expect(stack.node.metadata.find(m => m.type === 'aws:cdk:asset')).toBeDefined(); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation: { + Bucket: { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + }, + }); + + }); + }); + + describe('.fromBucket()', () => { + test('should create a new build from bucket', () => { + build = gamelift.Build.fromBucket(stack, 'ImportedBuild', contentBucket, 'content'); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + }, + }); + + }); + }); + + describe('with necessary props only', () => { + beforeEach(() => { + build = new gamelift.Build(stack, 'Build', defaultProps); + }); + + test('should create a role and use it with the build', () => { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'gamelift.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + // Role policy should grant reading from the assets bucket + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + contentBucketAccessStatement, + ], + }, + Roles: [ + { + Ref: 'BuildServiceRole1F57E904', + }, + ], + }); + + // check the build using the role + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + RoleArn: { + 'Fn::GetAtt': [ + 'BuildServiceRole1F57E904', + 'Arn', + ], + }, + }, + }); + }); + + test('should return correct buildId from CloudFormation', () => { + expect(stack.resolve(build.buildId)).toEqual({ Ref: 'Build45A36621' }); + }); + + test('with a custom role should use it and set it in CloudFormation', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/TestRole'); + build = new gamelift.Build(stack, 'BuildWithRole', { + ...defaultProps, + role, + }); + + expect(build.grantPrincipal).toEqual(role); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation: { + RoleArn: role.roleArn, + }, + }); + }); + + test('with a custom buildName should set it in CloudFormation', () => { + build = new gamelift.Build(stack, 'BuildWithName', { + ...defaultProps, + buildName: buildName, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + Name: buildName, + }); + }); + + test('with all optional attributes should set it in CloudFormation', () => { + build = new gamelift.Build(stack, 'BuildWithName', { + ...defaultProps, + buildName: buildName, + operatingSystem: gamelift.OperatingSystem.AMAZON_LINUX_2, + buildVersion: '1.0', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + Name: buildName, + OperatingSystem: gamelift.OperatingSystem.AMAZON_LINUX_2, + Version: '1.0', + }); + }); + + test('with an incorrect buildName (>1024)', () => { + let incorrectBuildName = ''; + for (let i = 0; i < 1025; i++) { + incorrectBuildName += 'A'; + } + + expect(() => new gamelift.Build(stack, 'BuildWithWrongName', { + content, + buildName: incorrectBuildName, + })).toThrow(/Build name can not be longer than 1024 characters but has 1025 characters./); + }); + }); + }); +}); + + diff --git a/packages/@aws-cdk/aws-gamelift/test/content.test.ts b/packages/@aws-cdk/aws-gamelift/test/content.test.ts new file mode 100644 index 0000000000000..0c1f6e0617121 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/content.test.ts @@ -0,0 +1,280 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as gamelift from '../lib'; + +describe('Code', () => { + let stack: cdk.Stack; + let content: gamelift.Content; + + beforeEach(() => { + const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + stack = new cdk.Stack(app, 'Stack'); + }); + + describe('.fromBucket()', () => { + const key = 'content'; + let bucket: s3.IBucket; + + test('with valid bucket name and key and bound by build sets the right path and grants the build permissions to read from it', () => { + bucket = s3.Bucket.fromBucketName(stack, 'Bucket', 'bucketname'); + content = gamelift.Content.fromBucket(bucket, key); + new gamelift.Build(stack, 'Build1', { + content: content, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + }, + }); + + // Role policy should grant reading from the assets bucket + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::bucketname', + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::bucketname/content', + ], + ], + }, + ], + }, + ], + }, + Roles: [ + { + Ref: 'Build1ServiceRole24FABCB7', + }, + ], + }); + }); + }); + + describe('.fromAsset()', () => { + const directoryPath = path.join(__dirname, 'my-game-build'); + + beforeEach(() => { + content = gamelift.Content.fromAsset(directoryPath); + }); + + test("with valid and existing file path and bound to job sets job's script location and permissions stack metadata", () => { + new gamelift.Build(stack, 'Build1', { + content: content, + }); + + expect(stack.node.metadata.find(m => m.type === 'aws:cdk:asset')).toBeDefined(); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation: { + Bucket: { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + Key: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3VersionKey720D3160', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3VersionKey720D3160', + }, + ], + }, + ], + }, + ], + ], + }, + RoleArn: { + 'Fn::GetAtt': [ + 'Build1ServiceRole24FABCB7', + 'Arn', + ], + }, + }, + }); + // Role policy should grant reading from the assets bucket + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', + { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':s3:::', + { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + '/*', + ], + ], + }, + ], + }, + ], + }, + Roles: [ + { + Ref: 'Build1ServiceRole24FABCB7', + }, + ], + }); + }); + + test('with an unsupported file path throws', () => { + // GIVEN + const fileAsset = gamelift.Content.fromAsset(path.join(__dirname, 'my-game-build', 'index.js')); + + // THEN + expect(() => new gamelift.Build(stack, 'Build1', { content: fileAsset })) + .toThrow(/Asset must be a \.zip file or a directory/); + }); + + test('used in more than 1 build in the same stack should be reused', () => { + new gamelift.Build(stack, 'Build1', { + content: content, + }); + new gamelift.Build(stack, 'Build2', { + content: content, + }); + const StorageLocation = { + Bucket: { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + Key: { + 'Fn::Join': [ + '', + [ + { + 'Fn::Select': [ + 0, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3VersionKey720D3160', + }, + ], + }, + ], + }, + { + 'Fn::Select': [ + 1, + { + 'Fn::Split': [ + '||', + { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3VersionKey720D3160', + }, + ], + }, + ], + }, + ], + ], + }, + RoleArn: { + 'Fn::GetAtt': [ + 'Build1ServiceRole24FABCB7', + 'Arn', + ], + }, + }; + + expect(stack.node.metadata.find(m => m.type === 'aws:cdk:asset')).toBeDefined(); + // Job1 and Job2 use reuse the asset + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation, + }); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Build', { + StorageLocation, + }); + }); + + test('throws if trying to rebind in another stack', () => { + new gamelift.Build(stack, 'Build1', { + content, + }); + const differentStack = new cdk.Stack(); + + expect(() => new gamelift.Build(differentStack, 'Build2', { + content, + })).toThrow(/Asset is already associated with another stack/); + }); + }); +}); diff --git a/packages/@aws-cdk/aws-gamelift/test/gamelift.test.ts b/packages/@aws-cdk/aws-gamelift/test/gamelift.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-gamelift/test/gamelift.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.build.ts b/packages/@aws-cdk/aws-gamelift/test/integ.build.ts new file mode 100644 index 0000000000000..d216cbccd9212 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.build.ts @@ -0,0 +1,13 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as gamelift from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-gamelift-build'); + +new gamelift.Build(stack, 'Build', { + content: gamelift.Content.fromAsset(path.join(__dirname, 'my-game-build')), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-gamelift/test/integ.script.ts b/packages/@aws-cdk/aws-gamelift/test/integ.script.ts new file mode 100644 index 0000000000000..fb85f5b00ab37 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/integ.script.ts @@ -0,0 +1,13 @@ +import * as path from 'path'; +import * as cdk from '@aws-cdk/core'; +import * as gamelift from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-gamelift-script'); + +new gamelift.Script(stack, 'Script', { + content: gamelift.Content.fromAsset(path.join(__dirname, 'my-game-script')), +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-gamelift/test/my-game-build.zip b/packages/@aws-cdk/aws-gamelift/test/my-game-build.zip new file mode 100644 index 0000000000000..4a13be08c2721 Binary files /dev/null and b/packages/@aws-cdk/aws-gamelift/test/my-game-build.zip differ diff --git a/packages/@aws-cdk/aws-gamelift/test/my-game-build/index.js b/packages/@aws-cdk/aws-gamelift/test/my-game-build/index.js new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/my-game-build/index.js @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/my-game-script/index.js b/packages/@aws-cdk/aws-gamelift/test/my-game-script/index.js new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/my-game-script/index.js @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js new file mode 100644 index 0000000000000..73c02658c48d9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7/index.js @@ -0,0 +1 @@ +console.log('Hello World'); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.assets.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.assets.json new file mode 100644 index 0000000000000..b5db81d532125 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.assets.json @@ -0,0 +1,32 @@ +{ + "version": "21.0.0", + "files": { + "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7": { + "source": { + "path": "asset.6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222": { + "source": { + "path": "aws-gamelift-script.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.template.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.template.json new file mode 100644 index 0000000000000..fe0c724ffaad9 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/aws-gamelift-script.template.json @@ -0,0 +1,129 @@ +{ + "Resources": { + "ScriptServiceRole23DD8079": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "gamelift.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ScriptServiceRoleDefaultPolicyEE85DAE7": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetBucket*", + "s3:GetObject*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ScriptServiceRoleDefaultPolicyEE85DAE7", + "Roles": [ + { + "Ref": "ScriptServiceRole23DD8079" + } + ] + } + }, + "Script09016516": { + "Type": "AWS::GameLift::Script", + "Properties": { + "StorageLocation": { + "Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "Key": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "RoleArn": { + "Fn::GetAtt": [ + "ScriptServiceRole23DD8079", + "Arn" + ] + } + } + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } + } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/cdk.out b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/cdk.out new file mode 100644 index 0000000000000..8ecc185e9dbee --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"21.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/integ.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/integ.json new file mode 100644 index 0000000000000..87f04519b0e06 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/integ.json @@ -0,0 +1,14 @@ +{ + "version": "21.0.0", + "testCases": { + "integ.script": { + "stacks": [ + "aws-gamelift-script" + ], + "diffAssets": false, + "stackUpdateWorkflow": true + } + }, + "synthContext": {}, + "enableLookups": false +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/manifest.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/manifest.json new file mode 100644 index 0000000000000..5bc5612710eb2 --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/manifest.json @@ -0,0 +1,64 @@ +{ + "version": "21.0.0", + "artifacts": { + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + }, + "aws-gamelift-script.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-gamelift-script.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-gamelift-script": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-gamelift-script.template.json", + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/9c561e93c7a2947a15dba683670660e922cf493e17b2a6f8ca03cf221442c222.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-gamelift-script.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-gamelift-script.assets" + ], + "metadata": { + "/aws-gamelift-script/Script/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ScriptServiceRole23DD8079" + } + ], + "/aws-gamelift-script/Script/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "ScriptServiceRoleDefaultPolicyEE85DAE7" + } + ], + "/aws-gamelift-script/Script/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Script09016516" + } + ] + }, + "displayName": "aws-gamelift-script" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/tree.json b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/tree.json new file mode 100644 index 0000000000000..8a6195c91e0cc --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.integ.snapshot/tree.json @@ -0,0 +1,202 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.1.33" + } + }, + "script-test-assets": { + "id": "script-test-assets", + "path": "script-test-assets", + "children": { + "Script": { + "id": "Script", + "path": "script-test-assets/Script", + "children": { + "Service Role": { + "id": "Service Role", + "path": "script-test-assets/Script/Service Role", + "children": { + "Resource": { + "id": "Resource", + "path": "script-test-assets/Script/Service Role/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "gamelift.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "script-test-assets/Script/Service Role/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "script-test-assets/Script/Service Role/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "policyName": "ScriptServiceRoleDefaultPolicy90803718", + "roles": [ + { + "Ref": "ScriptServiceRole4643E19E" + } + ] + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-iam.Role", + "version": "0.0.0" + } + }, + "Content": { + "id": "Content", + "path": "script-test-assets/Script/Content", + "children": { + "Stage": { + "id": "Stage", + "path": "script-test-assets/Script/Content/Stage", + "constructInfo": { + "fqn": "@aws-cdk/core.AssetStaging", + "version": "0.0.0" + } + }, + "AssetBucket": { + "id": "AssetBucket", + "path": "script-test-assets/Script/Content/AssetBucket", + "constructInfo": { + "fqn": "@aws-cdk/aws-s3.BucketBase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-s3-assets.Asset", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "script-test-assets/Script/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::GameLift::Script", + "aws:cdk:cloudformation:props": { + "storageLocation": { + "bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "key": "6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7.zip", + "roleArn": { + "Fn::GetAtt": [ + "ScriptServiceRole4643E19E", + "Arn" + ] + } + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.CfnScript", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/aws-gamelift.Script", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/core.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-gamelift/test/script.test.ts b/packages/@aws-cdk/aws-gamelift/test/script.test.ts new file mode 100644 index 0000000000000..b7c5efc2f075d --- /dev/null +++ b/packages/@aws-cdk/aws-gamelift/test/script.test.ts @@ -0,0 +1,245 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import * as iam from '@aws-cdk/aws-iam'; +import * as s3 from '@aws-cdk/aws-s3'; +import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as gamelift from '../lib'; + +describe('script', () => { + const scriptId = 'script-test-identifier'; + const scriptArn = `arn:aws:gamelift:script-region:123456789012:script/${scriptId}`; + const scriptName = 'test-script'; + let stack: cdk.Stack; + + beforeEach(() => { + const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); + stack = new cdk.Stack(app, 'Base', { + env: { account: '111111111111', region: 'stack-region' }, + }); + }); + + describe('.fromScriptArn()', () => { + test('with required fields', () => { + const script = gamelift.Script.fromScriptArn(stack, 'ImportedScript', scriptArn); + + expect(script.scriptArn).toEqual(scriptArn); + expect(script.grantPrincipal).toEqual(new iam.UnknownPrincipal({ resource: script })); + }); + }); + + describe('.fromScriptAttributes()', () => { + test('with required attrs only', () => { + const script = gamelift.Script.fromScriptAttributes(stack, 'ImportedScript', { scriptArn }); + + expect(script.scriptId).toEqual(scriptId); + expect(script.scriptArn).toEqual(scriptArn); + expect(script.env.account).toEqual('123456789012'); + expect(script.env.region).toEqual('script-region'); + expect(script.grantPrincipal).toEqual(new iam.UnknownPrincipal({ resource: script })); + }); + + test('with all attrs', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/TestRole'); + const script = gamelift.Script.fromScriptAttributes(stack, 'ImportedScript', { scriptArn, role }); + + expect(scriptId).toEqual(scriptId); + expect(script.grantPrincipal).toEqual(role); + }); + }); + + describe('new', () => { + const localAsset = path.join(__dirname, 'my-game-script'); + const contentBucketName = 'bucketname'; + const contentBucketAccessStatement = { + Action: [ + 's3:GetObject*', + 's3:GetBucket*', + 's3:List*', + ], + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:s3:::${contentBucketName}`, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + `:s3:::${contentBucketName}/content`, + ], + ], + }, + ], + }; + let contentBucket: s3.IBucket; + let content: gamelift.Content; + let script: gamelift.Script; + let defaultProps: gamelift.ScriptProps; + + beforeEach(() => { + contentBucket = s3.Bucket.fromBucketName(stack, 'ContentBucket', contentBucketName); + content = gamelift.Content.fromBucket(contentBucket, 'content'); + defaultProps = { + content, + }; + }); + + describe('.fromAsset()', () => { + test('should create a new script from asset', () => { + script = gamelift.Script.fromAsset(stack, 'ImportedScript', localAsset); + + expect(stack.node.metadata.find(m => m.type === 'aws:cdk:asset')).toBeDefined(); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + Bucket: { + Ref: 'AssetParameters6019bfc8ab05a24b0ae9b5d8f4585cbfc7d1c30a23286d0b25ce7066a368a5d7S3Bucket72AA8348', + }, + }, + }); + + }); + }); + + describe('.fromBucket()', () => { + test('should create a new script from bucket', () => { + script = gamelift.Script.fromBucket(stack, 'ImportedScript', contentBucket, 'content'); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + }, + }); + + }); + }); + + describe('with necessary props only', () => { + beforeEach(() => { + script = new gamelift.Script(stack, 'Script', defaultProps); + }); + + test('should create a role and use it with the script', () => { + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Role', { + AssumeRolePolicyDocument: { + Statement: [ + { + Action: 'sts:AssumeRole', + Effect: 'Allow', + Principal: { + Service: 'gamelift.amazonaws.com', + }, + }, + ], + Version: '2012-10-17', + }, + }); + + // Role policy should grant reading from the assets bucket + Template.fromStack(stack).hasResourceProperties('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + contentBucketAccessStatement, + ], + }, + Roles: [ + { + Ref: 'ScriptServiceRole23DD8079', + }, + ], + }); + + // check the script using the role + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + Bucket: 'bucketname', + Key: 'content', + RoleArn: { + 'Fn::GetAtt': [ + 'ScriptServiceRole23DD8079', + 'Arn', + ], + }, + }, + }); + }); + + test('should return correct script attributes from CloudFormation', () => { + expect(stack.resolve(script.scriptId)).toEqual({ Ref: 'Script09016516' }); + expect(stack.resolve(script.scriptArn)).toEqual({ + 'Fn::GetAtt': [ + 'Script09016516', + 'Arn', + ], + }); + }); + + test('with a custom role should use it and set it in CloudFormation', () => { + const role = iam.Role.fromRoleArn(stack, 'Role', 'arn:aws:iam::123456789012:role/TestRole'); + script = new gamelift.Script(stack, 'ScriptWithRole', { + ...defaultProps, + role, + }); + + expect(script.grantPrincipal).toEqual(role); + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + StorageLocation: { + RoleArn: role.roleArn, + }, + }); + }); + + test('with a custom scriptName should set it in CloudFormation', () => { + script = new gamelift.Script(stack, 'ScriptWithName', { + ...defaultProps, + scriptName: scriptName, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + Name: scriptName, + }); + }); + + test('with all optional attributes should set it in CloudFormation', () => { + script = new gamelift.Script(stack, 'ScriptWithName', { + ...defaultProps, + scriptName: scriptName, + scriptVersion: '1.0', + }); + + Template.fromStack(stack).hasResourceProperties('AWS::GameLift::Script', { + Name: scriptName, + Version: '1.0', + }); + }); + + test('with an incorrect scriptName (>1024)', () => { + let incorrectScriptName = ''; + for (let i = 0; i < 1025; i++) { + incorrectScriptName += 'A'; + } + + expect(() => new gamelift.Script(stack, 'ScriptWithWrongName', { + content, + scriptName: incorrectScriptName, + })).toThrow(/Script name can not be longer than 1024 characters but has 1025 characters./); + }); + }); + }); +}); + + diff --git a/packages/@aws-cdk/aws-iam/test/principals.test.ts b/packages/@aws-cdk/aws-iam/test/principals.test.ts index e3b1078d2a933..37dc662121854 100644 --- a/packages/@aws-cdk/aws-iam/test/principals.test.ts +++ b/packages/@aws-cdk/aws-iam/test/principals.test.ts @@ -311,10 +311,10 @@ test('ServicePrincipalName returns just a string representing the principal', () // GIVEN const usEastStack = new Stack(undefined, undefined, { env: { region: 'us-east-1' } }); const afSouthStack = new Stack(undefined, undefined, { env: { region: 'af-south-1' } }); - const principalName = iam.ServicePrincipal.servicePrincipalName('ssm.amazonaws.com'); + const principalName = iam.ServicePrincipal.servicePrincipalName('states.amazonaws.com'); - expect(usEastStack.resolve(principalName)).toEqual('ssm.amazonaws.com'); - expect(afSouthStack.resolve(principalName)).toEqual('ssm.af-south-1.amazonaws.com'); + expect(usEastStack.resolve(principalName)).toEqual('states.us-east-1.amazonaws.com'); + expect(afSouthStack.resolve(principalName)).toEqual('states.af-south-1.amazonaws.com'); }); test('Passing non-string as accountId parameter in AccountPrincipal constructor should throw error', () => { @@ -327,14 +327,14 @@ test('ServicePrincipal in agnostic stack generates lookup table', () => { // WHEN new iam.Role(stack, 'Role', { - assumedBy: new iam.ServicePrincipal('ssm.amazonaws.com'), + assumedBy: new iam.ServicePrincipal('states.amazonaws.com'), }); // THEN const template = Template.fromStack(stack); const mappings = template.findMappings('ServiceprincipalMap'); - expect(mappings.ServiceprincipalMap['af-south-1']?.ssm).toEqual('ssm.af-south-1.amazonaws.com'); - expect(mappings.ServiceprincipalMap['us-east-1']?.ssm).toEqual('ssm.amazonaws.com'); + expect(mappings.ServiceprincipalMap['af-south-1']?.states).toEqual('states.af-south-1.amazonaws.com'); + expect(mappings.ServiceprincipalMap['us-east-1']?.states).toEqual('states.us-east-1.amazonaws.com'); }); test('Can enable session tags', () => { diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index db5b00c545b2a..37af190258d2a 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -245,6 +245,16 @@ export class AuroraEngineVersion { public static readonly VER_1_22_4 = AuroraEngineVersion.builtIn_5_6('1.22.4'); /** Version "5.6.mysql_aurora.1.22.5". */ public static readonly VER_1_22_5 = AuroraEngineVersion.builtIn_5_6('1.22.5'); + /** Version "5.6.mysql_aurora.1.23.0". */ + public static readonly VER_1_23_0 = AuroraEngineVersion.builtIn_5_6('1.23.0'); + /** Version "5.6.mysql_aurora.1.23.1". */ + public static readonly VER_1_23_1 = AuroraEngineVersion.builtIn_5_6('1.23.1'); + /** Version "5.6.mysql_aurora.1.23.2". */ + public static readonly VER_1_23_2 = AuroraEngineVersion.builtIn_5_6('1.23.2'); + /** Version "5.6.mysql_aurora.1.23.3". */ + public static readonly VER_1_23_3 = AuroraEngineVersion.builtIn_5_6('1.23.3'); + /** Version "5.6.mysql_aurora.1.23.4". */ + public static readonly VER_1_23_4 = AuroraEngineVersion.builtIn_5_6('1.23.4'); /** * Create a new AuroraEngineVersion with an arbitrary version. @@ -406,6 +416,8 @@ export class AuroraMysqlEngineVersion { public static readonly VER_3_01_1 = AuroraMysqlEngineVersion.builtIn_8_0('3.01.1'); /** Version "8.0.mysql_aurora.3.02.0". */ public static readonly VER_3_02_0 = AuroraMysqlEngineVersion.builtIn_8_0('3.02.0'); + /** Version "8.0.mysql_aurora.3.02.1". */ + public static readonly VER_3_02_1 = AuroraMysqlEngineVersion.builtIn_8_0('3.02.1'); /** * Create a new AuroraMysqlEngineVersion with an arbitrary version. diff --git a/packages/@aws-cdk/aws-s3/lib/bucket.ts b/packages/@aws-cdk/aws-s3/lib/bucket.ts index 4093110014dec..60c5498a33794 100644 --- a/packages/@aws-cdk/aws-s3/lib/bucket.ts +++ b/packages/@aws-cdk/aws-s3/lib/bucket.ts @@ -1354,10 +1354,15 @@ export interface BucketProps { readonly enforceSSL?: boolean; /** - * Specifies whether Amazon S3 should use an S3 Bucket Key with server-side - * encryption using KMS (SSE-KMS) for new objects in the bucket. + * Whether Amazon S3 should use its own intermediary key to generate data keys. * - * Only relevant, when Encryption is set to {@link BucketEncryption.KMS} + * Only relevant when using KMS for encryption. + * + * - If not enabled, every object GET and PUT will cause an API call to KMS (with the + * attendant cost implications of that). + * - If enabled, S3 will use its own time-limited key instead. + * + * Only relevant, when Encryption is set to `BucketEncryption.KMS` or `BucketEncryption.KMS_MANAGED`. * * @default - false */ @@ -1943,7 +1948,7 @@ export class Bucket extends BucketBase { } // if bucketKeyEnabled is set, encryption must be set to KMS. - if (props.bucketKeyEnabled && encryptionType !== BucketEncryption.KMS) { + if (props.bucketKeyEnabled && ![BucketEncryption.KMS, BucketEncryption.KMS_MANAGED].includes(encryptionType)) { throw new Error(`bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: ${encryptionType})`); } @@ -1983,7 +1988,10 @@ export class Bucket extends BucketBase { if (encryptionType === BucketEncryption.KMS_MANAGED) { const bucketEncryption = { serverSideEncryptionConfiguration: [ - { serverSideEncryptionByDefault: { sseAlgorithm: 'aws:kms' } }, + { + bucketKeyEnabled: props.bucketKeyEnabled, + serverSideEncryptionByDefault: { sseAlgorithm: 'aws:kms' }, + }, ], }; return { bucketEncryption }; @@ -2288,17 +2296,17 @@ export enum BucketEncryption { /** * Objects in the bucket are not encrypted. */ - UNENCRYPTED = 'NONE', + UNENCRYPTED = 'UNENCRYPTED', /** * Server-side KMS encryption with a master key managed by KMS. */ - KMS_MANAGED = 'MANAGED', + KMS_MANAGED = 'KMS_MANAGED', /** * Server-side encryption with a master key managed by S3. */ - S3_MANAGED = 'S3MANAGED', + S3_MANAGED = 'S3_MANAGED', /** * Server-side encryption with a KMS key managed by the user. diff --git a/packages/@aws-cdk/aws-s3/test/bucket.test.ts b/packages/@aws-cdk/aws-s3/test/bucket.test.ts index 5396f6504d585..ed727d615578a 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket.test.ts @@ -306,25 +306,19 @@ describe('bucket', () => { }); }); - test('bucketKeyEnabled can be enabled', () => { + test.each([s3.BucketEncryption.KMS, s3.BucketEncryption.KMS_MANAGED])('bucketKeyEnabled can be enabled with %p encryption', (encryption) => { const stack = new cdk.Stack(); - new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption: s3.BucketEncryption.KMS }); + new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption }); Template.fromStack(stack).hasResourceProperties('AWS::S3::Bucket', { 'BucketEncryption': { 'ServerSideEncryptionConfiguration': [ { 'BucketKeyEnabled': true, - 'ServerSideEncryptionByDefault': { - 'KMSMasterKeyID': { - 'Fn::GetAtt': [ - 'MyBucketKeyC17130CF', - 'Arn', - ], - }, + 'ServerSideEncryptionByDefault': Match.objectLike({ 'SSEAlgorithm': 'aws:kms', - }, + }), }, ], }, @@ -336,10 +330,10 @@ describe('bucket', () => { expect(() => { new s3.Bucket(stack, 'MyBucket', { bucketKeyEnabled: true, encryption: s3.BucketEncryption.S3_MANAGED }); - }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: S3MANAGED)"); + }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: S3_MANAGED)"); expect(() => { new s3.Bucket(stack, 'MyBucket3', { bucketKeyEnabled: true }); - }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: NONE)"); + }).toThrow("bucketKeyEnabled is specified, so 'encryption' must be set to KMS (value: UNENCRYPTED)"); }); diff --git a/packages/@aws-cdk/cloudformation-include/README.md b/packages/@aws-cdk/cloudformation-include/README.md index 6e10d290a5c67..66e8f1e47e296 100644 --- a/packages/@aws-cdk/cloudformation-include/README.md +++ b/packages/@aws-cdk/cloudformation-include/README.md @@ -132,7 +132,9 @@ make sure to download the unprocessed template However, certain unprocessed templates can fail when used with the `CfnInclude` class. The most common reason for the failure is that the unprocessed template can contain cycles between resources, which get removed after the Transform is processed, -but is not allowed when being included (as pure CloudFormation does not permit cycles). +but is not allowed when being included (as pure CloudFormation does not permit cycles). To enable cycle processing behavior similar +to cloudformation, set `allowCyclicalReferences` of CfnIncludeProps to true. + When that happens, you should instead download the processed template from the CloudFormation AWS Console (make sure the "View processed template" checkbox is **checked** in that case): diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index 778bd9a124152..d4b6b9877c126 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -57,6 +57,15 @@ export interface CfnIncludeProps { * @default - parameters will retain their original definitions */ readonly parameters?: { [parameterName: string]: any }; + + /** + * Specifies whether to allow cyclical references, effectively disregarding safeguards meant to avoid undeployable + * templates. This should only be set to true in the case of templates utilizing cloud transforms (e.g. SAM) that + * after processing the transform will no longer contain any circular references. + * + * @default - will throw an error on detecting any cyclical references + */ + readonly allowCyclicalReferences?: boolean; } /** @@ -99,10 +108,16 @@ export class CfnInclude extends core.CfnElement { private readonly nestedStacksToInclude: { [name: string]: CfnIncludeProps }; private readonly template: any; private readonly preserveLogicalIds: boolean; + private readonly allowCyclicalReferences: boolean; + private logicalIdToPlaceholderMap: Map; constructor(scope: Construct, id: string, props: CfnIncludeProps) { super(scope, id); + this.allowCyclicalReferences = props.allowCyclicalReferences ?? false; + + this.logicalIdToPlaceholderMap = new Map(); + this.parametersToReplace = props.parameters || {}; // read the template into a JS object @@ -584,10 +599,31 @@ export class CfnInclude extends core.CfnElement { return cfnCondition; } + private getPlaceholderID(): string { + return `Placeholder${this.logicalIdToPlaceholderMap.size}`; + } + private getOrCreateResource(logicalId: string, cycleChain: string[] = []): core.CfnResource { cycleChain = cycleChain.concat([logicalId]); if (cycleChain.length !== new Set(cycleChain).size) { - throw new Error(`Found a cycle between resources in the template: ${cycleChain.join(' depends on ')}`); + if (!this.allowCyclicalReferences) { + throw new Error(`Found a cycle between resources in the template: ${cycleChain.join(' depends on ')}`); + } + //only allow one placeholder per logical id + if (this.logicalIdToPlaceholderMap.get(logicalId)) { + return this.resources[this.logicalIdToPlaceholderMap.get(logicalId)!]; + } + let placeholderResourceAttributes: any = this.template.Resources[logicalId]; + let placeholderId: string = this.getPlaceholderID(); + this.logicalIdToPlaceholderMap.set(logicalId, placeholderId); + let placeholderInstance = new core.CfnResource(this, placeholderId, { + type: placeholderResourceAttributes.Type, + properties: {}, + }); + placeholderInstance.overrideLogicalId(placeholderId); + this.resources[placeholderId] = placeholderInstance; + + return placeholderInstance; } const ret = this.resources[logicalId]; @@ -652,6 +688,17 @@ export class CfnInclude extends core.CfnElement { } } + /* + 1. remove placeholder version of object created for cycle breaking + 2. override logical id before deletion so references to the placeholder instead reference the original + */ + if (this.logicalIdToPlaceholderMap.get(logicalId)) { + let placeholderId: string = this.logicalIdToPlaceholderMap.get(logicalId)!; + this.resources[placeholderId].overrideLogicalId(logicalId); + this.node.tryRemoveChild(placeholderId); + delete this.resources[placeholderId]; + } + this.overrideLogicalIdIfNeeded(l1Instance, logicalId); this.resources[logicalId] = l1Instance; diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index 2fa4a3f4d1f6a..dcdade326cb62 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; import * as core from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import * as constructs from 'constructs'; @@ -141,16 +142,120 @@ describe('CDK Include', () => { }).toThrow(/Short-form Fn::GetAtt must contain a '.' in its string argument, got: 'Bucket1Arn'/); }); - test('detects a cycle between resources in the template', () => { + /** + * A->B + * B->A + * simplified version of cycle-in-resources.json, an example of cyclical references + */ + test('by default does not accept a cycle between resources in the template', () => { expect(() => { includeTestTemplate(stack, 'cycle-in-resources.json'); }).toThrow(/Found a cycle between resources in the template: Bucket1 depends on Bucket2 depends on Bucket1/); }); + + /** + * A->B + * B->C + * C->{D,A} + * D->B + * simplified version of multi-cycle-in-resources.json, an example of multiple cyclical references + */ + test('by default does not accept multiple cycles between resources in the template', () => { + expect(() => { + includeTestTemplate(stack, 'multi-cycle-in-resources.json'); + }).toThrow(/Found a cycle between resources in the template: Bucket1 depends on Bucket2 depends on Bucket3 depends on Bucket4 depends on Bucket2/); + }); + + /** + * A->B + * B->{C,A} + * C->A + * simplified version of multi-cycle-multi-dest-in-resources.json, an example of multiple cyclical references that + * include visiting the same destination more than once + */ + test('by default does not accept multiple cycles and multiple destinations between resources in the template', () => { + expect(() => { + includeTestTemplate(stack, 'multi-cycle-multi-dest-in-resources.json'); + }).toThrow(/Found a cycle between resources in the template: Bucket1 depends on Bucket2 depends on Bucket3 depends on Bucket1/); + }); + + /** + * A->B + * B->A + * simplified version of cycle-in-resources.json, an example of cyclical references + */ + test('accepts a cycle between resources in the template if allowed', () => { + includeTestTemplate(stack, 'cycle-in-resources.json', { allowCyclicalReferences: true }); + Template.fromStack(stack, { skipCyclicalDependenciesCheck: true }).templateMatches( + { + Resources: { + Bucket2: { Type: 'AWS::S3::Bucket', DependsOn: ['Bucket1'] }, + Bucket1: { + Type: 'AWS::S3::Bucket', + Properties: { BucketName: { Ref: 'Bucket2' } }, + }, + }, + }, + ); + }); + + /** + * A->B + * B->C + * C->{D,A} + * D->B + * simplified version of multi-cycle-in-resources.json, an example of multiple cyclical references + */ + test('accepts multiple cycles between resources in the template if allowed', () => { + includeTestTemplate(stack, 'multi-cycle-in-resources.json', { allowCyclicalReferences: true }); + Template.fromStack(stack, { skipCyclicalDependenciesCheck: true }).templateMatches( + { + Resources: { + Bucket2: { Type: 'AWS::S3::Bucket', DependsOn: ['Bucket3'] }, + Bucket3: { Type: 'AWS::S3::Bucket', DependsOn: ['Bucket4', 'Bucket1'] }, + Bucket4: { Type: 'AWS::S3::Bucket', DependsOn: ['Bucket2'] }, + Bucket1: { + Type: 'AWS::S3::Bucket', + Properties: { BucketName: { Ref: 'Bucket2' } }, + }, + }, + }, + ); + }); + + /** + * A->B + * B->{C,A} + * C->A + * simplified version of multi-cycle-multi-dest-in-resources.json, an example of multiple cyclical references that + * include visiting the same destination more than once + */ + test('accepts multiple cycles and multiple destinations between resources in the template if allowed', () => { + includeTestTemplate(stack, 'multi-cycle-multi-dest-in-resources.json', { allowCyclicalReferences: true }); + Template.fromStack(stack, { skipCyclicalDependenciesCheck: true }).templateMatches( + { + Resources: { + Bucket2: { Type: 'AWS::S3::Bucket', DependsOn: ['Bucket3', 'Bucket1'] }, + Bucket3: { Type: 'AWS::S3::Bucket', DependsOn: ['Bucket1'] }, + Bucket1: { + Type: 'AWS::S3::Bucket', + Properties: { BucketName: { Ref: 'Bucket2' } }, + }, + }, + }, + ); + }); }); -function includeTestTemplate(scope: constructs.Construct, testTemplate: string): inc.CfnInclude { +interface IncludeTestTemplateProps { + /** @default false */ + readonly allowCyclicalReferences?: boolean; +} + +function includeTestTemplate(scope: constructs.Construct, testTemplate: string, props: IncludeTestTemplateProps = {}): inc.CfnInclude { return new inc.CfnInclude(scope, 'MyScope', { templateFile: _testTemplateFilePath(testTemplate), + allowCyclicalReferences: props.allowCyclicalReferences, }); } diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/multi-cycle-in-resources.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/multi-cycle-in-resources.json new file mode 100644 index 0000000000000..06d96804f91c3 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/multi-cycle-in-resources.json @@ -0,0 +1,24 @@ +{ + "Resources": { + "Bucket1": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "Bucket2" + } + } + }, + "Bucket2": { + "Type": "AWS::S3::Bucket", + "DependsOn": "Bucket3" + }, + "Bucket3": { + "Type": "AWS::S3::Bucket", + "DependsOn": ["Bucket4", "Bucket1"] + }, + "Bucket4": { + "Type": "AWS::S3::Bucket", + "DependsOn": "Bucket2" + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/multi-cycle-multi-dest-in-resources.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/multi-cycle-multi-dest-in-resources.json new file mode 100644 index 0000000000000..d8ed03161b255 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/multi-cycle-multi-dest-in-resources.json @@ -0,0 +1,20 @@ +{ + "Resources": { + "Bucket1": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "Bucket2" + } + } + }, + "Bucket2": { + "Type": "AWS::S3::Bucket", + "DependsOn": ["Bucket3", "Bucket1"] + }, + "Bucket3": { + "Type": "AWS::S3::Bucket", + "DependsOn": ["Bucket1"] + } + } +} diff --git a/packages/@aws-cdk/core/lib/app.ts b/packages/@aws-cdk/core/lib/app.ts index 5973445ab74b4..49359c4a94c35 100644 --- a/packages/@aws-cdk/core/lib/app.ts +++ b/packages/@aws-cdk/core/lib/app.ts @@ -1,5 +1,6 @@ import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; +import * as fs from 'fs-extra'; import { addCustomSynthesis, ICustomSynthesis } from './private/synthesis'; import { TreeMetadata } from './private/tree-metadata'; import { Stage } from './stage'; @@ -173,13 +174,13 @@ export class App extends Stage { this.node.setContext(k, v); } - // read from environment - const contextJson = process.env[cxapi.CONTEXT_ENV]; - const contextFromEnvironment = contextJson - ? JSON.parse(contextJson) - : { }; + // reconstructing the context from the two possible sources: + const context = { + ...this.readContextFromEnvironment(), + ...this.readContextFromTempFile(), + }; - for (const [k, v] of Object.entries(contextFromEnvironment)) { + for (const [k, v] of Object.entries(context)) { this.node.setContext(k, v); } @@ -188,6 +189,16 @@ export class App extends Stage { this.node.setContext(k, v); } } + + private readContextFromTempFile() { + const location = process.env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV]; + return location ? fs.readJSONSync(location) : {}; + } + + private readContextFromEnvironment() { + const contextJson = process.env[cxapi.CONTEXT_ENV]; + return contextJson ? JSON.parse(contextJson) : {}; + } } /** diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 8ca911b83b8aa..b56d13d4bf642 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -730,6 +730,19 @@ export class Stack extends Construct implements ITaggable { this.templateOptions.transforms.push(transform); } + /** + * Adds an arbitary key-value pair, with information you want to record about the stack. + * These get translated to the Metadata section of the generated template. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html + */ + public addMetadata(key: string, value: any) { + if (!this.templateOptions.metadata) { + this.templateOptions.metadata = {}; + } + this.templateOptions.metadata[key] = value; + } + /** * Called implicitly by the `addDependency` helper function in order to * realize a dependency between two top-level stacks at the assembly level. diff --git a/packages/@aws-cdk/core/test/app.test.ts b/packages/@aws-cdk/core/test/app.test.ts index d9ab8c0396f3d..ea9c58551d530 100644 --- a/packages/@aws-cdk/core/test/app.test.ts +++ b/packages/@aws-cdk/core/test/app.test.ts @@ -1,6 +1,9 @@ +import * as os from 'os'; +import * as path from 'path'; import { ContextProvider } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; +import * as fs from 'fs-extra'; import { CfnResource, DefaultStackSynthesizer, Stack, StackProps } from '../lib'; import { Annotations } from '../lib/annotations'; import { App, AppProps } from '../lib/app'; @@ -101,29 +104,55 @@ describe('app', () => { }); }); - test('context can be passed through CDK_CONTEXT', () => { - process.env[cxapi.CONTEXT_ENV] = JSON.stringify({ + test('context can be passed through CONTEXT_OVERFLOW_LOCATION_ENV', async () => { + const contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-context')); + const overflow = path.join(contextDir, 'overflow.json'); + fs.writeJSONSync(overflow, { key1: 'val1', key2: 'val2', }); + process.env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = overflow; + const prog = new App(); expect(prog.node.tryGetContext('key1')).toEqual('val1'); expect(prog.node.tryGetContext('key2')).toEqual('val2'); }); - test('context passed through CDK_CONTEXT has precedence', () => { + test('context can be passed through CDK_CONTEXT', async () => { process.env[cxapi.CONTEXT_ENV] = JSON.stringify({ key1: 'val1', key2: 'val2', }); + + const prog = new App(); + expect(prog.node.tryGetContext('key1')).toEqual('val1'); + expect(prog.node.tryGetContext('key2')).toEqual('val2'); + }); + + test('context passed through CONTEXT_OVERFLOW_LOCATION_ENV is merged with the context passed through CONTEXT_ENV', async () => { + const contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-context')); + const contextLocation = path.join(contextDir, 'context-temp.json'); + fs.writeJSONSync(contextLocation, { + key1: 'val1', + key2: 'val2', + }); + process.env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextLocation; + + process.env[cxapi.CONTEXT_ENV] = JSON.stringify({ + key3: 'val3', + key4: 'val4', + }); + const prog = new App({ context: { - key1: 'val3', - key2: 'val4', + key1: 'val5', + key2: 'val6', }, }); expect(prog.node.tryGetContext('key1')).toEqual('val1'); expect(prog.node.tryGetContext('key2')).toEqual('val2'); + expect(prog.node.tryGetContext('key3')).toEqual('val3'); + expect(prog.node.tryGetContext('key4')).toEqual('val4'); }); test('context passed through finalContext prop has precedence', () => { diff --git a/packages/@aws-cdk/core/test/stack.test.ts b/packages/@aws-cdk/core/test/stack.test.ts index b2fc01b337a87..b9692f8d77e3d 100644 --- a/packages/@aws-cdk/core/test/stack.test.ts +++ b/packages/@aws-cdk/core/test/stack.test.ts @@ -1263,6 +1263,21 @@ describe('regionalFact', () => { }, }); }); + + test('stack.addMetadata() adds metadata', () => { + const stack = new Stack(); + + stack.addMetadata('Instances', { Description: 'Information about the instances' }); + stack.addMetadata('Databases', { Description: 'Information about the databases' } ); + + expect(toCloudFormation(stack)).toEqual({ + Metadata: { + Instances: { Description: 'Information about the instances' }, + Databases: { Description: 'Information about the databases' }, + }, + }); + }); + }); class StackWithPostProcessor extends Stack { diff --git a/packages/@aws-cdk/cx-api/lib/cxapi.ts b/packages/@aws-cdk/cx-api/lib/cxapi.ts index 9b179e9a71b5f..8137dcf3907f6 100644 --- a/packages/@aws-cdk/cx-api/lib/cxapi.ts +++ b/packages/@aws-cdk/cx-api/lib/cxapi.ts @@ -4,6 +4,11 @@ export const OUTDIR_ENV = 'CDK_OUTDIR'; export const CONTEXT_ENV = 'CDK_CONTEXT_JSON'; +/** + * The name of the temporary file where the context is stored. + */ +export const CONTEXT_OVERFLOW_LOCATION_ENV = 'CONTEXT_OVERFLOW_LOCATION_ENV'; + /** * Environment variable set by the CDK CLI with the default AWS account ID. */ diff --git a/packages/@aws-cdk/region-info/lib/aws-entities.ts b/packages/@aws-cdk/region-info/lib/aws-entities.ts index 0c28399449434..d91e34848c73f 100644 --- a/packages/@aws-cdk/region-info/lib/aws-entities.ts +++ b/packages/@aws-cdk/region-info/lib/aws-entities.ts @@ -1,8 +1,3 @@ -/** - * After this point, SSM only creates regional principals - */ -export const RULE_SSM_PRINCIPALS_ARE_REGIONAL = Symbol('SSM_PRINCIPALS_ARE_REGIONAL'); - /** * After this point, S3 website domains look like `s3-website.REGION.s3.amazonaws.com` * @@ -49,7 +44,6 @@ export const AWS_REGIONS_AND_RULES: readonly (string | symbol)[] = [ 'ap-northeast-3', // Asia Pacific (Osaka) 'us-gov-east-1', // AWS GovCloud (US-East) 'eu-north-1', // Europe (Stockholm) - RULE_SSM_PRINCIPALS_ARE_REGIONAL, 'ap-east-1', // Asia Pacific (Hong Kong) 'me-south-1', // Middle East (Bahrain) 'eu-south-1', // Europe (Milan) diff --git a/packages/@aws-cdk/region-info/lib/default.ts b/packages/@aws-cdk/region-info/lib/default.ts index 39a8db5ddedc9..c240c55ec1068 100644 --- a/packages/@aws-cdk/region-info/lib/default.ts +++ b/packages/@aws-cdk/region-info/lib/default.ts @@ -1,5 +1,3 @@ -import { before, RULE_SSM_PRINCIPALS_ARE_REGIONAL } from './aws-entities'; - /** * Provides default values for certain regional information points. */ @@ -81,12 +79,6 @@ export class Default { } switch (service) { - // SSM turned from global to regional at some point - case 'ssm': - return before(region, RULE_SSM_PRINCIPALS_ARE_REGIONAL) - ? universal - : regional; - // CodeDeploy is regional+partitional in CN, only regional everywhere else case 'codedeploy': return region.startsWith('cn-') diff --git a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap index dae9b4dda9d16..2ce71cae5df83 100644 --- a/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap +++ b/packages/@aws-cdk/region-info/test/__snapshots__/region-info.test.js.snap @@ -30,7 +30,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.af-south-1.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.af-south-1.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", @@ -63,7 +63,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.ap-east-1.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.ap-east-1.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", @@ -294,7 +294,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.ap-southeast-3.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.ap-southeast-3.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", @@ -492,7 +492,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.eu-south-1.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.eu-south-1.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", @@ -525,7 +525,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.eu-south-2.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.eu-south-2.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", @@ -657,7 +657,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.me-south-1.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.me-south-1.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "com.amazonaws.vpce", @@ -888,7 +888,7 @@ Object { "s3": "s3.amazonaws.com", "sns": "sns.amazonaws.com", "sqs": "sqs.amazonaws.com", - "ssm": "ssm.us-iso-west-1.amazonaws.com", + "ssm": "ssm.amazonaws.com", "states": "states.amazonaws.com", }, "vpcEndPointServiceNamePrefix": "gov.ic.c2s.vpce", diff --git a/packages/@aws-cdk/region-info/test/default.test.ts b/packages/@aws-cdk/region-info/test/default.test.ts index b9c7a4375f8fd..10651a4788070 100644 --- a/packages/@aws-cdk/region-info/test/default.test.ts +++ b/packages/@aws-cdk/region-info/test/default.test.ts @@ -5,7 +5,7 @@ const urlSuffix = '.nowhere.null'; describe('servicePrincipal', () => { for (const suffix of ['', '.amazonaws.com', '.amazonaws.com.cn']) { - for (const service of ['codedeploy', 'states', 'ssm']) { + for (const service of ['codedeploy', 'states']) { test(`${service}${suffix}`, () => { expect(Default.servicePrincipal(`${service}${suffix}`, region, urlSuffix)).toBe(`${service}.${region}.amazonaws.com`); }); @@ -58,11 +58,15 @@ describe('servicePrincipal', () => { describe('spot-check some service principals', () => { test('ssm', () => { + // SSM has advertised in its documentation that it is regional after a certain point, but that + // documentation only applies to SSM Inventory, not SSM Automation. Plus, there is no need for + // a different service principal, as all accounts are (at least currently) included in the global + // one. expectServicePrincipals('ssm.amazonaws.com', { 'us-east-1': 'ssm.amazonaws.com', 'eu-north-1': 'ssm.amazonaws.com', - 'ap-east-1': 'ssm.ap-east-1.amazonaws.com', - 'eu-south-1': 'ssm.eu-south-1.amazonaws.com', + 'ap-east-1': 'ssm.amazonaws.com', + 'eu-south-1': 'ssm.amazonaws.com', }); }); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 2be431787ca1a..57356153cf7b2 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -336,19 +336,19 @@ The `progress` key can also be specified as a user setting (`~/.cdk.json`) #### CloudFormation Change Sets vs direct stack updates -By default CDK will create a CloudFormation change with the changes that will -be deployed, and then executes it. This behavior can be controlled with the +By default, CDK creates a CloudFormation change set with the changes that will +be deployed and then executes it. This behavior can be controlled with the `--method` parameter: - `--method=change-set` (default): create and execute the change set. -- `--method=prepare-change-set`: create teh change set but don't execute it. +- `--method=prepare-change-set`: create the change set but don't execute it. This is useful if you have external tools that will inspect the change set or you have an approval process for change sets. - `--method=direct`: do not create a change set but apply the change immediately. This is typically a bit faster than creating a change set, but it loses the progress information. -To have deploy faster without using change sets: +To deploy faster without using change sets: ```console $ cdk deploy --method=direct diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index 34fae3c59c4c6..01259f7a11763 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -1,10 +1,14 @@ import * as childProcess from 'child_process'; +import * as os from 'os'; import * as path from 'path'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; -import { debug } from '../../logging'; +import * as semver from 'semver'; +import { debug, warning } from '../../logging'; import { Configuration, PROJECT_CONFIG, USER_DEFAULTS } from '../../settings'; +import { loadTree, some } from '../../tree'; +import { splitBySize } from '../../util/objects'; import { versionNumber } from '../../version'; import { SdkProvider } from '../aws-auth'; @@ -44,7 +48,6 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom context[cxapi.BUNDLING_STACKS] = bundlingStacks; debug('context:', context); - env[cxapi.CONTEXT_ENV] = JSON.stringify(context); const build = config.settings.get(['build']); if (build) { @@ -83,9 +86,28 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom debug('env:', env); + const envVariableSizeLimit = os.platform() === 'win32' ? 32760 : 131072; + const [smallContext, overflow] = splitBySize(context, spaceAvailableForContext(env, envVariableSizeLimit)); + + // Store the safe part in the environment variable + env[cxapi.CONTEXT_ENV] = JSON.stringify(smallContext); + + // If there was any overflow, write it to a temporary file + let contextOverflowLocation; + if (Object.keys(overflow ?? {}).length > 0) { + const contextDir = await fs.mkdtemp(path.join(os.tmpdir(), 'cdk-context')); + contextOverflowLocation = path.join(contextDir, 'context-overflow.json'); + fs.writeJSONSync(contextOverflowLocation, overflow); + env[cxapi.CONTEXT_OVERFLOW_LOCATION_ENV] = contextOverflowLocation; + } + await exec(commandLine.join(' ')); - return createAssembly(outdir); + const assembly = createAssembly(outdir); + + contextOverflowCleanup(contextOverflowLocation, assembly); + + return assembly; function createAssembly(appDir: string) { try { @@ -215,3 +237,33 @@ async function guessExecutable(commandLine: string[]) { } return commandLine; } + +function contextOverflowCleanup(location: string | undefined, assembly: cxapi.CloudAssembly) { + if (location) { + fs.removeSync(path.dirname(location)); + + const tree = loadTree(assembly); + const frameworkDoesNotSupportContextOverflow = some(tree, node => { + const fqn = node.constructInfo?.fqn; + const version = node.constructInfo?.version; + return (fqn === 'aws-cdk-lib.App' && version != null && semver.lte(version, '2.38.0')) + || fqn === '@aws-cdk/core.App'; // v1 + }); + + // We're dealing with an old version of the framework here. It is unaware of the temporary + // file, which means that it will ignore the context overflow. + if (frameworkDoesNotSupportContextOverflow) { + warning('Part of the context could not be sent to the application. Please update the AWS CDK library to the latest version.'); + } + } +} + +function spaceAvailableForContext(env: { [key: string]: string }, limit: number) { + const size = (value: string) => value != null ? Buffer.byteLength(value) : 0; + + const usedSpace = Object.entries(env) + .map(([k, v]) => k === cxapi.CONTEXT_ENV ? size(k) : size(k) + size(v)) + .reduce((a, b) => a + b, 0); + + return Math.max(0, limit - usedSpace); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/commands/doctor.ts b/packages/aws-cdk/lib/commands/doctor.ts index e1942bbd06b2b..5ea4ddc6367eb 100644 --- a/packages/aws-cdk/lib/commands/doctor.ts +++ b/packages/aws-cdk/lib/commands/doctor.ts @@ -51,7 +51,7 @@ function displayCdkEnvironmentVariables() { print('ℹ️ CDK environment variables:'); let healthy = true; for (const key of keys.sort()) { - if (key === cxapi.CONTEXT_ENV || key === cxapi.OUTDIR_ENV) { + if (key === cxapi.CONTEXT_ENV || key === cxapi.CONTEXT_OVERFLOW_LOCATION_ENV || key === cxapi.OUTDIR_ENV) { print(` - ${chalk.red(key)} = ${chalk.green(process.env[key]!)} (⚠️ reserved for use by the CDK toolkit)`); healthy = false; } else { diff --git a/packages/aws-cdk/lib/notices.ts b/packages/aws-cdk/lib/notices.ts index 3d4b48a7154b5..7370b84a36f9f 100644 --- a/packages/aws-cdk/lib/notices.ts +++ b/packages/aws-cdk/lib/notices.ts @@ -3,7 +3,8 @@ import * as https from 'https'; import * as path from 'path'; import * as fs from 'fs-extra'; import * as semver from 'semver'; -import { debug, print, trace } from './logging'; +import { debug, print } from './logging'; +import { some, ConstructTreeNode, loadTreeFromDir } from './tree'; import { flatMap } from './util'; import { cdkCacheDir } from './util/directories'; import { versionNumber } from './version'; @@ -79,7 +80,7 @@ export function filterNotices(data: Notice[], options: FilterNoticeOptions): Not const filter = new NoticeFilter({ cliVersion: options.cliVersion ?? versionNumber(), acknowledgedIssueNumbers: options.acknowledgedIssueNumbers ?? new Set(), - tree: loadTree(options.outdir ?? 'cdk.out').tree, + tree: loadTreeFromDir(options.outdir ?? 'cdk.out'), }); return data.filter(notice => filter.apply(notice)); } @@ -336,51 +337,3 @@ function match(query: Component[], tree: ConstructTreeNode): boolean { return semver.satisfies(target ?? '', pattern); } } - -function loadTree(outdir: string) { - try { - return fs.readJSONSync(path.join(outdir, 'tree.json')); - } catch (e) { - trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); - return {}; - } -} - -/** - * Source information on a construct (class fqn and version) - */ -interface ConstructInfo { - readonly fqn: string; - readonly version: string; -} - -/** - * A node in the construct tree. - * @internal - */ -interface ConstructTreeNode { - readonly id: string; - readonly path: string; - readonly children?: { [key: string]: ConstructTreeNode }; - readonly attributes?: { [key: string]: any }; - - /** - * Information on the construct class that led to this node, if available - */ - readonly constructInfo?: ConstructInfo; -} - -function some(node: ConstructTreeNode, predicate: (n: ConstructTreeNode) => boolean): boolean { - return node != null && (predicate(node) || findInChildren()); - - function findInChildren(): boolean { - if (node.children == null) { return false; } - - for (const name in node.children) { - if (some(node.children[name], predicate)) { - return true; - } - } - return false; - } -} diff --git a/packages/aws-cdk/lib/tree.ts b/packages/aws-cdk/lib/tree.ts new file mode 100644 index 0000000000000..57ae5685ae17c --- /dev/null +++ b/packages/aws-cdk/lib/tree.ts @@ -0,0 +1,58 @@ +import * as path from 'path'; +import { CloudAssembly } from '@aws-cdk/cx-api'; +import * as fs from 'fs-extra'; +import { trace } from './logging'; + +/** + * Source information on a construct (class fqn and version) + */ +export interface ConstructInfo { + readonly fqn: string; + readonly version: string; +} + +/** + * A node in the construct tree. + */ +export interface ConstructTreeNode { + readonly id: string; + readonly path: string; + readonly children?: { [key: string]: ConstructTreeNode }; + readonly attributes?: { [key: string]: any }; + + /** + * Information on the construct class that led to this node, if available + */ + readonly constructInfo?: ConstructInfo; +} + +/** + * Whether the provided predicate is true for at least one element in the construct (sub-)tree. + */ +export function some(node: ConstructTreeNode, predicate: (n: ConstructTreeNode) => boolean): boolean { + return node != null && (predicate(node) || findInChildren()); + + function findInChildren(): boolean { + return Object.values(node.children ?? {}).some(child => some(child, predicate)); + } +} + +export function loadTree(assembly: CloudAssembly) { + try { + const outdir = assembly.directory; + const fileName = assembly.tree()?.file; + return fileName ? fs.readJSONSync(path.join(outdir, fileName)).tree : {}; + } catch (e) { + trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); + return {}; + } +} + +export function loadTreeFromDir(outdir: string) { + try { + return fs.readJSONSync(path.join(outdir, 'tree.json')).tree; + } catch (e) { + trace(`Failed to get tree.json file: ${e}. Proceeding with empty tree.`); + return {}; + } +} diff --git a/packages/aws-cdk/lib/util/objects.ts b/packages/aws-cdk/lib/util/objects.ts index 7fb660525687c..c372152a2cc5f 100644 --- a/packages/aws-cdk/lib/util/objects.ts +++ b/packages/aws-cdk/lib/util/objects.ts @@ -134,3 +134,39 @@ export function deepMerge(...objects: Array | undefined>) { others.forEach(other => mergeOne(into, other)); return into; } + +/** + * Splits the given object into two, such that: + * + * 1. The size of the first object (after stringified in UTF-8) is less than or equal to the provided size limit. + * 2. Merging the two objects results in the original one. + */ +export function splitBySize(data: any, maxSizeBytes: number): [any, any] { + if (maxSizeBytes < 2) { + // It's impossible to fit anything in the first object + return [undefined, data]; + } + const entries = Object.entries(data); + return recurse(0, 0); + + function recurse(index: number, runningTotalSize: number): [any, any] { + if (index >= entries.length) { + // Everything fits in the first object + return [data, undefined]; + } + + const size = runningTotalSize + entrySize(entries[index]); + return (size > maxSizeBytes) ? cutAt(index) : recurse(index + 1, size); + } + + function entrySize(entry: [string, unknown]) { + return Buffer.byteLength(JSON.stringify(Object.fromEntries([entry]))); + } + + function cutAt(index: number): [any, any] { + return [ + Object.fromEntries(entries.slice(0, index)), + Object.fromEntries(entries.slice(index)), + ]; + } +} \ No newline at end of file diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index dcd2f52dd01df..ba1c5ec0179d8 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -88,7 +88,8 @@ "ts-jest": "^27.1.5", "ts-mock-imports": "^1.3.8", "xml-js": "^1.6.11", - "axios": "^0.27.2" + "axios": "^0.27.2", + "fast-check": "^2.25.0" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", diff --git a/packages/aws-cdk/test/integ/common/jest-test.bash b/packages/aws-cdk/test/integ/common/jest-test.bash index b29ca90036d38..7a96a9f845155 100755 --- a/packages/aws-cdk/test/integ/common/jest-test.bash +++ b/packages/aws-cdk/test/integ/common/jest-test.bash @@ -5,7 +5,7 @@ function invokeJest() { # package.json. if ! npx --no-install jest --version; then echo 'Looks like we need to install jest first. Hold on.' >& 2 - npm install --prefix .. jest jest-junit aws-sdk axios + npm install --prefix .. jest@^27 jest-junit@^14 aws-sdk@^2 axios@^0.27.2 fi # This must --runInBand because parallelism is arranged for inside the tests diff --git a/packages/aws-cdk/test/tree.test.ts b/packages/aws-cdk/test/tree.test.ts new file mode 100644 index 0000000000000..0feb5725c21df --- /dev/null +++ b/packages/aws-cdk/test/tree.test.ts @@ -0,0 +1,114 @@ +import * as path from 'path'; +import { ConstructTreeNode, loadTreeFromDir, some } from '../lib/tree'; + +describe('some', () => { + const tree: ConstructTreeNode = { + id: 'App', + path: '', + children: { + Tree: { + id: 'Tree', + path: 'Tree', + constructInfo: { + fqn: '@aws-cdk/core.Construct', + version: '1.162.0', + }, + }, + stack: { + id: 'stack', + path: 'stack', + children: { + bucket: { + id: 'bucket', + path: 'stack/bucket', + children: { + Resource: { + id: 'Resource', + path: 'stack/bucket/Resource', + attributes: { + 'aws:cdk:cloudformation:type': 'AWS::S3::Bucket', + 'aws:cdk:cloudformation:props': {}, + }, + constructInfo: { + fqn: '@aws-cdk/aws-s3.CfnBucket', + version: '1.162.0', + }, + }, + }, + constructInfo: { + fqn: '@aws-cdk/aws-s3.Bucket', + version: '1.162.0', + }, + }, + CDKMetadata: { + id: 'CDKMetadata', + path: 'stack/CDKMetadata', + children: { + Default: { + id: 'Default', + path: 'stack/CDKMetadata/Default', + constructInfo: { + fqn: '@aws-cdk/core.CfnResource', + version: '1.162.0', + }, + }, + Condition: { + id: 'Condition', + path: 'stack/CDKMetadata/Condition', + constructInfo: { + fqn: '@aws-cdk/core.CfnCondition', + version: '1.162.0', + }, + }, + }, + constructInfo: { + fqn: '@aws-cdk/core.Construct', + version: '1.162.0', + }, + }, + }, + constructInfo: { + fqn: '@aws-cdk/core.Stack', + version: '1.162.0', + }, + }, + }, + constructInfo: { + fqn: '@aws-cdk/core.App', + version: '1.162.0', + }, + }; + + test('tree matches predicate', () => { + expect(some(tree, node => node.constructInfo?.fqn === '@aws-cdk/aws-s3.Bucket')).toBe(true); + }); + + test('tree does not match predicate', () => { + expect(some(tree, node => node.constructInfo?.fqn === '@aws-cdk/aws-lambda.Function')).toBe(false); + }); + + test('childless tree', () => { + const childless = { + id: 'App', + path: '', + constructInfo: { + fqn: '@aws-cdk/core.App', + version: '1.162.0', + }, + }; + + expect(some(childless, node => node.path.length > 0)).toBe(false); + }); +}); + +describe('loadTreeFromDir', () => { + test('can find tree', () => { + const tree = loadTreeFromDir(path.join(__dirname, 'cloud-assembly-trees/built-with-1_144_0')); + expect(tree.id).toEqual('App'); + }); + + test('cannot find tree', () => { + const tree = loadTreeFromDir(path.join(__dirname, 'cloud-assembly-trees/foo')); + expect(tree).toEqual({}); + }); +}); \ No newline at end of file diff --git a/packages/aws-cdk/test/util/objects.test.ts b/packages/aws-cdk/test/util/objects.test.ts index ad8db09e09002..ef9b23d2e4aed 100644 --- a/packages/aws-cdk/test/util/objects.test.ts +++ b/packages/aws-cdk/test/util/objects.test.ts @@ -1,4 +1,5 @@ -import { deepClone, deepGet, deepMerge, deepSet } from '../../lib/util'; +import * as fc from 'fast-check'; +import { deepClone, deepGet, deepMerge, deepSet, splitBySize } from '../../lib/util'; test('deepSet can set deeply', () => { const obj = {}; @@ -44,3 +45,20 @@ test('deepMerge does not overwrite if rightmost is "undefined"', () => { expect(original).toEqual({ a: 1 }); }); + +describe('splitBySize', () => { + test('objects are split at the right place', () => { + fc.assert( + fc.property(fc.object(), fc.integer({ min: 2 }), (data, size) => { + const [first, second] = splitBySize(data, size); + + expect(Buffer.from(JSON.stringify(first)).length).toBeLessThanOrEqual(size); + expect(merge(first, second)).toEqual(data); + }), + ); + + function merge(fst: any, snd: any) { + return { ...(fst ?? {}), ...(snd ?? {}) }; + } + }); +}); \ No newline at end of file diff --git a/tools/@aws-cdk/prlint/lint.ts b/tools/@aws-cdk/prlint/lint.ts index 425be04d6f8e4..d01c3f2121126 100644 --- a/tools/@aws-cdk/prlint/lint.ts +++ b/tools/@aws-cdk/prlint/lint.ts @@ -29,6 +29,19 @@ export interface GitHubFile { readonly filename: string; } +export interface Review { + id: number; + user: { + login: string + }; + body: string; + state: string; +} + +export interface Comment { + id: number; +} + class LinterError extends Error { constructor(message: string) { super(message); @@ -158,37 +171,97 @@ export interface PullRequestLinterProps { export class PullRequestLinter { private readonly client: Octokit; private readonly prParams: { owner: string, repo: string, pull_number: number }; + private readonly issueParams: { owner: string, repo: string, issue_number: number }; constructor(private readonly props: PullRequestLinterProps) { this.client = props.client; this.prParams = { owner: props.owner, repo: props.repo, pull_number: props.number }; + this.issueParams = { owner: props.owner, repo: props.repo, issue_number: props.number }; } /** - * Dismisses previous reviews by aws-cdk-automation when changes have been made to the pull request. + * Deletes the previous linter comment if it exists. */ - private async dismissPreviousPRLinterReviews(): Promise { - const reviews = await this.client.pulls.listReviews(this.prParams); - reviews.data.forEach(async (review: any) => { - if (review.user?.login === 'aws-cdk-automation' && review.state !== 'DISMISSED') { - await this.client.pulls.dismissReview({ - ...this.prParams, - review_id: review.id, - message: 'Pull Request updated. Dissmissing previous PRLinter Review.', - }) - } + private async deletePRLinterComment(): Promise { + // Since previous versions of this pr linter didn't add comments, we need to do this check first. + const comment = await this.findExistingComment(); + if (comment) { + await this.client.issues.deleteComment({ + ...this.issueParams, + comment_id: comment.id, + }); + }; + }; + + /** + * Dismisses previous reviews by aws-cdk-automation when the pull request succeeds the linter. + * @param existingReview The review created by a previous run of the linter + */ + private async dismissPRLinterReview(existingReview?: Review): Promise { + if (existingReview) { + await this.client.pulls.dismissReview({ + ...this.prParams, + review_id: existingReview.id, + message: '✅ Updated pull request passes all PRLinter validations. Dissmissing previous PRLinter review.' + }) + } + } + + /** + * Creates a new review and comment for first run with failure or creates a new comment with new failures for existing reviews. + * @param failureMessages The failures received by the pr linter validation checks. + * @param existingReview The review created by a previous run of the linter. + */ + private async createOrUpdatePRLinterReview(failureMessages: string[], existingReview?: Review): Promise { + const body = `The pull request linter fails with the following errors:${this.formatErrors(failureMessages)}PRs must pass status checks before we can provide a meaningful review.`; + if (!existingReview) { + await this.client.pulls.createReview({ + ...this.prParams, + body: 'The pull request linter has failed. See the aws-cdk-automation comment below for failure reasons.' + + ' If you believe this pull request should receive an exemption, please comment and provide a justification.', + event: 'REQUEST_CHANGES', + }) + } + + await this.client.issues.createComment({ + ...this.issueParams, + body, }) + + throw new LinterError(body); + } + + /** + * Finds existing review, if present + * @returns Existing review, if present + */ + private async findExistingReview(): Promise { + const reviews = await this.client.pulls.listReviews(this.prParams); + return reviews.data.find((review) => review.user?.login === 'aws-cdk-automation' && review.state !== 'DISMISSED') as Review; + } + + /** + * Finds existing comment from previous review, if present + * @returns Existing comment, if present + */ + private async findExistingComment(): Promise { + const comments = await this.client.issues.listComments(this.issueParams); + return comments.data.find((comment) => comment.user?.login === 'aws-cdk-automation' && comment.body?.startsWith('The pull request linter fails with the following errors:')) as Comment; } /** * Creates a new review, requesting changes, with the reasons that the linter did not pass. - * @param failureReasons The list of reasons why the linter failed + * @param result The result of the PR Linter run. */ - private async communicateResult(failureReasons: string[]): Promise { - const body = `The Pull Request Linter fails with the following errors:${this.formatErrors(failureReasons)}PRs must pass status checks before we can provide a meaningful review.`; - await this.client.pulls.createReview({ ...this.prParams, body, event: 'REQUEST_CHANGES', }); - throw new LinterError(body); + private async communicateResult(result: ValidationCollector): Promise { + const existingReview = await this.findExistingReview(); + if (result.isValid()) { + console.log("✅ Success"); + await this.dismissPRLinterReview(existingReview); + } else { + await this.createOrUpdatePRLinterReview(result.errors, existingReview); + } } /** @@ -245,8 +318,8 @@ export class PullRequestLinter { testRuleSet: [ { test: noCliChanges } ], }); - await this.dismissPreviousPRLinterReviews(); - validationCollector.isValid() ? console.log("✅ Success") : await this.communicateResult(validationCollector.errors); + await this.deletePRLinterComment(); + await this.communicateResult(validationCollector); } private formatErrors(errors: string[]) { diff --git a/tools/@aws-cdk/prlint/test/lint.test.ts b/tools/@aws-cdk/prlint/test/lint.test.ts index 5cd298eb65a49..9a22289c7589d 100644 --- a/tools/@aws-cdk/prlint/test/lint.test.ts +++ b/tools/@aws-cdk/prlint/test/lint.test.ts @@ -168,7 +168,7 @@ describe('integration tests required on features', () => { ]; const prLinter = configureMock(issue, files); await expect(prLinter.validate()).rejects.toThrow( - 'The Pull Request Linter fails with the following errors:' + + 'The pull request linter fails with the following errors:' + '\n\n\t❌ Features must contain a change to an integration test file and the resulting snapshot.' + '\n\nPRs must pass status checks before we can provide a meaningful review.' ); @@ -198,7 +198,7 @@ describe('integration tests required on features', () => { ]; const prLinter = configureMock(issue, files); await expect(prLinter.validate()).rejects.toThrow( - 'The Pull Request Linter fails with the following errors:' + + 'The pull request linter fails with the following errors:' + '\n\n\t❌ Features must contain a change to an integration test file and the resulting snapshot.' + '\n\nPRs must pass status checks before we can provide a meaningful review.' ); @@ -228,7 +228,7 @@ describe('integration tests required on features', () => { ]; const prLinter = configureMock(issue, files); await expect(prLinter.validate()).rejects.toThrow( - 'The Pull Request Linter fails with the following errors:' + + 'The pull request linter fails with the following errors:' + '\n\n\t❌ Fixes must contain a change to an integration test file and the resulting snapshot.' + '\n\nPRs must pass status checks before we can provide a meaningful review.' ); @@ -258,7 +258,7 @@ describe('integration tests required on features', () => { ]; const prLinter = configureMock(issue, files); await expect(prLinter.validate()).rejects.toThrow( - 'The Pull Request Linter fails with the following errors:' + + 'The pull request linter fails with the following errors:' + '\n\n\t❌ Fixes must contain a change to an integration test file and the resulting snapshot.' + '\n\nPRs must pass status checks before we can provide a meaningful review.' ); @@ -355,12 +355,21 @@ function configureMock(pr: linter.GitHubPr, prFiles?: linter.GitHubFile[]): lint }, listReviews(_props: { _owner: string, _repo: string, _pull_number: number }) { - return { data: [{ id: 1111122222, user: { login: 'aws-cdk-automation' }, state: 'CHANGES_REQUESTED' }] }; + return { data: [{ id: 1111122222, user: { login: 'aws-cdk-automation' }, state: 'CHANGES_REQUESTED' }] }; }, dismissReview() {}, + }; - } + const issuesClient = { + createComment() {}, + + deleteComment() {}, + + listComments() { + return { data: [{ id: 1212121212, user: { login: 'aws-cdk-automation' }, body: 'The pull request linter fails with the following errors:' }] } + } + }; return new linter.PullRequestLinter({ owner: 'aws', repo: 'aws-cdk', @@ -369,6 +378,7 @@ function configureMock(pr: linter.GitHubPr, prFiles?: linter.GitHubFile[]): lint // hax hax client: { pulls: pullsClient as any, + issues: issuesClient as any, } as any, }) }