From dadc9d129b7dc72da4cb9d70e26cefdf6bf6b84e Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 21 May 2020 11:33:51 +0200 Subject: [PATCH 01/33] chore: integ test flakiness (#8124) Stacks that are being deployed using roles need to be deleted while the role is still present. Our test was deleting the role before the stack was being deleted, and so would fail. This was a race condition due to a missing `await`. Fix both. Co-authored-by: Elad Ben-Israel --- packages/aws-cdk/test/integ/cli/cli.integtest.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/aws-cdk/test/integ/cli/cli.integtest.ts b/packages/aws-cdk/test/integ/cli/cli.integtest.ts index a6cc887384353..731f901d90a0d 100644 --- a/packages/aws-cdk/test/integ/cli/cli.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/cli.integtest.ts @@ -316,8 +316,14 @@ test('deploy with role', async () => { options: ['--role-arn', roleArn], }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await cdkDestroy('test-2'); + } finally { - deleteRole(); + await deleteRole(); } async function deleteRole() { From 0a3785b7626633fcbdf26ab793c70f2bc017314b Mon Sep 17 00:00:00 2001 From: Niranjan Jayakar Date: Thu, 21 May 2020 11:47:17 +0100 Subject: [PATCH 02/33] feat(cloudtrail): user specified log group (#8079) Allow for users to set their own log group that CloudTrail must send events to. Expose a log group instance property that returns the user specified or auto-created log group. closes #6162 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudtrail/README.md | 4 +- .../@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts | 34 +++++++++++---- .../aws-cloudtrail/test/cloudtrail.test.ts | 42 ++++++++++++++++++- 3 files changed, 68 insertions(+), 12 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudtrail/README.md b/packages/@aws-cdk/aws-cloudtrail/README.md index 11f2f93760ff9..313f425125fb3 100644 --- a/packages/@aws-cdk/aws-cloudtrail/README.md +++ b/packages/@aws-cdk/aws-cloudtrail/README.md @@ -51,7 +51,9 @@ const trail = new cloudtrail.Trail(this, 'CloudTrail', { ``` This creates the same setup as above - but also logs events to a created CloudWatch Log stream. -By default, the created log group has a retention period of 365 Days, but this is also configurable. +By default, the created log group has a retention period of 365 Days, but this is also configurable +via the `cloudWatchLogsRetention` property. If you would like to specify the log group explicitly, +use the `cloudwatchLogGroup` property. For using CloudTrail event selector to log specific S3 events, you can use the `CloudTrailProps` configuration object. diff --git a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts index d12c7be68dd8b..471587a0696ef 100644 --- a/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts +++ b/packages/@aws-cdk/aws-cloudtrail/lib/cloudtrail.ts @@ -63,12 +63,19 @@ export interface TrailProps { readonly sendToCloudWatchLogs?: boolean; /** - * How long to retain logs in CloudWatchLogs. Ignored if sendToCloudWatchLogs is false + * How long to retain logs in CloudWatchLogs. + * Ignored if sendToCloudWatchLogs is false or if cloudWatchLogGroup is set. * - * @default logs.RetentionDays.OneYear + * @default logs.RetentionDays.ONE_YEAR */ readonly cloudWatchLogsRetention?: logs.RetentionDays; + /** + * Log Group to which CloudTrail to push logs to. Ignored if sendToCloudWatchLogs is set to false. + * @default - a new log group is created and used. + */ + readonly cloudWatchLogGroup?: logs.ILogGroup; + /** The AWS Key Management Service (AWS KMS) key ID that you want to use to encrypt CloudTrail logs. * * @default - No encryption. @@ -171,6 +178,12 @@ export class Trail extends Resource { */ public readonly trailSnsTopicArn: string; + /** + * The CloudWatch log group to which CloudTrail events are sent. + * `undefined` if `sendToCloudWatchLogs` property is false. + */ + public readonly logGroup?: logs.ILogGroup; + private s3bucket: s3.IBucket; private eventSelectors: EventSelector[] = []; @@ -200,19 +213,22 @@ export class Trail extends Resource { }, })); - let logGroup: logs.CfnLogGroup | undefined; let logsRole: iam.IRole | undefined; if (props.sendToCloudWatchLogs) { - logGroup = new logs.CfnLogGroup(this, 'LogGroup', { - retentionInDays: props.cloudWatchLogsRetention || logs.RetentionDays.ONE_YEAR, - }); + if (props.cloudWatchLogGroup) { + this.logGroup = props.cloudWatchLogGroup; + } else { + this.logGroup = new logs.LogGroup(this, 'LogGroup', { + retention: props.cloudWatchLogsRetention ?? logs.RetentionDays.ONE_YEAR, + }); + } logsRole = new iam.Role(this, 'LogsRole', { assumedBy: cloudTrailPrincipal }); logsRole.addToPolicy(new iam.PolicyStatement({ actions: ['logs:PutLogEvents', 'logs:CreateLogStream'], - resources: [logGroup.attrArn], + resources: [this.logGroup.logGroupArn], })); } @@ -234,8 +250,8 @@ export class Trail extends Resource { kmsKeyId: props.kmsKey && props.kmsKey.keyArn, s3BucketName: this.s3bucket.bucketName, s3KeyPrefix: props.s3KeyPrefix, - cloudWatchLogsLogGroupArn: logGroup && logGroup.attrArn, - cloudWatchLogsRoleArn: logsRole && logsRole.roleArn, + cloudWatchLogsLogGroupArn: this.logGroup?.logGroupArn, + cloudWatchLogsRoleArn: logsRole?.roleArn, snsTopicName: props.snsTopic, eventSelectors: this.eventSelectors, }); diff --git a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts index 019b0a9472626..bfcb91c06ddba 100644 --- a/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts +++ b/packages/@aws-cdk/aws-cloudtrail/test/cloudtrail.test.ts @@ -2,7 +2,7 @@ import { SynthUtils } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { RetentionDays } from '@aws-cdk/aws-logs'; +import { LogGroup, RetentionDays } from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack } from '@aws-cdk/core'; import { ReadWriteType, Trail } from '../lib'; @@ -176,7 +176,7 @@ describe('cloudtrail', () => { Effect: 'Allow', Action: ['logs:PutLogEvents', 'logs:CreateLogStream'], Resource: { - 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroupAAD65144', 'Arn'], + 'Fn::GetAtt': ['MyAmazingCloudTrailLogGroup2BE67F87', 'Arn'], }, }], }, @@ -205,6 +205,44 @@ describe('cloudtrail', () => { const trail: any = SynthUtils.synthesize(stack).template.Resources.MyAmazingCloudTrail54516E8D; expect(trail.DependsOn).toEqual([logsRolePolicyName, logsRoleName, 'MyAmazingCloudTrailS3Policy39C120B0']); }); + + test('enabled and with custom log group', () => { + const stack = getTestStack(); + const cloudWatchLogGroup = new LogGroup(stack, 'MyLogGroup', { + retention: RetentionDays.FIVE_DAYS, + }); + new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: true, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + cloudWatchLogGroup, + }); + + expect(stack).toHaveResource('AWS::Logs::LogGroup', { + RetentionInDays: 5, + }); + + expect(stack).toHaveResource('AWS::CloudTrail::Trail', { + CloudWatchLogsLogGroupArn: stack.resolve(cloudWatchLogGroup.logGroupArn), + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Resource: stack.resolve(cloudWatchLogGroup.logGroupArn), + }], + }, + }); + }); + + test('disabled', () => { + const stack = getTestStack(); + const t = new Trail(stack, 'MyAmazingCloudTrail', { + sendToCloudWatchLogs: false, + cloudWatchLogsRetention: RetentionDays.ONE_WEEK, + }); + expect(t.logGroup).toBeUndefined(); + expect(stack).not.toHaveResource('AWS::Logs::LogGroup'); + }); }); describe('with event selectors', () => { From 903701aafd7343185dafdbe114c001a33ad13824 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 21 May 2020 05:06:47 -0700 Subject: [PATCH 03/33] chore(cloudwatch-actions): clear linter exclusions (#8046) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts | 4 ++++ .../@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts | 4 ++++ packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts | 3 +++ packages/@aws-cdk/aws-cloudwatch-actions/package.json | 7 ------- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts index bcb4b2ed1c581..2241796f47d2b 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/appscaling.ts @@ -9,6 +9,10 @@ export class ApplicationScalingAction implements cloudwatch.IAlarmAction { constructor(private readonly stepScalingAction: appscaling.StepScalingAction) { } + /** + * Returns an alarm action configuration to use an ApplicationScaling StepScalingAction + * as an alarm action + */ public bind(_scope: cdk.Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.stepScalingAction.scalingPolicyArn }; } diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts index 577f56bd47fd5..5ec6e62fe246c 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/autoscaling.ts @@ -9,6 +9,10 @@ export class AutoScalingAction implements cloudwatch.IAlarmAction { constructor(private readonly stepScalingAction: autoscaling.StepScalingAction) { } + /** + * Returns an alarm action configuration to use an AutoScaling StepScalingAction + * as an alarm action + */ public bind(_scope: cdk.Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.stepScalingAction.scalingPolicyArn }; } diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts b/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts index 0067cf4518c28..deb882be507b3 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts +++ b/packages/@aws-cdk/aws-cloudwatch-actions/lib/sns.ts @@ -9,6 +9,9 @@ export class SnsAction implements cloudwatch.IAlarmAction { constructor(private readonly topic: sns.ITopic) { } + /** + * Returns an alarm action configuration to use an SNS topic as an alarm action + */ public bind(_scope: Construct, _alarm: cloudwatch.IAlarm): cloudwatch.AlarmActionConfig { return { alarmActionArn: this.topic.topicArn }; } diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index eefec8c4a581d..8f8d1edd98766 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -90,13 +90,6 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "stable", - "awslint": { - "exclude": [ - "docs-public-apis:@aws-cdk/aws-cloudwatch-actions.ApplicationScalingAction.bind", - "docs-public-apis:@aws-cdk/aws-cloudwatch-actions.AutoScalingAction.bind", - "docs-public-apis:@aws-cdk/aws-cloudwatch-actions.SnsAction.bind" - ] - }, "awscdkio": { "announce": false }, From 395c07c0cac7739743fc71d71fddd8880b608ead Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Thu, 21 May 2020 06:01:28 -0700 Subject: [PATCH 04/33] fix(events): cannot use the same target account for 2 cross-account event sources (#8068) We hard code the SID of the EventBusPolicy that we generate in the account of the target of a cross-account CloudWatch Event rule. Which means that, if you have two sources in different accounts generating events into the same target account, you will get an error on CloudFormation deployment time about a duplicate SID. Include the source account ID when generating the SID to make it unique. Fixes #8010 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-events/lib/rule.ts | 2 +- packages/@aws-cdk/aws-events/test/test.rule.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index e8e7cfcfa3431..582cd570e8bc8 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -244,7 +244,7 @@ export class Rule extends Resource implements IRule { }); new CfnEventBusPolicy(eventBusPolicyStack, 'GivePermToOtherAccount', { action: 'events:PutEvents', - statementId: 'MySid', + statementId: `Allow-account-${sourceAccount}`, principal: sourceAccount, }); } diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index b478fb10ec0fa..304bf91ed4dcb 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -717,7 +717,7 @@ export = { const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-us-west-2-${targetAccount}`) as cdk.Stack; expect(eventBusPolicyStack).to(haveResourceLike('AWS::Events::EventBusPolicy', { 'Action': 'events:PutEvents', - 'StatementId': 'MySid', + 'StatementId': `Allow-account-${sourceAccount}`, 'Principal': sourceAccount, })); From 3d30ffa38c51ae26686287e993af445ea3067766 Mon Sep 17 00:00:00 2001 From: karupanerura Date: Thu, 21 May 2020 22:58:02 +0900 Subject: [PATCH 05/33] feat(elbv2): Supports new types of listener rule conditions (#7848) ### Commit Message feat(elbv2): Supports new types of listener rule conditions Fixes #3888 ### End Commit Message ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-elasticloadbalancingv2/README.md | 17 +- .../lib/alb/application-listener-rule.ts | 65 ++++- .../lib/alb/application-listener.ts | 24 +- .../lib/alb/conditions.ts | 190 ++++++++++++++ .../aws-elasticloadbalancingv2/lib/index.ts | 1 + .../test/alb/test.listener.ts | 248 +++++++++++++++++- 6 files changed, 521 insertions(+), 24 deletions(-) create mode 100644 packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 22764ebe22de1..4ef361dd5512c 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -64,16 +64,17 @@ updated to allow the network traffic. #### Conditions It's possible to route traffic to targets based on conditions in the incoming -HTTP request. Path- and host-based conditions are supported. For example, the -following will route requests to the indicated AutoScalingGroup only if the -requested host in the request is either for `example.com/ok` or -`example.com/path`: +HTTP request. For example, the following will route requests to the indicated +AutoScalingGroup only if the requested host in the request is either for +`example.com/ok` or `example.com/path`: ```ts listener.addTargets('Example.Com Fleet', { priority: 10, - pathPatterns: ['/ok', '/path'], - hostHeader: 'example.com', + conditions: [ + ListenerCondition.hostHeaders(['example.com']), + ListenerCondition.pathPatterns(['/ok', '/path']), + ], port: 8080, targets: [asg] }); @@ -126,8 +127,10 @@ Here's an example of serving a fixed response at the `/ok` URL: ```ts listener.addAction('Fixed', { - pathPatterns: ['/ok'], priority: 10, + conditions: [ + ListenerCondition.pathPatterns(['/ok']), + ], action: ListenerAction.fixedResponse(200, { contentType: elbv2.ContentType.TEXT_PLAIN, messageBody: 'OK', diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts index c2eab3160fd6a..a8de884e9f2c0 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener-rule.ts @@ -4,6 +4,7 @@ import { IListenerAction } from '../shared/listener-action'; import { IApplicationListener } from './application-listener'; import { ListenerAction } from './application-listener-action'; import { IApplicationTargetGroup } from './application-target-group'; +import { ListenerCondition } from './conditions'; /** * Basic properties for defining a rule on a listener @@ -58,6 +59,15 @@ export interface BaseApplicationListenerRuleProps { */ readonly redirectResponse?: RedirectResponse; + /** + * Rule applies if matches the conditions. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html + * + * @default - No conditions. + */ + readonly conditions?: ListenerCondition[]; + /** * Rule applies if the requested host matches the indicated host * @@ -66,6 +76,7 @@ export interface BaseApplicationListenerRuleProps { * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions * * @default - No host condition. + * @deprecated Use `conditions` instead. */ readonly hostHeader?: string; @@ -74,7 +85,7 @@ export interface BaseApplicationListenerRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default - No path condition. - * @deprecated Use `pathPatterns` instead. + * @deprecated Use `conditions` instead. */ readonly pathPattern?: string; @@ -85,6 +96,7 @@ export interface BaseApplicationListenerRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default - No path conditions. + * @deprecated Use `conditions` instead. */ readonly pathPatterns?: string[]; } @@ -187,7 +199,8 @@ export class ApplicationListenerRule extends cdk.Construct { */ public readonly listenerRuleArn: string; - private readonly conditions: {[key: string]: string[] | undefined} = {}; + private readonly conditions: ListenerCondition[]; + private readonly legacyConditions: {[key: string]: string[]} = {}; private readonly listener: IApplicationListener; private action?: IListenerAction; @@ -195,9 +208,11 @@ export class ApplicationListenerRule extends cdk.Construct { constructor(scope: cdk.Construct, id: string, props: ApplicationListenerRuleProps) { super(scope, id); + this.conditions = props.conditions || []; + const hasPathPatterns = props.pathPatterns || props.pathPattern; - if (!props.hostHeader && !hasPathPatterns) { - throw new Error('At least one of \'hostHeader\', \'pathPattern\' or \'pathPatterns\' is required when defining a load balancing rule.'); + if (this.conditions.length === 0 && !props.hostHeader && !hasPathPatterns) { + throw new Error('At least one of \'conditions\', \'hostHeader\', \'pathPattern\' or \'pathPatterns\' is required when defining a load balancing rule.'); } const possibleActions: Array = ['action', 'targetGroups', 'fixedResponse', 'redirectResponse']; @@ -248,9 +263,25 @@ export class ApplicationListenerRule extends cdk.Construct { /** * Add a non-standard condition to this rule + * + * If the condition conflicts with an already set condition, it will be overwritten by the one you specified. + * + * @deprecated use `addCondition` instead. */ public setCondition(field: string, values: string[] | undefined) { - this.conditions[field] = values; + if (values === undefined) { + delete this.legacyConditions[field]; + return; + } + + this.legacyConditions[field] = values; + } + + /** + * Add a non-standard condition to this rule + */ + public addCondition(condition: ListenerCondition) { + this.conditions.push(condition); } /** @@ -322,20 +353,28 @@ export class ApplicationListenerRule extends cdk.Construct { if (this.action === undefined) { return ['Listener rule needs at least one action']; } + + const legacyConditionFields = Object.keys(this.legacyConditions); + if (legacyConditionFields.length === 0 && this.conditions.length === 0) { + return ['Listener rule needs at least one condition']; + } + return []; } /** * Render the conditions for this rule */ - private renderConditions() { - const ret = new Array<{ field: string, values: string[] }>(); - for (const [field, values] of Object.entries(this.conditions)) { - if (values !== undefined) { - ret.push({ field, values }); - } - } - return ret; + private renderConditions(): any { + const legacyConditions = Object.entries(this.legacyConditions).map(([field, values]) => { + return { field, values }; + }); + const conditions = this.conditions.map(condition => condition.renderRawCondition()); + + return [ + ...legacyConditions, + ...conditions, + ]; } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index c3560eb519993..c087f690f70e5 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -10,6 +10,7 @@ import { ApplicationListenerCertificate } from './application-listener-certifica import { ApplicationListenerRule, FixedResponse, RedirectResponse, validateFixedResponse, validateRedirectResponse } from './application-listener-rule'; import { IApplicationLoadBalancer } from './application-load-balancer'; import { ApplicationTargetGroup, IApplicationLoadBalancerTarget, IApplicationTargetGroup } from './application-target-group'; +import { ListenerCondition } from './conditions'; /** * Basic properties for an ApplicationListener @@ -274,6 +275,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis // TargetGroup.registerListener is called inside ApplicationListenerRule. new ApplicationListenerRule(this, id + 'Rule', { listener: this, + conditions: props.conditions, hostHeader: props.hostHeader, pathPattern: props.pathPattern, pathPatterns: props.pathPatterns, @@ -319,6 +321,7 @@ export class ApplicationListener extends BaseListener implements IApplicationLis }); this.addTargetGroups(id, { + conditions: props.conditions, hostHeader: props.hostHeader, pathPattern: props.pathPattern, pathPatterns: props.pathPatterns, @@ -618,6 +621,15 @@ export interface AddRuleProps { */ readonly priority?: number; + /** + * Rule applies if matches the conditions. + * + * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html + * + * @default - No conditions. + */ + readonly conditions?: ListenerCondition[]; + /** * Rule applies if the requested host matches the indicated host * @@ -628,6 +640,7 @@ export interface AddRuleProps { * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#host-conditions * * @default No host condition + * @deprecated Use `conditions` instead. */ readonly hostHeader?: string; @@ -640,7 +653,7 @@ export interface AddRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default No path condition - * @deprecated Use `pathPatterns` instead. + * @deprecated Use `conditions` instead. */ readonly pathPattern?: string; @@ -653,6 +666,7 @@ export interface AddRuleProps { * * @see https://docs.aws.amazon.com/elasticloadbalancing/latest/application/load-balancer-listeners.html#path-conditions * @default - No path condition. + * @deprecated Use `conditions` instead. */ readonly pathPatterns?: string[]; } @@ -770,7 +784,11 @@ export interface AddRedirectResponseProps extends AddRuleProps, RedirectResponse } function checkAddRuleProps(props: AddRuleProps) { - if ((props.hostHeader !== undefined || props.pathPattern !== undefined || props.pathPatterns !== undefined) !== (props.priority !== undefined)) { - throw new Error('Setting \'pathPattern\' or \'hostHeader\' also requires \'priority\', and vice versa'); + const conditionsCount = props.conditions?.length || 0; + const hasAnyConditions = conditionsCount !== 0 || + props.hostHeader !== undefined || props.pathPattern !== undefined || props.pathPatterns !== undefined; + const hasPriority = props.priority !== undefined; + if (hasAnyConditions !== hasPriority) { + throw new Error('Setting \'conditions\', \'pathPattern\' or \'hostHeader\' also requires \'priority\', and vice versa'); } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts new file mode 100644 index 0000000000000..ba5ebce6f70e4 --- /dev/null +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/conditions.ts @@ -0,0 +1,190 @@ +/** + * ListenerCondition providers definition. + */ +export abstract class ListenerCondition { + /** + * Create a host-header listener rule condition + * + * @param values Hosts for host headers + */ + public static hostHeaders(values: string[]): ListenerCondition { + return new HostHeaderListenerCondition(values); + } + + /** + * Create a http-header listener rule condition + * + * @param name HTTP header name + * @param values HTTP header values + */ + public static httpHeader(name: string, values: string[]): ListenerCondition { + return new HttpHeaderListenerCondition(name, values); + } + + /** + * Create a http-request-method listener rule condition + * + * @param values HTTP request methods + */ + public static httpRequestMethods(values: string[]): ListenerCondition { + return new HttpRequestMethodListenerCondition(values); + } + + /** + * Create a path-pattern listener rule condition + * + * @param values Path patterns + */ + public static pathPatterns(values: string[]): ListenerCondition { + return new PathPatternListenerCondition(values); + } + + /** + * Create a query-string listener rule condition + * + * @param values Query string key/value pairs + */ + public static queryStrings(values: QueryStringCondition[]): ListenerCondition { + return new QueryStringListenerCondition(values); + } + + /** + * Create a source-ip listener rule condition + * + * @param values Source ips + */ + public static sourceIps(values: string[]): ListenerCondition { + return new SourceIpListenerCondition(values); + } + + /** + * Render the raw Cfn listener rule condition object. + */ + public abstract renderRawCondition(): any; +} + +/** + * Properties for the key/value pair of the query string + */ +export interface QueryStringCondition { + /** + * The query string key for the condition + * + * @default - Any key can be matched. + */ + readonly key?: string; + + /** + * The query string value for the condition + */ + readonly value: string; +} + +/** + * Host header config of the listener rule condition + */ +class HostHeaderListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'host-header', + hostHeaderConfig: { + values: this.values, + }, + }; + } +} + +/** + * HTTP header config of the listener rule condition + */ +class HttpHeaderListenerCondition extends ListenerCondition { + constructor(public readonly name: string, public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'http-header', + httpHeaderConfig: { + httpHeaderName: this.name, + values: this.values, + }, + }; + } +} + +/** + * HTTP reqeust method config of the listener rule condition + */ +class HttpRequestMethodListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'http-request-method', + httpRequestMethodConfig: { + values: this.values, + }, + }; + } +} + +/** + * Path pattern config of the listener rule condition + */ +class PathPatternListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'path-pattern', + pathPatternConfig: { + values: this.values, + }, + }; + } +} + +/** + * Query string config of the listener rule condition + */ +class QueryStringListenerCondition extends ListenerCondition { + constructor(public readonly values: QueryStringCondition[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'query-string', + queryStringConfig: { + values: this.values, + }, + }; + } +} + +/** + * Source ip config of the listener rule condition + */ +class SourceIpListenerCondition extends ListenerCondition { + constructor(public readonly values: string[]) { + super(); + } + + public renderRawCondition(): any { + return { + field: 'source-ip', + sourceIpConfig: { + values: this.values, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts index 1442972c82a50..9f8833b15bfda 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/index.ts @@ -7,6 +7,7 @@ export * from './alb/application-listener-rule'; export * from './alb/application-load-balancer'; export * from './alb/application-target-group'; export * from './alb/application-listener-action'; +export * from './alb/conditions'; export * from './nlb/network-listener'; export * from './nlb/network-load-balancer'; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts index 7588119a3762f..825e4472db7cb 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/test.listener.ts @@ -993,7 +993,253 @@ export = { test.done(); }, - 'Add path patterns to imported application listener'(test: Test) { + 'Add additonal condition to listener rule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const group1 = new elbv2.ApplicationTargetGroup(stack, 'Group1', { vpc, port: 80 }); + const group2 = new elbv2.ApplicationTargetGroup(stack, 'Group2', { vpc, port: 81, protocol: elbv2.ApplicationProtocol.HTTP }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + certificateArns: ['cert1'], + defaultTargetGroups: [group2], + }); + listener.addTargetGroups('TargetGroup1', { + priority: 10, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.httpHeader('Accept', ['application/vnd.myapp.v2+json']), + ], + targetGroups: [group1], + }); + listener.addTargetGroups('TargetGroup2', { + priority: 20, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + ], + targetGroups: [group2], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'http-header', + HttpHeaderConfig: { + HttpHeaderName: 'Accept', + Values: ['application/vnd.myapp.v2+json'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 20, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + ], + })); + + test.done(); + }, + + 'Add multiple additonal condition to listener rule'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const group1 = new elbv2.ApplicationTargetGroup(stack, 'Group1', { vpc, port: 80 }); + const group2 = new elbv2.ApplicationTargetGroup(stack, 'Group2', { vpc, port: 81, protocol: elbv2.ApplicationProtocol.HTTP }); + const group3 = new elbv2.ApplicationTargetGroup(stack, 'Group3', { vpc, port: 82, protocol: elbv2.ApplicationProtocol.HTTP }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + certificateArns: ['cert1'], + defaultTargetGroups: [group3], + }); + listener.addTargetGroups('TargetGroup1', { + priority: 10, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.sourceIps(['192.0.2.0/24']), + elbv2.ListenerCondition.queryStrings([{ key: 'version', value: '2' }, { value: 'foo*' }]), + ], + targetGroups: [group1], + }); + listener.addTargetGroups('TargetGroup2', { + priority: 20, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.httpHeader('Accept', ['application/vnd.myapp.v2+json']), + ], + targetGroups: [group1], + }); + listener.addTargetGroups('TargetGroup3', { + priority: 30, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + elbv2.ListenerCondition.httpRequestMethods(['PUT', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK']), + ], + targetGroups: [group2], + }); + listener.addTargetGroups('TargetGroup4', { + priority: 40, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['app.test']), + ], + targetGroups: [group3], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'source-ip', + SourceIpConfig: { + Values: ['192.0.2.0/24'], + }, + }, + { + Field: 'query-string', + QueryStringConfig: { + Values: [ + { + Key: 'version', + Value: '2', + }, + { + Value: 'foo*', + }, + ], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 20, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'http-header', + HttpHeaderConfig: { + HttpHeaderName: 'Accept', + Values: ['application/vnd.myapp.v2+json'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 30, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + { + Field: 'http-request-method', + HttpRequestMethodConfig: { + Values: ['PUT', 'COPY', 'LOCK', 'MKCOL', 'MOVE', 'PROPFIND', 'PROPPATCH', 'UNLOCK'], + }, + }, + ], + })); + + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 40, + Conditions: [ + { + Field: 'host-header', + HostHeaderConfig: { + Values: ['app.test'], + }, + }, + ], + })); + + test.done(); + }, + + 'Can exist together legacy style conditions and modan style conditions'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'Stack'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'LB', { vpc }); + const group1 = new elbv2.ApplicationTargetGroup(stack, 'Group1', { vpc, port: 80 }); + const group2 = new elbv2.ApplicationTargetGroup(stack, 'Group2', { vpc, port: 81, protocol: elbv2.ApplicationProtocol.HTTP }); + + // WHEN + const listener = lb.addListener('Listener', { + port: 443, + certificateArns: ['cert1'], + defaultTargetGroups: [group2], + }); + listener.addTargetGroups('TargetGroup1', { + hostHeader: 'app.test', + pathPattern: '/test', + conditions: [ + elbv2.ListenerCondition.sourceIps(['192.0.2.0/24']), + ], + priority: 10, + targetGroups: [group1], + }); + + // THEN + expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 10, + Conditions: [ + { + Field: 'host-header', + Values: ['app.test'], + }, + { + Field: 'path-pattern', + Values: ['/test'], + }, + { + Field: 'source-ip', + SourceIpConfig: { + Values: ['192.0.2.0/24'], + }, + }, + ], + })); + + test.done(); + }, + + 'Add condition to imported application listener'(test: Test) { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Stack'); From 21f9f768e8be9114b09a370761015002771d43f6 Mon Sep 17 00:00:00 2001 From: Eli Polonsky Date: Thu, 21 May 2020 17:54:39 +0300 Subject: [PATCH 06/33] chore(cli): simpler error message on cloud assembly version mismatch (#8129) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The error message we currently print when a user has a CLI version that is not compatible with the framework is: ```console Cloud assembly schema version mismatch: Maximum schema version supported is 1.33.0, but found 2.0.0. Please upgrade your CLI in order to interact with this app. ``` This is pretty cryptic, we shouldn't be starting the message with `Cloud Assembly`, as most users aren't really versed in, or even aware of it. In addition, the versions mentioned here are confusing and might give the impression we are asking users to upgrade to CLI version `2.0.0`. This PR simplifies the message to: ``` ❯ cdk synth [14:27:15] The CLI version you are using does not support your application version. Please upgrade the CLI to the latest version. (Your application requires a CLI that supports a cloud assembly of version '2.0.0' or above.) ``` It clearly states what wrongs (CLI version incompatible) and how to fix (upgrade) right of the bat. The tail of the message is an attempt to explain why the CLI does not support the framework. Fixes #7901 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/api/cxapp/exec.ts | 2 +- packages/aws-cdk/test/api/exec.test.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/aws-cdk/lib/api/cxapp/exec.ts b/packages/aws-cdk/lib/api/cxapp/exec.ts index db4d75f86fc38..93ddb1264f2ff 100644 --- a/packages/aws-cdk/lib/api/cxapp/exec.ts +++ b/packages/aws-cdk/lib/api/cxapp/exec.ts @@ -81,7 +81,7 @@ export async function execProgram(aws: SdkProvider, config: Configuration): Prom if (error.message.includes(cxschema.VERSION_MISMATCH)) { // this means the CLI version is too old. // we instruct the user to upgrade. - throw new Error(`${error.message}.\nPlease upgrade your CLI in order to interact with this app.`); + throw new Error(`This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.\n(${error.message})`); } throw error; } diff --git a/packages/aws-cdk/test/api/exec.test.ts b/packages/aws-cdk/test/api/exec.test.ts index 057e50cb8e938..ec653da6d6a77 100644 --- a/packages/aws-cdk/test/api/exec.test.ts +++ b/packages/aws-cdk/test/api/exec.test.ts @@ -72,8 +72,8 @@ test('cli throws when manifest version > schema version', async () => { mockVersionNumber.restore(); } - const expectedError = `Cloud assembly schema version mismatch: Maximum schema version supported is ${currentSchemaVersion}, but found ${mockManifestVersion}.` - + '\nPlease upgrade your CLI in order to interact with this app.'; + const expectedError = 'This CDK CLI is not compatible with the CDK library used by your application. Please upgrade the CLI to the latest version.' + + `\n(Cloud assembly schema version mismatch: Maximum schema version supported is ${currentSchemaVersion}, but found ${mockManifestVersion})`; config.settings.set(['app'], 'cdk.out'); From 01fa4086a653ccb168e69654983d6ad159e5be82 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Thu, 21 May 2020 19:47:26 +0200 Subject: [PATCH 07/33] chore: add test deploying old-style synthesis to new-style bootstrap (#8127) We did not have a test deploying an old-style synthesized stack to a new-style bootstrapping environment. Now we do. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/integ/cli/bootstrapping.integtest.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index a84d942c6ae52..3c674470abe4c 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -92,6 +92,27 @@ test('deploy new style synthesis to new style bootstrap', async () => { }); }); +test('deploy old style synthesis to new style bootstrap', async () => { + const bootstrapStackName = fullStackName('bootstrap-stack'); + + await cdk(['bootstrap', + '--toolkit-stack-name', bootstrapStackName, + '--qualifier', QUALIFIER, + '--cloudformation-execution-policies', 'arn:aws:iam::aws:policy/AdministratorAccess', + ], { + modEnv: { + CDK_NEW_BOOTSTRAP: '1', + }, + }); + + // Deploy stack that uses file assets + await cdkDeploy('lambda', { + options: [ + '--toolkit-stack-name', bootstrapStackName, + ], + }); +}); + test('deploying new style synthesis to old style bootstrap fails', async () => { const bootstrapStackName = fullStackName('bootstrap-stack'); From ed433057853b419ad3f00276285633e50e0eb5c9 Mon Sep 17 00:00:00 2001 From: Elad Ben-Israel Date: Thu, 21 May 2020 22:03:25 +0300 Subject: [PATCH 08/33] chore(release): 1.41.0 (#8130) Co-authored-by: AWS CDK Team Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com> --- CHANGELOG.md | 18 ++++++++++++++++++ lerna.json | 2 +- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6fbd89a09be23..0386d2dbfb9a6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,24 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.41.0](https://github.com/aws/aws-cdk/compare/v1.40.0...v1.41.0) (2020-05-21) + + +### Features + +* **cloudtrail:** create cloudwatch event without needing to create a Trail ([#8076](https://github.com/aws/aws-cdk/issues/8076)) ([0567a23](https://github.com/aws/aws-cdk/commit/0567a2360ac713e3171c9a82767611174dadb6c6)), closes [#6716](https://github.com/aws/aws-cdk/issues/6716) +* **cognito:** user pool - case sensitivity for sign in ([460394f](https://github.com/aws/aws-cdk/commit/460394f3dc4737cee80504d6c8ef106ecc3b67d5)), closes [#7988](https://github.com/aws/aws-cdk/issues/7988) [#7235](https://github.com/aws/aws-cdk/issues/7235) +* **core:** CfnJson enables intrinsics in hash keys ([#8099](https://github.com/aws/aws-cdk/issues/8099)) ([195cd40](https://github.com/aws/aws-cdk/commit/195cd405d9f0869875de2ec78661aee3af2c7c7d)), closes [#8084](https://github.com/aws/aws-cdk/issues/8084) +* **secretsmanager:** adds grantWrite to Secret ([#7858](https://github.com/aws/aws-cdk/issues/7858)) ([3fed84b](https://github.com/aws/aws-cdk/commit/3fed84ba9eec3f53c662966e366aa629209b7bf5)) +* **sns:** add support for subscription DLQ in SNS ([383cdb8](https://github.com/aws/aws-cdk/commit/383cdb86effeafdf5d0767ed379b16b3d78a933b)) +* **stepfunctions:** new service integration classes for Lambda, SNS, and SQS ([#7946](https://github.com/aws/aws-cdk/issues/7946)) ([c038848](https://github.com/aws/aws-cdk/commit/c0388483524832ca7863de4ee9c472b8ab39de8e)), closes [#6715](https://github.com/aws/aws-cdk/issues/6715) [#6489](https://github.com/aws/aws-cdk/issues/6489) + + +### Bug Fixes + +* **apigateway:** contextAccountId in AccessLogField incorrectly resolves to requestId ([7b89e80](https://github.com/aws/aws-cdk/commit/7b89e805c716fa73d41cc97fcb728634e7a59136)), closes [#7952](https://github.com/aws/aws-cdk/issues/7952) [#7951](https://github.com/aws/aws-cdk/issues/7951) +* **autoscaling:** add noDevice as a volume type ([#7253](https://github.com/aws/aws-cdk/issues/7253)) ([751958b](https://github.com/aws/aws-cdk/commit/751958b69225fdfc52622781c618f5a77f881fb6)), closes [#7242](https://github.com/aws/aws-cdk/issues/7242) + ## [1.40.0](https://github.com/aws/aws-cdk/compare/v1.39.0...v1.40.0) (2020-05-20) diff --git a/lerna.json b/lerna.json index b32aeb94bcf30..baa3940d032fb 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.40.0" + "version": "1.41.0" } From 60814b74510952bcf394bb7532cde6df4fd8da12 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Thu, 21 May 2020 13:03:21 -0700 Subject: [PATCH 09/33] chore: add node version to issue template (#8121) Motivation: Since the CLI leverages Node.js, it's a useful piece of information about the user's environment. It would be helpful in diagnosing and triaging issues. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .github/ISSUE_TEMPLATE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 3116ffd4a844d..3e08711b183e4 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -25,6 +25,7 @@ falling prey to the [X/Y problem][2]! - **CDK CLI Version:** - **Module Version:** + - **Node.js Version:** - **OS:** - **Language:** From 35a01a079af40da291007da08af6690c9a81c101 Mon Sep 17 00:00:00 2001 From: Vincent Lesierse Date: Fri, 22 May 2020 00:25:32 +0200 Subject: [PATCH 10/33] feat(eks): improve security using IRSA conditions (#8084) Currently the `ServiceAccount`construct creates a role with no conditions to the trust relationship or assume role policy. Without this it is possible for other pods in the same namespace to assume the role. To tighten this security the conditions needs to be set. Documentation: https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html#create-service-account-iam-role - [x] Add condition to the policy document using a custom resource - [x] Add unit tests - [x] Add integration tests - [x] Adjust README and remove warning ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-eks/README.md | 2 - .../lib/cluster-resource-handler/cluster.ts | 1 + .../@aws-cdk/aws-eks/lib/cluster-resource.ts | 2 + packages/@aws-cdk/aws-eks/lib/cluster.ts | 17 +- .../@aws-cdk/aws-eks/lib/service-account.ts | 17 +- .../test/integ.eks-cluster.expected.json | 363 +++++++++++++++++- .../aws-eks/test/integ.eks-cluster.ts | 3 + .../test/test.cluster-resource-provider.ts | 2 + .../aws-eks/test/test.service-account.ts | 8 + 9 files changed, 389 insertions(+), 26 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 4f9103c9504fa..e6d94a2838257 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -548,8 +548,6 @@ cluster.addResource('mypod', { }); ``` -> Warning: Currently there are no condition set on the IAM Role which results that there are no restrictions on other pods to assume the role. This will be improved in the near future. - ### Roadmap - [ ] AutoScaling (combine EC2 and Kubernetes scaling) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts index 47449325d4304..8733463cce31b 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource-handler/cluster.ts @@ -199,6 +199,7 @@ export class ClusterResourceHandler extends ResourceHandler { Arn: cluster.arn, CertificateAuthorityData: cluster.certificateAuthority?.data, OpenIdConnectIssuerUrl: cluster.identity?.oidc?.issuer, + OpenIdConnectIssuer: cluster.identity?.oidc?.issuer?.substring(8), // Strips off https:// from the issuer url }, }; } diff --git a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts index e4c8f13917e8c..52557776c97e8 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster-resource.ts @@ -19,6 +19,7 @@ export class ClusterResource extends Construct { public readonly attrArn: string; public readonly attrCertificateAuthorityData: string; public readonly attrOpenIdConnectIssuerUrl: string; + public readonly attrOpenIdConnectIssuer: string; public readonly ref: string; /** @@ -126,6 +127,7 @@ export class ClusterResource extends Construct { this.attrArn = Token.asString(resource.getAtt('Arn')); this.attrCertificateAuthorityData = Token.asString(resource.getAtt('CertificateAuthorityData')); this.attrOpenIdConnectIssuerUrl = Token.asString(resource.getAtt('OpenIdConnectIssuerUrl')); + this.attrOpenIdConnectIssuer = Token.asString(resource.getAtt('OpenIdConnectIssuer')); } /** diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 296e7beb07fe4..a65f1ecd57c06 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -635,6 +635,21 @@ export class Cluster extends Resource implements ICluster { return this._clusterResource.attrOpenIdConnectIssuerUrl; } + /** + * If this cluster is kubectl-enabled, returns the OpenID Connect issuer. + * This is because the values is only be retrieved by the API and not exposed + * by CloudFormation. If this cluster is not kubectl-enabled (i.e. uses the + * stock `CfnCluster`), this is `undefined`. + * @attribute + */ + public get clusterOpenIdConnectIssuer(): string { + if (!this._clusterResource) { + throw new Error('unable to obtain OpenID Connect issuer. Cluster must be kubectl-enabled'); + } + + return this._clusterResource.attrOpenIdConnectIssuer; + } + /** * An `OpenIdConnectProvider` resource associated with this cluster, and which can be used * to link this cluster to AWS IAM. @@ -708,7 +723,7 @@ export class Cluster extends Resource implements ICluster { * @param id the id of this service account * @param options service account options */ - public addServiceAccount(id: string, options: ServiceAccountOptions) { + public addServiceAccount(id: string, options: ServiceAccountOptions = { }) { return new ServiceAccount(this, id, { ...options, cluster: this, diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts index 93d43f700139b..24865c4a36f4b 100644 --- a/packages/@aws-cdk/aws-eks/lib/service-account.ts +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -1,5 +1,5 @@ import { AddToPrincipalPolicyResult, IPrincipal, IRole, OpenIdConnectPrincipal, PolicyStatement, PrincipalPolicyFragment, Role } from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; +import { CfnJson, Construct } from '@aws-cdk/core'; import { Cluster } from './cluster'; /** @@ -34,7 +34,6 @@ export interface ServiceAccountProps extends ServiceAccountOptions { * Service Account */ export class ServiceAccount extends Construct implements IPrincipal { - /** * The role which is linked to the service account. */ @@ -61,9 +60,19 @@ export class ServiceAccount extends Construct implements IPrincipal { this.serviceAccountName = props.name ?? this.node.uniqueId.toLowerCase(); this.serviceAccountNamespace = props.namespace ?? 'default'; - this.role = new Role(this, 'Role', { - assumedBy: new OpenIdConnectPrincipal(cluster.openIdConnectProvider), + /* Add conditions to the role to improve security. This prevents other pods in the same namespace to assume the role. + * See documentation: https://docs.aws.amazon.com/eks/latest/userguide/create-service-account-iam-policy-and-role.html + */ + const conditions = new CfnJson(this, 'ConditionJson', { + value: { + [`${cluster.clusterOpenIdConnectIssuer}:aud`]: 'sts.amazonaws.com', + [`${cluster.clusterOpenIdConnectIssuer}:sub`]: `system:serviceaccount:${this.serviceAccountNamespace}:${this.serviceAccountName}`, + }, + }); + const principal = new OpenIdConnectPrincipal(cluster.openIdConnectProvider).withConditions({ + StringEquals: conditions, }); + this.role = new Role(this, 'Role', { assumedBy: principal }); this.assumeRoleAction = this.role.assumeRoleAction; this.grantPrincipal = this.role.grantPrincipal; diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 982a27933f700..29b38c2393bc1 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -2211,6 +2211,130 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "ClusterMyServiceAccountConditionJson671C0633": { + "Type": "Custom::AWSCDKCfnJson", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderHandlerCF82AA57", + "Arn" + ] + }, + "Value": { + "Fn::Join": [ + "", + [ + "{\"", + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "OpenIdConnectIssuer" + ] + }, + ":aud\":\"sts.amazonaws.com\",\"", + { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "OpenIdConnectIssuer" + ] + }, + ":sub\":\"system:serviceaccount:default:awscdkeksclustertestclustermyserviceaccount4080bcdd\"}" + ] + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClusterMyServiceAccountRole85337B29": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "Fn::GetAtt": [ + "ClusterMyServiceAccountConditionJson671C0633", + "Value" + ] + } + }, + "Effect": "Allow", + "Principal": { + "Federated": { + "Ref": "ClusterOpenIdConnectProviderE7EB0530" + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "ClusterOpenIdConnectProviderE7EB0530": { + "Type": "Custom::AWSCDKOpenIdConnectProvider", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0", + "Arn" + ] + }, + "ClientIDList": [ + "sts.amazonaws.com" + ], + "ThumbprintList": [ + "9e99a48a9960b14926bb7f3b02e22da2b0ab7280" + ], + "Url": { + "Fn::GetAtt": [ + "Cluster9EE0221C", + "OpenIdConnectIssuerUrl" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "ClustermanifestServiceAccountD03C306D": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclustertestawscdkawseksKubectlProviderframeworkonEventC681B49AArn" + ] + }, + "Manifest": { + "Fn::Join": [ + "", + [ + "[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"awscdkeksclustertestclustermyserviceaccount4080bcdd\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"awscdkeksclustertestclustermyserviceaccount4080bcdd\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"", + { + "Fn::GetAtt": [ + "ClusterMyServiceAccountRole85337B29", + "Arn" + ] + }, + "\"}}}]" + ] + ] + }, + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "awscdkawseksClusterResourceProviderNestedStackawscdkawseksClusterResourceProviderNestedStackResource9827C454": { "Type": "AWS::CloudFormation::Stack", "Properties": { @@ -2224,7 +2348,7 @@ }, "/", { - "Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3BucketDF419A16" + "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3BucketB18DC500" }, "/", { @@ -2234,7 +2358,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B" + "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A" } ] } @@ -2247,7 +2371,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B" + "Ref": "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A" } ] } @@ -2257,11 +2381,11 @@ ] }, "Parameters": { - "referencetoawscdkeksclustertestAssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3Bucket21CE03E4Ref": { - "Ref": "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3BucketE9BEFBC2" + "referencetoawscdkeksclustertestAssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket35BE45A3Ref": { + "Ref": "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket221B7FEE" }, - "referencetoawscdkeksclustertestAssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKey7161DBC6Ref": { - "Ref": "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKeyC7391006" + "referencetoawscdkeksclustertestAssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKey60905A80Ref": { + "Ref": "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKeyA8C9A018" }, "referencetoawscdkeksclustertestAssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3BucketC7CBF350Ref": { "Ref": "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C" @@ -2332,6 +2456,183 @@ } } } + }, + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867": { + "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" + } + ] + } + }, + "AWSCDKCfnUtilsProviderCustomResourceProviderHandlerCF82AA57": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "AWSCDKCfnUtilsProviderCustomResourceProviderRoleFE0EE867" + ] + }, + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65": { + "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" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Resource": "*", + "Action": [ + "iam:CreateOpenIDConnectProvider", + "iam:DeleteOpenIDConnectProvider", + "iam:UpdateOpenIDConnectProviderThumbprint", + "iam:AddClientIDToOpenIDConnectProvider", + "iam:RemoveClientIDFromOpenIDConnectProvider" + ] + } + ] + } + } + ] + } + }, + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderHandlerF2C543E0": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Ref": "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3Bucket718B603F" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3VersionKey6B97A1A3" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3VersionKey6B97A1A3" + } + ] + } + ] + } + ] + ] + } + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "__entrypoint__.handler", + "Role": { + "Fn::GetAtt": [ + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65", + "Arn" + ] + }, + "Runtime": "nodejs12.x" + }, + "DependsOn": [ + "CustomAWSCDKOpenIdConnectProviderCustomResourceProviderRole517FED65" + ] } }, "Outputs": { @@ -2406,17 +2707,17 @@ } }, "Parameters": { - "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3BucketE9BEFBC2": { + "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3Bucket221B7FEE": { "Type": "String", - "Description": "S3 bucket for asset \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\"" + "Description": "S3 bucket for asset \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" }, - "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeS3VersionKeyC7391006": { + "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896S3VersionKeyA8C9A018": { "Type": "String", - "Description": "S3 key for asset version \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\"" + "Description": "S3 key for asset version \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" }, - "AssetParameters35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebeArtifactHash058BD37E": { + "AssetParameters01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896ArtifactHashED8C0EF9": { "Type": "String", - "Description": "Artifact hash for asset \"35ffa1014d8c5abddd237ff0b1d26a4d2972126d08cb88e44350b31fcc1a3ebe\"" + "Description": "Artifact hash for asset \"01ec3fa8451b6541733a25ec9c0c13a2b7dcee848ddad2edf6cb9c1f40cbc896\"" }, "AssetParameters5e49cf64d8027f48872790f80cdb76c5b836ecf9a70b71be1eb937a5c25a47c1S3Bucket663A709C": { "Type": "String", @@ -2442,17 +2743,41 @@ "Type": "String", "Description": "Artifact hash for asset \"a6d508eaaa0d3cddbb47a84123fc878809c8431c5466f360912f70b5b9770afb\"" }, - "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3BucketDF419A16": { + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3BucketEF5DD638": { + "Type": "String", + "Description": "S3 bucket for asset \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57S3VersionKey2EA0DB2E": { + "Type": "String", + "Description": "S3 key for asset version \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameterse02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57ArtifactHash95B71D2D": { + "Type": "String", + "Description": "Artifact hash for asset \"e02a38b06730095e29b3afe60b65afcdc3a4ad4716c2f21de5fd5dc58e194f57\"" + }, + "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3Bucket718B603F": { + "Type": "String", + "Description": "S3 bucket for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" + }, + "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319S3VersionKey6B97A1A3": { + "Type": "String", + "Description": "S3 key for asset version \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" + }, + "AssetParameters4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319ArtifactHash96BDDF33": { + "Type": "String", + "Description": "Artifact hash for asset \"4c04b604b3ea48cf40394c3b4b898525a99ce5f981bc13ad94bf126997416319\"" + }, + "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3BucketB18DC500": { "Type": "String", - "Description": "S3 bucket for asset \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\"" + "Description": "S3 bucket for asset \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" }, - "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9S3VersionKeyAA30989B": { + "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6S3VersionKeyBE7DFF7A": { "Type": "String", - "Description": "S3 key for asset version \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\"" + "Description": "S3 key for asset version \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" }, - "AssetParameters01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9ArtifactHash93C28EE4": { + "AssetParameters7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6ArtifactHash5F906FBC": { "Type": "String", - "Description": "Artifact hash for asset \"01e05ecb4864cd6d6d0dd901e087371b7ba00c9b173029d9a51e5a0b6d450dd9\"" + "Description": "Artifact hash for asset \"7c148fb102ee8790aaf67d5e2a2dce8f5d9b87285c8b7e91f984216ee66f1be6\"" }, "AssetParameters36525a61abfaf5764fad460fd03c24215fd00da60805807d6138c51be4d03dbcS3Bucket2D824DEF": { "Type": "String", diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts index a6eda3ab3eeee..f6e883f773140 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -66,6 +66,9 @@ class EksClusterStack extends TestStack { cluster.addChart('dashboard', { chart: 'kubernetes-dashboard', repository: 'https://kubernetes-charts.storage.googleapis.com' }); cluster.addChart('nginx-ingress', { chart: 'nginx-ingress', repository: 'https://helm.nginx.com/stable', namespace: 'kube-system' }); + // add a service account connected to a IAM role + cluster.addServiceAccount('MyServiceAccount'); + new CfnOutput(this, 'ClusterEndpoint', { value: cluster.clusterEndpoint }); new CfnOutput(this, 'ClusterArn', { value: cluster.clusterArn }); new CfnOutput(this, 'ClusterCertificateAuthorityData', { value: cluster.clusterCertificateAuthorityData }); diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts index 311116611a58f..29dcfac4e89b6 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster-resource-provider.ts @@ -100,6 +100,7 @@ export = { Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', OpenIdConnectIssuerUrl: undefined, + OpenIdConnectIssuer: undefined, }, }); test.done(); @@ -422,6 +423,7 @@ export = { Arn: 'arn:cluster-arn', CertificateAuthorityData: 'certificateAuthority-data', OpenIdConnectIssuerUrl: undefined, + OpenIdConnectIssuer: undefined, }, }); test.done(); diff --git a/packages/@aws-cdk/aws-eks/test/test.service-account.ts b/packages/@aws-cdk/aws-eks/test/test.service-account.ts index 9a3467023b47c..71b04ee993d04 100644 --- a/packages/@aws-cdk/aws-eks/test/test.service-account.ts +++ b/packages/@aws-cdk/aws-eks/test/test.service-account.ts @@ -50,6 +50,14 @@ export = { Ref: 'ClusterOpenIdConnectProviderE7EB0530', }, }, + Condition: { + StringEquals: { + 'Fn::GetAtt': [ + 'MyServiceAccountConditionJson1ED3BC54', + 'Value', + ], + }, + }, }, ], Version: '2012-10-17', From 86108890a51443dc06ec6325038c7b19cbdaee76 Mon Sep 17 00:00:00 2001 From: Pahud Hsieh Date: Fri, 22 May 2020 14:50:59 +0800 Subject: [PATCH 11/33] fix(aws-eks): kubectlEnabled: false conflicts with addNodegroup (#8119) fix(aws-eks): kubectlEnabled: false conflicts with addNodegroup This PR allows `cluster.addNodegroup()` when `kubectlEnabled` is `false` Closes: #7993 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-eks/lib/cluster.ts | 2 -- packages/@aws-cdk/aws-eks/test/test.cluster.ts | 12 ------------ packages/@aws-cdk/aws-eks/test/test.nodegroup.ts | 14 +++++++++++++- 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index a65f1ecd57c06..d1fb2bf60352b 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -510,8 +510,6 @@ export class Cluster extends Resource implements ICluster { * @param options options for creating a new nodegroup */ public addNodegroup(id: string, options?: NodegroupOptions): Nodegroup { - // initialize the awsAuth for this cluster - this._awsAuth = this._awsAuth ?? this.awsAuth; return new Nodegroup(this, `Nodegroup${id}`, { cluster: this, ...options, diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index c99324b0a8ea1..daeded7e80743 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -856,18 +856,6 @@ export = { ], }, }, - { - Action: 'sts:AssumeRole', - Effect: 'Allow', - Principal: { - AWS: { - 'Fn::GetAtt': [ - 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', - 'Outputs.StackawscdkawseksKubectlProviderHandlerServiceRole2C52B3ECArn', - ], - }, - }, - }, ], Version: '2012-10-17', }, diff --git a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts index b22918dd1b306..acd5871c6cb6b 100644 --- a/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts +++ b/packages/@aws-cdk/aws-eks/test/test.nodegroup.ts @@ -1,4 +1,4 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, haveResourceLike } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; @@ -90,6 +90,18 @@ export = { )); test.done(); }, + 'create nodegroups with kubectlEnabled is false'(test: Test) { + // GIVEN + const { stack, vpc } = testFixture(); + + // WHEN + const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 2 }); + // add a extra nodegroup + cluster.addNodegroup('extra-ng'); + // THEN + expect(stack).to(countResources('AWS::EKS::Nodegroup', 2)); + test.done(); + }, 'create nodegroup with instanceType provided'(test: Test) { // GIVEN const { stack, vpc } = testFixture(); From 8b19453e5996c05cbacea7b04957a812dbe3eda4 Mon Sep 17 00:00:00 2001 From: flemjame-at-amazon <57235867+flemjame-at-amazon@users.noreply.github.com> Date: Fri, 22 May 2020 04:12:46 -0400 Subject: [PATCH 12/33] docs(lambda): document adding execution permissions to provided IAM roles (#8041) ### Commit Message docs(lambda): document adding execution permissions to provided IAM roles If I am providing a Role for a Lambda function, it currently isn't given the basic execution permissions, so the function cannot log anything or, in the case of a VPC Lambda, it cannot create the network interfaces. The user has to add those permissions themselves, but it isn't clear from the documentation that that needs to happen. This commit adds documentation showing CDK users how to add the required permissions for execution. ### End Commit Message ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-lambda/README.md | 33 ++++++++++++++++++++ packages/@aws-cdk/aws-lambda/lib/function.ts | 6 ++++ 2 files changed, 39 insertions(+) diff --git a/packages/@aws-cdk/aws-lambda/README.md b/packages/@aws-cdk/aws-lambda/README.md index 536d2a9e71d9d..92ed4dc61392b 100644 --- a/packages/@aws-cdk/aws-lambda/README.md +++ b/packages/@aws-cdk/aws-lambda/README.md @@ -49,6 +49,39 @@ to our CDK project directory. This is especially important when we want to share this construct through a library. Different programming languages will have different techniques for bundling resources into libraries. +### Execution Role + +Lambda functions assume an IAM role during execution. In CDK by default, Lambda +functions will use an autogenerated Role if one is not provided. + +The autogenerated Role is automatically given permissions to execute the Lambda +function. To reference the autogenerated Role: + +```ts +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + +fn.role // the Role +``` + +You can also provide your own IAM role. Provided IAM roles will not automatically +be given permissions to execute the Lambda function. To provide a role and grant +it appropriate permissions: + +```ts +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + role: myRole // user-provided role +}); + +myRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaBasicExecutionRole")); +myRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName("service-role/AWSLambdaVPCAccessExecutionRole")); // only required if your function lives in a VPC +``` + ### Versions and Aliases You can use diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index f66a67f07e336..ea2d2bf1f18ef 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -99,6 +99,12 @@ export interface FunctionOptions extends EventInvokeConfigOptions { * It controls the permissions that the function will have. The Role must * be assumable by the 'lambda.amazonaws.com' service principal. * + * The default Role automatically has permissions granted for Lambda execution. If you + * provide a Role, you must add the relevant AWS managed policies yourself. + * + * The relevant managed policies are "service-role/AWSLambdaBasicExecutionRole" and + * "service-role/AWSLambdaVPCAccessExecutionRole". + * * @default - A unique role will be generated for this lambda function. * Both supplied and generated roles can always be changed by calling `addToRolePolicy`. */ From 9899c764553405360eb28f2a6b0aac0c4991a792 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 22 May 2020 09:26:44 -0700 Subject: [PATCH 13/33] chore(stepfunctions): add missing tests (#8147) added some missing tests for task, custom state, wait state ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../test/custom-state.test.ts | 52 ++++++- .../test/private/render-util.ts | 12 ++ .../aws-stepfunctions/test/task.test.ts | 127 ++++++++++++++++++ .../aws-stepfunctions/test/wait.test.ts | 79 +++++++++++ 4 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/task.test.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts index 723d917986899..9e58281059e8b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/custom-state.test.ts @@ -1,12 +1,16 @@ import '@aws-cdk/assert/jest'; import * as cdk from '@aws-cdk/core'; -import * as stepfunctions from '../lib'; +import * as sfn from '../lib'; +import { render } from './private/render-util'; describe('Custom State', () => { - test('maintains the state Json provided during construction', () => { + let stack: cdk.Stack; + let stateJson: any; + + beforeEach(() => { // GIVEN - const stack = new cdk.Stack(); - const stateJson = { + stack = new cdk.Stack(); + stateJson = { Type: 'Task', Resource: 'arn:aws:states:::dynamodb:putItem', Parameters: { @@ -19,9 +23,11 @@ describe('Custom State', () => { }, ResultPath: null, }; + }); + test('maintains the state Json provided during construction', () => { // WHEN - const customState = new stepfunctions.CustomState(stack, 'Custom', { + const customState = new sfn.CustomState(stack, 'Custom', { stateJson, }); @@ -31,4 +37,38 @@ describe('Custom State', () => { End: true, }); }); -}); + + test('can add a next state to the chain', () => { + // WHEN + const definition = new sfn.CustomState(stack, 'Custom', { + stateJson, + }).next(new sfn.Pass(stack, 'MyPass')); + + // THEN + expect(render(stack, definition)).toStrictEqual( + { + StartAt: 'Custom', + States: { + Custom: { + Next: 'MyPass', + Type: 'Task', + Resource: 'arn:aws:states:::dynamodb:putItem', + Parameters: { + TableName: 'MyTable', + Item: { + id: { + S: 'MyEntry', + }, + }, + }, + ResultPath: null, + }, + MyPass: { + Type: 'Pass', + End: true, + }, + }, + }, + ); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts new file mode 100644 index 0000000000000..ceb8998c7da92 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/private/render-util.ts @@ -0,0 +1,12 @@ +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../../lib'; + +/** + * Renders a state machine definition + * + * @param stack stack for the state machine + * @param definition state machine definition + */ +export function render(stack: cdk.Stack, definition: sfn.IChainable) { + return stack.resolve(new sfn.StateGraph(definition.startState, 'Test Graph').toGraphJson()); +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts new file mode 100644 index 0000000000000..623ee29689308 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/task.test.ts @@ -0,0 +1,127 @@ +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import * as cdk from '@aws-cdk/core'; +import * as sfn from '../lib'; + +describe('Task state', () => { + + let stack: cdk.Stack; + let task: sfn.Task; + + beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + task = new sfn.Task(stack, 'my-task', { + task: new FakeTask(), + }); + }); + + test('get named metric for the task', () => { + // WHEN + const metric = task.metric('my-metric'); + + // THEN + verifyMetric(metric, 'my-metric', 'Sum'); + }); + + test('add metric for number of times the task failed', () => { + // WHEN + const metric = task.metricFailed(); + + // THEN + verifyMetric(metric, 'Failed', 'Sum'); + }); + + test('add metric for number of times the metrics heartbeat timed out', () => { + // WHEN + const metric = task.metricHeartbeatTimedOut(); + + // THEN + verifyMetric(metric, 'HeartbeatTimedOut', 'Sum'); + }); + + test('add metric for task state run time', () => { + // WHEN + const metric = task.metricRunTime(); + + // THEN + verifyMetric(metric, 'RunTime', 'Average'); + }); + + test('add metric for task schedule time', () => { + // WHEN + const metric = task.metricScheduleTime(); + + // THEN + verifyMetric(metric, 'ScheduleTime', 'Average'); + }); + + test('add metric for number of times the task is scheduled', () => { + // WHEN + const metric = task.metricScheduled(); + + // THEN + verifyMetric(metric, 'Scheduled', 'Sum'); + }); + + test('add metric for number of times the task was started', () => { + // WHEN + const metric = task.metricStarted(); + + // THEN + verifyMetric(metric, 'Started', 'Sum'); + }); + + test('add metric for number of times the task succeeded', () => { + // WHEN + const metric = task.metricSucceeded(); + + // THEN + verifyMetric(metric, 'Succeeded', 'Sum'); + }); + + test('add metric for time between task being scheduled to closing', () => { + // WHEN + const metric = task.metricTime(); + + // THEN + verifyMetric(metric, 'Time', 'Average'); + }); + + test('add metric for number of times the task times out', () => { + // WHEN + const metric = task.metricTimedOut(); + + // THEN + verifyMetric(metric, 'TimedOut', 'Sum'); + }); + +}); + +function verifyMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + metricName, + namespace: 'AWS/States', + period: { + amount: 5, + unit: { + inMillis: 60000, + label: 'minutes', + }, + }, + statistic, + dimensions: { + Arn: 'resource', + }, + }); +} + +class FakeTask implements sfn.IStepFunctionsTask { + public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { + return { + resourceArn: 'resource', + metricPrefixSingular: '', + metricPrefixPlural: '', + metricDimensions: { Arn: 'resource' }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts new file mode 100644 index 0000000000000..94c67543e2e60 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/wait.test.ts @@ -0,0 +1,79 @@ +import '@aws-cdk/assert/jest'; +import * as cdk from '@aws-cdk/core'; +import { Pass, Wait, WaitTime } from '../lib'; +import { render } from './private/render-util'; + +describe('Wait State', () => { + test('wait time from ISO8601 timestamp', () => { + // GIVEN + const timestamp = '2025-01-01T00:00:00Z'; + + // WHEN + const waitTime = WaitTime.timestamp(timestamp); + + // THEN + expect(waitTime).toEqual({ + json: { + Timestamp: '2025-01-01T00:00:00Z', + }, + }); + }); + + test('wait time from seconds path in state object', () => { + // GIVEN + const secondsPath = '$.waitSeconds'; + + // WHEN + const waitTime = WaitTime.secondsPath(secondsPath); + + // THEN + expect(waitTime).toEqual({ + json: { + SecondsPath: '$.waitSeconds', + }, + }); + }); + + test('wait time from timestamp path in state object', () => { + // GIVEN + const path = '$.timestampPath'; + + // WHEN + const waitTime = WaitTime.timestampPath(path); + + // THEN + expect(waitTime).toEqual({ + json: { + TimestampPath: '$.timestampPath', + }, + }); + }); + + test('supports adding a next state', () => { + // GIVEN + const stack = new cdk.Stack(); + const chain = new Wait(stack, 'myWaitState', { + time: WaitTime.duration(cdk.Duration.seconds(30)), + }); + + // WHEN + chain.next(new Pass(stack, 'final pass', {})); + + // THEN + expect(render(stack, chain)).toEqual({ + StartAt: 'myWaitState', + States: { + 'final pass': { + End: true, + Type: 'Pass', + }, + 'myWaitState': { + Next: 'final pass', + Seconds: 30, + Type: 'Wait', + }, + }, + }); + }); + +}); \ No newline at end of file From a721e670cdc9888cd67ef1a24021004e18bfd23c Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Fri, 22 May 2020 11:42:53 -0700 Subject: [PATCH 14/33] feat(stepfunctions-tasks): task for starting a job run in AWS Glue (#8143) replacement for the current implementation of `RunGlueJob` where service integration and state level properties are merged. Follows the new integration pattern. Notable differences from the `RunGlueJob` implementation: * `arguments` prop is now of type `sfn.TaskInput` Rationale: old implementation precluded using task input as the arguments directly. Added a test for this as well. Updated the README. Note that the other unit tests and integ test have been left verbatim. This is a light sanity test that expected templates have not changed. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-stepfunctions-tasks/README.md | 15 +- .../lib/glue/run-glue-job-task.ts | 4 + .../lib/glue/start-job-run.ts | 119 ++++++++ .../aws-stepfunctions-tasks/lib/index.ts | 1 + .../glue/integ.start-job-run.expected.json | 268 ++++++++++++++++++ .../test/glue/integ.start-job-run.ts | 63 ++++ .../test/glue/start-job-run.test.ts | 173 +++++++++++ 7 files changed, 635 insertions(+), 8 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts create mode 100644 packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 92a473d886d10..c7cc3fe389099 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -509,14 +509,13 @@ Step Functions supports [AWS Glue](https://docs.aws.amazon.com/step-functions/la You can call the [`StartJobRun`](https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-StartJobRun) API from a `Task` state. ```ts -new sfn.Task(stack, 'Task', { - task: new tasks.RunGlueJobTask(jobName, { - arguments: { - key: 'value', - }, - timeout: cdk.Duration.minutes(30), - notifyDelayAfter: cdk.Duration.minutes(5), - }), +new GlueStartJobRun(stack, 'Task', { + jobName: 'my-glue-job', + arguments: { + key: 'value', + }, + timeout: cdk.Duration.minutes(30), + notifyDelayAfter: cdk.Duration.minutes(5), }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts index 854df949c4dc9..fd4722835a52e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/run-glue-job-task.ts @@ -5,6 +5,8 @@ import { getResourceArn } from '../resource-arn-suffix'; /** * Properties for RunGlueJobTask + * + * @deprecated use `GlueStartJobRun` */ export interface RunGlueJobTaskProps { @@ -63,6 +65,8 @@ export interface RunGlueJobTaskProps { * https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-JobRun * * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html + * + * @deprecated use `GlueStartJobRun` */ export class RunGlueJobTask implements sfn.IStepFunctionsTask { private readonly integrationPattern: sfn.ServiceIntegrationPattern; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts new file mode 100644 index 0000000000000..9df1a6a5ed852 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/glue/start-job-run.ts @@ -0,0 +1,119 @@ +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Duration, Stack } from '@aws-cdk/core'; +import { integrationResourceArn, validatePatternSupported } from '../private/task-utils'; + +/** + * Properties for starting an AWS Glue job as a task + */ +export interface GlueStartJobRunProps extends sfn.TaskStateBaseProps { + + /** + * Glue job name + */ + readonly glueJobName: string; + + /** + * The job arguments specifically for this run. + * + * For this job run, they replace the default arguments set in the job + * definition itself. + * + * @default - Default arguments set in the job definition + */ + readonly arguments?: sfn.TaskInput; + + /** + * The name of the SecurityConfiguration structure to be used with this job run. + * + * This must match the Glue API + * @see https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-common.html#aws-glue-api-regex-oneLine + * + * @default - Default configuration set in the job definition + */ + readonly securityConfiguration?: string; + + /** + * After a job run starts, the number of minutes to wait before sending a job run delay notification. + * + * Must be at least 1 minute. + * + * @default - Default delay set in the job definition + */ + readonly notifyDelayAfter?: Duration; +} + +/** + * Starts an AWS Glue job in a Task state + * + * OUTPUT: the output of this task is a JobRun structure, for details consult + * https://docs.aws.amazon.com/glue/latest/dg/aws-glue-api-jobs-runs.html#aws-glue-api-jobs-runs-JobRun + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-glue.html + */ +export class GlueStartJobRun extends sfn.TaskStateBase { + private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ + sfn.IntegrationPattern.REQUEST_RESPONSE, + sfn.IntegrationPattern.RUN_JOB, + ]; + + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + private readonly integrationPattern: sfn.IntegrationPattern; + + constructor(scope: Construct, id: string, private readonly props: GlueStartJobRunProps) { + super(scope, id, props); + this.integrationPattern = props.integrationPattern ?? sfn.IntegrationPattern.REQUEST_RESPONSE; + + validatePatternSupported(this.integrationPattern, GlueStartJobRun.SUPPORTED_INTEGRATION_PATTERNS); + + this.taskPolicies = this.getPolicies(); + + this.taskMetrics = { + metricPrefixSingular: 'GlueJob', + metricPrefixPlural: 'GlueJobs', + metricDimensions: { GlueJobName: this.props.glueJobName }, + }; + } + + protected renderTask(): any { + const notificationProperty = this.props.notifyDelayAfter ? { NotifyDelayAfter: this.props.notifyDelayAfter.toMinutes() } : null; + return { + Resource: integrationResourceArn('glue', 'startJobRun', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + JobName: this.props.glueJobName, + Arguments: this.props.arguments?.value, + Timeout: this.props.timeout?.toMinutes(), + SecurityConfiguration: this.props.securityConfiguration, + NotificationProperty: notificationProperty, + }), + TimeoutSeconds: undefined, + }; + } + + private getPolicies(): iam.PolicyStatement[] { + let iamActions: string[] | undefined; + if (this.integrationPattern === sfn.IntegrationPattern.REQUEST_RESPONSE) { + iamActions = ['glue:StartJobRun']; + } else if (this.integrationPattern === sfn.IntegrationPattern.RUN_JOB) { + iamActions = [ + 'glue:StartJobRun', + 'glue:GetJobRun', + 'glue:GetJobRuns', + 'glue:BatchStopJobRun', + ]; + } + + return [new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'glue', + resource: 'job', + resourceName: this.props.glueJobName, + }), + ], + actions: iamActions, + })]; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 9759c4a621251..7b45086a4e48e 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -23,5 +23,6 @@ export * from './emr/emr-cancel-step'; export * from './emr/emr-modify-instance-fleet-by-name'; export * from './emr/emr-modify-instance-group-by-name'; export * from './glue/run-glue-job-task'; +export * from './glue/start-job-run'; export * from './batch/run-batch-job'; export * from './dynamodb/call-dynamodb'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json new file mode 100644 index 0000000000000..1f916f6be06f1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.expected.json @@ -0,0 +1,268 @@ +{ + "Parameters": { + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B": { + "Type": "String", + "Description": "S3 bucket for asset \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + }, + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC": { + "Type": "String", + "Description": "S3 key for asset version \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + }, + "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dArtifactHashEC764944": { + "Type": "String", + "Description": "Artifact hash for asset \"d030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0d\"" + } + }, + "Resources": { + "GlueJobRole1CD031E0": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "glue.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/service-role/AWSGlueServiceRole" + ] + ] + } + ] + } + }, + "GlueJobRoleDefaultPolicy3D94D6F1": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:GetObject*", + "s3:GetBucket*", + "s3:List*" + ], + "Effect": "Allow", + "Resource": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + } + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":s3:::", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "GlueJobRoleDefaultPolicy3D94D6F1", + "Roles": [ + { + "Ref": "GlueJobRole1CD031E0" + } + ] + } + }, + "GlueJob": { + "Type": "AWS::Glue::Job", + "Properties": { + "Command": { + "Name": "glueetl", + "PythonVersion": "3", + "ScriptLocation": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3BucketB8F6851B" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersd030bb7913ca422df69f29b2ea678ab4e5085bb3cbb17029e4b101d2dc4e3e0dS3VersionKey7BCC06FC" + } + ] + } + ] + } + ] + ] + } + }, + "Role": { + "Fn::GetAtt": [ + "GlueJobRole1CD031E0", + "Arn" + ] + }, + "GlueVersion": "1.0", + "Name": "My Glue Job" + } + }, + "StateMachineRole543B9670": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "states.", + { + "Ref": "AWS::Region" + }, + ".amazonaws.com" + ] + ] + } + } + } + ], + "Version": "2012-10-17" + } + } + }, + "StateMachineRoleDefaultPolicyDA5F7DA8": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "glue:StartJobRun", + "glue:GetJobRun", + "glue:GetJobRuns", + "glue:BatchStopJobRun" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":glue:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":job/My Glue Job" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "StateMachineRoleDefaultPolicyDA5F7DA8", + "Roles": [ + { + "Ref": "StateMachineRole543B9670" + } + ] + } + }, + "StateMachine81935E76": { + "Type": "AWS::StepFunctions::StateMachine", + "Properties": { + "DefinitionString": { + "Fn::Join": [ + "", + [ + "{\"StartAt\":\"Start Task\",\"States\":{\"Start Task\":{\"Type\":\"Pass\",\"Next\":\"Glue Job Task\"},\"Glue Job Task\":{\"Next\":\"End Task\",\"Type\":\"Task\",\"Resource\":\"arn:", + { + "Ref": "AWS::Partition" + }, + ":states:::glue:startJobRun.sync\",\"Parameters\":{\"JobName\":\"My Glue Job\",\"Arguments\":{\"--enable-metrics\":\"true\"}}},\"End Task\":{\"Type\":\"Pass\",\"End\":true}}}" + ] + ] + }, + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRole543B9670", + "Arn" + ] + } + }, + "DependsOn": [ + "StateMachineRoleDefaultPolicyDA5F7DA8", + "StateMachineRole543B9670" + ] + } + }, + "Outputs": { + "StateMachineARNOutput": { + "Value": { + "Ref": "StateMachine81935E76" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts new file mode 100644 index 0000000000000..d63e2c5f586cc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/integ.start-job-run.ts @@ -0,0 +1,63 @@ +import * as glue from '@aws-cdk/aws-glue'; +import * as iam from '@aws-cdk/aws-iam'; +import * as assets from '@aws-cdk/aws-s3-assets'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as path from 'path'; +import { GlueStartJobRun } from '../../lib/glue/start-job-run'; + +/* + * Stack verification steps: + * * aws stepfunctions start-execution --state-machine-arn + * * aws stepfunctions describe-execution --execution-arn + * The "describe-execution" call should eventually return status "SUCCEEDED". + * NOTE: It will take up to 15 minutes for the step function to complete due to the cold start time + * for AWS Glue, which as of 02/2020, is around 10-15 minutes. + */ + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'aws-stepfunctions-integ'); + +const codeAsset = new assets.Asset(stack, 'Glue Job Script', { + path: path.join(__dirname, 'my-glue-script/job.py'), +}); + +const jobRole = new iam.Role(stack, 'Glue Job Role', { + assumedBy: new iam.ServicePrincipal('glue'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSGlueServiceRole'), + ], +}); +codeAsset.grantRead(jobRole); + +const job = new glue.CfnJob(stack, 'Glue Job', { + name: 'My Glue Job', + glueVersion: '1.0', + command: { + name: 'glueetl', + pythonVersion: '3', + scriptLocation: `s3://${codeAsset.s3BucketName}/${codeAsset.s3ObjectKey}`, + }, + role: jobRole.roleArn, +}); + +const jobTask = new GlueStartJobRun(stack, 'Glue Job Task', { + glueJobName: job.name!, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromObject({ + '--enable-metrics': 'true', + }), +}); + +const startTask = new sfn.Pass(stack, 'Start Task'); +const endTask = new sfn.Pass(stack, 'End Task'); + +const stateMachine = new sfn.StateMachine(stack, 'State Machine', { + definition: sfn.Chain.start(startTask).next(jobTask).next(endTask), +}); + +new cdk.CfnOutput(stack, 'State Machine ARN Output', { + value: stateMachine.stateMachineArn, +}); + +app.synth(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts new file mode 100644 index 0000000000000..e3773bd701966 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/glue/start-job-run.test.ts @@ -0,0 +1,173 @@ +import '@aws-cdk/assert/jest'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Duration, Stack } from '@aws-cdk/core'; +import * as tasks from '../../lib'; +import { GlueStartJobRun } from '../../lib/glue/start-job-run'; + +const glueJobName = 'GlueJob'; +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('Invoke glue job with just job ARN', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun', + ], + ], + }, + End: true, + Parameters: { + JobName: glueJobName, + }, + }); +}); + +test('Invoke glue job with full properties', () => { + const jobArguments = { + key: 'value', + }; + const timeoutMinutes = 1440; + const glueJobTimeout = Duration.minutes(timeoutMinutes); + const securityConfiguration = 'securityConfiguration'; + const notifyDelayAfterMinutes = 10; + const notifyDelayAfter = Duration.minutes(notifyDelayAfterMinutes); + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromObject(jobArguments), + timeout: glueJobTimeout, + securityConfiguration, + notifyDelayAfter, + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + JobName: glueJobName, + Arguments: jobArguments, + Timeout: timeoutMinutes, + SecurityConfiguration: securityConfiguration, + NotificationProperty: { + NotifyDelayAfter: notifyDelayAfterMinutes, + }, + }, + }); +}); + +test('job arguments can reference state input', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + arguments: sfn.TaskInput.fromDataAt('$.input'), + }); + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::glue:startJobRun.sync', + ], + ], + }, + End: true, + Parameters: { + 'JobName': glueJobName, + 'Arguments.$': '$.input', + }, + }); +}); + +test('permitted role actions limited to start job run if service integration pattern is REQUEST_RESPONSE', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: 'glue:StartJobRun', + }], + }, + }); +}); + +test('permitted role actions include start, get, and stop job run if service integration pattern is RUN_JOB', () => { + const task = new GlueStartJobRun(stack, 'Task', { + glueJobName, + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + }); + + new sfn.StateMachine(stack, 'SM', { + definition: task, + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [{ + Action: [ + 'glue:StartJobRun', + 'glue:GetJobRun', + 'glue:GetJobRuns', + 'glue:BatchStopJobRun', + ], + }], + }, + }); +}); + +test('Task throws if WAIT_FOR_TASK_TOKEN is supplied as service integration pattern', () => { + expect(() => { + new sfn.Task(stack, 'Task', { + task: new tasks.RunGlueJobTask(glueJobName, { + integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, + }), + }); + }).toThrow(/Invalid Service Integration Pattern: WAIT_FOR_TASK_TOKEN is not supported to call Glue./i); +}); From 86eac6af074bf78a921c52d613eca0dd4a514a49 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Sun, 24 May 2020 14:11:45 -0700 Subject: [PATCH 15/33] feat(stepfunctions): support paths in Pass state (#8070) The Pass state supports JsonPath values in the `parameters` field to filter the state input and serve as input to the field. Added a method to render parameters which will generate the ASL JSON format if a path is used in a parameter. Closes #7181 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-stepfunctions/README.md | 33 ++++++++++++++++--- .../aws-stepfunctions/lib/states/pass.ts | 11 +++++++ .../test/state-machine-resources.test.ts | 20 +++++++++++ 3 files changed, 60 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 557527bd02f1c..b7560ad80883e 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -131,20 +131,45 @@ directly in the Amazon States language. ### Pass -A `Pass` state does no work, but it can optionally transform the execution's -JSON state. +A `Pass` state passes its input to its output, without performing work. +Pass states are useful when constructing and debugging state machines. + +The following example injects some fixed data into the state machine through +the `result` field. The `result` field will be added to the input and the result +will be passed as the state's output. ```ts // Makes the current JSON state { ..., "subObject": { "hello": "world" } } const pass = new stepfunctions.Pass(this, 'Add Hello World', { - result: { hello: "world" }, - resultPath: '$.subObject', + result: { hello: 'world' }, + resultPath: '$.subObject', }); // Set the next state pass.next(nextState); ``` +The `Pass` state also supports passing key-value pairs as input. Values can +be static, or selected from the input with a path. + +The following example filters the `greeting` field from the state input +and also injects a field called `otherData`. + +```ts +const pass = new stepfunctions.Pass(this, 'Filter input and inject data', { + parameters: { // input to the pass state + input: stepfunctions.DataAt('$.input.greeting') + otherData: 'some-extra-stuff' + }, +}); +``` + +The object specified in `parameters` will be the input of the `Pass` state. +Since neither `Result` nor `ResultPath` are supplied, the `Pass` state copies +its input through to its output. + +Learn more about the [Pass state](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-pass-state.html) + ### Wait A `Wait` state waits for a given number of seconds, or until the current time diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index e081b8fdc734b..982980456caaa 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -1,5 +1,6 @@ import * as cdk from '@aws-cdk/core'; import {Chain} from '../chain'; +import { FieldUtils } from '../fields'; import {IChainable, INextable} from '../types'; import { StateType } from './private/state-type'; import {renderJsonPath, State } from './state'; @@ -147,7 +148,17 @@ export class Pass extends State implements INextable { Result: this.result ? this.result.value : undefined, ResultPath: renderJsonPath(this.resultPath), ...this.renderInputOutput(), + ...this.renderParameters(), ...this.renderNextEnd(), }; } + + /** + * Render Parameters in ASL JSON format + */ + private renderParameters(): any { + return FieldUtils.renderObject({ + Parameters: this.parameters, + }); + } } diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts index cc6fd2f7ec486..012a193087b9b 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-machine-resources.test.ts @@ -274,6 +274,26 @@ describe('State Machine Resources', () => { }); }), + test('parameters can be selected from the input with a path', () => { + // GIVEN + const stack = new cdk.Stack(); + const task = new stepfunctions.Pass(stack, 'Pass', { + parameters: { + input: stepfunctions.Data.stringAt('$.myField'), + }, + }); + + // WHEN + const taskState = task.toStateJson(); + + // THEN + expect(taskState).toEqual({ End: true, + Parameters: + { 'input.$': '$.myField'}, + Type: 'Pass', + }); + }), + test('State machines must depend on their roles', () => { // GIVEN const stack = new cdk.Stack(); From 1ecc3f8e319d908246a51b30c3f5695fd260a3d1 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 25 May 2020 10:21:05 +0200 Subject: [PATCH 16/33] chore: clear 'breaking changes' history (#8128) We've been accumulating breaking change exceptions. Time to clear them out to make sure no future breakage accidentally slips through. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- allowed-breaking-changes.txt | 59 +----------------------------------- 1 file changed, 1 insertion(+), 58 deletions(-) diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 94d3c3c05f46a..8b137891791fe 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -1,58 +1 @@ -incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition.addVolume -incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume -change-return-type:@aws-cdk/core.Fn.getAtt -new-argument:@aws-cdk/aws-iam.ManagedPolicy. -new-argument:@aws-cdk/aws-iam.ManagedPolicy. -removed:@aws-cdk/aws-apigateway.AwsIntegration.props -removed:@aws-cdk/aws-apigateway.HttpIntegration.props -removed:@aws-cdk/aws-apigateway.Integration.props -removed:@aws-cdk/aws-apigateway.LambdaIntegration.props -removed:@aws-cdk/aws-apigateway.MockIntegration.props -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.schedule -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.cluster -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.desiredTaskCount -removed:@aws-cdk/aws-ecs-patterns.ScheduledEc2TaskDefinitionOptions.vpc -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.schedule -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.cluster -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.desiredTaskCount -removed:@aws-cdk/aws-ecs-patterns.ScheduledFargateTaskDefinitionOptions.vpc -incompatible-argument:@aws-cdk/aws-lambda.Function. -incompatible-argument:@aws-cdk/aws-lambda.SingletonFunction. -incompatible-argument:@aws-cdk/aws-lambda.Function.addEnvironment -changed-type:@aws-cdk/aws-dynamodb.Table.tableStreamArn -incompatible-argument:@aws-cdk/aws-apigateway.LambdaRestApi.addModel -incompatible-argument:@aws-cdk/aws-apigateway.Model. -incompatible-argument:@aws-cdk/aws-apigateway.RestApi.addModel -incompatible-argument:@aws-cdk/aws-apigateway.ProxyResource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.Resource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.ResourceBase.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.IResource.addProxy -incompatible-argument:@aws-cdk/aws-apigateway.RequestAuthorizer. -incompatible-argument:@aws-cdk/aws-servicediscovery.Service.fromServiceAttributes -removed:@aws-cdk/core.ConstructNode.addReference -removed:@aws-cdk/core.ConstructNode.references -removed:@aws-cdk/core.OutgoingReference -change-return-type:@aws-cdk/aws-lambda-destinations.EventBridgeDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.LambdaDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.SnsDestination.bind -change-return-type:@aws-cdk/aws-lambda-destinations.SqsDestination.bind -removed:@aws-cdk/cdk-assets-schema.DockerImageDestination.imageUri -incompatible-argument:@aws-cdk/aws-iam.FederatedPrincipal. -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addCondition -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addConditions -incompatible-argument:@aws-cdk/aws-iam.PolicyStatement.addFederatedPrincipal -incompatible-argument:@aws-cdk/aws-iam.PrincipalPolicyFragment. -changed-type:@aws-cdk/aws-iam.FederatedPrincipal.conditions -changed-type:@aws-cdk/aws-iam.PrincipalPolicyFragment.conditions -changed-type:@aws-cdk/aws-iam.PrincipalWithConditions.conditions -removed:@aws-cdk/cdk-assets-schema.Placeholders -# Following two are because we're turning: properties: {string=>any} into a union of typed interfaces -# Needs to be removed after next release. -incompatible-argument:@aws-cdk/cloud-assembly-schema.Manifest.save -change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load -removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_DEPLOY_ACTION_ROLE_ARN -removed:@aws-cdk/core.DefaultStackSynthesizerProps.deployActionRoleArn + From 7d0c173bdce97d43eaffaacd303a25d8c142fee3 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 May 2020 09:15:37 +0000 Subject: [PATCH 17/33] chore(deps): bump @typescript-eslint/eslint-plugin from 2.34.0 to 3.0.0 (#8135) Bumps [@typescript-eslint/eslint-plugin](https://github.com/typescript-eslint/typescript-eslint/tree/HEAD/packages/eslint-plugin) from 2.34.0 to 3.0.0. - [Release notes](https://github.com/typescript-eslint/typescript-eslint/releases) - [Changelog](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/CHANGELOG.md) - [Commits](https://github.com/typescript-eslint/typescript-eslint/commits/v3.0.0/packages/eslint-plugin) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- tools/cdk-build-tools/package.json | 2 +- yarn.lock | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 86188e2ef0d19..0ddee7d957395 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,7 +39,7 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^2.34.0", + "@typescript-eslint/eslint-plugin": "^3.0.0", "@typescript-eslint/parser": "^2.19.2", "awslint": "0.0.0", "colors": "^1.4.0", diff --git a/yarn.lock b/yarn.lock index 8b2d06e54f111..b4fffb20a086b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1641,14 +1641,15 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.3.tgz#38fb31d82ed07dea87df6bd565721d11979fd761" integrity sha512-mhdQq10tYpiNncMkg1vovCud5jQm+rWeRVz6fxjCJlY6uhDlAn9GnMSmBa2DQwqPf/jS5YR0K/xChDEh1jdOQg== -"@typescript-eslint/eslint-plugin@^2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.34.0.tgz#6f8ce8a46c7dea4a6f1d171d2bb8fbae6dac2be9" - integrity sha512-4zY3Z88rEE99+CNvTbXSyovv2z9PNOVffTWD2W8QF5s2prBQtwN2zadqERcrHpcR7O/+KMI3fcTAmUUhK/iQcQ== +"@typescript-eslint/eslint-plugin@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-3.0.0.tgz#02f8ec6b5ce814bda80dfc22463f108bed1f699b" + integrity sha512-lcZ0M6jD4cqGccYOERKdMtg+VWpoq3NSnWVxpc/AwAy0zhkUYVioOUZmfNqiNH8/eBNGhCn6HXd6mKIGRgNc1Q== dependencies: - "@typescript-eslint/experimental-utils" "2.34.0" + "@typescript-eslint/experimental-utils" "3.0.0" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" + semver "^7.3.2" tsutils "^3.17.1" "@typescript-eslint/experimental-utils@2.28.0": @@ -1661,13 +1662,13 @@ eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/experimental-utils@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-2.34.0.tgz#d3524b644cdb40eebceca67f8cf3e4cc9c8f980f" - integrity sha512-eS6FTkq+wuMJ+sgtuNTtcqavWXqsflWcfBnlYhg/nS4aZ1leewkXGbvBhaapn1q6qf4M71bsR1tez5JTRMuqwA== +"@typescript-eslint/experimental-utils@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-3.0.0.tgz#1ddf53eeb61ac8eaa9a77072722790ac4f641c03" + integrity sha512-BN0vmr9N79M9s2ctITtChRuP1+Dls0x/wlg0RXW1yQ7WJKPurg6X3Xirv61J2sjPif4F8SLsFMs5Nzte0WYoTQ== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/typescript-estree" "2.34.0" + "@typescript-eslint/typescript-estree" "3.0.0" eslint-scope "^5.0.0" eslint-utils "^2.0.0" @@ -1694,10 +1695,10 @@ semver "^6.3.0" tsutils "^3.17.1" -"@typescript-eslint/typescript-estree@2.34.0": - version "2.34.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-2.34.0.tgz#14aeb6353b39ef0732cc7f1b8285294937cf37d5" - integrity sha512-OMAr+nJWKdlVM9LOqCqh3pQQPwxHAN7Du8DR6dmwCrAmxtiXQnhHJ6tBNtf+cggqfo51SG/FCwnKhXCIM7hnVg== +"@typescript-eslint/typescript-estree@3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-3.0.0.tgz#fa40e1b76ccff880130be054d9c398e96004bf42" + integrity sha512-nevQvHyNghsfLrrByzVIH4ZG3NROgJ8LZlfh3ddwPPH4CH7W4GAiSx5qu+xHuX5pWsq6q/eqMc1io840ZhAnUg== dependencies: debug "^4.1.1" eslint-visitor-keys "^1.1.0" From edbeff5264d18a4a5b8228d400852aba733321ab Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Mon, 25 May 2020 14:35:06 +0200 Subject: [PATCH 18/33] chore: fix tsc-3.9 issues ahead of upgrade (#8182) Fixes one issue where the typechecker in 3.9 is stricter when matching type intersections. The particular issue was with a `string`-valued `enum` attempting to match against the `string` type. Added a better typed guard for this particular case fixed it. Additionally, the new incremental build support would cause certain `.json` files to not be `require`-able due to not being listed under `include` in the `tsconfig.json` file generated by `jsii`. Instead of copying the SDK metadata JSON document from the `aws-sdk` package, inlined the data in a `.generated.ts` module, which provides a cleaner type structure. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-events-targets/build-tools/gen.js | 40 ++++ .../aws-events-targets/lib/aws-api.ts | 2 +- .../@aws-cdk/aws-events-targets/package.json | 2 +- .../lib/private/schema-helpers.ts | 13 +- .../cdk-assets-schema/lib/validate.ts | 4 +- .../cdk-assets-schema/test/validate.test.ts | 184 ++++++++++++------ 6 files changed, 176 insertions(+), 69 deletions(-) create mode 100644 packages/@aws-cdk/aws-events-targets/build-tools/gen.js diff --git a/packages/@aws-cdk/aws-events-targets/build-tools/gen.js b/packages/@aws-cdk/aws-events-targets/build-tools/gen.js new file mode 100644 index 0000000000000..406c72cd9b11e --- /dev/null +++ b/packages/@aws-cdk/aws-events-targets/build-tools/gen.js @@ -0,0 +1,40 @@ +#!/usr/bin/env node + +/** + * Writes lib/sdk-api-metadata.generated.ts from the metadata gathered from the + * aws-sdk package. + */ + +const fs = require('fs'); +const path = require('path'); + +const packageInfo = require('aws-sdk/package.json'); +const sdkMetadata = require('aws-sdk/apis/metadata.json'); + +fs.writeFileSync( + path.resolve(__dirname, '..', 'lib', 'sdk-api-metadata.generated.ts'), + [ + 'export interface AwsSdkMetadata {', + ' readonly [service: string]: {', + ' readonly name: string;', + ' readonly cors?: boolean;', + ' readonly dualstackAvailable?: boolean;', + ' readonly prefix?: string;', + ' readonly versions?: readonly string[];', + ' readonly xmlNoDefaultLists?: boolean;', + ' readonly [key: string]: unknown;', + ' };', + '}', + '', + // The generated code is probably not going to be super clean as far as linters are concerned... + '/* eslint-disable */', + '/* tslint:disable */', + '', + // Just mention where the data comes from, as a basic courtesy... + '/**', + ` * Extracted from ${packageInfo.name} version ${packageInfo.version} (${packageInfo.license}).`, + ' */', + // And finally, we export the data: + `export const metadata: AwsSdkMetadata = ${JSON.stringify(sdkMetadata, null, 2)};`, + ].join('\n'), +); diff --git a/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts b/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts index fdf0ac50eafa0..b47f23e6e9a2b 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/aws-api.ts @@ -2,7 +2,7 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as path from 'path'; -import * as metadata from './sdk-api-metadata.json'; +import { metadata } from './sdk-api-metadata.generated'; import { addLambdaPermission } from './util'; /** diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 5142667aed462..0216eabf50638 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -48,7 +48,7 @@ }, "cdk-build": { "pre": [ - "cp -f $(node -p 'require.resolve(\"aws-sdk/apis/metadata.json\")') lib/sdk-api-metadata.json && rm -f lib/sdk-api-metadata.d.ts" + "node ./build-tools/gen.js" ], "jest": true }, diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts index a124b366a22ad..9ae1d1fcdb39a 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/private/schema-helpers.ts @@ -1,3 +1,5 @@ +import { FileAssetPackaging } from '../file-asset'; + /** * Validate that a given key is of a given type in an object * @@ -51,4 +53,13 @@ export function isObjectAnd(p: (x: object) => A): (x: unknown) => A { export function assertIsObject(x: unknown): asserts x is object { if (typeof x !== 'object' || x === null) { throw new Error(`Expected a map, got '${x}'`); } -} \ No newline at end of file +} + +export function isFileAssetPackaging(x: unknown): FileAssetPackaging { + const str = isString(x); + const validValues = Object.values(FileAssetPackaging) as string[]; // Explicit cast needed because this is a string-valued enum + if (!validValues.includes(str)) { + throw new Error(`Expected a FileAssetPackaging (one of ${validValues.map(v => `'${v}'`).join(', ')}), got '${str}'`); + } + return x as any; +} diff --git a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts index 041124f0b2061..2660a6adae98f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts +++ b/packages/@aws-cdk/cdk-assets-schema/lib/validate.ts @@ -3,7 +3,7 @@ import { DockerImageAsset } from './docker-image-asset'; import { FileAsset } from './file-asset'; import { ManifestFile } from './manifest-schema'; import { loadMyPackageJson } from './private/my-package-json'; -import { assertIsObject, expectKey, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; +import { assertIsObject, expectKey, isFileAssetPackaging, isMapOf, isObjectAnd, isString } from './private/schema-helpers'; const PACKAGE_VERSION = loadMyPackageJson().version; @@ -63,7 +63,7 @@ function isFileAsset(entry: object): FileAsset { expectKey(entry, 'source', source => { assertIsObject(source); expectKey(source, 'path', isString); - expectKey(source, 'packaging', isString, true); + expectKey(source, 'packaging', isFileAssetPackaging, true); return source; }); diff --git a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts index 0f4c22e482e61..145ae265aec5f 100644 --- a/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts +++ b/packages/@aws-cdk/cdk-assets-schema/test/validate.test.ts @@ -1,81 +1,137 @@ -import { AssetManifestSchema } from '../lib'; +import { AssetManifestSchema, FileAssetPackaging } from '../lib'; -test('Correctly validate Docker image asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - dockerImages: { - asset: { - source: { - directory: '.', - }, - destinations: { - dest: { - region: 'us-north-20', - repositoryName: 'REPO', - imageTag: 'TAG', +describe('Docker image asset', () => { + test('valid input', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + dockerImages: { + asset: { + source: { + directory: '.', + }, + destinations: { + dest: { + region: 'us-north-20', + repositoryName: 'REPO', + imageTag: 'TAG', + }, }, }, }, - }, - }); - }).not.toThrow(); -}); + }); + }).not.toThrow(); + }); -test('Throw on invalid Docker image asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - dockerImages: { - asset: { - source: { }, - destinations: { }, + test('invalid input', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + dockerImages: { + asset: { + source: {}, + destinations: {}, + }, }, - }, - }); - }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }); + }).toThrow(/dockerImages: source: Expected key 'directory' missing/); + }); }); -test('Correctly validate File asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - files: { - asset: { - source: { - path: 'a/b/c', - }, - destinations: { - dest: { - region: 'us-north-20', - bucketName: 'Bouquet', - objectKey: 'key', +describe('File asset', () => { + describe('valid input', () => { + test('without packaging', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, }, }, - }, - }, + }); + }).not.toThrow(); }); - }).not.toThrow(); -}); -test('Throw on invalid file asset', () => { - expect(() => { - AssetManifestSchema.validate({ - version: AssetManifestSchema.currentVersion(), - files: { - asset: { - source: { - path: 3, + for (const packaging of Object.values(FileAssetPackaging)) { + test(`with "${packaging}" packaging`, () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + packaging, + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, + }, + }); + }).not.toThrow(); + }); + } + }); + + describe('invalid input', () => { + test('bad "source.path" property', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 3, + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, + }, }, - destinations: { - dest: { - region: 'us-north-20', - bucketName: 'Bouquet', - objectKey: 'key', + }); + }).toThrow(/Expected a string, got '3'/); + }); + + test('bad "source.packaging" property', () => { + expect(() => { + AssetManifestSchema.validate({ + version: AssetManifestSchema.currentVersion(), + files: { + asset: { + source: { + path: 'a/b/c', + packaging: 'BLACK_HOLE', + }, + destinations: { + dest: { + region: 'us-north-20', + bucketName: 'Bouquet', + objectKey: 'key', + }, + }, }, }, - }, - }, + }); + }).toThrow(/Expected a FileAssetPackaging \(one of [^)]+\), got 'BLACK_HOLE'/); }); - }).toThrow(/Expected a string, got '3'/); + }); }); From af2ea60e7ae4aaab17ddd10a9142e1809b4c8246 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 25 May 2020 15:24:43 +0200 Subject: [PATCH 19/33] fix(cli): paper cuts (#8164) Fixing a number of small paper cuts in the CLI. Specifically: - When using `--cloudformation-execution-policies` or `--trust`, the positional argument that follows (typically an environment name) would be ignored, because of the way we configure yargs. Make it so that the options takes a single argument, and must be repeated for multiple arguments, making it a lot easier to use. - When a stack fails to create and is destroyed before being redeployed, the `deployStack()` routine would forget that the stack had been deleted and attempt to create a change set to update the stack, which would promptly fail. Remember we deleted the stack, so that we'll create a changeset to create a new one. - When a stack fails to create the first time, and the next deploy uses the same template, the "skip deploy" optimization we introduced to speed up deployment of stacks with nested stacks incorrectly skips the deployment. - Wrap the SDK objects, and when an AWS fails output information about the call that failed. Due to a lack of stack traces in NodeJS, it would otherwise be very hard to figure out where the error was happening. - Using the SDK wrapper, when the error looks like it's an error in assuming a role, replace it with an error message that describes the most probable cause: missing role/failure to bootstrap. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/bin/cdk.ts | 21 ++- .../aws-cdk/lib/api/aws-auth/sdk-provider.ts | 2 +- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 121 +++++++++++++++++- packages/aws-cdk/lib/api/deploy-stack.ts | 22 ++-- .../aws-cdk/lib/api/util/cloudformation.ts | 9 ++ .../aws-cdk/test/api/deploy-stack.test.ts | 95 +++++++++++--- .../aws-cdk/test/api/sdk-provider.test.ts | 27 +++- 7 files changed, 262 insertions(+), 35 deletions(-) diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index c32f75234584e..37c5a0887d52c 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -21,6 +21,19 @@ import * as version from '../lib/version'; // tslint:disable:no-shadowed-variable max-line-length async function parseCommandLineArguments() { + // Use the following configuration for array arguments: + // + // { type: 'array', default: [], nargs: 1, requiresArg: true } + // + // The default behavior of yargs is to eat all strings following an array argument: + // + // ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) + // ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. + // + // By using the config above, every --arg will only consume one argument, so you can do the following: + // + // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. + const initTemplateLanuages = await availableInitLanguages; return yargs .env('CDK') @@ -56,8 +69,8 @@ async function parseCommandLineArguments() { .option('qualifier', { type: 'string', desc: 'Unique string to distinguish multiple bootstrap stacks', default: undefined }) .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) .option('execute', {type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true}) - .option('trust', { type: 'array', desc: 'The (space-separated) list of AWS account IDs that should be trusted to perform deployments into this environment', default: [], hidden: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The (space-separated) list of Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed', default: [], hidden: true }) + .option('trust', { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated)', default: [], nargs: 1, requiresArg: true, hidden: true }) + .option('cloudformation-execution-policies', { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment. Required if --trust was passed (may be repeated)', default: [], nargs: 1, requiresArg: true, hidden: true }) .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }), ) .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', yargs => yargs @@ -296,6 +309,8 @@ initCommandLine() }) .catch(err => { error(err.message); - debug(err.stack); + if (err.stack) { + debug(err.stack); + } process.exitCode = 1; }); diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts index 98c2e03ac1a62..499a5af46beb0 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk-provider.ts @@ -127,7 +127,7 @@ export class SdkProvider { * If `region` is undefined, the default value will be used. */ public async withAssumedRole(roleArn: string, externalId: string | undefined, region: string | undefined) { - debug(`Assuming role '${roleArn}'`); + debug(`Assuming role '${roleArn}'.`); region = region ?? this.defaultRegion; const creds = new AWS.ChainableTemporaryCredentials({ diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index d84b14627cba4..91f0f4de7ca02 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -64,27 +64,27 @@ export class SDK implements ISDK { } public cloudFormation(): AWS.CloudFormation { - return new AWS.CloudFormation(this.config); + return wrapServiceErrorHandling(new AWS.CloudFormation(this.config)); } public ec2(): AWS.EC2 { - return new AWS.EC2(this.config); + return wrapServiceErrorHandling(new AWS.EC2(this.config)); } public ssm(): AWS.SSM { - return new AWS.SSM(this.config); + return wrapServiceErrorHandling(new AWS.SSM(this.config)); } public s3(): AWS.S3 { - return new AWS.S3(this.config); + return wrapServiceErrorHandling(new AWS.S3(this.config)); } public route53(): AWS.Route53 { - return new AWS.Route53(this.config); + return wrapServiceErrorHandling(new AWS.Route53(this.config)); } public ecr(): AWS.ECR { - return new AWS.ECR(this.config); + return wrapServiceErrorHandling(new AWS.ECR(this.config)); } public async currentAccount(): Promise { @@ -103,4 +103,113 @@ export class SDK implements ISDK { } } +/** + * Return a wrapping object for the underlying service object + * + * Responds to failures in the underlying service calls, in two different + * ways: + * + * - When errors are encountered, log the failing call and the error that + * it triggered (at debug level). This is necessary because the lack of + * stack traces in NodeJS otherwise makes it very hard to suss out where + * a certain AWS error occurred. + * - The JS SDK has a funny business of wrapping any credential-based error + * in a super-generic (and in our case wrong) exception. If we then use a + * 'ChainableTemporaryCredentials' and the target role doesn't exist, + * the error message that shows up by default is super misleading + * (https://github.com/aws/aws-sdk-js/issues/3272). We can fix this because + * the exception contains the "inner exception", so we unwrap and throw + * the correct error ("cannot assume role"). + * + * The wrapping business below is slightly more complicated than you'd think + * because we must hook into the `promise()` method of the object that's being + * returned from the methods of the object that we wrap, so there's two + * levels of wrapping going on, and also some exceptions to the wrapping magic. + */ +function wrapServiceErrorHandling(serviceObject: A): A { + const classObject = serviceObject.constructor.prototype; + + return new Proxy(serviceObject, { + get(obj: A, prop: string) { + const real = (obj as any)[prop]; + // Things we don't want to intercept: + // - Anything that's not a function. + // - 'constructor', s3.upload() will use this to do some magic and we need the underlying constructor. + // - Any method that's not on the service class (do not intercept 'makeRequest' and other helpers). + if (prop === 'constructor' || !classObject.hasOwnProperty(prop) || !isFunction(real)) { return real; } + + // NOTE: This must be a function() and not an () => { + // because I need 'this' to be dynamically bound and not statically bound. + // If your linter complains don't listen to it! + return function(this: any) { + // Call the underlying function. If it returns an object with a promise() + // method on it, wrap that 'promise' method. + const args = [].slice.call(arguments, 0); + const response = real.apply(this, args); + + // Don't intercept unless the return value is an object with a '.promise()' method. + if (typeof response !== 'object' || !response) { return response; } + if (!('promise' in response)) { return response; } + + // Return an object with the promise method replaced with a wrapper which will + // do additional things to errors. + return Object.assign(Object.create(response), { + promise() { + return response.promise().catch((e: Error) => { + e = makeDetailedException(e); + debug(`Call failed: ${prop}(${JSON.stringify(args[0])}) => ${e.message}`); + return Promise.reject(e); // Re-'throw' the new error + }); + }, + }); + }; + }, + }); +} + const CURRENT_ACCOUNT_KEY = Symbol('current_account_key'); + +function isFunction(x: any): x is (...args: any[]) => any { + return x && {}.toString.call(x) === '[object Function]'; +} + +/** + * Extract a more detailed error out of a generic error if we can + */ +function makeDetailedException(e: Error): Error { + // This is the super-generic "something's wrong" error that the JS SDK wraps other errors in. + // https://github.com/aws/aws-sdk-js/blob/f0ac2e53457c7512883d0677013eacaad6cd8a19/lib/event_listeners.js#L84 + if (e.message.startsWith('Missing credentials in config')) { + const original = (e as any).originalError; + if (original) { + // When the SDK does a 'util.copy', they lose the Error-ness of the inner error + // (they copy the Error's properties into a plain object) so make it an Error object again. + e = Object.assign(new Error(), original); + } + } + + // At this point, the error might still be a generic "ChainableTemporaryCredentials failed" + // error which wraps the REAL error (AssumeRole failed). We're going to replace the error + // message with one that's more likely to help users, and tell them the most probable + // fix (bootstrapping). The underlying service call failure will be appended below. + if (e.message === 'Could not load credentials from ChainableTemporaryCredentials') { + e.message = 'Could not assume role in target account (did you bootstrap the environment with the right \'--trust\'s?)'; + } + + // Replace the message on this error with a concatenation of all inner error messages. + // Must more clear what's going on that way. + e.message = allChainedExceptionMessages(e); + return e; +} + +/** + * Return the concatenated message of all exceptions in the AWS exception chain + */ +function allChainedExceptionMessages(e: Error | undefined) { + const ret = new Array(); + while (e) { + ret.push(e.message); + e = (e as any).originalError; + } + return ret.join(': '); +} \ No newline at end of file diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 952b9c9b2374d..99b18c3136b9f 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -173,6 +173,19 @@ export async function deployStack(options: DeployStackOptions): Promise { Changes: [], })), executeChangeSet: jest.fn((_o) => ({})), + deleteStack: jest.fn((_o) => ({})), getTemplate: jest.fn((_o) => ({ TemplateBody: JSON.stringify(DEFAULT_FAKE_TEMPLATE) })), updateTerminationProtection: jest.fn((_o) => ({ StackId: 'stack-id' })), }; @@ -190,6 +191,55 @@ test('deploy is skipped if template did not change', async () => { expect(cfnMocks.executeChangeSet).not.toBeCalled(); }); +test('if existing stack failed to create, it is deleted and recreated', async () => { + // GIVEN + givenStackExists( + { StackStatus: 'ROLLBACK_COMPLETE' }, // This is for the initial check + { StackStatus: 'DELETE_COMPLETE' }, // Poll the successful deletion + { StackStatus: 'CREATE_COMPLETE' }, // Poll the recreation + ); + givenTemplateIs({ + DifferentThan: 'TheDefault', + }); + + // WHEN + await deployStack({ + stack: FAKE_STACK, + sdk, + sdkProvider, + resolvedEnvironment: mockResolvedEnvironment(), + }); + + // THEN + expect(cfnMocks.deleteStack).toHaveBeenCalled(); + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + ChangeSetType: 'CREATE', + })); +}); + +test('if existing stack failed to create, it is deleted and recreated even if the template did not change', async () => { + // GIVEN + givenStackExists( + { StackStatus: 'ROLLBACK_COMPLETE' }, // This is for the initial check + { StackStatus: 'DELETE_COMPLETE' }, // Poll the successful deletion + { StackStatus: 'CREATE_COMPLETE' }, // Poll the recreation + ); + + // WHEN + await deployStack({ + stack: FAKE_STACK, + sdk, + sdkProvider, + resolvedEnvironment: mockResolvedEnvironment(), + }); + + // THEN + expect(cfnMocks.deleteStack).toHaveBeenCalled(); + expect(cfnMocks.createChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + ChangeSetType: 'CREATE', + })); +}); + test('deploy not skipped if template did not change and --force is applied', async () => { // GIVEN givenStackExists(); @@ -296,10 +346,7 @@ test('deploy not skipped if template did not change but one tag removed', async test('deploy not skipped if template changed', async () => { // GIVEN givenStackExists(); - cfnMocks.getTemplate!.mockReset(); - cfnMocks.getTemplate!.mockReturnValue({ - TemplateBody: JSON.stringify({ changed: 123 }), - }); + givenTemplateIs({ changed: 123 }); // WHEN await deployStack({ @@ -476,19 +523,37 @@ test('updateTerminationProtection called when termination protection is undefine /** * Set up the mocks so that it looks like the stack exists to start with + * + * The last element of this array will be continuously repeated. */ -function givenStackExists(overrides: Partial = {}) { +function givenStackExists(...overrides: Array>) { cfnMocks.describeStacks!.mockReset(); + + if (overrides.length === 0) { + overrides = [{}]; + } + + const baseResponse = { + StackName: 'mock-stack-name', + StackId: 'mock-stack-id', + CreationTime: new Date(), + StackStatus: 'CREATE_COMPLETE', + EnableTerminationProtection: false, + }; + + for (const override of overrides.slice(0, overrides.length - 1)) { + cfnMocks.describeStacks!.mockImplementationOnce(() => ({ + Stacks: [ {...baseResponse, ...override }], + })); + } cfnMocks.describeStacks!.mockImplementation(() => ({ - Stacks: [ - { - StackName: 'mock-stack-name', - StackId: 'mock-stack-id', - CreationTime: new Date(), - StackStatus: 'CREATE_COMPLETE', - EnableTerminationProtection: false, - ...overrides, - }, - ], + Stacks: [ {...baseResponse, ...overrides[overrides.length - 1] }], })); } + +function givenTemplateIs(template: any) { + cfnMocks.getTemplate!.mockReset(); + cfnMocks.getTemplate!.mockReturnValue({ + TemplateBody: JSON.stringify(template), + }); +} \ No newline at end of file diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index 21bb61dfbb9ed..6e6a4f91511c4 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -177,7 +177,6 @@ describe('CLI compatible credentials loading', () => { // WHEN const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, - ec2creds: false, profile: 'assumable', httpOptions: { proxyAddress: 'http://DOESNTMATTER/', @@ -189,6 +188,32 @@ describe('CLI compatible credentials loading', () => { // THEN -- the fake proxy agent got called, we don't care about the result expect(called).toEqual(true); }); + + test('error we get from assuming a role is useful', async () => { + // GIVEN + // Because of the way ChainableTemporaryCredentials gets its STS client, it's not mockable + // using 'mock-aws-sdk'. So instead, we have to mess around with its internals. + function makeAssumeRoleFail(s: ISDK) { + (s as any).credentials.service.assumeRole = jest.fn().mockImplementation((_request, cb) => { + cb(new Error('Nope!')); + }); + } + + const provider = await SdkProvider.withAwsCliCompatibleDefaults({ + ...defaultCredOptions, + httpOptions: { + proxyAddress: 'http://localhost:8080/', + }, + }); + + // WHEN + const sdk = await provider.withAssumedRole('bla.role.arn', undefined, undefined); + makeAssumeRoleFail(sdk); + + // THEN - error message contains both a helpful hint and the underlying AssumeRole message + await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('did you bootstrap'); + await expect(sdk.s3().listBuckets().promise()).rejects.toThrow('Nope!'); + }); }); describe('Plugins', () => { From 575f1db0474327c61c4ac626608c9f443ce231d2 Mon Sep 17 00:00:00 2001 From: Adam Ruka Date: Mon, 25 May 2020 07:13:07 -0700 Subject: [PATCH 20/33] feat(codepipeline): use a special bootstrapless synthesizer for cross-region support Stacks (#8091) Fixes #8082 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/cross-region-support-stack.ts | 3 + .../@aws-cdk/aws-codepipeline/lib/pipeline.ts | 23 ++++++- .../@aws-cdk/aws-codepipeline/package.json | 1 + .../aws-codepipeline/test/test.pipeline.ts | 43 ++++++++++++- .../bootstrapless-synthesizer.ts | 60 +++++++++++++++++++ .../stack-synthesizers/default-synthesizer.ts | 40 ++++++++++--- .../core/lib/stack-synthesizers/index.ts | 3 +- 7 files changed, 162 insertions(+), 11 deletions(-) create mode 100644 packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts diff --git a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts index 47227f4fb689d..00d0c5ca29493 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/cross-region-support-stack.ts @@ -71,6 +71,8 @@ export interface CrossRegionSupportStackProps { * @example '012345678901' */ readonly account: string; + + readonly synthesizer: cdk.IStackSynthesizer | undefined; } /** @@ -90,6 +92,7 @@ export class CrossRegionSupportStack extends cdk.Stack { region: props.region, account: props.account, }, + synthesizer: props.synthesizer, }); const crossRegionSupportConstruct = new CrossRegionSupportConstruct(this, 'Default'); diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index 05b4c174f6aa6..26c86887e98bc 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -2,7 +2,10 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Construct, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { + App, BootstraplessSynthesizer, Construct, DefaultStackSynthesizer, + IStackSynthesizer, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, +} from '@aws-cdk/core'; import { ActionCategory, IAction, IPipeline, IStage } from './action'; import { CfnPipeline } from './codepipeline.generated'; import { CrossRegionSupportConstruct, CrossRegionSupportStack } from './cross-region-support-stack'; @@ -483,6 +486,7 @@ export class Pipeline extends PipelineBase { pipelineStackName: pipelineStack.stackName, region: actionRegion, account: pipelineAccount, + synthesizer: this.getCrossRegionSupportSynthesizer(), }); } @@ -492,6 +496,23 @@ export class Pipeline extends PipelineBase { }; } + private getCrossRegionSupportSynthesizer(): IStackSynthesizer | undefined { + if (this.stack.synthesizer instanceof DefaultStackSynthesizer) { + // if we have the new synthesizer, + // we need a bootstrapless copy of it, + // because we don't want to require bootstrapping the environment + // of the pipeline account in this replication region + return new BootstraplessSynthesizer({ + deployRoleArn: this.stack.synthesizer.deployRoleArn, + cloudFormationExecutionRoleArn: this.stack.synthesizer.cloudFormationExecutionRoleArn, + }); + } else { + // any other synthesizer: just return undefined + // (ie., use the default based on the context settings) + return undefined; + } + } + private generateNameForDefaultBucketKeyAlias(): string { const prefix = 'alias/codepipeline-'; const maxAliasLength = 256; diff --git a/packages/@aws-cdk/aws-codepipeline/package.json b/packages/@aws-cdk/aws-codepipeline/package.json index 36154be191da0..0a94e85b6a724 100644 --- a/packages/@aws-cdk/aws-codepipeline/package.json +++ b/packages/@aws-cdk/aws-codepipeline/package.json @@ -68,6 +68,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts index 153e24d882f8a..5d1c91edd51af 100644 --- a/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/test/test.pipeline.ts @@ -3,6 +3,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Test } from 'nodeunit'; import * as codepipeline from '../lib'; import { FakeBuildAction } from './fake-build-action'; @@ -46,7 +47,7 @@ export = { }, 'that is cross-region': { - 'validates that source actions are in the same account as the pipeline'(test: Test) { + 'validates that source actions are in the same region as the pipeline'(test: Test) { const app = new cdk.App(); const stack = new cdk.Stack(app, 'PipelineStack', { env: { region: 'us-west-1', account: '123456789012' }}); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -296,6 +297,46 @@ export = { test.done(); }, + + 'generates the support stack containing the replication Bucket without the need to bootstrap in that environment'(test: Test) { + const app = new cdk.App({ + treeMetadata: false, // we can't set the context otherwise, because App will have a child + }); + app.node.setContext(cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT, true); + + const pipelineStack = new cdk.Stack(app, 'PipelineStack', { + env: { region: 'us-west-2', account: '123456789012' }, + }); + const sourceOutput = new codepipeline.Artifact(); + new codepipeline.Pipeline(pipelineStack, 'Pipeline', { + stages: [ + { + stageName: 'Source', + actions: [new FakeSourceAction({ + actionName: 'Source', + output: sourceOutput, + })], + }, + { + stageName: 'Build', + actions: [new FakeBuildAction({ + actionName: 'Build', + input: sourceOutput, + region: 'eu-south-1', + })], + }, + ], + }); + + const assembly = app.synth(); + const supportStackArtifact = assembly.getStackByName('PipelineStack-support-eu-south-1'); + test.equal(supportStackArtifact.assumeRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-deploy-role-123456789012-us-west-2'); + test.equal(supportStackArtifact.cloudFormationExecutionRoleArn, + 'arn:${AWS::Partition}:iam::123456789012:role/cdk-hnb659fds-cfn-exec-role-123456789012-us-west-2'); + + test.done(); + }, }, 'that is cross-account': { diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts new file mode 100644 index 0000000000000..a1149f91e4990 --- /dev/null +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/bootstrapless-synthesizer.ts @@ -0,0 +1,60 @@ +import { DockerImageAssetLocation, DockerImageAssetSource, FileAssetLocation, FileAssetSource } from '../assets'; +import { ISynthesisSession } from '../construct-compat'; +import { addStackArtifactToAssembly, assertBound } from './_shared'; +import { DefaultStackSynthesizer } from './default-synthesizer'; + +/** + * Construction properties of {@link BootstraplessSynthesizer}. + */ +export interface BootstraplessSynthesizerProps { + /** + * The deploy Role ARN to use. + * + * @default - No deploy role (use CLI credentials) + * + */ + readonly deployRoleArn?: string; + + /** + * The CFN execution Role ARN to use. + * + * @default - No CloudFormation role (use CLI credentials) + */ + readonly cloudFormationExecutionRoleArn?: string; +} + +/** + * A special synthesizer that behaves similarly to DefaultStackSynthesizer, + * but doesn't require bootstrapping the environment it operates in. + * Because of that, stacks using it cannot have assets inside of them. + * Used by the CodePipeline construct for the support stacks needed for + * cross-region replication S3 buckets. + */ +export class BootstraplessSynthesizer extends DefaultStackSynthesizer { + constructor(props: BootstraplessSynthesizerProps) { + super({ + deployRoleArn: props.deployRoleArn, + cloudFormationExecutionRole: props.cloudFormationExecutionRoleArn, + }); + } + + public addFileAsset(_asset: FileAssetSource): FileAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public addDockerImageAsset(_asset: DockerImageAssetSource): DockerImageAssetLocation { + throw new Error('Cannot add assets to a Stack that uses the BootstraplessSynthesizer'); + } + + public synthesizeStackArtifacts(session: ISynthesisSession): void { + assertBound(this.stack); + + // do _not_ treat the template as an asset, + // because this synthesizer doesn't have a bootstrap bucket to put it in + addStackArtifactToAssembly(session, this.stack, { + assumeRoleArn: this.deployRoleArn, + cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + requiresBootstrapStackVersion: 1, + }, []); + } +} diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts index 13b65a5d8613c..ace086a9c4bd3 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/default-synthesizer.ts @@ -140,11 +140,11 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { */ public static readonly DEFAULT_FILE_ASSETS_BUCKET_NAME = 'cdk-${Qualifier}-assets-${AWS::AccountId}-${AWS::Region}'; - private stack?: Stack; + private _stack?: Stack; private bucketName?: string; private repositoryName?: string; - private deployRoleArn?: string; - private cloudFormationExecutionRoleArn?: string; + private _deployRoleArn?: string; + private _cloudFormationExecutionRoleArn?: string; private assetPublishingRoleArn?: string; private readonly files: NonNullable = {}; @@ -154,7 +154,7 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { } public bind(stack: Stack): void { - this.stack = stack; + this._stack = stack; const qualifier = this.props.qualifier ?? stack.node.tryGetContext(BOOTSTRAP_QUALIFIER_CONTEXT) ?? DefaultStackSynthesizer.DEFAULT_QUALIFIER; @@ -176,8 +176,8 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { // tslint:disable:max-line-length this.bucketName = specialize(this.props.fileAssetsBucketName ?? DefaultStackSynthesizer.DEFAULT_FILE_ASSETS_BUCKET_NAME); this.repositoryName = specialize(this.props.imageAssetsRepositoryName ?? DefaultStackSynthesizer.DEFAULT_IMAGE_ASSETS_REPOSITORY_NAME); - this.deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); - this.cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); + this._deployRoleArn = specialize(this.props.deployRoleArn ?? DefaultStackSynthesizer.DEFAULT_DEPLOY_ROLE_ARN); + this._cloudFormationExecutionRoleArn = specialize(this.props.cloudFormationExecutionRole ?? DefaultStackSynthesizer.DEFAULT_CLOUDFORMATION_ROLE_ARN); this.assetPublishingRoleArn = specialize(this.props.assetPublishingRoleArn ?? DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN); // tslint:enable:max-line-length } @@ -259,13 +259,37 @@ export class DefaultStackSynthesizer implements IStackSynthesizer { const artifactId = this.writeAssetManifest(session); addStackArtifactToAssembly(session, this.stack, { - assumeRoleArn: this.deployRoleArn, - cloudFormationExecutionRoleArn: this.cloudFormationExecutionRoleArn, + assumeRoleArn: this._deployRoleArn, + cloudFormationExecutionRoleArn: this._cloudFormationExecutionRoleArn, stackTemplateAssetObjectUrl: templateManifestUrl, requiresBootstrapStackVersion: 1, }, [artifactId]); } + /** + * Returns the ARN of the deploy Role. + */ + public get deployRoleArn(): string { + if (!this._deployRoleArn) { + throw new Error('deployRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._deployRoleArn; + } + + /** + * Returns the ARN of the CFN execution Role. + */ + public get cloudFormationExecutionRoleArn(): string { + if (!this._cloudFormationExecutionRoleArn) { + throw new Error('cloudFormationExecutionRoleArn getter can only be called after the synthesizer has been bound to a Stack'); + } + return this._cloudFormationExecutionRoleArn; + } + + protected get stack(): Stack | undefined { + return this._stack; + } + /** * Add the stack's template as one of the manifest assets * diff --git a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts index 5920f19bae2c9..b4ad67384729d 100644 --- a/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts +++ b/packages/@aws-cdk/core/lib/stack-synthesizers/index.ts @@ -1,4 +1,5 @@ export * from './types'; export * from './default-synthesizer'; export * from './legacy'; -export * from './nested'; \ No newline at end of file +export * from './bootstrapless-synthesizer'; +export * from './nested'; From 03935280f1addef392c9b4460737cce8bb2eb8c9 Mon Sep 17 00:00:00 2001 From: Romain Marcadier Date: Mon, 25 May 2020 17:58:01 +0200 Subject: [PATCH 21/33] fix(dynamodb): the maximum number of nonKeyAttributes is 100, not 20 (#8186) The validation for `nonKeyAttributes` count on the secondaryt indexes was incorrectly checked at `20`, while the real limit is `100` (it has been raised since the code was initially authored). Fixes #8095 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-dynamodb/lib/table.ts | 4 ++-- packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index 186db2266c2fa..f63ff13a25cf4 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -1114,9 +1114,9 @@ export class Table extends TableBase { * @param nonKeyAttributes a list of non-key attribute names */ private validateNonKeyAttributes(nonKeyAttributes: string[]) { - if (this.nonKeyAttributes.size + nonKeyAttributes.length > 20) { + if (this.nonKeyAttributes.size + nonKeyAttributes.length > 100) { // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes - throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 20'); + throw new RangeError('a maximum number of nonKeyAttributes across all of secondary indexes is 100'); } // store all non-key attributes diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index c422330e2c1ce..068cfaf5b0edb 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -1114,7 +1114,7 @@ test('error when adding a global secondary index with projection type INCLUDE, b const table = new Table(stack, CONSTRUCT_NAME, { partitionKey: TABLE_PARTITION_KEY, sortKey: TABLE_SORT_KEY }); const gsiNonKeyAttributeGenerator = NON_KEY_ATTRIBUTE_GENERATOR(GSI_NON_KEY); const gsiNonKeyAttributes: string[] = []; - for (let i = 0; i < 21; i++) { + for (let i = 0; i < 101; i++) { gsiNonKeyAttributes.push(gsiNonKeyAttributeGenerator.next().value); } @@ -1124,7 +1124,7 @@ test('error when adding a global secondary index with projection type INCLUDE, b sortKey: GSI_SORT_KEY, projectionType: ProjectionType.INCLUDE, nonKeyAttributes: gsiNonKeyAttributes, - })).toThrow(/a maximum number of nonKeyAttributes across all of secondary indexes is 20/); + })).toThrow(/a maximum number of nonKeyAttributes across all of secondary indexes is 100/); }); test('error when adding a global secondary index with read or write capacity on a PAY_PER_REQUEST table', () => { From 8e473e56be81d0de4192af42e2202538ac37d515 Mon Sep 17 00:00:00 2001 From: Rico Huijbers Date: Mon, 25 May 2020 19:24:50 +0200 Subject: [PATCH 22/33] chore: fix build (#8187) Not every `e.message` is a `string`, I guess. It turns out it can also be `undefined`? This commit fixes the integ tests. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/lib/api/aws-auth/sdk.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 91f0f4de7ca02..239f85fef51bc 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -179,7 +179,7 @@ function isFunction(x: any): x is (...args: any[]) => any { function makeDetailedException(e: Error): Error { // This is the super-generic "something's wrong" error that the JS SDK wraps other errors in. // https://github.com/aws/aws-sdk-js/blob/f0ac2e53457c7512883d0677013eacaad6cd8a19/lib/event_listeners.js#L84 - if (e.message.startsWith('Missing credentials in config')) { + if (typeof e.message === 'string' && e.message.startsWith('Missing credentials in config')) { const original = (e as any).originalError; if (original) { // When the SDK does a 'util.copy', they lose the Error-ness of the inner error From 032be698858b732b22a800de20ad3bab7ece46d7 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Mon, 25 May 2020 18:57:35 +0000 Subject: [PATCH 23/33] chore(deps): bump uuid from 8.0.0 to 8.1.0 (#8190) Bumps [uuid](https://github.com/uuidjs/uuid) from 8.0.0 to 8.1.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v8.0.0...v8.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- packages/aws-cdk/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index df0a393e63587..c2eb985d051a6 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -85,7 +85,7 @@ "semver": "^7.2.2", "source-map-support": "^0.5.19", "table": "^5.4.6", - "uuid": "^8.0.0", + "uuid": "^8.1.0", "yaml": "^1.10.0", "yargs": "^15.3.1" }, diff --git a/yarn.lock b/yarn.lock index b4fffb20a086b..c6114d649a7c0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9730,10 +9730,10 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.0.0: - version "8.0.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== +uuid@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" + integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== v8-compile-cache@^2.0.3: version "2.1.0" From 106c0afcdd2066d03545aeb49901492665cf7453 Mon Sep 17 00:00:00 2001 From: Hailey Gu <47661750+HaileyGu@users.noreply.github.com> Date: Mon, 25 May 2020 23:51:40 +0200 Subject: [PATCH 24/33] chore(core): fix grammar in comments (#8191) Fix grammar errors from "if **there** the construct is valid." to "if the construct is valid." ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/core/lib/construct-compat.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/core/lib/construct-compat.ts b/packages/@aws-cdk/core/lib/construct-compat.ts index 5a9315a0850ca..341943a748bca 100644 --- a/packages/@aws-cdk/core/lib/construct-compat.ts +++ b/packages/@aws-cdk/core/lib/construct-compat.ts @@ -91,7 +91,7 @@ export class Construct extends constructs.Construct implements IConstruct { * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * - * @returns An array of validation error messages, or an empty array if there the construct is valid. + * @returns An array of validation error messages, or an empty array if the construct is valid. */ protected onValidate(): string[] { return this.validate(); @@ -132,7 +132,7 @@ export class Construct extends constructs.Construct implements IConstruct { * This method can be implemented by derived constructs in order to perform * validation logic. It is called on all constructs before synthesis. * - * @returns An array of validation error messages, or an empty array if there the construct is valid. + * @returns An array of validation error messages, or an empty array if the construct is valid. */ protected validate(): string[] { return []; From c4b25bae23215a09a37b88a07cb4f21d3e9e5f13 Mon Sep 17 00:00:00 2001 From: Alexander Fallenstedt Date: Mon, 25 May 2020 15:46:31 -0700 Subject: [PATCH 25/33] chore(cognito): fix typo (#8194) Fix typo ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts index db7fcaa8e163e..e2a76c64120ef 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-attr.ts @@ -17,7 +17,7 @@ export interface RequiredAttributes { readonly birthdate?: boolean; /** - * Whether theb user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute. + * Whether the user's e-mail address, represented as an RFC 5322 [RFC5322] addr-spec, is a required attribute. * @default false */ readonly email?: boolean; From 44e4d66e263ef88a4ab73f9ba60f103802559f43 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 26 May 2020 02:24:46 +0200 Subject: [PATCH 26/33] chore(backup): use interface instead of concrete class for resources (#8193) Allows to work with imported resources. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-backup/lib/resource.ts | 8 ++-- .../aws-backup/test/selection.test.ts | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/packages/@aws-cdk/aws-backup/lib/resource.ts b/packages/@aws-cdk/aws-backup/lib/resource.ts index 5f3073642c05b..c0cd0fd2b8878 100644 --- a/packages/@aws-cdk/aws-backup/lib/resource.ts +++ b/packages/@aws-cdk/aws-backup/lib/resource.ts @@ -64,14 +64,14 @@ export class BackupResource { /** * A DynamoDB table */ - public static fromDynamoDbTable(table: dynamodb.Table) { + public static fromDynamoDbTable(table: dynamodb.ITable) { return BackupResource.fromArn(table.tableArn); } /** * An EC2 instance */ - public static fromEc2Instance(instance: ec2.Instance) { + public static fromEc2Instance(instance: ec2.IInstance) { return BackupResource.fromArn(Stack.of(instance).formatArn({ service: 'ec2', resource: 'instance', @@ -82,7 +82,7 @@ export class BackupResource { /** * An EFS file system */ - public static fromEfsFileSystem(fileSystem: efs.FileSystem) { + public static fromEfsFileSystem(fileSystem: efs.IFileSystem) { return BackupResource.fromArn(Stack.of(fileSystem).formatArn({ service: 'elasticfilesystem', resource: 'file-system', @@ -93,7 +93,7 @@ export class BackupResource { /** * A RDS database instance */ - public static fromRdsDatabaseInstance(instance: rds.DatabaseInstance) { + public static fromRdsDatabaseInstance(instance: rds.IDatabaseInstance) { return BackupResource.fromArn(instance.instanceArn); } diff --git a/packages/@aws-cdk/aws-backup/test/selection.test.ts b/packages/@aws-cdk/aws-backup/test/selection.test.ts index 4d8e7652a6925..75d1f6e6eade8 100644 --- a/packages/@aws-cdk/aws-backup/test/selection.test.ts +++ b/packages/@aws-cdk/aws-backup/test/selection.test.ts @@ -290,3 +290,44 @@ test('fromEc2Instance', () => { }, }); }); + +test('fromDynamoDbTable', () => { + // GIVEN + const newTable = new dynamodb.Table(stack, 'New', { + partitionKey: { + name: 'id', + type: dynamodb.AttributeType.STRING, + }, + }); + const existingTable = dynamodb.Table.fromTableArn(stack, 'Existing', 'arn:aws:dynamodb:eu-west-1:123456789012:table/existing'); + + // WHEN + plan.addSelection('Selection', { + resources: [ + BackupResource.fromDynamoDbTable(newTable), + BackupResource.fromDynamoDbTable(existingTable), + ], + }); + + // THEN + expect(stack).toHaveResource('AWS::Backup::BackupSelection', { + BackupSelection: { + IamRoleArn: { + 'Fn::GetAtt': [ + 'PlanSelectionRole6D10F4B7', + 'Arn', + ], + }, + Resources: [ + { + 'Fn::GetAtt': [ + 'New8A81B073', + 'Arn', + ], + }, + 'arn:aws:dynamodb:eu-west-1:123456789012:table/existing', + ], + SelectionName: 'Selection', + }, + }); +}); From 995088abb00d9c75adbb65845998a8328bb5ba14 Mon Sep 17 00:00:00 2001 From: Barun Ray Date: Tue, 26 May 2020 11:43:59 +0530 Subject: [PATCH 27/33] feat(codeguruprofiler): ProfilingGroup (#7895) fixes https://github.com/aws/aws-cdk/issues/6984 by creating L2 construct and functions to allow for policies to be assigned to execution roles. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-codeguruprofiler/README.md | 20 +- .../aws-codeguruprofiler/lib/index.ts | 1 + .../lib/profiling-group.ts | 180 ++++++++ .../aws-codeguruprofiler/package.json | 6 +- .../test/integ.profiler-group.expected.json | 132 ++++++ .../test/integ.profiler-group.ts | 28 ++ .../test/profiling-group.test.ts | 393 ++++++++++++++++++ 7 files changed, 758 insertions(+), 2 deletions(-) create mode 100644 packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts create mode 100644 packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json create mode 100644 packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts create mode 100644 packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts diff --git a/packages/@aws-cdk/aws-codeguruprofiler/README.md b/packages/@aws-cdk/aws-codeguruprofiler/README.md index 23b5ff77af24f..5fcb3137d296b 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/README.md +++ b/packages/@aws-cdk/aws-codeguruprofiler/README.md @@ -9,8 +9,26 @@ --- -This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aws-cdk) project. +Amazon CodeGuru Profiler collects runtime performance data from your live applications, and provides recommendations that can help you fine-tune your application performance. + +### Installation + +Import to your project: ```ts import * as codeguruprofiler from '@aws-cdk/aws-codeguruprofiler'; ``` + +### Basic usage + +Here's how to setup a profiling group and give your compute role permissions to publish to the profiling group to the profiling agent can publish profiling information: + +```ts +// The execution role of your application that publishes to the ProfilingGroup via CodeGuru Profiler Profiling Agent. (the following is merely an example) +const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), +}); + +const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup'); +profilingGroup.grantPublish(publishAppRole); +``` diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts index 1dca345aee39a..6ee79ba3c2171 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/index.ts @@ -1,2 +1,3 @@ // AWS::CodeGuruProfiler CloudFormation Resources: export * from './codeguruprofiler.generated'; +export * from './profiling-group'; diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts new file mode 100644 index 0000000000000..f4d356e093204 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts @@ -0,0 +1,180 @@ +import { Grant, IGrantable } from '@aws-cdk/aws-iam'; +import { Construct, IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { CfnProfilingGroup } from './codeguruprofiler.generated'; + +/** + * IResource represents a Profiling Group. + */ +export interface IProfilingGroup extends IResource { + + /** + * A name for the profiling group. + * + * @attribute + */ + readonly profilingGroupName: string; + + /** + * Grant access to publish profiling information to the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:ConfigureAgent + * - codeguru-profiler:PostAgentProfile + * + * @param grantee Principal to grant publish rights to + */ + grantPublish(grantee: IGrantable): Grant; + + /** + * Grant access to read profiling information from the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:GetProfile + * - codeguru-profiler:DescribeProfilingGroup + * + * @param grantee Principal to grant read rights to + */ + grantRead(grantee: IGrantable): Grant; + +} + +abstract class ProfilingGroupBase extends Resource implements IProfilingGroup { + + public abstract readonly profilingGroupName: string; + + public abstract readonly profilingGroupArn: string; + + /** + * Grant access to publish profiling information to the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:ConfigureAgent + * - codeguru-profiler:PostAgentProfile + * + * @param grantee Principal to grant publish rights to + */ + public grantPublish(grantee: IGrantable) { + // https://docs.aws.amazon.com/codeguru/latest/profiler-ug/security-iam.html#security-iam-access-control + return Grant.addToPrincipal({ + grantee, + actions: ['codeguru-profiler:ConfigureAgent', 'codeguru-profiler:PostAgentProfile'], + resourceArns: [this.profilingGroupArn], + }); + } + + /** + * Grant access to read profiling information from the Profiling Group to the given identity. + * + * This will grant the following permissions: + * + * - codeguru-profiler:GetProfile + * - codeguru-profiler:DescribeProfilingGroup + * + * @param grantee Principal to grant read rights to + */ + public grantRead(grantee: IGrantable) { + // https://docs.aws.amazon.com/codeguru/latest/profiler-ug/security-iam.html#security-iam-access-control + return Grant.addToPrincipal({ + grantee, + actions: ['codeguru-profiler:GetProfile', 'codeguru-profiler:DescribeProfilingGroup'], + resourceArns: [this.profilingGroupArn], + }); + } + +} + +/** + * Properties for creating a new Profiling Group. + */ +export interface ProfilingGroupProps { + + /** + * A name for the profiling group. + * @default - automatically generated name. + */ + readonly profilingGroupName?: string; + +} + +/** + * A new Profiling Group. + */ +export class ProfilingGroup extends ProfilingGroupBase { + + /** + * Import an existing Profiling Group provided a Profiling Group Name. + * + * @param scope The parent creating construct + * @param id The construct's name + * @param profilingGroupName Profiling Group Name + */ + public static fromProfilingGroupName(scope: Construct, id: string, profilingGroupName: string): IProfilingGroup { + const stack = Stack.of(scope); + + return this.fromProfilingGroupArn(scope, id, stack.formatArn({ + service: 'codeguru-profiler', + resource: 'profilingGroup', + resourceName: profilingGroupName, + })); + } + + /** + * Import an existing Profiling Group provided an ARN. + * + * @param scope The parent creating construct + * @param id The construct's name + * @param profilingGroupArn Profiling Group ARN + */ + public static fromProfilingGroupArn(scope: Construct, id: string, profilingGroupArn: string): IProfilingGroup { + class Import extends ProfilingGroupBase { + public readonly profilingGroupName = Stack.of(scope).parseArn(profilingGroupArn).resource; + public readonly profilingGroupArn = profilingGroupArn; + } + + return new Import(scope, id); + } + + /** + * The name of the Profiling Group. + * + * @attribute + */ + public readonly profilingGroupName: string; + + /** + * The ARN of the Profiling Group. + * + * @attribute + */ + public readonly profilingGroupArn: string; + + constructor(scope: Construct, id: string, props: ProfilingGroupProps = {}) { + super(scope, id, { + physicalName: props.profilingGroupName ?? Lazy.stringValue({ produce: () => this.generateUniqueId() }), + }); + + const profilingGroup = new CfnProfilingGroup(this, 'ProfilingGroup', { + profilingGroupName: this.physicalName, + }); + + this.profilingGroupName = this.getResourceNameAttribute(profilingGroup.ref); + + this.profilingGroupArn = this.getResourceArnAttribute(profilingGroup.attrArn, { + service: 'codeguru-profiler', + resource: 'profilingGroup', + resourceName: this.physicalName, + }); + } + + private generateUniqueId(): string { + const name = this.node.uniqueId; + if (name.length > 240) { + return name.substring(0, 120) + name.substring(name.length - 120); + } + return name; + } + +} diff --git a/packages/@aws-cdk/aws-codeguruprofiler/package.json b/packages/@aws-cdk/aws-codeguruprofiler/package.json index 71efbaf81a1d0..a114721c9e514 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/package.json +++ b/packages/@aws-cdk/aws-codeguruprofiler/package.json @@ -67,14 +67,18 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0" }, "peerDependencies": { - "@aws-cdk/core": "0.0.0" + "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-iam": "0.0.0", + "constructs": "^3.0.2" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json new file mode 100644 index 0000000000000..8ea1221f6bbe8 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.expected.json @@ -0,0 +1,132 @@ +{ + "Resources": { + "MyProfilingGroup829F0507": { + "Type": "AWS::CodeGuruProfiler::ProfilingGroup", + "Properties": { + "ProfilingGroupName": "ProfilerGroupIntegrationTestMyProfilingGroup81DA69A3" + } + }, + "PublishAppRole9FEBD682": { + "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" + } + } + }, + "PublishAppRoleDefaultPolicyCA1E15C3": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeguru-profiler:ConfigureAgent", + "codeguru-profiler:PostAgentProfile" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProfilingGroup829F0507", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "PublishAppRoleDefaultPolicyCA1E15C3", + "Roles": [ + { + "Ref": "PublishAppRole9FEBD682" + } + ] + } + }, + "ReadAppRole52FE6317": { + "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" + } + } + }, + "ReadAppRoleDefaultPolicy4BB8955C": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "codeguru-profiler:GetProfile", + "codeguru-profiler:DescribeProfilingGroup" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyProfilingGroup829F0507", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "ReadAppRoleDefaultPolicy4BB8955C", + "Roles": [ + { + "Ref": "ReadAppRole52FE6317" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts new file mode 100644 index 0000000000000..d947e85e823a4 --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/integ.profiler-group.ts @@ -0,0 +1,28 @@ +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { ProfilingGroup } from '../lib'; + +class ProfilerGroupIntegrationTest extends Stack { + constructor(scope: App, id: string, props?: StackProps) { + super(scope, id, props); + + const profilingGroup = new ProfilingGroup(this, 'MyProfilingGroup'); + + const publishAppRole = new Role(this, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + profilingGroup.grantPublish(publishAppRole); + + const readAppRole = new Role(this, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + profilingGroup.grantRead(readAppRole); + + } +} + +const app = new App(); + +new ProfilerGroupIntegrationTest(app, 'ProfilerGroupIntegrationTest'); + +app.synth(); diff --git a/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts new file mode 100644 index 0000000000000..0fbf063cccfaa --- /dev/null +++ b/packages/@aws-cdk/aws-codeguruprofiler/test/profiling-group.test.ts @@ -0,0 +1,393 @@ +import { expect } from '@aws-cdk/assert'; +import { AccountRootPrincipal, Role } from '@aws-cdk/aws-iam'; +import { Stack } from '@aws-cdk/core'; +import { ProfilingGroup } from '../lib'; + +// tslint:disable:object-literal-key-quotes + +describe('profiling group', () => { + + test('attach read permission to Profiling group via fromProfilingGroupArn', () => { + const stack = new Stack(); + // dummy role to test out read permissions on ProfilingGroup + const readAppRole = new Role(stack, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const profilingGroup = ProfilingGroup.fromProfilingGroupArn(stack, 'MyProfilingGroup', 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup'); + profilingGroup.grantRead(readAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'ReadAppRole52FE6317': { + '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', + }, + }, + }, + 'ReadAppRoleDefaultPolicy4BB8955C': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:GetProfile', + 'codeguru-profiler:DescribeProfilingGroup', + ], + 'Effect': 'Allow', + 'Resource': 'arn:aws:codeguru-profiler:us-east-1:1234567890:profilingGroup/MyAwesomeProfilingGroup', + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'ReadAppRoleDefaultPolicy4BB8955C', + 'Roles': [ + { + 'Ref': 'ReadAppRole52FE6317', + }, + ], + }, + }, + }, + }); + }); + + test('attach publish permission to Profiling group via fromProfilingGroupName', () => { + const stack = new Stack(); + // dummy role to test out publish permissions on ProfilingGroup + const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + const profilingGroup = ProfilingGroup.fromProfilingGroupName(stack, 'MyProfilingGroup', 'MyAwesomeProfilingGroup'); + profilingGroup.grantPublish(publishAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'PublishAppRole9FEBD682': { + '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', + }, + }, + }, + 'PublishAppRoleDefaultPolicyCA1E15C3': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:ConfigureAgent', + 'codeguru-profiler:PostAgentProfile', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::Join': [ + '', + [ + 'arn:', + { + 'Ref': 'AWS::Partition', + }, + ':codeguru-profiler:', + { + 'Ref': 'AWS::Region', + }, + ':', + { + 'Ref': 'AWS::AccountId', + }, + ':profilingGroup/MyAwesomeProfilingGroup', + ], + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'PublishAppRoleDefaultPolicyCA1E15C3', + 'Roles': [ + { + 'Ref': 'PublishAppRole9FEBD682', + }, + ], + }, + }, + }, + }); + }); + + test('default profiling group', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + }, + }); + }); + + test('default profiling group without name', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroup', { + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyProfilingGroup', + }, + }, + }, + }); + }); + + test('default profiling group without name when name exceeding limit is generated', () => { + const stack = new Stack(); + new ProfilingGroup(stack, 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSize_InOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharacters_InSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConcatenateThemToGetTheIdentifier', { + }); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGreaterThanTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca4B39908C': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyProfilingGroupWithAReallyLongProfilingGroupNameThatExceedsTheLimitOfProfilingGroupNameSizeInOrderToDoSoTheNameMustBeGrnTwoHundredAndFiftyFiveCharactersInSuchCasesWePickUpTheFirstOneTwentyCharactersFromTheBeginningAndTheEndAndConca2FE009B0', + }, + }, + }, + }); + }); + + test('grant publish permissions profiling group', () => { + const stack = new Stack(); + const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + const publishAppRole = new Role(stack, 'PublishAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + profilingGroup.grantPublish(publishAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + 'PublishAppRole9FEBD682': { + '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', + }, + }, + }, + 'PublishAppRoleDefaultPolicyCA1E15C3': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:ConfigureAgent', + 'codeguru-profiler:PostAgentProfile', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyProfilingGroup829F0507', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'PublishAppRoleDefaultPolicyCA1E15C3', + 'Roles': [ + { + 'Ref': 'PublishAppRole9FEBD682', + }, + ], + }, + }, + }, + }); + }); + + test('grant read permissions profiling group', () => { + const stack = new Stack(); + const profilingGroup = new ProfilingGroup(stack, 'MyProfilingGroup', { + profilingGroupName: 'MyAwesomeProfilingGroup', + }); + const readAppRole = new Role(stack, 'ReadAppRole', { + assumedBy: new AccountRootPrincipal(), + }); + + profilingGroup.grantRead(readAppRole); + + expect(stack).toMatch({ + 'Resources': { + 'MyProfilingGroup829F0507': { + 'Type': 'AWS::CodeGuruProfiler::ProfilingGroup', + 'Properties': { + 'ProfilingGroupName': 'MyAwesomeProfilingGroup', + }, + }, + 'ReadAppRole52FE6317': { + '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', + }, + }, + }, + 'ReadAppRoleDefaultPolicy4BB8955C': { + 'Type': 'AWS::IAM::Policy', + 'Properties': { + 'PolicyDocument': { + 'Statement': [ + { + 'Action': [ + 'codeguru-profiler:GetProfile', + 'codeguru-profiler:DescribeProfilingGroup', + ], + 'Effect': 'Allow', + 'Resource': { + 'Fn::GetAtt': [ + 'MyProfilingGroup829F0507', + 'Arn', + ], + }, + }, + ], + 'Version': '2012-10-17', + }, + 'PolicyName': 'ReadAppRoleDefaultPolicy4BB8955C', + 'Roles': [ + { + 'Ref': 'ReadAppRole52FE6317', + }, + ], + }, + }, + }, + }); + }); + +}); From f26063f61e9a363aeb5fe4191c534b08855b8694 Mon Sep 17 00:00:00 2001 From: Simon-Pierre Gingras <892367+spg@users.noreply.github.com> Date: Tue, 26 May 2020 04:45:47 -0400 Subject: [PATCH 28/33] docs(rds): invalid master username (#5076) ## Commit Message docs(rds): invalid master username (#5076) ## End Commit Message Fixes the following error that occurs when `username` is set to `admin`: ``` 5/9 | 10:13:25 AM | CREATE_FAILED | AWS::RDS::DBCluster | Database (DatabaseB269D8BB) MasterUsername admin cannot be used as it is a reserved word used by the engine (Service: AmazonRDS; Status Code: 400; Error Code: InvalidParameterValue; Request ID: 0ac76793-...) ``` --- packages/@aws-cdk/aws-rds/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 7a6e8e8ca9597..070ac5ca1698c 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -23,7 +23,7 @@ your instances will be launched privately or publicly: const cluster = new DatabaseCluster(this, 'Database', { engine: DatabaseClusterEngine.AURORA, masterUser: { - username: 'admin' + username: 'clusteradmin' }, instanceProps: { instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), From 524440c5454d15276c92581a08d4ee7cad1790eb Mon Sep 17 00:00:00 2001 From: Vincent Lesierse Date: Tue, 26 May 2020 17:05:04 +0200 Subject: [PATCH 29/33] fix(eks): unable to add multiple service accounts (#8122) When two services accounts are added to a single cluster it will throw an error on the resource name. This is because the service account resource name is not unique to the cluster regardless the unique service account name. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-eks/lib/service-account.ts | 2 +- .../test/integ.eks-cluster.expected.json | 2 +- .../aws-eks/test/test.service-account.ts | 45 +++++++++++++++++++ 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts index 24865c4a36f4b..83da66fbfef73 100644 --- a/packages/@aws-cdk/aws-eks/lib/service-account.ts +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -78,7 +78,7 @@ export class ServiceAccount extends Construct implements IPrincipal { this.grantPrincipal = this.role.grantPrincipal; this.policyFragment = this.role.policyFragment; - cluster.addResource('ServiceAccount', { + cluster.addResource(`${id}ServiceAccountResource`, { apiVersion: 'v1', kind: 'ServiceAccount', metadata: { diff --git a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json index 29b38c2393bc1..164377d944797 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.expected.json @@ -2298,7 +2298,7 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, - "ClustermanifestServiceAccountD03C306D": { + "ClustermanifestMyServiceAccountServiceAccountResource0EC03615": { "Type": "Custom::AWSCDK-EKS-KubernetesResource", "Properties": { "ServiceToken": { diff --git a/packages/@aws-cdk/aws-eks/test/test.service-account.ts b/packages/@aws-cdk/aws-eks/test/test.service-account.ts index 71b04ee993d04..8c83c62da2810 100644 --- a/packages/@aws-cdk/aws-eks/test/test.service-account.ts +++ b/packages/@aws-cdk/aws-eks/test/test.service-account.ts @@ -65,5 +65,50 @@ export = { })); test.done(); }, + 'should have allow multiple services accounts'(test: Test) { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + cluster.addServiceAccount('MyServiceAccount'); + cluster.addServiceAccount('MyOtherServiceAccount'); + + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + ServiceToken: { + 'Fn::GetAtt': [ + 'awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B', + 'Outputs.StackawscdkawseksKubectlProviderframeworkonEvent8897FD9BArn', + ], + }, + Manifest: { + 'Fn::Join': [ + '', + [ + '[{\"apiVersion\":\"v1\",\"kind\":\"ServiceAccount\",\"metadata\":{\"name\":\"stackclustermyotherserviceaccounta472761a\",\"namespace\":\"default\",\"labels\":{\"app.kubernetes.io/name\":\"stackclustermyotherserviceaccounta472761a\"},\"annotations\":{\"eks.amazonaws.com/role-arn\":\"', + { + 'Fn::GetAtt': [ + 'ClusterMyOtherServiceAccountRole764583C5', + 'Arn', + ], + }, + '\"}}}]', + ], + ], + }, + })); + test.done(); + }, + 'should have unique resource name'(test: Test) { + // GIVEN + const { cluster } = testFixtureCluster(); + + // WHEN + cluster.addServiceAccount('MyServiceAccount'); + + // THEN + test.throws(() => cluster.addServiceAccount('MyServiceAccount')); + test.done(); + }, }, }; From fab09b158c6f82196127edf21d3493e9645c6a96 Mon Sep 17 00:00:00 2001 From: Pahud Hsieh Date: Wed, 27 May 2020 00:56:38 +0800 Subject: [PATCH 30/33] chore(aws-eks): update README with correct service account syntax (#8112) chore(aws-eks): update README with correct service account syntax `serviceAccountName` should be at `spec.serviceAccountName` and add a CfnOutput sample to demo how to get the IAM role of this service account. My working sample here https://twitter.com/pahudnet/status/1263286407092514817 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-eks/README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index e6d94a2838257..d77259e7fb3fb 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -526,26 +526,33 @@ With services account you can provide Kubernetes Pods access to AWS resources. ```ts // add service account -const serviceAccount = cluster.addServiceAccount('MyServiceAccount'); +const sa = cluster.addServiceAccount('MyServiceAccount'); const bucket = new Bucket(this, 'Bucket'); bucket.grantReadWrite(serviceAccount); -cluster.addResource('mypod', { +const mypod = cluster.addResource('mypod', { apiVersion: 'v1', kind: 'Pod', metadata: { name: 'mypod' }, spec: { + serviceAccountName: sa.serviceAccountName containers: [ { name: 'hello', image: 'paulbouwer/hello-kubernetes:1.5', ports: [ { containerPort: 8080 } ], - serviceAccountName: serviceAccount.serviceAccountName + } ] } }); + +// create the resource after the service account +mypod.node.addDependency(sa); + +// print the IAM role arn for this service account +new cdk.CfnOutput(this, 'ServiceAccountIamRole', { value: sa.role.roleArn }) ``` ### Roadmap From c2e9c77277556bf4674821538ed6ee0179a2f081 Mon Sep 17 00:00:00 2001 From: "Eric Z. Beard" Date: Tue, 26 May 2020 11:27:35 -0700 Subject: [PATCH 31/33] docs: add note on global npm installs, change OpenJDK URL (#8208) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- CONTRIBUTING.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ddd76ca329bbf..ad4328ebde45a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -77,7 +77,8 @@ you need to have the following SDKs and tools locally: - We recommend using a version in [Active LTS](https://nodejs.org/en/about/releases/) - ⚠️ versions `13.0.0` to `13.6.0` are not supported due to compatibility issues with our dependencies. - [Yarn >= 1.19.1](https://yarnpkg.com/lang/en/docs/install) -- [Java OpenJDK 8](http://openjdk.java.net/install/) +- [Java OpenJDK 8](https://docs.aws.amazon.com/corretto/latest/corretto-8-ug/downloads-list.html) +- [Apache Maven](http://maven.apache.org/install.html) - [.NET Core SDK 3.1](https://www.microsoft.com/net/download) - [Python 3.6.5](https://www.python.org/downloads/release/python-365/) - [Ruby 2.5.1](https://www.ruby-lang.org/en/news/2018/03/28/ruby-2-5-1-released/) @@ -91,6 +92,13 @@ $ yarn install $ yarn build ``` +If you get compiler errors when building, a common cause is globally installed tools like tslint and typescript. Try uninstalling them. + +``` +npm uninstall -g tslint +npm uninstall -g typescript +``` + Alternatively, the [Full Docker build](#full-docker-build) workflow can be used so that you don't have to worry about installing all those tools on your local machine and instead only depend on having a working Docker install. @@ -197,7 +205,7 @@ Examples: ### Step 4: Commit -Create a commit with the proposed change changes: +Create a commit with the proposed changes: * Commit title and message (and PR title and description) must adhere to [conventionalcommits](https://www.conventionalcommits.org). * The title must begin with `feat(module): title`, `fix(module): title`, `refactor(module): title` or From b4adf07f423f621177397019ad84bde4e7b61196 Mon Sep 17 00:00:00 2001 From: Shiv Lakshminarayan Date: Tue, 26 May 2020 12:16:40 -0700 Subject: [PATCH 32/33] chore(stepfunctions): add tests for state transition metrics (#8179) added some more missing tests. now that we have increased coverage, use the base configuration. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-stepfunctions/jest.config.js | 10 +--- .../test/state-transition-metrics.test.ts | 54 +++++++++++++++++++ 2 files changed, 55 insertions(+), 9 deletions(-) create mode 100644 packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts diff --git a/packages/@aws-cdk/aws-stepfunctions/jest.config.js b/packages/@aws-cdk/aws-stepfunctions/jest.config.js index d984ff822379b..cd664e1d069e5 100644 --- a/packages/@aws-cdk/aws-stepfunctions/jest.config.js +++ b/packages/@aws-cdk/aws-stepfunctions/jest.config.js @@ -1,10 +1,2 @@ const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); -module.exports = { - ...baseConfig, - coverageThreshold: { - global: { - ...baseConfig.coverageThreshold.global, - branches: 75, - }, - }, -}; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts new file mode 100644 index 0000000000000..59a66b15a8445 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions/test/state-transition-metrics.test.ts @@ -0,0 +1,54 @@ +import { Metric } from '@aws-cdk/aws-cloudwatch'; +import { StateTransitionMetric } from '../lib'; + +describe('State Transition Metrics', () => { + test('add a named state transition metric', () => { + // WHEN + const metric = StateTransitionMetric.metric('my-metric'); + + // THEN + verifyTransitionMetric(metric, 'my-metric', 'Average'); + }); + + test('metric for available state transitions.', () => { + // WHEN + const metric = StateTransitionMetric.metricProvisionedBucketSize(); + + // THEN + verifyTransitionMetric(metric, 'ProvisionedBucketSize', 'Average'); + }); + + test('metric for provisioned steady-state execution rate', () => { + // WHEN + const metric = StateTransitionMetric.metricProvisionedRefillRate(); + + // THEN + verifyTransitionMetric(metric, 'ProvisionedRefillRate', 'Average'); + }); + + test('metric for state-transitions per second', () => { + // WHEN + const metric = StateTransitionMetric.metricConsumedCapacity(); + + // THEN + verifyTransitionMetric(metric, 'ConsumedCapacity', 'Average'); + }); + + test('metric for the number of throttled state transitions', () => { + // WHEN + const metric = StateTransitionMetric.metricThrottledEvents(); + + // THEN + verifyTransitionMetric(metric, 'ThrottledEvents', 'Sum'); + }); +}); + +function verifyTransitionMetric(metric: Metric, metricName: string, statistic: string) { + expect(metric).toEqual({ + period: { amount: 5, unit: { label: 'minutes', inMillis: 60000 } }, + dimensions: { ServiceMetric: 'StateTransition' }, + namespace: 'AWS/States', + metricName, + statistic, + }); +} From 04490b134a05ec34523541a3ca282ba8957a7964 Mon Sep 17 00:00:00 2001 From: Jonathan Goldwasser Date: Tue, 26 May 2020 23:57:33 +0200 Subject: [PATCH 33/33] fix(lambda-nodejs): build fails on Windows (#8140) This is because the operations of [`path`](https://nodejs.org/api/path.html) are OS specific. But for the container working directory and inside the container we never want to use Windows style paths. Fixes #8107 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-lambda-nodejs/lib/builder.ts | 4 ++-- .../aws-lambda-nodejs/test/builder.test.ts | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts index 41ad7aa0df53a..dd8e3ba2f8565 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/lib/builder.ts @@ -111,11 +111,11 @@ export class Builder { '-v', `${this.options.projectRoot}:${containerProjectRoot}`, '-v', `${path.resolve(this.options.outDir)}:${containerOutDir}`, ...(this.options.cacheDir ? ['-v', `${path.resolve(this.options.cacheDir)}:${containerCacheDir}`] : []), - '-w', path.dirname(containerEntryPath), + '-w', path.dirname(containerEntryPath).replace(/\\/g, '/'), // Always use POSIX paths in the container 'parcel-bundler', ]; const parcelArgs = [ - 'parcel', 'build', containerEntryPath, + 'parcel', 'build', containerEntryPath.replace(/\\/g, '/'), // Always use POSIX paths in the container '--out-dir', containerOutDir, '--out-file', 'index.js', '--global', this.options.global, diff --git a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts index e6e32655a187e..6c7f5e41ae3e0 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts +++ b/packages/@aws-cdk/aws-lambda-nodejs/test/builder.test.ts @@ -20,6 +20,10 @@ jest.mock('child_process', () => ({ }), })); +beforeEach(() => { + jest.clearAllMocks(); +}); + test('calls docker with the correct args', () => { const builder = new Builder({ entry: '/project/folder/entry.ts', @@ -58,6 +62,24 @@ test('calls docker with the correct args', () => { ]); }); +test('with Windows paths', () => { + const builder = new Builder({ + entry: 'C:\\my-project\\lib\\entry.ts', + global: 'handler', + outDir: '/out-dir', + cacheDir: '/cache-dir', + nodeDockerTag: 'lts-alpine', + nodeVersion: '12', + projectRoot: 'C:\\my-project', + }); + builder.build(); + + // docker run + expect(spawnSync).toHaveBeenCalledWith('docker', expect.arrayContaining([ + 'parcel', 'build', expect.stringContaining('/lib/entry.ts'), + ])); +}); + test('throws in case of error', () => { const builder = new Builder({ entry: '/project/folder/error',