diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 69581271f774d..d263a2a35e27e 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -1,7 +1,7 @@ --- name: "\U0001F41B Bug Report" about: Report a bug -title: "(module): " +title: "[module] " labels: bug, needs-triage --- diff --git a/.github/ISSUE_TEMPLATE/doc.md b/.github/ISSUE_TEMPLATE/doc.md index 53cacd3ae3bfd..022c04c22ed80 100644 --- a/.github/ISSUE_TEMPLATE/doc.md +++ b/.github/ISSUE_TEMPLATE/doc.md @@ -1,7 +1,7 @@ --- name: "📕 Documentation Issue" about: Issue in the reference documentation or developer guide -title: "(module): " +title: "[module] " labels: feature-request, documentation, needs-triage --- diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md index 20a51e351f0e4..5fecac4f7ce57 100644 --- a/.github/ISSUE_TEMPLATE/feature-request.md +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -1,7 +1,7 @@ --- name: "\U0001F680 Feature Request" about: Request a new feature -title: "(module): " +title: "[module] " labels: feature-request, needs-triage --- diff --git a/.github/ISSUE_TEMPLATE/general-issues.md b/.github/ISSUE_TEMPLATE/general-issues.md index 8cb4679f0889d..1f2d2b6ff707f 100644 --- a/.github/ISSUE_TEMPLATE/general-issues.md +++ b/.github/ISSUE_TEMPLATE/general-issues.md @@ -1,7 +1,7 @@ --- name: "\U00002753 General Issue" about: Create a new issue -title: "(module): " +title: "[module] " labels: needs-triage --- diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 64b8fd2e08019..6529fc960b41f 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -1,9 +1,7 @@ name: "Set Issue Label and Assignee" on: issues: - types: [opened] - pull_request: - types: [opened] + types: [opened, edited] jobs: test: @@ -15,157 +13,156 @@ jobs: title-or-body: 'title' parameters: > [ - {"keywords":["(cli)","(command line)"],"labels":["package/tools"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/alexa-ask)","(alexa-ask)","(alexa ask)"],"labels":["@aws-cdk/alexa-ask"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/assert)","(assert)"],"labels":["@aws-cdk/assert"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/assets)","(assets)"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-amazonmq)","(aws-amazonmq)","(amazonmq)","(amazon mq)"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-apigateway)","(aws-apigateway)","(apigateway)", "(api gateway)"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-apigatewayv2)","(aws-apigatewayv2)","(apigatewayv2)","(apigateway v2)"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-appconfig)","(aws-appconfig)","(appconfig)","(app config)"],"labels":["@aws-cdk/aws-appconfig"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-applicationautoscaling)","(aws-applicationautoscaling)","(applicationautoscaling)","(application autoscaling)"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-appstream)","(aws-appstream)","(appstream)","(app stream)"],"labels":["@aws-cdk/aws-appstream"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-appsync)","(aws-appsync)","(appsync)","(app sync)"],"labels":["@aws-cdk/aws-appsync"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-athena)","(aws-athena)","(athena)"],"labels":["@aws-cdk/aws-athena"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-autoscaling)","(aws-autoscaling)","(autoscaling)","(auto scaling)"],"labels":["@aws-cdk/aws-autoscaling"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-autoscaling-api)","(aws-autoscaling-api)","(autoscaling-api)","(autoscaling api)"],"labels":["@aws-cdk/aws-autoscaling-api"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-autoscaling-common)","(aws-autoscaling-common)","(autoscaling-common)","(autoscaling common)"],"labels":["@aws-cdk/aws-autoscaling-common"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-autoscaling-hooktargets)","(aws-autoscaling-hooktargets)","(autoscaling-hooktargets)","(autoscaling hooktargets)"],"labels":["@aws-cdk/aws-autoscaling-hooktargets"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-autoscalingplans)","(aws-autoscalingplans)","(autoscalingplans)","(autoscaling plans)"],"labels":["@aws-cdk/aws-autoscalingplans"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-backup)","(aws-backup)","(backup)"],"labels":["@aws-cdk/aws-backup"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-batch)","(aws-batch)","(batch)"],"labels":["@aws-cdk/aws-batch"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-budgets)","(aws-budgets)","(budgets)"],"labels":["@aws-cdk/aws-budgets"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-cassandra)","(aws-cassandra)","(cassandra)"],"labels":["@aws-cdk/aws-cassandra"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-ce)","(aws-ce)","(ce)"],"labels":["@aws-cdk/aws-ce"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-certificatemanager)","(aws-certificatemanager)","(certificatemanager)","(certificate manager)"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-chatbot)","(aws-chatbot)","(chatbot)"],"labels":["@aws-cdk/aws-chatbot"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-cloud9)","(aws-cloud9)","(cloud9)","(cloud 9)"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-cloudformation)","(aws-cloudformation)","(cloudformation)","(cloud formation)"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-cloudtrail)","(aws-cloudtrail)","(cloudtrail)","(cloud trail)"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codestarconnections)","(aws-codestarconnections)","(codestarconnections)","(codestar connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-cognito)","(aws-cognito)","(cognito)"],"labels":["@aws-cdk/aws-cognito"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-config)","(aws-config)","(config)"],"labels":["@aws-cdk/aws-config"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-datapipeline)","(aws-datapipeline)","(datapipeline)","(data pipeline)"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["RomainMuller"]}, - {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-directoryservice)","(aws-directoryservice)","(directoryservice)","(directory service)"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["RomainMuller"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["RomainMuller"]}, - {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-ecs)","(aws-ecs)","(ecs)"],"labels":["@aws-cdk/aws-ecs"],"assignees":["uttarasridhar"]}, - {"keywords":["(@aws-cdk/aws-ecs-patterns)","(aws-ecs-patterns)","(ecs-patterns)","(ecs patterns)"],"labels":["@aws-cdk/aws-ecs-patterns"],"assignees":["uttarasridhar"]}, - {"keywords":["(@aws-cdk/aws-efs)","(aws-efs)","(efs)"],"labels":["@aws-cdk/aws-efs"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-eks)","(aws-eks)","(eks)"],"labels":["@aws-cdk/aws-eks"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-elasticache)","(aws-elasticache)","(elasticache)","(elastic cache)"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-elasticloadbalancing)","(aws-elasticloadbalancing)","(elasticloadbalancing)","(elastic loadbalancing)","(elb)"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2)","(aws-elasticloadbalancingv2)","(elasticloadbalancingv2)","(elbv2)","(elb v2)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2-targets)","(aws-elasticloadbalancingv2-targets)","(elasticloadbalancingv2-targets)","(elasticloadbalancingv2 targets)","(elbv2 targets)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-elasticsearch)","(aws-elasticsearch)","(elasticsearch)","(elastic search)"],"labels":["@aws-cdk/aws-elasticsearch"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-emr)","(aws-emr)","(emr)"],"labels":["@aws-cdk/aws-emr"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-events)","(aws-events)","(events)", "eventbridge"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-events-targets)","(aws-events-targets)","(events-targets)","(events targets)"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-fms)","(aws-fms)","(fms)"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-fsx)","(aws-fsx)","(fsx)"],"labels":["@aws-cdk/aws-fsx"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-gamelift)","(aws-gamelift)","(gamelift)","(game lift)"],"labels":["@aws-cdk/aws-gamelift"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-globalaccelerator)","(aws-globalaccelerator)","(globalaccelerator)","(global accelerator)"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-glue)","(aws-glue)","(glue)"],"labels":["@aws-cdk/aws-glue"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-guardduty)","(aws-guardduty)","(guardduty)", "(guard duty)"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-iam)","(aws-iam)","(iam)"],"labels":["@aws-cdk/aws-iam"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-kinesis)","(aws-kinesis)","(kinesis)"],"labels":["@aws-cdk/aws-kinesis"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-kinesisanalytics)","(aws-kinesisanalytics)","(kinesisanalytics)", "(kinesis analytics)"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-kinesisfirehose)","(aws-kinesisfirehose)","(kinesisfirehose)", "(kinesis firehose)"],"labels":["@aws-cdk/aws-kinesisfirehose"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-kms)","(aws-kms)","(kms)"],"labels":["@aws-cdk/aws-kms"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-lakeformation)","(aws-lakeformation)","(lakeformation)", "(lake formation)"],"labels":["@aws-cdk/aws-lakeformation"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-lambda)","(aws-lambda)","(lambda)"],"labels":["@aws-cdk/aws-lambda"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-lambda-event-sources)","(aws-lambda-event-sources)","(lambda-event-sources)","(lambda event sources)"],"labels":["@aws-cdk/aws-lambda-event-sources"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-lambda-nodejs)","(aws-lambda-nodejs)","(lambda-nodejs)","(lambda nodejs)"],"labels":["@aws-cdk/aws-lambda-nodejs"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/aws-logs)","(aws-logs)","(logs)"],"labels":["@aws-cdk/aws-logs"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-logs-destinations)","(aws-logs-destinations)","(logs-destinations)","(logs destinations)"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-msk)","(aws-msk)","(msk)"],"labels":["@aws-cdk/aws-msk"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-neptune)","(aws-neptune)","(neptune)"],"labels":["@aws-cdk/aws-neptune"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-opsworks)","(aws-opsworks)","(opsworks)","(ops works)"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-opsworkscm)","(aws-opsworkscm)","(opsworkscm)", "(opsworks cm)"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-personalize)","(aws-personalize)","(personalize)"],"labels":["@aws-cdk/aws-personalize"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-pinpoint)","(aws-pinpoint)","(pinpoint)"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-pinpointemail)","(aws-pinpointemail)","(pinpointemail)", "(pinpoint email)"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-ram)","(aws-ram)","(ram)"],"labels":["@aws-cdk/aws-ram"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-redshift)","(aws-redshift)","(redshift)","(red shift)"],"labels":["@aws-cdk/aws-redshift"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-resourcegroups)","(aws-resourcegroups)","(resourcegroups)","(resource groups)"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-robomaker)","(aws-robomaker)","(robomaker)","(robo maker)"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-route53)","(aws-route53)","(route53)","(route 53)"],"labels":["@aws-cdk/aws-route53"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-route53-patterns)","(aws-route53-patterns)","(route53-patterns)","(route53 patterns)"],"labels":["@aws-cdk/aws-route53-patterns"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-route53-targets)","(aws-route53-targets)","(route53-targets)","(route53 targets)"],"labels":["@aws-cdk/aws-route53-targets"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-route53resolver)","(aws-route53resolver)","(route53resolver)","(route53 resolver)"],"labels":["@aws-cdk/aws-route53resolver"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-s3)","(aws-s3)","(s3)"],"labels":["@aws-cdk/aws-s3"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-s3-assets)","(aws-s3-assets)","(s3-assets)","(s3 assets)"],"labels":["@aws-cdk/aws-s3-assets"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-s3-deployment)","(aws-s3-deployment)","(s3-deployment)","(s3 deployment)"],"labels":["@aws-cdk/aws-s3-deployment"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-s3-notifications)","(aws-s3-notifications)","(s3-notifications)","(s3 notifications)"],"labels":["@aws-cdk/aws-s3-notifications"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-sagemaker)","(aws-sagemaker)","(sagemaker)","(sage maker)"],"labels":["@aws-cdk/aws-sagemaker"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/aws-sam)","(aws-sam)","(sam)"],"labels":["@aws-cdk/aws-sam"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-sdb)","(aws-sdb)","(sdb)"],"labels":["@aws-cdk/aws-sdb"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/aws-secretsmanager)","(aws-secretsmanager)","(secretsmanager)","(secrets manager)"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-servicediscovery)","(aws-servicediscovery)","(servicediscovery)","(service discovery)"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-ses)","(aws-ses)","(ses)"],"labels":["@aws-cdk/aws-ses"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-ses-actions)","(aws-ses-actions)","(ses-actions)","(ses actions)"],"labels":["@aws-cdk/aws-ses-actions"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-sns)","(aws-sns)","(sns)"],"labels":["@aws-cdk/aws-sns"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-sns-subscriptions)","(aws-sns-subscriptions)","(sns-subscriptions)","(sns subscriptions)"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-sqs)","(aws-sqs)","(sqs)"],"labels":["@aws-cdk/aws-sqs"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-ssm)","(aws-ssm)","(ssm)"],"labels":["@aws-cdk/aws-ssm"],"assignees":["MrArnoldPalmer"]}, - {"keywords":["(@aws-cdk/aws-stepfunctions)","(aws-stepfunctions)","(stepfunctions)","(step functions)"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-stepfunctions-tasks)","(aws-stepfunctions-tasks)","(stepfunctions-tasks)","(stepfunctions tasks)"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/aws-synthetics)","(aws-synthetics)","(synthetics)"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-transfer)","(aws-transfer)","(transfer)"],"labels":["@aws-cdk/aws-transfer"],"assignees":["iliapolo"]}, - {"keywords":["(@aws-cdk/aws-waf)","(aws-waf)","(waf)"],"labels":["@aws-cdk/aws-waf"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-wafregional)","(aws-wafregional)","(wafregional)","(waf regional)"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-wafv2)","(aws-wafv2)","(wafv2)","(waf v2)"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-workspaces)","(aws-workspaces)","(workspaces)"],"labels":["@aws-cdk/aws-workspaces"],"assignees":["NetaNir"]}, - {"keywords":["(@aws-cdk/cfnspec)","(cfnspec)","(cfn spec)"],"labels":["@aws-cdk/cfnspec"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/cloud-assembly-schema)","(cloud-assembly-schema)","(cloud assembly schema)"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["shivlaks"]}, - {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["skinny85"]}, - {"keywords":["(@aws-cdk/core)","(core)"],"labels":["@aws-cdk/core"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/custom-resources)","(custom-resources)","(custom resources)"],"labels":["@aws-cdk/custom-resources"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/cx-api)","(cx-api)","(cx api)"],"labels":["@aws-cdk/cx-api"],"assignees":["eladb"]}, - {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["RomainMuller"]} + {"keywords":["[cli]","[command line]"],"labels":["package/tools"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/alexa-ask]","[alexa-ask]","[alexa ask]"],"labels":["@aws-cdk/alexa-ask"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/app-delivery]","[app-delivery]","[app delivery]"],"labels":["@aws-cdk/app-delivery"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/assert]","[assert]"],"labels":["@aws-cdk/assert"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/assets]","[assets]"],"labels":["@aws-cdk/assets"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/aws-accessanalyzer]","[aws-accessanalyzer]","[accessanalyzer]","[access analyzer]"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-acmpca]","[aws-acmpca]","[acmpca]"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-amazonmq]","[aws-amazonmq]","[amazonmq]","[amazon mq]"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-amplify]","[aws-amplify]","[amplify]"],"labels":["@aws-cdk/aws-amplify"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-apigateway]","[aws-apigateway]","[apigateway]", "[api gateway]"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-apigatewayv2]","[aws-apigatewayv2]","[apigatewayv2]","[apigateway v2]"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-appconfig]","[aws-appconfig]","[appconfig]","[app config]"],"labels":["@aws-cdk/aws-appconfig"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-applicationautoscaling]","[aws-applicationautoscaling]","[applicationautoscaling]","[application autoscaling]"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-appmesh]","[aws-appmesh]","[appmesh]","[app mesh]"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-appstream]","[aws-appstream]","[appstream]","[app stream]"],"labels":["@aws-cdk/aws-appstream"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-appsync]","[aws-appsync]","[appsync]","[app sync]"],"labels":["@aws-cdk/aws-appsync"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-athena]","[aws-athena]","[athena]"],"labels":["@aws-cdk/aws-athena"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-autoscaling]","[aws-autoscaling]","[autoscaling]","[auto scaling]"],"labels":["@aws-cdk/aws-autoscaling"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-autoscaling-api]","[aws-autoscaling-api]","[autoscaling-api]","[autoscaling api]"],"labels":["@aws-cdk/aws-autoscaling-api"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-autoscaling-common]","[aws-autoscaling-common]","[autoscaling-common]","[autoscaling common]"],"labels":["@aws-cdk/aws-autoscaling-common"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-autoscaling-hooktargets]","[aws-autoscaling-hooktargets]","[autoscaling-hooktargets]","[autoscaling hooktargets]"],"labels":["@aws-cdk/aws-autoscaling-hooktargets"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-autoscalingplans]","[aws-autoscalingplans]","[autoscalingplans]","[autoscaling plans]"],"labels":["@aws-cdk/aws-autoscalingplans"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-backup]","[aws-backup]","[backup]"],"labels":["@aws-cdk/aws-backup"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-batch]","[aws-batch]","[batch]"],"labels":["@aws-cdk/aws-batch"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-budgets]","[aws-budgets]","[budgets]"],"labels":["@aws-cdk/aws-budgets"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-cassandra]","[aws-cassandra]","[cassandra]"],"labels":["@aws-cdk/aws-cassandra"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-ce]","[aws-ce]","[ce]"],"labels":["@aws-cdk/aws-ce"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-certificatemanager]","[aws-certificatemanager]","[certificatemanager]","[certificate manager]"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-chatbot]","[aws-chatbot]","[chatbot]"],"labels":["@aws-cdk/aws-chatbot"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-cloud9]","[aws-cloud9]","[cloud9]","[cloud 9]"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-cloudformation]","[aws-cloudformation]","[cloudformation]","[cloud formation]"],"labels":["@aws-cdk/aws-cloudformation"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/aws-cloudfront]","[aws-cloudfront]","[cloudfront]","[cloud front]"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-cloudtrail]","[aws-cloudtrail]","[cloudtrail]","[cloud trail]"],"labels":["@aws-cdk/aws-cloudtrail"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-cloudwatch]","[aws-cloudwatch]","[cloudwatch]","[cloud watch]"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-cloudwatch-actions]","[aws-cloudwatch-actions]","[cloudwatch-actions]","[cloudwatch actions]"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-codebuild]","[aws-codebuild]","[codebuild]","[code build]"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codecommit]","[aws-codecommit]","[codecommit]","[code commit]"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codedeploy]","[aws-codedeploy]","[codedeploy]","[code deploy]"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codeguruprofiler]","[aws-codeguruprofiler]","[codeguruprofiler]","[codeguru profiler]"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codepipeline]","[aws-codepipeline]","[codepipeline]","[code pipeline]"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codepipeline-actions]","[aws-codepipeline-actions]","[codepipeline-actions]","[codepipeline actions]"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codestar]","[aws-codestar]","[codestar]"],"labels":["@aws-cdk/aws-codestar"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codestarconnections]","[aws-codestarconnections]","[codestarconnections]","[codestar connections]"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-codestarnotifications]","[aws-codestarnotifications]","[codestarnotifications]","[codestar notifications]"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-cognito]","[aws-cognito]","[cognito]"],"labels":["@aws-cdk/aws-cognito"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-config]","[aws-config]","[config]"],"labels":["@aws-cdk/aws-config"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-datapipeline]","[aws-datapipeline]","[datapipeline]","[data pipeline]"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-dax]","[aws-dax]","[dax]"],"labels":["@aws-cdk/aws-dax"],"assignees":["RomainMuller"]}, + {"keywords":["[@aws-cdk/aws-detective]","[aws-detective]","[detective]"],"labels":["@aws-cdk/aws-detective"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-directoryservice]","[aws-directoryservice]","[directoryservice]","[directory service]"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-dlm]","[aws-dlm]","[dlm]"],"labels":["@aws-cdk/aws-dlm"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-dms]","[aws-dms]","[dms]"],"labels":["@aws-cdk/aws-dms"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-docdb]","[aws-docdb]","[docdb]","[doc db]"],"labels":["@aws-cdk/aws-docdb"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-dynamodb]","[aws-dynamodb]","[dynamodb]","[dynamo db]"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["RomainMuller"]}, + {"keywords":["[@aws-cdk/aws-dynamodb-global]","[aws-dynamodb-global]","[dynamodb-global]","[dynamodb global]"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["RomainMuller"]}, + {"keywords":["[@aws-cdk/aws-ec2]","[aws-ec2]","[ec2]", "[vpc]"],"labels":["@aws-cdk/aws-ec2"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-ecr]","[aws-ecr]","[ecr]"],"labels":["@aws-cdk/aws-ecr"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-ecr-assets]","[aws-ecr-assets]","[ecr-assets]","[ecr assets]", "[ecrassets]"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/aws-efs]","[aws-efs]","[efs]"],"labels":["@aws-cdk/aws-efs"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-eks]","[aws-eks]","[eks]"],"labels":["@aws-cdk/aws-eks"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/aws-elasticache]","[aws-elasticache]","[elasticache]","[elastic cache]"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-elasticbeanstalk]","[aws-elasticbeanstalk]","[elasticbeanstalk]","[elastic beanstalk]"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-elasticloadbalancing]","[aws-elasticloadbalancing]","[elasticloadbalancing]","[elastic loadbalancing]","[elb]"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-elasticloadbalancingv2]","[aws-elasticloadbalancingv2]","[elasticloadbalancingv2]","[elbv2]","[elb v2]"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-elasticloadbalancingv2-targets]","[aws-elasticloadbalancingv2-targets]","[elasticloadbalancingv2-targets]","[elasticloadbalancingv2 targets]","[elbv2 targets]"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-targets"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-elasticsearch]","[aws-elasticsearch]","[elasticsearch]","[elastic search]"],"labels":["@aws-cdk/aws-elasticsearch"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-emr]","[aws-emr]","[emr]"],"labels":["@aws-cdk/aws-emr"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-events]","[aws-events]","[events]", "eventbridge"],"labels":["@aws-cdk/aws-events"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-events-targets]","[aws-events-targets]","[events-targets]","[events targets]"],"labels":["@aws-cdk/aws-events-targets"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-eventschemas]","[aws-eventschemas]","[eventschemas]","[event schemas]"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-fms]","[aws-fms]","[fms]"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-fsx]","[aws-fsx]","[fsx]"],"labels":["@aws-cdk/aws-fsx"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-gamelift]","[aws-gamelift]","[gamelift]","[game lift]"],"labels":["@aws-cdk/aws-gamelift"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-globalaccelerator]","[aws-globalaccelerator]","[globalaccelerator]","[global accelerator]"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-glue]","[aws-glue]","[glue]"],"labels":["@aws-cdk/aws-glue"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-greengrass]","[aws-greengrass]","[greengrass]","[green grass]"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-guardduty]","[aws-guardduty]","[guardduty]", "[guard duty]"],"labels":["@aws-cdk/aws-guardduty"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-iam]","[aws-iam]","[iam]"],"labels":["@aws-cdk/aws-iam"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-imagebuilder]","[aws-imagebuilder]","[imagebuilder]","[image builder]"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-inspector]","[aws-inspector]","[inspector]"],"labels":["@aws-cdk/aws-inspector"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-iot]","[aws-iot]","[iot]"],"labels":["@aws-cdk/aws-iot"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-iot1click]","[aws-iot1click]","[iot1click]","[iot 1click]"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-iotanalytics]","[aws-iotanalytics]","[iotanalytics]","[iot analytics]"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-iotevents]","[aws-iotevents]","[iotevents]","[iot events]"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-iotthingsgraph]","[aws-iotthingsgraph]","[iotthingsgraph]","[iot things graph]"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-kinesis]","[aws-kinesis]","[kinesis]"],"labels":["@aws-cdk/aws-kinesis"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-kinesisanalytics]","[aws-kinesisanalytics]","[kinesisanalytics]", "[kinesis analytics]"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-kinesisfirehose]","[aws-kinesisfirehose]","[kinesisfirehose]", "[kinesis firehose]"],"labels":["@aws-cdk/aws-kinesisfirehose"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-kms]","[aws-kms]","[kms]"],"labels":["@aws-cdk/aws-kms"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-lakeformation]","[aws-lakeformation]","[lakeformation]", "[lake formation]"],"labels":["@aws-cdk/aws-lakeformation"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-lambda]","[aws-lambda]","[lambda]"],"labels":["@aws-cdk/aws-lambda"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-lambda-event-sources]","[aws-lambda-event-sources]","[lambda-event-sources]","[lambda event sources]"],"labels":["@aws-cdk/aws-lambda-event-sources"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-lambda-nodejs]","[aws-lambda-nodejs]","[lambda-nodejs]","[lambda nodejs]"],"labels":["@aws-cdk/aws-lambda-nodejs"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/aws-logs]","[aws-logs]","[logs]"],"labels":["@aws-cdk/aws-logs"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-logs-destinations]","[aws-logs-destinations]","[logs-destinations]","[logs destinations]"],"labels":["@aws-cdk/aws-logs-destinations"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-managedblockchain]","[aws-managedblockchain]","[managedblockchain]","[managed blockchain]"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-mediaconvert]","[aws-mediaconvert]","[mediaconvert]","[media convert]"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-medialive]","[aws-medialive]","[medialive]","[media live]"],"labels":["@aws-cdk/aws-medialive"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-mediastore]","[aws-mediastore]","[mediastore]","[media store]"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-msk]","[aws-msk]","[msk]"],"labels":["@aws-cdk/aws-msk"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-neptune]","[aws-neptune]","[neptune]"],"labels":["@aws-cdk/aws-neptune"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-networkmanager]","[aws-networkmanager]","[networkmanager]","[network manager]"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-opsworks]","[aws-opsworks]","[opsworks]","[ops works]"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-opsworkscm]","[aws-opsworkscm]","[opsworkscm]", "[opsworks cm]"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-personalize]","[aws-personalize]","[personalize]"],"labels":["@aws-cdk/aws-personalize"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-pinpoint]","[aws-pinpoint]","[pinpoint]"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-pinpointemail]","[aws-pinpointemail]","[pinpointemail]", "[pinpoint email]"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-qldb]","[aws-qldb]","[qldb]"],"labels":["@aws-cdk/aws-qldb"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-ram]","[aws-ram]","[ram]"],"labels":["@aws-cdk/aws-ram"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-rds]","[aws-rds]","[rds]"],"labels":["@aws-cdk/aws-rds"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-redshift]","[aws-redshift]","[redshift]","[red shift]"],"labels":["@aws-cdk/aws-redshift"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-resourcegroups]","[aws-resourcegroups]","[resourcegroups]","[resource groups]"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-robomaker]","[aws-robomaker]","[robomaker]","[robo maker]"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-route53]","[aws-route53]","[route53]","[route 53]"],"labels":["@aws-cdk/aws-route53"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-route53-patterns]","[aws-route53-patterns]","[route53-patterns]","[route53 patterns]"],"labels":["@aws-cdk/aws-route53-patterns"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-route53-targets]","[aws-route53-targets]","[route53-targets]","[route53 targets]"],"labels":["@aws-cdk/aws-route53-targets"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-route53resolver]","[aws-route53resolver]","[route53resolver]","[route53 resolver]"],"labels":["@aws-cdk/aws-route53resolver"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-s3]","[aws-s3]","[s3]"],"labels":["@aws-cdk/aws-s3"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-s3-assets]","[aws-s3-assets]","[s3-assets]","[s3 assets]"],"labels":["@aws-cdk/aws-s3-assets"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-s3-deployment]","[aws-s3-deployment]","[s3-deployment]","[s3 deployment]"],"labels":["@aws-cdk/aws-s3-deployment"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-s3-notifications]","[aws-s3-notifications]","[s3-notifications]","[s3 notifications]"],"labels":["@aws-cdk/aws-s3-notifications"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-sagemaker]","[aws-sagemaker]","[sagemaker]","[sage maker]"],"labels":["@aws-cdk/aws-sagemaker"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/aws-sam]","[aws-sam]","[sam]"],"labels":["@aws-cdk/aws-sam"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-sdb]","[aws-sdb]","[sdb]"],"labels":["@aws-cdk/aws-sdb"],"assignees":["nija-at"]}, + {"keywords":["[@aws-cdk/aws-secretsmanager]","[aws-secretsmanager]","[secretsmanager]","[secrets manager]"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/aws-securityhub]","[aws-securityhub]","[securityhub]","[security hub]"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-servicecatalog]","[aws-servicecatalog]","[servicecatalog]","[service catalog]"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-servicediscovery]","[aws-servicediscovery]","[servicediscovery]","[service discovery]"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-ses]","[aws-ses]","[ses]"],"labels":["@aws-cdk/aws-ses"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-ses-actions]","[aws-ses-actions]","[ses-actions]","[ses actions]"],"labels":["@aws-cdk/aws-ses-actions"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-sns]","[aws-sns]","[sns]"],"labels":["@aws-cdk/aws-sns"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-sns-subscriptions]","[aws-sns-subscriptions]","[sns-subscriptions]","[sns subscriptions]"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-sqs]","[aws-sqs]","[sqs]"],"labels":["@aws-cdk/aws-sqs"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-ssm]","[aws-ssm]","[ssm]"],"labels":["@aws-cdk/aws-ssm"],"assignees":["MrArnoldPalmer"]}, + {"keywords":["[@aws-cdk/aws-stepfunctions]","[aws-stepfunctions]","[stepfunctions]","[step functions]"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-stepfunctions-tasks]","[aws-stepfunctions-tasks]","[stepfunctions-tasks]","[stepfunctions tasks]"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/aws-synthetics]","[aws-synthetics]","[synthetics]"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-transfer]","[aws-transfer]","[transfer]"],"labels":["@aws-cdk/aws-transfer"],"assignees":["iliapolo"]}, + {"keywords":["[@aws-cdk/aws-waf]","[aws-waf]","[waf]"],"labels":["@aws-cdk/aws-waf"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-wafregional]","[aws-wafregional]","[wafregional]","[waf regional]"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-wafv2]","[aws-wafv2]","[wafv2]","[waf v2]"],"labels":["@aws-cdk/aws-wafv2"],"assignees":["rix0rrr"]}, + {"keywords":["[@aws-cdk/aws-workspaces]","[aws-workspaces]","[workspaces]"],"labels":["@aws-cdk/aws-workspaces"],"assignees":["NetaNir"]}, + {"keywords":["[@aws-cdk/cfnspec]","[cfnspec]","[cfn spec]"],"labels":["@aws-cdk/cfnspec"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/cloud-assembly-schema]","[cloud-assembly-schema]","[cloud assembly schema]"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/cloudformation-diff]","[cloudformation-diff]","[cloudformation diff]","[cfn diff]"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["shivlaks"]}, + {"keywords":["[@aws-cdk/cloudformation-include]","[cloudformation-include]","[cloudformation include]","[cfn include]"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["skinny85"]}, + {"keywords":["[@aws-cdk/core]","[core]"],"labels":["@aws-cdk/core"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/custom-resources]","[custom-resources]","[custom resources]"],"labels":["@aws-cdk/custom-resources"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/cx-api]","[cx-api]","[cx api]"],"labels":["@aws-cdk/cx-api"],"assignees":["eladb"]}, + {"keywords":["[@aws-cdk/region-info]","[region-info]","[region info]"],"labels":["@aws-cdk/region-info"],"assignees":["RomainMuller"]}, + {"keywords":["(@aws-cdk/aws-macie)","(aws-macie)","(macie)"],"labels":["@aws-cdk/aws-macie"],"assignees":["rix0rrr"]} ] diff --git a/CHANGELOG.md b/CHANGELOG.md index 7862a9041888f..28e25c02d359f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,30 @@ 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.47.0](https://github.com/aws/aws-cdk/compare/v1.46.0...v1.47.0) (2020-06-24) + + +### ⚠ BREAKING CHANGES + +* **stepfunctions-tasks:** `Dynamo*` tasks no longer implement`IStepFunctionsTask` and have been replaced by constructs that can be instantiated directly. See README for examples + +### Features + +* **cfn-include:** add support for retrieving parameter objects ([#8658](https://github.com/aws/aws-cdk/issues/8658)) ([52dc123](https://github.com/aws/aws-cdk/commit/52dc123ba8696abcfad99d8093e98cd39b5b104f)), closes [#8657](https://github.com/aws/aws-cdk/issues/8657) +* **cfn-include:** support logical id overrides ([#8529](https://github.com/aws/aws-cdk/issues/8529)) ([d9c4f5e](https://github.com/aws/aws-cdk/commit/d9c4f5e67c54e1a2a436978fbc28fffd92b24cd6)), closes [#7375](https://github.com/aws/aws-cdk/issues/7375) +* **cloudwatch:** CompositeAlarm ([#8498](https://github.com/aws/aws-cdk/issues/8498)) ([1e6d293](https://github.com/aws/aws-cdk/commit/1e6d293f4c445318b11bd6fe998325688a675807)) +* **efs:** access point ([#8631](https://github.com/aws/aws-cdk/issues/8631)) ([dde0ef5](https://github.com/aws/aws-cdk/commit/dde0ef52cc0cdbc40fd212f518f3cee4f30450b9)) +* **stepfunctions:** grant APIs for state machine construct ([#8486](https://github.com/aws/aws-cdk/issues/8486)) ([fe71364](https://github.com/aws/aws-cdk/commit/fe71364b6cd8274e937cc2dc9185249dcbbb9388)), closes [#5933](https://github.com/aws/aws-cdk/issues/5933) +* **stepfunctions-tasks:** task constructs to call DynamoDB APIs ([#8466](https://github.com/aws/aws-cdk/issues/8466)) ([a7cb3b7](https://github.com/aws/aws-cdk/commit/a7cb3b7633c433ecb0619c030914bfa497ee39bc)), closes [#8108](https://github.com/aws/aws-cdk/issues/8108) + + +### Bug Fixes + +* **appsync:** Not to throw an Error even if 'additionalAuthorizationModes' is undefined ([#8673](https://github.com/aws/aws-cdk/issues/8673)) ([6b5d77b](https://github.com/aws/aws-cdk/commit/6b5d77b452bccb35564d6acee118112156149eb0)), closes [#8666](https://github.com/aws/aws-cdk/issues/8666) [#8668](https://github.com/aws/aws-cdk/issues/8668) +* **cli:** cannot change policies or trust after initial bootstrap ([#8677](https://github.com/aws/aws-cdk/issues/8677)) ([6e6b23e](https://github.com/aws/aws-cdk/commit/6e6b23e329d8a1b6455210768371a5ab9de478ef)), closes [#6581](https://github.com/aws/aws-cdk/issues/6581) +* **cli:** crash on tiny reported terminal width ([#8675](https://github.com/aws/aws-cdk/issues/8675)) ([a186c24](https://github.com/aws/aws-cdk/commit/a186c24918fddc697270b794b6603add5a47e947)), closes [#8667](https://github.com/aws/aws-cdk/issues/8667) +* **toolkit:** CLI tool fails on CloudFormation Throttling ([#8711](https://github.com/aws/aws-cdk/issues/8711)) ([e512a40](https://github.com/aws/aws-cdk/commit/e512a4057b21d32432d4dc7ac14ae7caa812265d)), closes [#5637](https://github.com/aws/aws-cdk/issues/5637) + ## [1.46.0](https://github.com/aws/aws-cdk/compare/v1.45.0...v1.46.0) (2020-06-19) diff --git a/lerna.json b/lerna.json index cc8251a51f0a9..f416a842add0e 100644 --- a/lerna.json +++ b/lerna.json @@ -10,5 +10,5 @@ "tools/*" ], "rejectCycles": "true", - "version": "1.46.0" + "version": "1.47.0" } diff --git a/package.json b/package.json index 65f78e85c6d42..6442b77109e93 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,8 @@ "@aws-cdk/aws-codepipeline-actions/case/**", "@aws-cdk/aws-ecr-assets/minimatch", "@aws-cdk/aws-ecr-assets/minimatch/**", + "@aws-cdk/aws-eks/yaml", + "@aws-cdk/aws-eks/yaml/**", "@aws-cdk/aws-lambda-nodejs/parcel-bundler", "@aws-cdk/aws-lambda-nodejs/parcel-bundler/**", "@aws-cdk/cloud-assembly-schema/jsonschema", diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 815abfab9ddc1..8eefc0da1c185 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -21,7 +21,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/aws-backup/README.md b/packages/@aws-cdk/aws-backup/README.md index 531a324855f26..57e47aee1081a 100644 --- a/packages/@aws-cdk/aws-backup/README.md +++ b/packages/@aws-cdk/aws-backup/README.md @@ -6,6 +6,10 @@ > All classes with the `Cfn` prefix in this module ([CFN Resources](https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib)) are always stable and safe to use. +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + --- diff --git a/packages/@aws-cdk/aws-backup/lib/vault.ts b/packages/@aws-cdk/aws-backup/lib/vault.ts index 849f7dfa5cfa5..6f109306ce1c3 100644 --- a/packages/@aws-cdk/aws-backup/lib/vault.ts +++ b/packages/@aws-cdk/aws-backup/lib/vault.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as sns from '@aws-cdk/aws-sns'; -import { Aws, Construct, IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; +import { Construct, IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; import { CfnBackupVault } from './backup.generated'; /** @@ -21,7 +21,9 @@ export interface IBackupVault extends IResource { */ export interface BackupVaultProps { /** - * The name of a logical container where backups are stored. + * The name of a logical container where backups are stored. Backup vaults + * are identified by names that are unique to the account used to create + * them and the AWS Region where they are created. * * @default - A CDK generated name */ @@ -158,7 +160,7 @@ export class BackupVault extends Resource implements IBackupVault { private uniqueVaultName() { // Max length of 50 chars, get the last 50 chars - const id = `${this.node.uniqueId}${Aws.STACK_NAME}`; + const id = this.node.uniqueId; return id.substring(Math.max(id.length - 50, 0), id.length); } } diff --git a/packages/@aws-cdk/aws-backup/package.json b/packages/@aws-cdk/aws-backup/package.json index ce47e6dc2b249..43461ce79359f 100644 --- a/packages/@aws-cdk/aws-backup/package.json +++ b/packages/@aws-cdk/aws-backup/package.json @@ -99,7 +99,7 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", "awscdkio": { "announce": false } diff --git a/packages/@aws-cdk/aws-backup/test/integ.backup.expected.json b/packages/@aws-cdk/aws-backup/test/integ.backup.expected.json index bad27ac4ec97f..2ba1356211377 100644 --- a/packages/@aws-cdk/aws-backup/test/integ.backup.expected.json +++ b/packages/@aws-cdk/aws-backup/test/integ.backup.expected.json @@ -29,17 +29,7 @@ "Vault23237E5B": { "Type": "AWS::Backup::BackupVault", "Properties": { - "BackupVaultName": { - "Fn::Join": [ - "", - [ - "cdkbackupVaultC2A6D3CB", - { - "Ref": "AWS::StackName" - } - ] - ] - } + "BackupVaultName": "cdkbackupVaultC2A6D3CB" }, "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" diff --git a/packages/@aws-cdk/aws-backup/test/vault.test.ts b/packages/@aws-cdk/aws-backup/test/vault.test.ts index 31e202a16b163..b8291be0e74ff 100644 --- a/packages/@aws-cdk/aws-backup/test/vault.test.ts +++ b/packages/@aws-cdk/aws-backup/test/vault.test.ts @@ -16,17 +16,7 @@ test('create a vault', () => { // THEN expect(stack).toHaveResource('AWS::Backup::BackupVault', { - BackupVaultName: { - 'Fn::Join': [ - '', - [ - 'Vault', - { - Ref: 'AWS::StackName', - }, - ], - ], - }, + BackupVaultName: 'Vault', }); }); diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 50148bd7eb8af..9daff3e256225 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index c0f699140ba69..158b50fc11d40 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -64,7 +64,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index f72cf8072fa80..a3ebadc8e8cee 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -70,7 +70,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 39f382e390a3f..1fb64aef65eaf 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -70,7 +70,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 557f8079b9b34..b34c52fc4c7ec 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.156", + "@types/lodash": "^4.14.157", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts index 3566acf7c7aee..b910cc24a41f0 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-domain.ts @@ -83,6 +83,8 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { public readonly domainName: string; private isCognitoDomain: boolean; + private cloudFrontCustomResource?: AwsCustomResource; + constructor(scope: Construct, id: string, props: UserPoolDomainProps) { super(scope, id); @@ -113,25 +115,27 @@ export class UserPoolDomain extends Resource implements IUserPoolDomain { * The domain name of the CloudFront distribution associated with the user pool domain. */ public get cloudFrontDomainName(): string { - const sdkCall: AwsSdkCall = { - service: 'CognitoIdentityServiceProvider', - action: 'describeUserPoolDomain', - parameters: { - Domain: this.domainName, - }, - physicalResourceId: PhysicalResourceId.of(this.domainName), - }; - const customResource = new AwsCustomResource(this, 'CloudFrontDomainName', { - resourceType: 'Custom::UserPoolCloudFrontDomainName', - onCreate: sdkCall, - onUpdate: sdkCall, - policy: AwsCustomResourcePolicy.fromSdkCalls({ - // DescribeUserPoolDomain only supports access level '*' - // https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazoncognitouserpools.html#amazoncognitouserpools-actions-as-permissions - resources: [ '*' ], - }), - }); - return customResource.getResponseField('DomainDescription.CloudFrontDistribution'); + if (!this.cloudFrontCustomResource) { + const sdkCall: AwsSdkCall = { + service: 'CognitoIdentityServiceProvider', + action: 'describeUserPoolDomain', + parameters: { + Domain: this.domainName, + }, + physicalResourceId: PhysicalResourceId.of(this.domainName), + }; + this.cloudFrontCustomResource = new AwsCustomResource(this, 'CloudFrontDomainName', { + resourceType: 'Custom::UserPoolCloudFrontDomainName', + onCreate: sdkCall, + onUpdate: sdkCall, + policy: AwsCustomResourcePolicy.fromSdkCalls({ + // DescribeUserPoolDomain only supports access level '*' + // https://docs.aws.amazon.com/IAM/latest/UserGuide/list_amazoncognitouserpools.html#amazoncognitouserpools-actions-as-permissions + resources: [ '*' ], + }), + }); + } + return this.cloudFrontCustomResource.getResponseField('DomainDescription.CloudFrontDistribution'); } /** diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts index 41407985c8ed1..cb281e11f369a 100644 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-domain.test.ts @@ -103,7 +103,7 @@ describe('User Pool Client', () => { })).not.toThrow(); }); - test('custom resource is added when cloudFrontDistribution method is called', () => { + test('custom resource is added when cloudFrontDomainName property is used', () => { // GIVEN const stack = new Stack(); const pool = new UserPool(stack, 'Pool'); @@ -137,6 +137,21 @@ describe('User Pool Client', () => { }); }); + test('cloudFrontDomainName property can be called multiple times', () => { + const stack = new Stack(); + const pool = new UserPool(stack, 'Pool'); + const domain = pool.addDomain('Domain', { + cognitoDomain: { + domainPrefix: 'cognito-domain-prefix', + }, + }); + + const cfDomainNameFirst = domain.cloudFrontDomainName; + const cfDomainNameSecond = domain.cloudFrontDomainName; + + expect(cfDomainNameSecond).toEqual(cfDomainNameFirst); + }); + describe('signInUrl', () => { test('returns the expected URL', () => { // GIVEN diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index 8be1059887c3a..833c699d5d80e 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -64,8 +64,8 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.0", - "aws-sdk": "^2.703.0", + "@types/jest": "^26.0.3", + "aws-sdk": "^2.706.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-ec2/README.md b/packages/@aws-cdk/aws-ec2/README.md index 318ffb55a1274..d3d9e070684f8 100644 --- a/packages/@aws-cdk/aws-ec2/README.md +++ b/packages/@aws-cdk/aws-ec2/README.md @@ -584,7 +584,6 @@ EBS volume for the bastion host can be encrypted like: }); ``` - ## Block Devices To add EBS block device mappings, specify the `blockDeviceMappings` property. The follow example sets the EBS-backed @@ -608,6 +607,74 @@ new ec2.Instance(this, 'Instance', { ``` +## Volumes + +Whereas a `BlockDeviceVolume` is an EBS volume that is created and destroyed as part of the creation and destruction of a specific instance. A `Volume` is for when you want an EBS volume separate from any particular instance. A `Volume` is an EBS block device that can be attached to, or detached from, any instance at any time. Some types of `Volume`s can also be attached to multiple instances at the same time to allow you to have shared storage between those instances. + +A notable restriction is that a Volume can only be attached to instances in the same availability zone as the Volume itself. + +The following demonstrates how to create a 500 GiB encrypted Volume in the `us-west-2a` availability zone, and give a role the ability to attach that Volume to a specific instance: + +```ts +const instance = new ec2.Instance(this, 'Instance', { + // ... +}); +const role = new iam.Role(stack, 'SomeRole', { + assumedBy: new iam.AccountRootPrincipal(), +}); +const volume = new ec2.Volume(this, 'Volume', { + availabilityZone: 'us-west-2a', + size: cdk.Size.gibibytes(500), + encrypted: true, +}); + +volume.grantAttachVolume(role, [instance]); +``` + +### Instances Attaching Volumes to Themselves + +If you need to grant an instance the ability to attach/detach an EBS volume to/from itself, then using `grantAttachVolume` and `grantDetachVolume` as outlined above +will lead to an unresolvable circular reference between the instance role and the instance. In this case, use `grantAttachVolumeByResourceTag` and `grantDetachVolumeByResourceTag` as follows: + +```ts +const instance = new ec2.Instance(this, 'Instance', { + // ... +}); +const volume = new ec2.Volume(this, 'Volume', { + // ... +}); + +const attachGrant = volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance]); +const detachGrant = volume.grantDetachVolumeByResourceTag(instance.grantPrincipal, [instance]); +``` + +### Attaching Volumes + +The Amazon EC2 documentation for +[Linux Instances](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/AmazonEBS.html) and +[Windows Instances](https://docs.aws.amazon.com/AWSEC2/latest/WindowsGuide/ebs-volumes.html) contains information on how +to attach and detach your Volumes to/from instances, and how to format them for use. + +The following is a sample skeleton of EC2 UserData that can be used to attach a Volume to the Linux instance that it is running on: + +```ts +const volume = new ec2.Volume(this, 'Volume', { + // ... +}); +const instance = new ec2.Instance(this, 'Instance', { + // ... +}); +volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance]); +const targetDevice = '/dev/xvdz'; +instance.userData.addCommands( + // Attach the volume to /dev/xvdz + `aws --region ${Stack.of(this).region} ec2 attach-volume --volume-id ${volume.volumeId} --instance-id ${instance.instanceId} --device ${targetDevice}`, + // Wait until the volume has attached + `while ! test -e ${targetDevice}; do sleep 1; done` + // The volume will now be mounted. You may have to add additional code to format the volume if it has not been prepared. +); +``` + ## VPC Flow Logs VPC Flow Logs is a feature that enables you to capture information about the IP traffic going to and from network interfaces in your VPC. Flow log data can be published to Amazon CloudWatch Logs and Amazon S3. After you've created a flow log, you can retrieve and view its data in the chosen destination. (https://docs.aws.amazon.com/vpc/latest/userguide/flow-logs.html). diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index 8f27607a85f4a..28b8c9ee46554 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -383,23 +383,96 @@ export enum InstanceClass { * What size of instance to use */ export enum InstanceSize { + /** + * Instance size NANO (nano) + */ NANO = 'nano', + + /** + * Instance size MICRO (micro) + */ MICRO = 'micro', + + /** + * Instance size SMALL (small) + */ SMALL = 'small', + + /** + * Instance size MEDIUM (medium) + */ MEDIUM = 'medium', + + /** + * Instance size LARGE (large) + */ LARGE = 'large', + + /** + * Instance size XLARGE (xlarge) + */ XLARGE = 'xlarge', + + /** + * Instance size XLARGE2 (2xlarge) + */ XLARGE2 = '2xlarge', + + /** + * Instance size XLARGE4 (4xlarge) + */ XLARGE4 = '4xlarge', + + /** + * Instance size XLARGE6 (6xlarge) + */ + XLARGE6 = '6xlarge', + + /** + * Instance size XLARGE8 (8xlarge) + */ XLARGE8 = '8xlarge', + + /** + * Instance size XLARGE9 (9xlarge) + */ XLARGE9 = '9xlarge', + + /** + * Instance size XLARGE10 (10xlarge) + */ XLARGE10 = '10xlarge', + + /** + * Instance size XLARGE12 (12xlarge) + */ XLARGE12 = '12xlarge', + + /** + * Instance size XLARGE16 (16xlarge) + */ XLARGE16 = '16xlarge', + + /** + * Instance size XLARGE18 (18xlarge) + */ XLARGE18 = '18xlarge', + + /** + * Instance size XLARGE24 (24xlarge) + */ XLARGE24 = '24xlarge', + + /** + * Instance size XLARGE32 (32xlarge) + */ XLARGE32 = '32xlarge', + + /** + * Instance size METAL (metal) + */ METAL = 'metal', + } /** diff --git a/packages/@aws-cdk/aws-ec2/lib/volume.ts b/packages/@aws-cdk/aws-ec2/lib/volume.ts index 5108ea2effff1..6a6445dc87379 100644 --- a/packages/@aws-cdk/aws-ec2/lib/volume.ts +++ b/packages/@aws-cdk/aws-ec2/lib/volume.ts @@ -1,5 +1,10 @@ -import { Construct } from '@aws-cdk/core'; -import { CfnInstance } from './ec2.generated'; +import * as crypto from 'crypto'; + +import { AccountRootPrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; +import { IKey, ViaServicePrincipal } from '@aws-cdk/aws-kms'; +import { Construct, IResource, Resource, Size, SizeRoundingBehavior, Stack, Tag, Token } from '@aws-cdk/core'; +import { CfnInstance, CfnVolume } from './ec2.generated'; +import { IInstance } from './instance'; /** * Block device @@ -210,4 +215,482 @@ export enum EbsDeviceVolumeType { * Cold HDD */ SC1 = 'sc1', -} \ No newline at end of file + + /** + * General purpose SSD volume that balances price and performance for a wide variety of workloads. + */ + GENERAL_PURPOSE_SSD = GP2, + + /** + * Highest-performance SSD volume for mission-critical low-latency or high-throughput workloads. + */ + PROVISIONED_IOPS_SSD = IO1, + + /** + * Low-cost HDD volume designed for frequently accessed, throughput-intensive workloads. + */ + THROUGHPUT_OPTIMIZED_HDD = ST1, + + /** + * Lowest cost HDD volume designed for less frequently accessed workloads. + */ + COLD_HDD = SC1, + + /** + * Magnetic volumes are backed by magnetic drives and are suited for workloads where data is accessed infrequently, and scenarios where low-cost + * storage for small volume sizes is important. + */ + MAGNETIC = STANDARD, +} + +/** + * An EBS Volume in AWS EC2. + */ +export interface IVolume extends IResource { + /** + * The EBS Volume's ID + * + * @attribute + */ + readonly volumeId: string; + + /** + * The availability zone that the EBS Volume is contained within (ex: us-west-2a) + */ + readonly availabilityZone: string; + + /** + * The customer-managed encryption key that is used to encrypt the Volume. + * + * @attribute + */ + readonly encryptionKey?: IKey; + + /** + * Grants permission to attach this Volume to an instance. + * CAUTION: Granting an instance permission to attach to itself using this method will lead to + * an unresolvable circular reference between the instance role and the instance. + * Use {@link IVolume.grantAttachVolumeToSelf} to grant an instance permission to attach this + * volume to itself. + * + * @param grantee the principal being granted permission. + * @param instances the instances to which permission is being granted to attach this + * volume to. If not specified, then permission is granted to attach + * to all instances in this account. + */ + grantAttachVolume(grantee: IGrantable, instances?: IInstance[]): Grant; + + /** + * Grants permission to attach the Volume by a ResourceTag condition. If you are looking to + * grant an Instance, AutoScalingGroup, EC2-Fleet, SpotFleet, ECS host, etc the ability to attach + * this volume to **itself** then this is the method you want to use. + * + * This is implemented by adding a Tag with key `VolumeGrantAttach-` to the given + * constructs and this Volume, and then conditioning the Grant such that the grantee is only + * given the ability to AttachVolume if both the Volume and the destination Instance have that + * tag applied to them. + * + * If you need to call this method multiple times on different sets of constructs, then provide a + * unique `tagKeySuffix` for each call; failure to do so will result in an inability to attach this + * volume to some of the grants because it will overwrite the tag. + * + * @param grantee the principal being granted permission. + * @param constructs The list of constructs that will have the generated resource tag applied to them. + * @param tagKeySuffix A suffix to use on the generated Tag key in place of the generated hash value. + * Defaults to a hash calculated from this volume. + */ + grantAttachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant; + + /** + * Grants permission to detach this Volume from an instance + * CAUTION: Granting an instance permission to detach from itself using this method will lead to + * an unresolvable circular reference between the instance role and the instance. + * Use {@link IVolume.grantDetachVolumeFromSelf} to grant an instance permission to detach this + * volume from itself. + * + * @param grantee the principal being granted permission. + * @param instances the instances to which permission is being granted to detach this + * volume from. If not specified, then permission is granted to detach + * from all instances in this account. + */ + grantDetachVolume(grantee: IGrantable, instances?: IInstance[]): Grant; + + /** + * Grants permission to detach the Volume by a ResourceTag condition. + * + * This is implemented via the same mechanism as {@link IVolume.grantAttachVolumeByResourceTag}, + * and is subject to the same conditions. + * + * @param grantee the principal being granted permission. + * @param constructs The list of constructs that will have the generated resource tag applied to them. + * @param tagKeySuffix A suffix to use on the generated Tag key in place of the generated hash value. + * Defaults to a hash calculated from this volume. + */ + grantDetachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant; +} + +/** + * Properties of an EBS Volume + */ +export interface VolumeProps { + /** + * The value of the physicalName property of this resource. + * + * @default The physical name will be allocated by CloudFormation at deployment time + */ + readonly volumeName?: string; + + /** + * The Availability Zone in which to create the volume. + */ + readonly availabilityZone: string; + + /** + * The size of the volume, in GiBs. You must specify either a snapshot ID or a volume size. + * See {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html#ebs-volume-characteristics|Volume Characteristics} + * for details on the allowable size for each type of volume. + * + * @default If you're creating the volume from a snapshot and don't specify a volume size, the default is the snapshot size. + */ + readonly size?: Size; + + /** + * The snapshot from which to create the volume. You must specify either a snapshot ID or a volume size. + * + * @default The EBS volume is not created from a snapshot. + */ + readonly snapshotId?: string; + + /** + * Indicates whether Amazon EBS Multi-Attach is enabled. + * See {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volumes-multi.html#considerations|Considerations and limitations} + * for the constraints of multi-attach. + * + * @default false + */ + readonly enableMultiAttach?: boolean; + + /** + * Specifies whether the volume should be encrypted. The effect of setting the encryption state to true depends on the volume origin + * (new or from a snapshot), starting encryption state, ownership, and whether encryption by default is enabled. For more information, + * see {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#encryption-by-default|Encryption by Default} + * in the Amazon Elastic Compute Cloud User Guide. + * + * Encrypted Amazon EBS volumes must be attached to instances that support Amazon EBS encryption. For more information, see + * {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#EBSEncryption_supported_instances|Supported Instance Types.} + * + * @default false + */ + readonly encrypted?: boolean; + + /** + * The customer-managed encryption key that is used to encrypt the Volume. The encrypted property must + * be true if this is provided. + * + * Note: If using an {@link aws-kms.IKey} created from a {@link aws-kms.Key.fromKeyArn()} here, + * then the KMS key **must** have the following in its Key policy; otherwise, the Volume + * will fail to create. + * + * { + * "Effect": "Allow", + * "Principal": { "AWS": " ex: arn:aws:iam::00000000000:root" }, + * "Resource": "*", + * "Action": [ + * "kms:DescribeKey", + * "kms:GenerateDataKeyWithoutPlainText", + * ], + * "Condition": { + * "StringEquals": { + * "kms:ViaService": "ec2..amazonaws.com", (eg: ec2.us-east-1.amazonaws.com) + * "kms:CallerAccount": "0000000000" (your account ID) + * } + * } + * } + * + * @default The default KMS key for the account, region, and EC2 service is used. + */ + readonly encryptionKey?: IKey; + + /** + * Indicates whether the volume is auto-enabled for I/O operations. By default, Amazon EBS disables I/O to the volume from attached EC2 + * instances when it determines that a volume's data is potentially inconsistent. If the consistency of the volume is not a concern, and + * you prefer that the volume be made available immediately if it's impaired, you can configure the volume to automatically enable I/O. + * + * @default false + */ + readonly autoEnableIo?: boolean; + + /** + * The type of the volume; what type of storage to use to form the EBS Volume. + * + * @default {@link EbsDeviceVolumeType.GENERAL_PURPOSE_SSD} + */ + readonly volumeType?: EbsDeviceVolumeType; + + /** + * The number of I/O operations per second (IOPS) to provision for the volume, with a maximum ratio of 50 IOPS/GiB. + * See {@link https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html#EBSVolumeTypes_piops|Provisioned IOPS SSD (io1) volumes} + * for more information. + * + * This parameter is valid only for PROVISIONED_IOPS_SSD volumes. + * + * @default None -- Required for {@link EbsDeviceVolumeType.PROVISIONED_IOPS_SSD} + */ + readonly iops?: number; +} + +/** + * Attributes required to import an existing EBS Volume into the Stack. + */ +export interface VolumeAttributes { + /** + * The EBS Volume's ID + */ + readonly volumeId: string; + + /** + * The availability zone that the EBS Volume is contained within (ex: us-west-2a) + */ + readonly availabilityZone: string; + + /** + * The customer-managed encryption key that is used to encrypt the Volume. + * + * @default None -- The EBS Volume is not using a customer-managed KMS key for encryption. + */ + readonly encryptionKey?: IKey; +} + +/** + * Common behavior of Volumes. Users should not use this class directly, and instead use ``Volume``. + */ +abstract class VolumeBase extends Resource implements IVolume { + public abstract readonly volumeId: string; + public abstract readonly availabilityZone: string; + public abstract readonly encryptionKey?: IKey; + + public grantAttachVolume(grantee: IGrantable, instances?: IInstance[]): Grant { + const result = Grant.addToPrincipal({ + grantee, + actions: [ 'ec2:AttachVolume' ], + resourceArns : this.collectGrantResourceArns(instances), + }); + + if (this.encryptionKey) { + // When attaching a volume, the EC2 Service will need to grant to itself permission + // to be able to decrypt the encryption key. We restrict the CreateGrant for principle + // of least privilege, in accordance with best practices. + // See: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#ebs-encryption-permissions + const kmsGrant: Grant = this.encryptionKey.grant(grantee, 'kms:CreateGrant'); + kmsGrant.principalStatement!.addConditions( + { + Bool: { 'kms:GrantIsForAWSResource': true }, + StringEquals: { + 'kms:ViaService': `ec2.${Stack.of(this).region}.amazonaws.com`, + 'kms:GrantConstraintType': 'EncryptionContextSubset', + }, + }, + ); + } + + return result; + } + + public grantAttachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant { + const tagKey = `VolumeGrantAttach-${tagKeySuffix ?? this.stringHash(this.node.uniqueId)}`; + const tagValue = this.calculateResourceTagValue(constructs); + const grantCondition: { [key: string]: string } = {}; + grantCondition[`ec2:ResourceTag/${tagKey}`] = tagValue; + + const result = this.grantAttachVolume(grantee); + result.principalStatement!.addCondition( + 'ForAnyValue:StringEquals', grantCondition, + ); + + // The ResourceTag condition requires that all resources involved in the operation have + // the given tag, so we tag this and all constructs given. + Tag.add(this, tagKey, tagValue); + constructs.forEach(construct => Tag.add(construct, tagKey, tagValue)); + + return result; + } + + public grantDetachVolume(grantee: IGrantable, instances?: IInstance[]): Grant { + const result = Grant.addToPrincipal({ + grantee, + actions: [ 'ec2:DetachVolume' ], + resourceArns : this.collectGrantResourceArns(instances), + }); + // Note: No encryption key permissions are required to detach an encrypted volume. + return result; + } + + public grantDetachVolumeByResourceTag(grantee: IGrantable, constructs: Construct[], tagKeySuffix?: string): Grant { + const tagKey = `VolumeGrantDetach-${tagKeySuffix ?? this.stringHash(this.node.uniqueId)}`; + const tagValue = this.calculateResourceTagValue(constructs); + const grantCondition: { [key: string]: string } = {}; + grantCondition[`ec2:ResourceTag/${tagKey}`] = tagValue; + + const result = this.grantDetachVolume(grantee); + result.principalStatement!.addCondition( + 'ForAnyValue:StringEquals', grantCondition, + ); + + // The ResourceTag condition requires that all resources involved in the operation have + // the given tag, so we tag this and all constructs given. + Tag.add(this, tagKey, tagValue); + constructs.forEach(construct => Tag.add(construct, tagKey, tagValue)); + + return result; + } + + private collectGrantResourceArns(instances?: IInstance[]): string[] { + const stack = Stack.of(this); + const resourceArns: string[] = [ + `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:volume/${this.volumeId}`, + ]; + const instanceArnPrefix = `arn:${stack.partition}:ec2:${stack.region}:${stack.account}:instance`; + if (instances) { + instances.forEach(instance => resourceArns.push(`${instanceArnPrefix}/${instance?.instanceId}`)); + } else { + resourceArns.push(`${instanceArnPrefix}/*`); + } + return resourceArns; + } + + private stringHash(value: string): string { + const md5 = crypto.createHash('md5').update(value).digest('hex'); + return md5.slice(0, 8).toUpperCase(); + } + + private calculateResourceTagValue(constructs: Construct[]): string { + const md5 = crypto.createHash('md5'); + constructs.forEach(construct => md5.update(construct.node.uniqueId)); + return md5.digest('hex'); + } +} + +/** + * Creates a new EBS Volume in AWS EC2. + */ +export class Volume extends VolumeBase { + /** + * Import an existing EBS Volume into the Stack. + * + * @param scope the scope of the import. + * @param id the ID of the imported Volume in the construct tree. + * @param attrs the attributes of the imported Volume + */ + public static fromVolumeAttributes(scope: Construct, id: string, attrs: VolumeAttributes): IVolume { + class Import extends VolumeBase { + public readonly volumeId = attrs.volumeId; + public readonly availabilityZone = attrs.availabilityZone; + public readonly encryptionKey = attrs.encryptionKey; + } + // Check that the provided volumeId looks like it could be valid. + if (!Token.isUnresolved(attrs.volumeId) && !/^vol-[0-9a-fA-F]+$/.test(attrs.volumeId)) { + throw new Error('`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); + } + return new Import(scope, id); + } + + public readonly volumeId: string; + public readonly availabilityZone: string; + public readonly encryptionKey?: IKey; + + constructor(scope: Construct, id: string, props: VolumeProps) { + super(scope, id, { + physicalName: props.volumeName, + }); + + this.validateProps(props); + + const resource = new CfnVolume(this, 'Resource', { + availabilityZone: props.availabilityZone, + autoEnableIo: props.autoEnableIo, + encrypted: props.encrypted, + kmsKeyId: props.encryptionKey?.keyArn, + iops: props.iops, + multiAttachEnabled: props.enableMultiAttach ?? false, + size: props.size?.toGibibytes({rounding: SizeRoundingBehavior.FAIL}), + snapshotId: props.snapshotId, + volumeType: props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }); + + this.volumeId = resource.ref; + this.availabilityZone = props.availabilityZone; + this.encryptionKey = props.encryptionKey; + + if (this.encryptionKey) { + // Per: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/EBSEncryption.html#ebs-encryption-requirements + const principal = + new ViaServicePrincipal(`ec2.${Stack.of(this).region}.amazonaws.com`, new AccountRootPrincipal()).withConditions({ + StringEquals: { + 'kms:CallerAccount': Stack.of(this).account, + }, + }); + const grant = this.encryptionKey.grant(principal, + // Describe & Generate are required to be able to create the CMK-encrypted Volume. + 'kms:DescribeKey', + 'kms:GenerateDataKeyWithoutPlainText', + ); + if (props.snapshotId) { + // ReEncrypt is required for when re-encrypting from an encrypted snapshot. + grant.principalStatement?.addActions('kms:ReEncrypt*'); + } + } + } + + protected validateProps(props: VolumeProps) { + if (!Token.isUnresolved(props.availabilityZone) && !/^[a-z]{2}-[a-z]+-[1-9]+[a-z]$/.test(props.availabilityZone)) { + throw new Error('`availabilityZone` is a region followed by a letter (ex: `us-east-1a`), or a token'); + } + + if (!(props.size || props.snapshotId)) { + throw new Error('Must provide at least one of `size` or `snapshotId`'); + } + + if (props.snapshotId && !Token.isUnresolved(props.snapshotId) && !/^snap-[0-9a-fA-F]+$/.test(props.snapshotId)) { + throw new Error('`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); + } + + if (props.encryptionKey && !props.encrypted) { + throw new Error('`encrypted` must be true when providing an `encryptionKey`.'); + } + + if (props.iops) { + if (props.volumeType !== EbsDeviceVolumeType.PROVISIONED_IOPS_SSD) { + throw new Error('`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`/`IO1`'); + } + + if (props.iops < 100 || props.iops > 64000) { + throw new Error('`iops` must be in the range 100 to 64,000, inclusive.'); + } + + if (props.size && (props.iops > 50 * props.size.toGibibytes({rounding: SizeRoundingBehavior.FAIL}))) { + throw new Error('`iops` has a maximum ratio of 50 IOPS/GiB.'); + } + } + + if (props.enableMultiAttach && props.volumeType !== EbsDeviceVolumeType.PROVISIONED_IOPS_SSD) { + throw new Error('multi-attach is supported exclusively on `PROVISIONED_IOPS_SSD` volumes.'); + } + + if (props.size) { + const size = props.size.toGibibytes({rounding: SizeRoundingBehavior.FAIL}); + // Enforce maximum volume size: + // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ebs-volume-types.html#ebs-volume-characteristics + const sizeRanges: { [key: string]: { Min: number, Max: number } } = {}; + sizeRanges[EbsDeviceVolumeType.GENERAL_PURPOSE_SSD] = { Min: 1, Max: 16000 }; + sizeRanges[EbsDeviceVolumeType.PROVISIONED_IOPS_SSD] = { Min: 4, Max: 16000 }; + sizeRanges[EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD] = { Min: 500, Max: 16000 }; + sizeRanges[EbsDeviceVolumeType.COLD_HDD] = { Min: 500, Max: 16000 }; + sizeRanges[EbsDeviceVolumeType.MAGNETIC] = { Min: 1, Max: 1000 }; + const volumeType = props.volumeType ?? EbsDeviceVolumeType.GENERAL_PURPOSE_SSD; + const { Min, Max } = sizeRanges[volumeType]; + if (size < Min || size > Max) { + throw new Error(`\`${volumeType}\` volumes must be between ${Min} GiB and ${Max} GiB in size.`); + } + } + } +} diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index 458f2d1b4bc2e..eadb31460c21d 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -74,6 +74,7 @@ "dependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", @@ -87,6 +88,7 @@ "peerDependencies": { "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-kms": "0.0.0", "@aws-cdk/aws-logs": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ssm": "0.0.0", @@ -289,23 +291,6 @@ "docs-public-apis:@aws-cdk/aws-ec2.VpnConnectionProps", "docs-public-apis:@aws-cdk/aws-ec2.VpnTunnelOption", "docs-public-apis:@aws-cdk/aws-ec2.AmazonLinuxStorage", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.NANO", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.MICRO", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.SMALL", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.MEDIUM", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.LARGE", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE2", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE4", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE8", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE9", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE10", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE12", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE16", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE18", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE24", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.XLARGE32", - "docs-public-apis:@aws-cdk/aws-ec2.InstanceSize.METAL", "docs-public-apis:@aws-cdk/aws-ec2.OperatingSystemType.LINUX", "docs-public-apis:@aws-cdk/aws-ec2.OperatingSystemType.WINDOWS", "docs-public-apis:@aws-cdk/aws-ec2.Protocol.ALL", diff --git a/packages/@aws-cdk/aws-ec2/test/test.volume.ts b/packages/@aws-cdk/aws-ec2/test/test.volume.ts new file mode 100644 index 0000000000000..dbef8b6ecfebe --- /dev/null +++ b/packages/@aws-cdk/aws-ec2/test/test.volume.ts @@ -0,0 +1,1487 @@ +import { + expect as cdkExpect, + haveResource, + haveResourceLike, + ResourcePart, +} from '@aws-cdk/assert'; +import { + AccountRootPrincipal, + Role, +} from '@aws-cdk/aws-iam'; +import * as kms from '@aws-cdk/aws-kms'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import { + AmazonLinuxGeneration, + EbsDeviceVolumeType, + Instance, + InstanceType, + MachineImage, + Volume, + Vpc, +} from '../lib'; + +export = { + 'basic volume'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::Volume', { + AvailabilityZone: 'us-east-1a', + MultiAttachEnabled: false, + Size: 8, + VolumeType: 'gp2', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'fromVolumeAttributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const encryptionKey = new kms.Key(stack, 'Key'); + const volumeId = 'vol-000000'; + const availabilityZone = 'us-east-1a'; + + // WHEN + const volume = Volume.fromVolumeAttributes(stack, 'Volume', { + volumeId, + availabilityZone, + encryptionKey, + }); + + // THEN + test.strictEqual(volume.volumeId, volumeId); + test.strictEqual(volume.availabilityZone, availabilityZone); + test.strictEqual(volume.encryptionKey, encryptionKey); + test.done(); + }, + + 'tagged volume'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + cdk.Tag.add(volume, 'TagKey', 'TagValue'); + + // THEN + cdkExpect(stack).to(haveResource('AWS::EC2::Volume', { + AvailabilityZone: 'us-east-1a', + MultiAttachEnabled: false, + Size: 8, + VolumeType: 'gp2', + Tags: [{ + Key: 'TagKey', + Value: 'TagValue', + }], + }, ResourcePart.Properties)); + + test.done(); + }, + + 'autoenableIO'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + autoEnableIo: true, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + AutoEnableIO: true, + }, ResourcePart.Properties)); + + test.done(); + }, + + 'encryption'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + encrypted: true, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Encrypted: true, + }, ResourcePart.Properties)); + + test.done(); + }, + + 'encryption with kms'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const encryptionKey = new kms.Key(stack, 'Key'); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + encrypted: true, + encryptionKey, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Encrypted: true, + KmsKeyId: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + }, ResourcePart.Properties)); + cdkExpect(stack).to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + {}, + { + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':iam::', + { + Ref: 'AWS::AccountId', + }, + ':root', + ], + ], + }, + }, + Resource: '*', + Action: [ + 'kms:DescribeKey', + 'kms:GenerateDataKeyWithoutPlainText', + ], + Condition: { + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + 'kms:CallerAccount': { + Ref: 'AWS::AccountId', + }, + }, + }, + }, + ], + }, + })); + + test.done(); + }, + + 'encryption with kms from snapshot'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const encryptionKey = new kms.Key(stack, 'Key'); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + encrypted: true, + encryptionKey, + snapshotId: 'snap-1234567890', + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + {}, + { + Action: [ + 'kms:DescribeKey', + 'kms:GenerateDataKeyWithoutPlainText', + 'kms:ReEncrypt*', + ], + }, + ], + }, + })); + + test.done(); + }, + + 'iops'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + iops: 500, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Iops: 500, + VolumeType: 'io1', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'multi-attach'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + iops: 500, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + enableMultiAttach: true, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + MultiAttachEnabled: true, + }, ResourcePart.Properties)); + + test.done(); + }, + + 'snapshotId'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + snapshotId: 'snap-00000000', + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + SnapshotId: 'snap-00000000', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'volume: standard'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.MAGNETIC, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'standard', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'volume: io1'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'io1', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'volume: gp2'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'gp2', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'volume: st1'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'st1', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'volume: sc1'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + volumeType: EbsDeviceVolumeType.COLD_HDD, + }); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + VolumeType: 'sc1', + }, ResourcePart.Properties)); + + test.done(); + }, + + 'grantAttachVolume to any instance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantAttachVolume(role); + + // THEN + cdkExpect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:AttachVolume', + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':volume/', + { + Ref: 'VolumeA92988D3', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', + ], + ], + }, + ], + }], + }, + })); + test.done(); + }, + + 'grantAttachVolume to any instance with encryption'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const encryptionKey = new kms.Key(stack, 'Key'); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encrypted: true, + encryptionKey, + }); + + // WHEN + volume.grantAttachVolume(role); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::KMS::Key', { + KeyPolicy: { + Statement: [ + {}, + {}, + { + Effect: 'Allow', + Principal: { + AWS: { + 'Fn::GetAtt': [ + 'Role1ABCC5F0', + 'Arn', + ], + }, + }, + Action: 'kms:CreateGrant', + Condition: { + Bool: { + 'kms:GrantIsForAWSResource': true, + }, + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + 'kms:GrantConstraintType': 'EncryptionContextSubset', + }, + }, + Resource: '*', + }, + ], + }, + })); + + test.done(); + }, + + 'grantAttachVolume to any instance with KMS.fromKeyArn() encryption'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const kmsKey = new kms.Key(stack, 'Key'); + // kmsKey policy is not strictly necessary for the test. + // Demonstrating how to properly construct the Key. + const principal = + new kms.ViaServicePrincipal(`ec2.${stack.region}.amazonaws.com`, new AccountRootPrincipal()).withConditions({ + StringEquals: { + 'kms:CallerAccount': stack.account, + }, + }); + kmsKey.grant(principal, + // Describe & Generate are required to be able to create the CMK-encrypted Volume. + 'kms:DescribeKey', + 'kms:GenerateDataKeyWithoutPlainText', + // ReEncrypt is required for when the CMK is rotated. + 'kms:ReEncrypt*', + ); + + const encryptionKey = kms.Key.fromKeyArn(stack, 'KeyArn', kmsKey.keyArn); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encrypted: true, + encryptionKey, + }); + + // WHEN + volume.grantAttachVolume(role); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [ + {}, + { + Effect: 'Allow', + Action: 'kms:CreateGrant', + Resource: { + 'Fn::GetAtt': [ + 'Key961B73FD', + 'Arn', + ], + }, + Condition: { + Bool: { + 'kms:GrantIsForAWSResource': true, + }, + StringEquals: { + 'kms:ViaService': { + 'Fn::Join': [ + '', + [ + 'ec2.', + { + Ref: 'AWS::Region', + }, + '.amazonaws.com', + ], + ], + }, + 'kms:GrantConstraintType': 'EncryptionContextSubset', + }, + }, + }, + ], + }, + })); + + test.done(); + }, + + 'grantAttachVolume to specific instances'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const vpc = new Vpc(stack, 'Vpc'); + const instance1 = new Instance(stack, 'Instance1', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const instance2 = new Instance(stack, 'Instance2', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantAttachVolume(role, [instance1, instance2]); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:AttachVolume', + Effect: 'Allow', + Resource: [ + {}, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance14BC3991D', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance255F35265', + }, + ], + ], + }, + ], + }], + }, + })); + + test.done(); + }, + + 'grantAttachVolume to instance self'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new Vpc(stack, 'Vpc'); + const instance = new Instance(stack, 'Instance', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance]); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:AttachVolume', + Effect: 'Allow', + Resource: [ + {}, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', + ], + ], + }, + ], + Condition: { + 'ForAnyValue:StringEquals': { + 'ec2:ResourceTag/VolumeGrantAttach-BD7A9717': 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + }, + }], + }, + })); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Tags: [ + { + Key: 'VolumeGrantAttach-BD7A9717', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Instance', { + Tags: [ + {}, + { + Key: 'VolumeGrantAttach-BD7A9717', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + + test.done(); + }, + + 'grantAttachVolume to instance self with suffix'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new Vpc(stack, 'Vpc'); + const instance = new Instance(stack, 'Instance', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantAttachVolumeByResourceTag(instance.grantPrincipal, [instance], 'TestSuffix'); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:AttachVolume', + Effect: 'Allow', + Resource: [ + {}, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', + ], + ], + }, + ], + Condition: { + 'ForAnyValue:StringEquals': { + 'ec2:ResourceTag/VolumeGrantAttach-TestSuffix': 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + }, + }], + }, + })); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Tags: [ + { + Key: 'VolumeGrantAttach-TestSuffix', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Instance', { + Tags: [ + {}, + { + Key: 'VolumeGrantAttach-TestSuffix', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + + test.done(); + }, + + 'grantDetachVolume to any instance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantDetachVolume(role); + + // THEN + cdkExpect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:DetachVolume', + Effect: 'Allow', + Resource: [ + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':volume/', + { + Ref: 'VolumeA92988D3', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', + ], + ], + }, + ], + }], + }, + })); + test.done(); + }, + + 'grantDetachVolume from specific instance'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const role = new Role(stack, 'Role', { assumedBy: new AccountRootPrincipal() }); + const vpc = new Vpc(stack, 'Vpc'); + const instance1 = new Instance(stack, 'Instance1', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const instance2 = new Instance(stack, 'Instance2', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantDetachVolume(role, [instance1, instance2]); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:DetachVolume', + Effect: 'Allow', + Resource: [ + {}, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance14BC3991D', + }, + ], + ], + }, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/', + { + Ref: 'Instance255F35265', + }, + ], + ], + }, + ], + }], + }, + })); + + test.done(); + }, + + 'grantDetachVolume from instance self'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new Vpc(stack, 'Vpc'); + const instance = new Instance(stack, 'Instance', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantDetachVolumeByResourceTag(instance.grantPrincipal, [instance]); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:DetachVolume', + Effect: 'Allow', + Resource: [ + {}, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', + ], + ], + }, + ], + Condition: { + 'ForAnyValue:StringEquals': { + 'ec2:ResourceTag/VolumeGrantDetach-BD7A9717': 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + }, + }], + }, + })); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Tags: [ + { + Key: 'VolumeGrantDetach-BD7A9717', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Instance', { + Tags: [ + {}, + { + Key: 'VolumeGrantDetach-BD7A9717', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + + test.done(); + }, + + 'grantDetachVolume from instance self with suffix'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new Vpc(stack, 'Vpc'); + const instance = new Instance(stack, 'Instance', { + vpc, + instanceType: new InstanceType('t3.small'), + machineImage: MachineImage.latestAmazonLinux({ generation: AmazonLinuxGeneration.AMAZON_LINUX_2 }), + availabilityZone: 'us-east-1a', + }); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // WHEN + volume.grantDetachVolumeByResourceTag(instance.grantPrincipal, [instance], 'TestSuffix'); + + // THEN + cdkExpect(stack).to(haveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: 'ec2:DetachVolume', + Effect: 'Allow', + Resource: [ + {}, + { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':ec2:', + { + Ref: 'AWS::Region', + }, + ':', + { + Ref: 'AWS::AccountId', + }, + ':instance/*', + ], + ], + }, + ], + Condition: { + 'ForAnyValue:StringEquals': { + 'ec2:ResourceTag/VolumeGrantDetach-TestSuffix': 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + }, + }], + }, + })); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Volume', { + Tags: [ + { + Key: 'VolumeGrantDetach-TestSuffix', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + cdkExpect(stack).to(haveResourceLike('AWS::EC2::Instance', { + Tags: [ + {}, + { + Key: 'VolumeGrantDetach-TestSuffix', + Value: 'd9a17c1c9e8ef6866e4dbeef41c741b2', + }, + ], + }, ResourcePart.Properties)); + + test.done(); + }, + + 'validation fromVolumeAttributes'(test: Test) { + // GIVEN + let idx: number = 0; + const stack = new cdk.Stack(); + const volume = new Volume(stack, 'Volume', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + + // THEN + test.doesNotThrow(() => { + Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { + volumeId: volume.volumeId, + availabilityZone: volume.availabilityZone, + }); + }); + test.doesNotThrow(() => { + Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { + volumeId: 'vol-0123456789abcdefABCDEF', + availabilityZone: 'us-east-1a', + }); + }); + test.throws(() => { + Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { + volumeId: ' vol-0123456789abcdefABCDEF', // leading invalid character(s) + availabilityZone: 'us-east-1a', + }); + }, Error, '`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); + test.throws(() => { + Volume.fromVolumeAttributes(stack, `Volume${idx++}`, { + volumeId: 'vol-0123456789abcdefABCDEF ', // trailing invalid character(s) + availabilityZone: 'us-east-1a', + }); + }, Error, '`volumeId` does not match expected pattern. Expected `vol-` (ex: `vol-05abe246af`) or a Token'); + test.done(); + }, + + 'validation required props'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const key = new kms.Key(stack, 'Key'); + let idx: number = 0; + + // THEN + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + }); + }, Error, 'Must provide at least one of `size` or `snapshotId`'); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + }); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + snapshotId: 'snap-000000000', + }); + }); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + snapshotId: 'snap-000000000', + }); + }); + + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encryptionKey: key, + }); + }, Error, '`encrypted` must be true when providing an `encryptionKey`.'); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encrypted: false, + encryptionKey: key, + }); + }, Error, '`encrypted` must be true when providing an `encryptionKey`.'); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + encrypted: true, + encryptionKey: key, + }); + }); + + test.done(); + }, + + 'validation availabilityZone'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const volume = new Volume(stack, 'ForToken', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + let idx: number = 0; + + // THEN + test.doesNotThrow(() => { + // Should not throw if we provide a token for the AZ + new Volume(stack, `Volume${idx++}`, { + availabilityZone: volume.volumeId, + size: cdk.Size.gibibytes(8), + }); + }); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1', + }); + }, Error, '`availabilityZone` is a region followed by a letter (ex: `us-east-1a`), or a token'); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'Virginia', + }); + }, Error, '`availabilityZone` is a region followed by a letter (ex: `us-east-1a`), or a token'); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: ' us-east-1a', // leading character(s) + }); + }, Error, '`availabilityZone` is a region followed by a letter (ex: `us-east-1a`), or a token'); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a ', // trailing character(s) + }); + }, Error, '`availabilityZone` is a region followed by a letter (ex: `us-east-1a`), or a token'); + + test.done(); + }, + + 'validation snapshotId'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const volume = new Volume(stack, 'ForToken', { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(8), + }); + let idx: number = 0; + + // THEN + test.doesNotThrow(() => { + // Should not throw if we provide a Token for the snapshotId + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + snapshotId: volume.volumeId, + }); + }); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + snapshotId: 'snap-0123456789abcdefABCDEF', + }); + }); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + snapshotId: ' snap-1234', // leading extra character(s) + }); + }, Error, '`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + snapshotId: 'snap-1234 ', // trailing extra character(s) + }); + }, Error, '`snapshotId` does match expected pattern. Expected `snap-` (ex: `snap-05abe246af`) or Token'); + + test.done(); + }, + + 'validation iops'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + let idx: number = 0; + + // THEN + // Test: Type of volume + for (const volumeType of [ + EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, + EbsDeviceVolumeType.COLD_HDD, + EbsDeviceVolumeType.MAGNETIC, + ]) { + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + iops: 100, + volumeType, + }); + }, Error, '`iops` may only be specified if the `volumeType` is `PROVISIONED_IOPS_SSD`/`IO1`'); + } + + // Test: iops in range + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(10), + iops: 99, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + }, Error, '`iops` must be in the range 100 to 64,000, inclusive.'); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(10), + iops: 100, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + }); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(1300), + iops: 64000, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + }); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(1300), + iops: 64001, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + }, Error, '`iops` must be in the range 100 to 64,000, inclusive.'); + + // Test: iops ratio + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(10), + iops: 500, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + }); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(10), + iops: 501, + volumeType: EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, + }); + }, Error, '`iops` has a maximum ratio of 50 IOPS/GiB.'); + + test.done(); + }, + + 'validation multi-attach'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + let idx: number = 0; + + // THEN + for (const volumeType of [ + EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, + EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, + EbsDeviceVolumeType.COLD_HDD, + EbsDeviceVolumeType.MAGNETIC, + ]) { + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(500), + iops: 100, + enableMultiAttach: true, + volumeType, + }); + }, Error, 'multi-attach is supported exclusively on `PROVISIONED_IOPS_SSD` volumes.'); + } + + test.done(); + }, + + 'validation size in range'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + let idx: number = 0; + + // THEN + for (const testData of [ + [EbsDeviceVolumeType.GENERAL_PURPOSE_SSD, 1, 16000], + [EbsDeviceVolumeType.PROVISIONED_IOPS_SSD, 4, 16000], + [EbsDeviceVolumeType.THROUGHPUT_OPTIMIZED_HDD, 500, 16000], + [EbsDeviceVolumeType.COLD_HDD, 500, 16000], + [EbsDeviceVolumeType.MAGNETIC, 1, 1000], + ]) { + const volumeType = testData[0] as EbsDeviceVolumeType; + const min = testData[1] as number; + const max = testData[2] as number; + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(min - 1), + volumeType, + }); + }, Error, `\`${volumeType}\` volumes must be between ${min} GiB and ${max} GiB in size.`); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(min), + volumeType, + }); + }); + test.doesNotThrow(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(max), + volumeType, + }); + }); + test.throws(() => { + new Volume(stack, `Volume${idx++}`, { + availabilityZone: 'us-east-1a', + size: cdk.Size.gibibytes(max + 1), + volumeType, + }); + }, Error, `\`${volumeType}\` volumes must be between ${min} GiB and ${max} GiB in size.`); + } + + test.done(); + }, + +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 17693856ed8fd..09f786c2ea760 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -436,7 +436,7 @@ export = { Type: 'A', AliasTarget: { HostedZoneId: { 'Fn::GetAtt': ['ServiceLBE9A1ADBC', 'CanonicalHostedZoneID'] }, - DNSName: { 'Fn::GetAtt': ['ServiceLBE9A1ADBC', 'DNSName'] }, + DNSName: { 'Fn::Join': ['', [ 'dualstack.', { 'Fn::GetAtt': ['ServiceLBE9A1ADBC', 'DNSName'] } ] ] }, }, })); @@ -501,7 +501,7 @@ export = { Type: 'A', AliasTarget: { HostedZoneId: { 'Fn::GetAtt': ['ServiceLBE9A1ADBC', 'CanonicalHostedZoneID'] }, - DNSName: { 'Fn::GetAtt': ['ServiceLBE9A1ADBC', 'DNSName'] }, + DNSName: { 'Fn::Join': [ '', [ 'dualstack.', { 'Fn::GetAtt': ['ServiceLBE9A1ADBC', 'DNSName'] } ] ] }, }, })); diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json index 3f560edd9c37a..e7434c8e2eafc 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/integ.load-balanced-fargate-service.expected.json @@ -618,9 +618,18 @@ "Type": "A", "AliasTarget": { "DNSName": { - "Fn::GetAtt": [ - "myServiceLB168895E1", - "DNSName" + "Fn::Join": + [ + "", + [ + "dualstack.", + { + "Fn::GetAtt": [ + "myServiceLB168895E1", + "DNSName" + ] + } + ] ] }, "HostedZoneId": { diff --git a/packages/@aws-cdk/aws-efs/README.md b/packages/@aws-cdk/aws-efs/README.md index ec2ff3352755a..01d766117a6b1 100644 --- a/packages/@aws-cdk/aws-efs/README.md +++ b/packages/@aws-cdk/aws-efs/README.md @@ -45,12 +45,10 @@ and group override any identity information provided by the NFS client. The file access point's root directory. Applications using the access point can only access data in its own directory and below. To learn more, see [Mounting a File System Using EFS Access Points](https://docs.aws.amazon.com/efs/latest/ug/efs-access-points.html). -Use `AccessPoint` to create an access point: +Use `addAccessPoint` to create an access point from a fileSystem: ```ts -new AccessPoint(stack, 'AccessPoint', { - fileSystem -}); +fileSystem.addAccessPoint('AccessPoint'); ``` By default, when you create an access point, the root(`/`) directory is exposed to the client connecting to diff --git a/packages/@aws-cdk/aws-efs/lib/access-point.ts b/packages/@aws-cdk/aws-efs/lib/access-point.ts index 39c9fabb68a04..6fd4b6e927bce 100644 --- a/packages/@aws-cdk/aws-efs/lib/access-point.ts +++ b/packages/@aws-cdk/aws-efs/lib/access-point.ts @@ -65,14 +65,9 @@ export interface PosixUser { } /** - * Properties for the AccessPoint + * Options to create an AccessPoint */ -export interface AccessPointProps { - /** - * The efs filesystem - */ - readonly fileSystem: IFileSystem; - +export interface AccessPointOptions { /** * Specifies the POSIX IDs and permissions to apply when creating the access point's root directory. If the * root directory specified by `path` does not exist, EFS creates the root directory and applies the @@ -103,6 +98,16 @@ export interface AccessPointProps { readonly posixUser?: PosixUser; } +/** + * Properties for the AccessPoint + */ +export interface AccessPointProps extends AccessPointOptions { + /** + * The efs filesystem + */ + readonly fileSystem: IFileSystem; +} + /** * Represents the AccessPoint */ @@ -137,7 +142,7 @@ export class AccessPoint extends Resource implements IAccessPoint { constructor(scope: Construct, id: string, props: AccessPointProps) { super(scope, id); - const resource = new CfnAccessPoint(scope, 'Resource', { + const resource = new CfnAccessPoint(this, 'Resource', { fileSystemId: props.fileSystem.fileSystemId, rootDirectory: { creationInfo: props.createAcl ? { diff --git a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts index aa42acb2b6372..35c6ef381a8d5 100644 --- a/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts +++ b/packages/@aws-cdk/aws-efs/lib/efs-file-system.ts @@ -1,6 +1,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; import { Construct, IResource, RemovalPolicy, Resource, Size, Tag } from '@aws-cdk/core'; +import { AccessPoint, AccessPointOptions } from './access-point'; import { CfnFileSystem, CfnMountTarget } from './efs.generated'; // tslint:disable:max-line-length @@ -272,4 +273,14 @@ export class FileSystem extends Resource implements IFileSystem { }); }); } + + /** + * create access point from this filesystem + */ + public addAccessPoint(id: string, accessPointOptions: AccessPointOptions = {}): AccessPoint { + return new AccessPoint(this, id, { + fileSystem: this, + ...accessPointOptions, + }); + } } diff --git a/packages/@aws-cdk/aws-efs/test/access-point.test.ts b/packages/@aws-cdk/aws-efs/test/access-point.test.ts index 6b7343b1e5f52..761594507c779 100644 --- a/packages/@aws-cdk/aws-efs/test/access-point.test.ts +++ b/packages/@aws-cdk/aws-efs/test/access-point.test.ts @@ -15,7 +15,14 @@ beforeEach(() => { }); }); -test('default access point is created correctly', () => { +test('addAccessPoint correctly', () => { + // WHEN + fileSystem.addAccessPoint('MyAccessPoint'); + // THEN + expectCDK(stack).to(haveResource('AWS::EFS::AccessPoint')); +}); + +test('new AccessPoint correctly', () => { // WHEN new AccessPoint(stack, 'MyAccessPoint', { fileSystem, diff --git a/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json b/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json index f5eeff3153703..8c1fa47948791 100644 --- a/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json +++ b/packages/@aws-cdk/aws-efs/test/integ.efs.expected.json @@ -542,7 +542,7 @@ } } }, - "Resource": { + "FileSystemAccessPointF8178182": { "Type": "AWS::EFS::AccessPoint", "Properties": { "FileSystemId": { diff --git a/packages/@aws-cdk/aws-efs/test/integ.efs.ts b/packages/@aws-cdk/aws-efs/test/integ.efs.ts index 3906e374b8130..0afad3e6f4a29 100644 --- a/packages/@aws-cdk/aws-efs/test/integ.efs.ts +++ b/packages/@aws-cdk/aws-efs/test/integ.efs.ts @@ -1,6 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { AccessPoint, FileSystem } from '../lib'; +import { FileSystem } from '../lib'; const app = new cdk.App(); const stack = new cdk.Stack(app, 'test-efs-integ'); @@ -11,8 +11,7 @@ const fileSystem = new FileSystem(stack, 'FileSystem', { vpc, }); -new AccessPoint(stack, 'AccessPoint', { - fileSystem, +fileSystem.addAccessPoint('AccessPoint', { createAcl: { ownerGid: '1000', ownerUid: '1000', @@ -23,4 +22,4 @@ new AccessPoint(stack, 'AccessPoint', { gid: '1000', uid: '1000', }, -}); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/addons/neuron-device-plugin.yaml b/packages/@aws-cdk/aws-eks/lib/addons/neuron-device-plugin.yaml new file mode 100644 index 0000000000000..48d66be1c8343 --- /dev/null +++ b/packages/@aws-cdk/aws-eks/lib/addons/neuron-device-plugin.yaml @@ -0,0 +1,66 @@ +# source: https://github.com/aws/aws-neuron-sdk/blob/master/docs/neuron-container-tools/k8s-neuron-device-plugin.yml +# https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/ +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: neuron-device-plugin-daemonset + namespace: kube-system +spec: + selector: + matchLabels: + name: neuron-device-plugin-ds + updateStrategy: + type: RollingUpdate + template: + metadata: + annotations: + scheduler.alpha.kubernetes.io/critical-pod: "" + labels: + name: neuron-device-plugin-ds + spec: + tolerations: + - key: CriticalAddonsOnly + operator: Exists + - key: aws.amazon.com/neuron + operator: Exists + effect: NoSchedule + # Mark this pod as a critical add-on; when enabled, the critical add-on + # scheduler reserves resources for critical add-on pods so that they can + # be rescheduled after a failure. + # See https://kubernetes.io/docs/tasks/administer-cluster/guaranteed-scheduling-critical-addon-pods/ + priorityClassName: "system-node-critical" + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: "beta.kubernetes.io/instance-type" + operator: In + values: + - inf1.xlarge + - inf1.2xlarge + - inf1.6xlarge + - inf1.4xlarge + - matchExpressions: + - key: "node.kubernetes.io/instance-type" + operator: In + values: + - inf1.xlarge + - inf1.2xlarge + - inf1.6xlarge + - inf1.24xlarge + containers: + - image: 790709498068.dkr.ecr.us-west-2.amazonaws.com/neuron-device-plugin:1.0.9043.0 + imagePullPolicy: Always + name: k8s-neuron-device-plugin-ctr + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + volumeMounts: + - name: device-plugin + mountPath: /var/lib/kubelet/device-plugins + volumes: + - name: device-plugin + hostPath: + path: /var/lib/kubelet/device-plugins \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks/lib/cluster.ts b/packages/@aws-cdk/aws-eks/lib/cluster.ts index 491d357387073..5cf344f067f75 100644 --- a/packages/@aws-cdk/aws-eks/lib/cluster.ts +++ b/packages/@aws-cdk/aws-eks/lib/cluster.ts @@ -3,6 +3,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as ssm from '@aws-cdk/aws-ssm'; import { CfnOutput, Construct, IResource, Resource, Stack, Tag, Token } from '@aws-cdk/core'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as YAML from 'yaml'; import { AwsAuth } from './aws-auth'; import { clusterArnComponents, ClusterResource } from './cluster-resource'; import { CfnCluster, CfnClusterProps } from './eks.generated'; @@ -385,6 +388,8 @@ export class Cluster extends Resource implements ICluster { private _spotInterruptHandler?: HelmChart; + private _neuronDevicePlugin?: KubernetesResource; + private readonly version: string | undefined; /** @@ -537,6 +542,10 @@ export class Cluster extends Resource implements ICluster { machineImageType: options.machineImageType, }); + if (nodeTypeForInstanceType(options.instanceType) === NodeType.INFERENTIA) { + this.addNeuronDevicePlugin(); + } + return asg; } @@ -834,6 +843,20 @@ export class Cluster extends Resource implements ICluster { return this._spotInterruptHandler; } + /** + * Installs the Neuron device plugin on the cluster if it's not + * already added. + */ + private addNeuronDevicePlugin() { + if (!this._neuronDevicePlugin) { + const fileContents = fs.readFileSync(path.join(__dirname, 'addons/neuron-device-plugin.yaml'), 'utf8'); + const sanitized = YAML.parse(fileContents); + this._neuronDevicePlugin = this.addResource('NeuronDevicePlugin', sanitized); + } + + return this._neuronDevicePlugin; + } + /** * Opportunistically tag subnets with the required tags. * @@ -1112,6 +1135,7 @@ export class EksOptimizedImage implements ec2.IMachineImage { this.amiParameterName = `/aws/service/eks/optimized-ami/${this.kubernetesVersion}/` + ( this.nodeType === NodeType.STANDARD ? 'amazon-linux-2/' : '' ) + ( this.nodeType === NodeType.GPU ? 'amazon-linux-2-gpu/' : '' ) + + (this.nodeType === NodeType.INFERENTIA ? 'amazon-linux-2-gpu/' : '') + 'recommended/image_id'; } @@ -1176,6 +1200,11 @@ export enum NodeType { * GPU instances */ GPU = 'GPU', + + /** + * Inferentia instances + */ + INFERENTIA = 'INFERENTIA', } /** @@ -1222,7 +1251,10 @@ export enum MachineImageType { } const GPU_INSTANCETYPES = ['p2', 'p3', 'g4']; +const INFERENTIA_INSTANCETYPES = ['inf1']; function nodeTypeForInstanceType(instanceType: ec2.InstanceType) { - return GPU_INSTANCETYPES.includes(instanceType.toString().substring(0, 2)) ? NodeType.GPU : NodeType.STANDARD; + return GPU_INSTANCETYPES.includes(instanceType.toString().substring(0, 2)) ? NodeType.GPU : + INFERENTIA_INSTANCETYPES.includes(instanceType.toString().substring(0, 4)) ? NodeType.INFERENTIA : + NodeType.STANDARD; } diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 9a7d145f01c93..df01d4bdbe553 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -64,7 +64,8 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.703.0", + "@types/yaml": "1.2.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", @@ -80,8 +81,12 @@ "@aws-cdk/aws-ssm": "0.0.0", "@aws-cdk/core": "0.0.0", "@aws-cdk/custom-resources": "0.0.0", - "constructs": "^3.0.2" + "constructs": "^3.0.2", + "yaml": "1.10.0" }, + "bundledDependencies": [ + "yaml" + ], "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-autoscaling": "0.0.0", 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 4278bee450139..a787602404c54 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 @@ -28,7 +28,7 @@ } } }, - "ClusterDefaultVpcFA9F2722": { + "Vpc8378EB38": { "Type": "AWS::EC2::VPC", "Properties": { "CidrBlock": "10.0.0.0/16", @@ -38,17 +38,17 @@ "Tags": [ { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc" + "Value": "aws-cdk-eks-cluster-test/Vpc" } ] } }, - "ClusterDefaultVpcPublicSubnet1Subnet3BFE1BDA": { + "VpcPublicSubnet1Subnet5C2D37C4": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.0.0/19", "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": true, @@ -67,16 +67,16 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" } ] } }, - "ClusterDefaultVpcPublicSubnet1RouteTable1DCCDD98": { + "VpcPublicSubnet1RouteTable6C95E38E": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "Tags": [ { @@ -85,38 +85,38 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" } ] } }, - "ClusterDefaultVpcPublicSubnet1RouteTableAssociationAFBE6789": { + "VpcPublicSubnet1RouteTableAssociation97140677": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPublicSubnet1RouteTable1DCCDD98" + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" }, "SubnetId": { - "Ref": "ClusterDefaultVpcPublicSubnet1Subnet3BFE1BDA" + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" } } }, - "ClusterDefaultVpcPublicSubnet1DefaultRouteCF22EF6E": { + "VpcPublicSubnet1DefaultRoute3DA9E72A": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPublicSubnet1RouteTable1DCCDD98" + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" }, "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { - "Ref": "ClusterDefaultVpcIGW756BE43E" + "Ref": "VpcIGWD7BA715C" } }, "DependsOn": [ - "ClusterDefaultVpcVPCGWC1D00388" + "VpcVPCGWBF912B6E" ] }, - "ClusterDefaultVpcPublicSubnet1EIP498E2BD2": { + "VpcPublicSubnet1EIPD7E02669": { "Type": "AWS::EC2::EIP", "Properties": { "Domain": "vpc", @@ -127,22 +127,22 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" } ] } }, - "ClusterDefaultVpcPublicSubnet1NATGateway6E21013E": { + "VpcPublicSubnet1NATGateway4D7517AA": { "Type": "AWS::EC2::NatGateway", "Properties": { "AllocationId": { "Fn::GetAtt": [ - "ClusterDefaultVpcPublicSubnet1EIP498E2BD2", + "VpcPublicSubnet1EIPD7E02669", "AllocationId" ] }, "SubnetId": { - "Ref": "ClusterDefaultVpcPublicSubnet1Subnet3BFE1BDA" + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" }, "Tags": [ { @@ -151,17 +151,17 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet1" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet1" } ] } }, - "ClusterDefaultVpcPublicSubnet2SubnetC4E9A966": { + "VpcPublicSubnet2Subnet691E08A3": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.32.0/19", "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": true, @@ -180,16 +180,16 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet2" } ] } }, - "ClusterDefaultVpcPublicSubnet2RouteTable6F1F5F47": { + "VpcPublicSubnet2RouteTable94F7E489": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "Tags": [ { @@ -198,83 +198,43 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet2" } ] } }, - "ClusterDefaultVpcPublicSubnet2RouteTableAssociationA8539C50": { + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPublicSubnet2RouteTable6F1F5F47" + "Ref": "VpcPublicSubnet2RouteTable94F7E489" }, "SubnetId": { - "Ref": "ClusterDefaultVpcPublicSubnet2SubnetC4E9A966" + "Ref": "VpcPublicSubnet2Subnet691E08A3" } } }, - "ClusterDefaultVpcPublicSubnet2DefaultRoute1FA8621E": { + "VpcPublicSubnet2DefaultRoute97F91067": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPublicSubnet2RouteTable6F1F5F47" + "Ref": "VpcPublicSubnet2RouteTable94F7E489" }, "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { - "Ref": "ClusterDefaultVpcIGW756BE43E" + "Ref": "VpcIGWD7BA715C" } }, "DependsOn": [ - "ClusterDefaultVpcVPCGWC1D00388" + "VpcVPCGWBF912B6E" ] }, - "ClusterDefaultVpcPublicSubnet2EIP265F4810": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc", - "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" - } - ] - } - }, - "ClusterDefaultVpcPublicSubnet2NATGateway4AF4B728": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "ClusterDefaultVpcPublicSubnet2EIP265F4810", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "ClusterDefaultVpcPublicSubnet2SubnetC4E9A966" - }, - "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet2" - } - ] - } - }, - "ClusterDefaultVpcPublicSubnet3Subnet1A46184A": { + "VpcPublicSubnet3SubnetBE12F0B6": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.64.0/19", "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": true, @@ -293,16 +253,16 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet3" } ] } }, - "ClusterDefaultVpcPublicSubnet3RouteTableC81F99EF": { + "VpcPublicSubnet3RouteTable93458DBB": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "Tags": [ { @@ -311,83 +271,43 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" + "Value": "aws-cdk-eks-cluster-test/Vpc/PublicSubnet3" } ] } }, - "ClusterDefaultVpcPublicSubnet3RouteTableAssociation7C5D21CC": { + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPublicSubnet3RouteTableC81F99EF" + "Ref": "VpcPublicSubnet3RouteTable93458DBB" }, "SubnetId": { - "Ref": "ClusterDefaultVpcPublicSubnet3Subnet1A46184A" + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" } } }, - "ClusterDefaultVpcPublicSubnet3DefaultRouteB6080504": { + "VpcPublicSubnet3DefaultRoute4697774F": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPublicSubnet3RouteTableC81F99EF" + "Ref": "VpcPublicSubnet3RouteTable93458DBB" }, "DestinationCidrBlock": "0.0.0.0/0", "GatewayId": { - "Ref": "ClusterDefaultVpcIGW756BE43E" + "Ref": "VpcIGWD7BA715C" } }, "DependsOn": [ - "ClusterDefaultVpcVPCGWC1D00388" + "VpcVPCGWBF912B6E" ] }, - "ClusterDefaultVpcPublicSubnet3EIP0CBF6D05": { - "Type": "AWS::EC2::EIP", - "Properties": { - "Domain": "vpc", - "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" - } - ] - } - }, - "ClusterDefaultVpcPublicSubnet3NATGatewayEF4BA49A": { - "Type": "AWS::EC2::NatGateway", - "Properties": { - "AllocationId": { - "Fn::GetAtt": [ - "ClusterDefaultVpcPublicSubnet3EIP0CBF6D05", - "AllocationId" - ] - }, - "SubnetId": { - "Ref": "ClusterDefaultVpcPublicSubnet3Subnet1A46184A" - }, - "Tags": [ - { - "Key": "kubernetes.io/role/elb", - "Value": "1" - }, - { - "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PublicSubnet3" - } - ] - } - }, - "ClusterDefaultVpcPrivateSubnet1Subnet03F39409": { + "VpcPrivateSubnet1Subnet536B997A": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.96.0/19", "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "AvailabilityZone": "test-region-1a", "MapPublicIpOnLaunch": false, @@ -406,16 +326,16 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet1" + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet1" } ] } }, - "ClusterDefaultVpcPrivateSubnet1RouteTable7844020C": { + "VpcPrivateSubnet1RouteTableB2C5B500": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "Tags": [ { @@ -424,40 +344,40 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet1" + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet1" } ] } }, - "ClusterDefaultVpcPrivateSubnet1RouteTableAssociationF8A67D95": { + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPrivateSubnet1RouteTable7844020C" + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" }, "SubnetId": { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" } } }, - "ClusterDefaultVpcPrivateSubnet1DefaultRouteD624C8BD": { + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPrivateSubnet1RouteTable7844020C" + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" }, "DestinationCidrBlock": "0.0.0.0/0", "NatGatewayId": { - "Ref": "ClusterDefaultVpcPublicSubnet1NATGateway6E21013E" + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" } } }, - "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7": { + "VpcPrivateSubnet2Subnet3788AAA1": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.128.0/19", "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "AvailabilityZone": "test-region-1b", "MapPublicIpOnLaunch": false, @@ -476,16 +396,16 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet2" + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet2" } ] } }, - "ClusterDefaultVpcPrivateSubnet2RouteTable1F9A5298": { + "VpcPrivateSubnet2RouteTableA678073B": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "Tags": [ { @@ -494,40 +414,40 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet2" + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet2" } ] } }, - "ClusterDefaultVpcPrivateSubnet2RouteTableAssociationE1240DF2": { + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPrivateSubnet2RouteTable1F9A5298" + "Ref": "VpcPrivateSubnet2RouteTableA678073B" }, "SubnetId": { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" } } }, - "ClusterDefaultVpcPrivateSubnet2DefaultRouteAB55737C": { + "VpcPrivateSubnet2DefaultRoute060D2087": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPrivateSubnet2RouteTable1F9A5298" + "Ref": "VpcPrivateSubnet2RouteTableA678073B" }, "DestinationCidrBlock": "0.0.0.0/0", "NatGatewayId": { - "Ref": "ClusterDefaultVpcPublicSubnet2NATGateway4AF4B728" + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" } } }, - "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839": { + "VpcPrivateSubnet3SubnetF258B56E": { "Type": "AWS::EC2::Subnet", "Properties": { "CidrBlock": "10.0.160.0/19", "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "AvailabilityZone": "test-region-1c", "MapPublicIpOnLaunch": false, @@ -546,16 +466,16 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet3" + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet3" } ] } }, - "ClusterDefaultVpcPrivateSubnet3RouteTableF71314D0": { + "VpcPrivateSubnet3RouteTableD98824C7": { "Type": "AWS::EC2::RouteTable", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "Tags": [ { @@ -564,53 +484,53 @@ }, { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc/PrivateSubnet3" + "Value": "aws-cdk-eks-cluster-test/Vpc/PrivateSubnet3" } ] } }, - "ClusterDefaultVpcPrivateSubnet3RouteTableAssociation3007DC36": { + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { "Type": "AWS::EC2::SubnetRouteTableAssociation", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPrivateSubnet3RouteTableF71314D0" + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" }, "SubnetId": { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } } }, - "ClusterDefaultVpcPrivateSubnet3DefaultRoute932EDFF0": { + "VpcPrivateSubnet3DefaultRoute94B74F0D": { "Type": "AWS::EC2::Route", "Properties": { "RouteTableId": { - "Ref": "ClusterDefaultVpcPrivateSubnet3RouteTableF71314D0" + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" }, "DestinationCidrBlock": "0.0.0.0/0", "NatGatewayId": { - "Ref": "ClusterDefaultVpcPublicSubnet3NATGatewayEF4BA49A" + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" } } }, - "ClusterDefaultVpcIGW756BE43E": { + "VpcIGWD7BA715C": { "Type": "AWS::EC2::InternetGateway", "Properties": { "Tags": [ { "Key": "Name", - "Value": "aws-cdk-eks-cluster-test/Cluster/DefaultVpc" + "Value": "aws-cdk-eks-cluster-test/Vpc" } ] } }, - "ClusterDefaultVpcVPCGWC1D00388": { + "VpcVPCGWBF912B6E": { "Type": "AWS::EC2::VPCGatewayAttachment", "Properties": { "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" }, "InternetGatewayId": { - "Ref": "ClusterDefaultVpcIGW756BE43E" + "Ref": "VpcIGWD7BA715C" } } }, @@ -657,7 +577,7 @@ } ], "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" } } }, @@ -724,6 +644,27 @@ "ToPort": 443 } }, + "ClusterControlPlaneSecurityGroupfromawscdkeksclustertestClusterInferenceInstancesInstanceSecurityGroup42C57C51443E3176F85": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkeksclustertestClusterInferenceInstancesInstanceSecurityGroup42C57C51:443", + "FromPort": 443, + "GroupId": { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", + "GroupId" + ] + }, + "ToPort": 443 + } + }, "ClusterCreationRole360249B6": { "Type": "AWS::IAM::Role", "Properties": { @@ -873,22 +814,22 @@ ], "subnetIds": [ { - "Ref": "ClusterDefaultVpcPublicSubnet1Subnet3BFE1BDA" + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" }, { - "Ref": "ClusterDefaultVpcPublicSubnet2SubnetC4E9A966" + "Ref": "VpcPublicSubnet2Subnet691E08A3" }, { - "Ref": "ClusterDefaultVpcPublicSubnet3Subnet1A46184A" + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } ] } @@ -963,6 +904,13 @@ "Arn" ] }, + "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]},{\\\"rolearn\\\":\\\"", + { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceRole59AC6F56", + "Arn" + ] + }, "\\\",\\\"username\\\":\\\"system:node:{{EC2PrivateDNSName}}\\\",\\\"groups\\\":[\\\"system:bootstrappers\\\",\\\"system:nodes\\\"]}]\",\"mapUsers\":\"[]\",\"mapAccounts\":\"[]\"}}]" ] ] @@ -1059,13 +1007,13 @@ }, "Subnets": [ { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } ], "ForceUpdateEnabled": true, @@ -1177,7 +1125,7 @@ } ], "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" } } }, @@ -1406,13 +1354,13 @@ ], "VPCZoneIdentifier": [ { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } ] }, @@ -1465,7 +1413,7 @@ } ], "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" } } }, @@ -1708,13 +1656,13 @@ ], "VPCZoneIdentifier": [ { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } ] }, @@ -1767,7 +1715,7 @@ } ], "VpcId": { - "Ref": "ClusterDefaultVpcFA9F2722" + "Ref": "Vpc8378EB38" } } }, @@ -1997,13 +1945,13 @@ ], "VPCZoneIdentifier": [ { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } ] }, @@ -2053,6 +2001,317 @@ "UpdateReplacePolicy": "Delete", "DeletionPolicy": "Delete" }, + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "aws-cdk-eks-cluster-test/Cluster/InferenceInstances/InstanceSecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "Tags": [ + { + "Key": { + "Fn::Join": [ + "", + [ + "kubernetes.io/cluster/", + { + "Ref": "Cluster9EE0221C" + } + ] + ] + }, + "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/InferenceInstances" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "ClusterInferenceInstancesInstanceSecurityGroupfromawscdkeksclustertestClusterInferenceInstancesInstanceSecurityGroup42C57C51ALLTRAFFICB6138869": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "-1", + "Description": "from awscdkeksclustertestClusterInferenceInstancesInstanceSecurityGroup42C57C51:ALL TRAFFIC", + "GroupId": { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", + "GroupId" + ] + } + } + }, + "ClusterInferenceInstancesInstanceSecurityGroupfromawscdkeksclustertestClusterControlPlaneSecurityGroup2F1301344437B48FD33": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkeksclustertestClusterControlPlaneSecurityGroup2F130134:443", + "FromPort": 443, + "GroupId": { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + }, + "ToPort": 443 + } + }, + "ClusterInferenceInstancesInstanceSecurityGroupfromawscdkeksclustertestClusterControlPlaneSecurityGroup2F130134102565535A460F673": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "from awscdkeksclustertestClusterControlPlaneSecurityGroup2F130134:1025-65535", + "FromPort": 1025, + "GroupId": { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ClusterControlPlaneSecurityGroupD274242C", + "GroupId" + ] + }, + "ToPort": 65535 + } + }, + "ClusterInferenceInstancesInstanceRole59AC6F56": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": { + "Fn::Join": [ + "", + [ + "ec2.", + { + "Ref": "AWS::URLSuffix" + } + ] + ] + } + } + } + ], + "Version": "2012-10-17" + }, + "ManagedPolicyArns": [ + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKSWorkerNodePolicy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEKS_CNI_Policy" + ] + ] + }, + { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::aws:policy/AmazonEC2ContainerRegistryReadOnly" + ] + ] + } + ], + "Tags": [ + { + "Key": { + "Fn::Join": [ + "", + [ + "kubernetes.io/cluster/", + { + "Ref": "Cluster9EE0221C" + } + ] + ] + }, + "Value": "owned" + }, + { + "Key": "Name", + "Value": "aws-cdk-eks-cluster-test/Cluster/InferenceInstances" + } + ] + } + }, + "ClusterInferenceInstancesInstanceProfile5A1209B4": { + "Type": "AWS::IAM::InstanceProfile", + "Properties": { + "Roles": [ + { + "Ref": "ClusterInferenceInstancesInstanceRole59AC6F56" + } + ] + } + }, + "ClusterInferenceInstancesLaunchConfig03BF48FE": { + "Type": "AWS::AutoScaling::LaunchConfiguration", + "Properties": { + "ImageId": { + "Ref": "SsmParameterValueawsserviceeksoptimizedami116amazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter" + }, + "InstanceType": "inf1.2xlarge", + "IamInstanceProfile": { + "Ref": "ClusterInferenceInstancesInstanceProfile5A1209B4" + }, + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ClusterInferenceInstancesInstanceSecurityGroupECB3FC45", + "GroupId" + ] + } + ], + "UserData": { + "Fn::Base64": { + "Fn::Join": [ + "", + [ + "#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ", + { + "Ref": "Cluster9EE0221C" + }, + " --kubelet-extra-args \"--node-labels lifecycle=OnDemand\" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack aws-cdk-eks-cluster-test --resource ClusterInferenceInstancesASGE90717C7 --region test-region" + ] + ] + } + } + }, + "DependsOn": [ + "ClusterInferenceInstancesInstanceRole59AC6F56" + ] + }, + "ClusterInferenceInstancesASGE90717C7": { + "Type": "AWS::AutoScaling::AutoScalingGroup", + "Properties": { + "MaxSize": "1", + "MinSize": "1", + "LaunchConfigurationName": { + "Ref": "ClusterInferenceInstancesLaunchConfig03BF48FE" + }, + "Tags": [ + { + "Key": { + "Fn::Join": [ + "", + [ + "kubernetes.io/cluster/", + { + "Ref": "Cluster9EE0221C" + } + ] + ] + }, + "PropagateAtLaunch": true, + "Value": "owned" + }, + { + "Key": "Name", + "PropagateAtLaunch": true, + "Value": "aws-cdk-eks-cluster-test/Cluster/InferenceInstances" + } + ], + "VPCZoneIdentifier": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + }, + "UpdatePolicy": { + "AutoScalingRollingUpdate": { + "WaitOnResourceSignals": false, + "PauseTime": "PT0S", + "SuspendProcesses": [ + "HealthCheck", + "ReplaceUnhealthy", + "AZRebalance", + "AlarmNotification", + "ScheduledActions" + ] + }, + "AutoScalingScheduledAction": { + "IgnoreUnmodifiedGroupSizeProperties": true + } + } + }, + "ClustermanifestNeuronDevicePlugin0B3E0D17": { + "Type": "Custom::AWSCDK-EKS-KubernetesResource", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "awscdkawseksKubectlProviderNestedStackawscdkawseksKubectlProviderNestedStackResourceA7AEBA6B", + "Outputs.awscdkeksclustertestawscdkawseksKubectlProviderframeworkonEventC681B49AArn" + ] + }, + "Manifest": "[{\"apiVersion\":\"apps/v1\",\"kind\":\"DaemonSet\",\"metadata\":{\"name\":\"neuron-device-plugin-daemonset\",\"namespace\":\"kube-system\"},\"spec\":{\"selector\":{\"matchLabels\":{\"name\":\"neuron-device-plugin-ds\"}},\"updateStrategy\":{\"type\":\"RollingUpdate\"},\"template\":{\"metadata\":{\"annotations\":{\"scheduler.alpha.kubernetes.io/critical-pod\":\"\"},\"labels\":{\"name\":\"neuron-device-plugin-ds\"}},\"spec\":{\"tolerations\":[{\"key\":\"CriticalAddonsOnly\",\"operator\":\"Exists\"},{\"key\":\"aws.amazon.com/neuron\",\"operator\":\"Exists\",\"effect\":\"NoSchedule\"}],\"priorityClassName\":\"system-node-critical\",\"affinity\":{\"nodeAffinity\":{\"requiredDuringSchedulingIgnoredDuringExecution\":{\"nodeSelectorTerms\":[{\"matchExpressions\":[{\"key\":\"beta.kubernetes.io/instance-type\",\"operator\":\"In\",\"values\":[\"inf1.xlarge\",\"inf1.2xlarge\",\"inf1.6xlarge\",\"inf1.4xlarge\"]}]},{\"matchExpressions\":[{\"key\":\"node.kubernetes.io/instance-type\",\"operator\":\"In\",\"values\":[\"inf1.xlarge\",\"inf1.2xlarge\",\"inf1.6xlarge\",\"inf1.24xlarge\"]}]}]}}},\"containers\":[{\"image\":\"790709498068.dkr.ecr.us-west-2.amazonaws.com/neuron-device-plugin:1.0.9043.0\",\"imagePullPolicy\":\"Always\",\"name\":\"k8s-neuron-device-plugin-ctr\",\"securityContext\":{\"allowPrivilegeEscalation\":false,\"capabilities\":{\"drop\":[\"ALL\"]}},\"volumeMounts\":[{\"name\":\"device-plugin\",\"mountPath\":\"/var/lib/kubelet/device-plugins\"}]}],\"volumes\":[{\"name\":\"device-plugin\",\"hostPath\":{\"path\":\"/var/lib/kubelet/device-plugins\"}}]}}}}]", + "ClusterName": { + "Ref": "Cluster9EE0221C" + }, + "RoleArn": { + "Fn::GetAtt": [ + "ClusterCreationRole360249B6", + "Arn" + ] + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "ClusterNodegroupextrangNodeGroupRole23AE23D0": { "Type": "AWS::IAM::Role", "Properties": { @@ -2132,13 +2391,13 @@ }, "Subnets": [ { - "Ref": "ClusterDefaultVpcPrivateSubnet1Subnet03F39409" + "Ref": "VpcPrivateSubnet1Subnet536B997A" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet2SubnetA526AEA7" + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" }, { - "Ref": "ClusterDefaultVpcPrivateSubnet3SubnetB64BC839" + "Ref": "VpcPrivateSubnet3SubnetF258B56E" } ], "ForceUpdateEnabled": true, @@ -2832,6 +3091,10 @@ "SsmParameterValueawsservicebottlerocketawsk8s115x8664latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { "Type": "AWS::SSM::Parameter::Value", "Default": "/aws/service/bottlerocket/aws-k8s-1.15/x86_64/latest/image_id" + }, + "SsmParameterValueawsserviceeksoptimizedami116amazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/aws/service/eks/optimized-ami/1.16/amazon-linux-2-gpu/recommended/image_id" } } -} +} \ No newline at end of file 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 ff6d62e74c20f..122efbdd9ecc6 100644 --- a/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/integ.eks-cluster.ts @@ -14,14 +14,18 @@ class EksClusterStack extends TestStack { assumedBy: new iam.AccountRootPrincipal(), }); + // just need one nat gateway to simplify the test + const vpc = new ec2.Vpc(this, 'Vpc', { maxAzs: 3, natGateways: 1 }); + // create the cluster with a default nodegroup capacity const cluster = new eks.Cluster(this, 'Cluster', { + vpc, mastersRole, defaultCapacity: 2, version: '1.16', }); - // // fargate profile for resources in the "default" namespace + // fargate profile for resources in the "default" namespace cluster.addFargateProfile('default', { selectors: [{ namespace: 'default' }], }); @@ -51,6 +55,12 @@ class EksClusterStack extends TestStack { }, }); + // inference instances + cluster.addCapacity('InferenceInstances', { + instanceType: new ec2.InstanceType('inf1.2xlarge'), + minCapacity: 1, + }); + // add a extra nodegroup cluster.addNodegroup('extra-ng', { instanceType: new ec2.InstanceType('t3.small'), diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 4b00ac1ec6e36..c7a09d794e7cc 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -2,7 +2,10 @@ import { countResources, expect, haveResource, haveResourceLike, not } from '@aw import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; +import * as fs from 'fs'; import { Test } from 'nodeunit'; +import * as path from 'path'; +import * as YAML from 'yaml'; import * as eks from '../lib'; import { KubectlLayer } from '../lib/kubectl-layer'; import { testFixture, testFixtureNoVpc } from './util'; @@ -1156,4 +1159,23 @@ export = { })); test.done(); }, + 'inference instances are supported'(test: Test) { + // GIVEN + const { stack } = testFixtureNoVpc(); + const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); + + // WHEN + cluster.addCapacity('InferenceInstances', { + instanceType: new ec2.InstanceType('inf1.2xlarge'), + minCapacity: 1, + }); + const fileContents = fs.readFileSync(path.join(__dirname, '../lib', 'addons/neuron-device-plugin.yaml'), 'utf8'); + const sanitized = YAML.parse(fileContents); + + // THEN + expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + Manifest: JSON.stringify([sanitized]), + })); + test.done(); + }, }}; diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 15152b7fc89a5..a51e40c0df0db 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -205,11 +205,13 @@ const ELBV2_ACCOUNTS: { [region: string]: string } = { 'us-east-2': '033677994240', 'us-west-1': '027434742980', 'us-west-2': '797873946194', + 'af-south-1': '098369216593', 'ca-central-1': '985666609251', 'eu-central-1': '054676820928', 'eu-west-1': '156460612806', 'eu-west-2': '652711504416', 'eu-west-3': '009996457667', + 'eu-south-1': '635631232127', 'eu-north-1': '897822967062', 'ap-east-1': '754344448648', 'ap-northeast-1': '582318560864', diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index 18f0665c69a02..01ab709164870 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -68,7 +68,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 6b79895bf02c6..1fa918db69d5c 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -68,16 +68,16 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.39", - "@types/lodash": "^4.14.156", + "@types/lodash": "^4.14.157", "@types/nodeunit": "^0.0.31", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "lodash": "^4.17.15", - "nock": "^12.0.3", + "nock": "^13.0.0", "nodeunit": "^0.11.3", "pkglint": "0.0.0", "sinon": "^9.0.2" diff --git a/packages/@aws-cdk/aws-route53-targets/README.md b/packages/@aws-cdk/aws-route53-targets/README.md index e3e8f99a0e5ba..3bd36f68ba46f 100644 --- a/packages/@aws-cdk/aws-route53-targets/README.md +++ b/packages/@aws-cdk/aws-route53-targets/README.md @@ -13,7 +13,7 @@ This library contains Route53 Alias Record targets for: new route53.ARecord(this, 'AliasRecord', { zone, target: route53.RecordTarget.fromAlias(new alias.ApiGateway(restApi)), - // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomainName(domainName)), + // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(domainName)), }); ``` * CloudFront distributions @@ -28,7 +28,7 @@ This library contains Route53 Alias Record targets for: new route53.ARecord(this, 'AliasRecord', { zone, target: route53.RecordTarget.fromAlias(new alias.LoadBalancerTarget(elbv2)), - // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomainName(domainName)), + // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(domainName)), }); ``` * Classic load balancers @@ -36,9 +36,14 @@ This library contains Route53 Alias Record targets for: new route53.ARecord(this, 'AliasRecord', { zone, target: route53.RecordTarget.fromAlias(new alias.ClassicLoadBalancerTarget(elb)), - // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomainName(domainName)), + // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(domainName)), }); ``` + +**Important:** Based on [AWS documentation](https://aws.amazon.com/de/premiumsupport/knowledge-center/alias-resource-record-set-route53-cli/), all alias record in Route 53 that points to a Elastic Load Balancer will always include *dualstack* for the DNSName to resolve IPv4/IPv6 addresses (without *dualstack* IPv6 will not resolve). + +For example, if the Amazon-provided DNS for the load balancer is `ALB-xxxxxxx.us-west-2.elb.amazonaws.com`, CDK will create alias target in Route 53 will be `dualstack.ALB-xxxxxxx.us-west-2.elb.amazonaws.com`. + * InterfaceVpcEndpoints **Important:** Based on the CFN docs for VPCEndpoints - [see here](attrDnsEntries) - the attributes returned for DnsEntries in CloudFormation is a combination of the hosted zone ID and the DNS name. The entries are ordered as follows: regional public DNS, zonal public DNS, private DNS, and wildcard DNS. This order is not enforced for AWS Marketplace services, and therefore this CDK construct is ONLY guaranteed to work with non-marketplace services. diff --git a/packages/@aws-cdk/aws-route53-targets/lib/classic-load-balancer-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/classic-load-balancer-target.ts index 076c390e8f704..8b25fbe4f331e 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/classic-load-balancer-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/classic-load-balancer-target.ts @@ -11,7 +11,7 @@ export class ClassicLoadBalancerTarget implements route53.IAliasRecordTarget { public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { return { hostedZoneId: this.loadBalancer.loadBalancerCanonicalHostedZoneNameId, - dnsName: this.loadBalancer.loadBalancerDnsName, + dnsName: `dualstack.${this.loadBalancer.loadBalancerDnsName}`, }; } } diff --git a/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts b/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts index c8902b98c0daa..d398256c57f8b 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/load-balancer-target.ts @@ -11,7 +11,7 @@ export class LoadBalancerTarget implements route53.IAliasRecordTarget { public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { return { hostedZoneId: this.loadBalancer.loadBalancerCanonicalHostedZoneId, - dnsName: this.loadBalancer.loadBalancerDnsName, + dnsName: `dualstack.${this.loadBalancer.loadBalancerDnsName}`, }; } } diff --git a/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts index 6aea222305741..59e9326e7b23e 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/classic-load-balancer-target.test.ts @@ -28,7 +28,7 @@ test('use classic ELB as record target', () => { // THEN expect(stack).toHaveResource('AWS::Route53::RecordSet', { AliasTarget: { - DNSName: { 'Fn::GetAtt': [ 'LB8A12904C', 'DNSName' ] }, + DNSName: { 'Fn::Join': [ '', [ 'dualstack.', { 'Fn::GetAtt': [ 'LB8A12904C', 'DNSName' ] } ] ] }, HostedZoneId: { 'Fn::GetAtt': [ 'LB8A12904C', 'CanonicalHostedZoneNameID' ] }, }, }); diff --git a/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json b/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json index 828c0d7f4b6a5..746f62cc9acc7 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json +++ b/packages/@aws-cdk/aws-route53-targets/test/integ.alb-alias-target.expected.json @@ -413,9 +413,17 @@ "Type": "A", "AliasTarget": { "DNSName": { - "Fn::GetAtt": [ - "LB8A12904C", - "DNSName" + "Fn::Join": [ + "", + [ + "dualstack.", + { + "Fn::GetAtt": [ + "LB8A12904C", + "DNSName" + ] + } + ] ] }, "HostedZoneId": { diff --git a/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts index 50ff50a1a122e..396d2d457bb85 100644 --- a/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts +++ b/packages/@aws-cdk/aws-route53-targets/test/load-balancer-target.test.ts @@ -28,7 +28,7 @@ test('use ALB as record target', () => { // THEN expect(stack).toHaveResource('AWS::Route53::RecordSet', { AliasTarget: { - DNSName: { 'Fn::GetAtt': [ 'LB8A12904C', 'DNSName' ] }, + DNSName: { 'Fn::Join': [ '', [ 'dualstack.', { 'Fn::GetAtt': [ 'LB8A12904C', 'DNSName' ] } ] ] }, HostedZoneId: { 'Fn::GetAtt': [ 'LB8A12904C', 'CanonicalHostedZoneID' ] }, }, }); diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index 60b155b6bcd7f..b6d65266506ec 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -64,7 +64,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts b/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts index 1ca13bb1e763d..a1ea5ac5ac42c 100644 --- a/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts +++ b/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts @@ -13,7 +13,7 @@ export class LambdaDestination implements s3.IBucketNotificationDestination { public bind(_scope: Construct, bucket: s3.IBucket): s3.BucketNotificationDestinationConfig { const permissionId = `AllowBucketNotificationsFrom${bucket.node.uniqueId}`; - if (this.fn.node.tryFindChild(permissionId) === undefined) { + if (this.fn.permissionsNode.tryFindChild(permissionId) === undefined) { this.fn.addPermission(permissionId, { sourceAccount: Stack.of(bucket).account, principal: new iam.ServicePrincipal('s3.amazonaws.com'), @@ -23,7 +23,7 @@ export class LambdaDestination implements s3.IBucketNotificationDestination { // if we have a permission resource for this relationship, add it as a dependency // to the bucket notifications resource, so it will be created first. - const permission = this.fn.node.tryFindChild(permissionId) as CfnResource | undefined; + const permission = this.fn.permissionsNode.tryFindChild(permissionId) as CfnResource | undefined; return { type: s3.BucketNotificationDestinationType.LAMBDA, diff --git a/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts index bc36b562776fd..76563c2b4cccc 100644 --- a/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-s3-notifications/test/lambda/lambda.test.ts @@ -70,3 +70,52 @@ test('lambda as notification target specified by function arn', () => { }, }); }); + +test('permissions are added as a dependency to the notifications resource when using singleton function', () => { + + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'MyBucket'); + const fn = new lambda.SingletonFunction(stack, 'MyFunction', { + uuid: 'uuid', + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('foo'), + }); + + const lambdaDestination = new s3n.LambdaDestination(fn); + + bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v1/'}); + + const notifications = stack.node.findAll().filter(c => c.node.id === 'Notifications')[0]; + const dependencies = notifications!.node.dependencies; + + expect(dependencies[0].target.node.id).toEqual('AllowBucketNotificationsFromMyBucket'); + +}); + +test('add multiple event notifications using a singleton function', () => { + + const stack = new Stack(); + const bucket = new s3.Bucket(stack, 'MyBucket'); + const fn = new lambda.SingletonFunction(stack, 'MyFunction', { + uuid: 'uuid', + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromInline('foo'), + }); + + const lambdaDestination = new s3n.LambdaDestination(fn); + + bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v1/'}); + bucket.addEventNotification(s3.EventType.OBJECT_CREATED, lambdaDestination, { prefix: 'v2/'}); + + expect(stack).toHaveResourceLike('Custom::S3BucketNotifications', { + NotificationConfiguration: { + LambdaFunctionConfigurations: [ + { Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v1/'}]}}}, + { Filter: { Key: { FilterRules: [{ Name: 'prefix', Value: 'v2/'}]}}}, + ], + }, + }); + +}); diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index f0c48b343da21..f49d0c41dc349 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -65,7 +65,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^25.5.4", diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/service.ts b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts index f81d9fd043376..c41625bf68a7e 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/service.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/service.ts @@ -123,7 +123,7 @@ export interface DnsServiceProps extends BaseServiceProps { export interface ServiceProps extends DnsServiceProps { /** - * The ID of the namespace that you want to use for DNS configuration. + * The namespace that you want to use for DNS configuration. */ readonly namespace: INamespace; } diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index f2dc1fc22533f..ebc4924d0ce86 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -65,7 +65,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md index 6ed90de00c40c..6665cc4196b23 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/README.md +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/README.md @@ -123,14 +123,10 @@ The following example adds the item from calling DynamoDB's `getItem` API to the input and passes it to the next state. ```ts -new sfn.Task(this, 'PutItem', { - task: tasks.CallDynamoDB.getItem({ - item: { - MessageId: new tasks.DynamoAttributeValue().withS('12345'), - }, - tableName: 'my-table', - }), - resultPath: `$.Item` +new tasks.DynamoGetItem(this, 'PutItem', { + item: { MessageId: { s: '12345'} }, + tableName: 'my-table', + resultPath: `$.Item`, }); ``` @@ -144,7 +140,7 @@ Most tasks take parameters. Parameter values can either be static, supplied dire in the workflow definition (by specifying their values), or a value available at runtime in the state machine's execution (either as its input or an output of a prior state). Parameter values available at runtime can be specified via the `Data` class, -using methods such as `Data.stringAt()`. +using methods such as `JsonPath.stringAt()`. The following example provides the field named `input` as the input to the Lambda function and invokes it asynchronously. @@ -152,7 +148,7 @@ and invokes it asynchronously. ```ts const submitJob = new tasks.LambdaInvoke(stack, 'Invoke Handler', { lambdaFunction: submitJobLambda, - payload: sfn.Data.StringAt('$.input'), + payload: sfn.JsonPath.StringAt('$.input'), invocationType: tasks.InvocationType.EVENT, }); ``` @@ -251,14 +247,9 @@ Read more about calling DynamoDB APIs [here](https://docs.aws.amazon.com/step-fu The [GetItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html) operation returns a set of attributes for the item with the given primary key. ```ts -new sfn.Task(this, 'Get Item', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'messageId', - value: new tasks.DynamoAttributeValue().withS('message-007'), - }, - tableName: 'my-table', - }), +new tasks.DynamoGetItem(this, 'Get Item', { + key: { messageId: tasks.DynamoAttributeValue.fromString('message-007') }, + table, }); ``` @@ -267,15 +258,13 @@ new sfn.Task(this, 'Get Item', { The [PutItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html) operation creates a new item, or replaces an old item with a new item. ```ts -new sfn.Task(this, 'PutItem', { - task: tasks.CallDynamoDB.putItem({ - item: { - MessageId: new tasks.DynamoAttributeValue().withS('message-007'), - Text: new tasks.DynamoAttributeValue().withS(sfn.Data.stringAt('$.bar')), - TotalCount: new tasks.DynamoAttributeValue().withN('10'), - }, - tableName: 'my-table', - }), +new tasks.DynamoPutItem(this, 'PutItem', { + item: { + MessageId: tasks.DynamoAttributeValue.fromString('message-007'), + Text: tasks.DynamoAttributeValue.fromString(sfn.JsonPath.stringAt('$.bar')), + TotalCount: tasks.DynamoAttributeValue.fromNumber(10), + }, + table, }); ``` @@ -284,15 +273,13 @@ new sfn.Task(this, 'PutItem', { The [DeleteItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html) operation deletes a single item in a table by primary key. ```ts -new sfn.Task(this, 'DeleteItem', { - task: tasks.CallDynamoDB.deleteItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS('message-007'), - }, - tableName: 'my-table', - }), - resultPath: 'DISCARD', +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '@aws-cdk/aws-stepfunctions-tasks'; + +new tasks.DynamoDeleteItem(this, 'DeleteItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, + table, + resultPath: sfn.JsonPath.DISCARD, }); ``` @@ -302,19 +289,14 @@ The [UpdateItem](https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/ to the table if it does not already exist. ```ts -const updateItemTask = new sfn.Task(this, 'UpdateItem', { - task: tasks.CallDynamoDB.updateItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS('message-007'), - }, - tableName: 'my-table', - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN(sfn.Data.stringAt('$.Item.TotalCount.N')), - ':rand': new tasks.DynamoAttributeValue().withN('20'), - }, - updateExpression: 'SET TotalCount = :val + :rand', - }), +new tasks.DynamoUpdateItem(this, 'UpdateItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString('message-007') }, + table, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), + ':rand': tasks.DynamoAttributeValue.fromNumber(20), + }, + updateExpression: 'SET TotalCount = :val + :rand', }); ``` @@ -340,7 +322,7 @@ new ecs.RunEcsFargateTask({ environment: [ { name: 'CONTAINER_INPUT', - value: Data.stringAt('$.valueFromStateData'), + value: JsonPath.stringAt('$.valueFromStateData'), } ] } @@ -479,7 +461,7 @@ Corresponds to the [`modifyInstanceGroups`](https://docs.aws.amazon.com/emr/late ```ts new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { clusterId: 'ClusterId', - instanceGroupName: sfn.Data.stringAt('$.InstanceGroupName'), + instanceGroupName: sfn.JsonPath.stringAt('$.InstanceGroupName'), instanceGroup: { instanceCount: 1, }, @@ -583,8 +565,8 @@ new tasks.LambdaInvoke(stack, 'Invoke with callback', { lambdaFunction: myLambda, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ - token: sfn.Context.taskToken, - input: sfn.Data.stringAt('$.someField'), + token: sfn.JsonPath.taskToken, + input: sfn.JsonPath.stringAt('$.someField'), }), }); ``` @@ -603,7 +585,7 @@ You can call the [`CreateTrainingJob`](https://docs.aws.amazon.com/sagemaker/lat ```ts new sfn.SagemakerTrainTask(this, 'TrainSagemaker', { - trainingJobName: sfn.Data.stringAt('$.JobName'), + trainingJobName: sfn.JsonPath.stringAt('$.JobName'), role, algorithmSpecification: { algorithmName: 'BlazingText', @@ -688,7 +670,7 @@ const task2 = new tasks.SnsPublish(this, 'Publish2', { topic, message: sfn.TaskInput.fromObject({ field1: 'somedata', - field2: sfn.Data.stringAt('$.field2'), + field2: sfn.JsonPath.stringAt('$.field2'), }) }); ``` @@ -710,7 +692,7 @@ const task = new StepFunctionsStartExecution(stack, 'ChildTask', { stateMachine: child, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, input: sfn.TaskInput.fromObject({ - token: sfn.Context.taskToken, + token: sfn.JsonPath.taskToken, foo: 'bar' }), name: 'MyExecutionName' @@ -750,7 +732,7 @@ const task2 = new tasks.SqsSendMessage(this, 'Send2', { queue, messageBody: sfn.TaskInput.fromObject({ field1: 'somedata', - field2: sfn.Data.stringAt('$.field2'), + field2: sfn.JsonPath.stringAt('$.field2'), }), }); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts deleted file mode 100644 index 048043ce8cd9c..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/call-dynamodb.ts +++ /dev/null @@ -1,791 +0,0 @@ -import * as iam from '@aws-cdk/aws-iam'; -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import { Stack, withResolved } from '@aws-cdk/core'; -import { getResourceArn } from '../resource-arn-suffix'; - -/** - * Determines the level of detail about provisioned throughput consumption that is returned. - */ -export enum DynamoConsumedCapacity { - /** - * The response includes the aggregate ConsumedCapacity for the operation, - * together with ConsumedCapacity for each table and secondary index that was accessed - */ - INDEXES = 'INDEXES', - - /** - * The response includes only the aggregate ConsumedCapacity for the operation. - */ - TOTAL = 'TOTAL', - - /** - * No ConsumedCapacity details are included in the response. - */ - NONE = 'NONE' -} - -/** - * Determines whether item collection metrics are returned. - */ -export enum DynamoItemCollectionMetrics { - /** - * If set to SIZE, the response includes statistics about item collections, - * if any, that were modified during the operation. - */ - SIZE = 'SIZE', - - /** - * If set to NONE, no statistics are returned. - */ - NONE = 'NONE' -} - -/** - * Use ReturnValues if you want to get the item attributes as they appear before or after they are changed - */ -export enum DynamoReturnValues { - /** - * Nothing is returned - */ - NONE = 'NONE', - - /** - * Returns all of the attributes of the item - */ - ALL_OLD = 'ALL_OLD', - - /** - * Returns only the updated attributes - */ - UPDATED_OLD = 'UPDATED_OLD', - - /** - * Returns all of the attributes of the item - */ - ALL_NEW = 'ALL_NEW', - - /** - * Returns only the updated attributes - */ - UPDATED_NEW = 'UPDATED_NEW' -} - -/** - * Map of string to AttributeValue - */ -export interface DynamoAttributeValueMap { - [key: string]: DynamoAttributeValue; -} - -/** - * Class to generate AttributeValue - */ -export class DynamoAttributeValue { - private attributeValue: any = {}; - - /** - * Sets an attribute of type String. For example: "S": "Hello" - */ - public withS(value: string) { - this.attributeValue.S = value; - return this; - } - - /** - * Sets an attribute of type Number. For example: "N": "123.45" - * Numbers are sent across the network to DynamoDB as strings, - * to maximize compatibility across languages and libraries. - * However, DynamoDB treats them as number type attributes for mathematical operations. - */ - public withN(value: string) { - this.attributeValue.N = value; - return this; - } - - /** - * Sets an attribute of type Binary. For example: "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" - */ - public withB(value: string) { - this.attributeValue.B = value; - return this; - } - - /** - * Sets an attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" ,"Zebra"] - */ - public withSS(value: string[]) { - this.attributeValue.SS = value; - return this; - } - - /** - * Sets an attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] - * Numbers are sent across the network to DynamoDB as strings, - * to maximize compatibility across languages and libraries. - * However, DynamoDB treats them as number type attributes for mathematical operations. - */ - public withNS(value: string[]) { - this.attributeValue.NS = value; - return this; - } - - /** - * Sets an attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] - */ - public withBS(value: string[]) { - this.attributeValue.BS = value; - return this; - } - - /** - * Sets an attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} - */ - public withM(value: DynamoAttributeValueMap) { - this.attributeValue.M = transformAttributeValueMap(value); - return this; - } - - /** - * Sets an attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N", "3.14159"}] - */ - public withL(value: DynamoAttributeValue[]) { - this.attributeValue.L = value.map(val => val.toObject()); - return this; - } - - /** - * Sets an attribute of type Null. For example: "NULL": true - */ - public withNULL(value: boolean) { - this.attributeValue.NULL = value; - return this; - } - - /** - * Sets an attribute of type Boolean. For example: "BOOL": true - */ - public withBOOL(value: boolean) { - this.attributeValue.BOOL = value; - return this; - } - - /** - * Return the attributeValue object - */ - public toObject() { - return this.attributeValue; - } -} - -/** - * Property for any key - */ -export interface DynamoAttribute { - /** - * The name of the attribute - */ - readonly name: string; - - /** - * The value of the attribute - */ - readonly value: DynamoAttributeValue; -} - -/** - * Class to generate projection expression - */ -export class DynamoProjectionExpression { - private expression: string[] = []; - - /** - * Adds the passed attribute to the chain - * - * @param attr Attribute name - */ - public withAttribute(attr: string): DynamoProjectionExpression { - if (this.expression.length) { - this.expression.push(`.${attr}`); - } else { - this.expression.push(attr); - } - return this; - } - - /** - * Adds the array literal access for passed index - * - * @param index array index - */ - public atIndex(index: number): DynamoProjectionExpression { - if (!this.expression.length) { - throw new Error('Expression must start with an attribute'); - } - - this.expression.push(`[${index}]`); - return this; - } - - /** - * converts and return the string expression - */ - public toString(): string { - return this.expression.join(''); - } -} - -/** - * Properties for DynamoGetItem Task - */ -export interface DynamoGetItemProps { - /** - * A attribute representing the partition key of the item to retrieve. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key - */ - readonly partitionKey: DynamoAttribute; - - /** - * The name of the table containing the requested item. - */ - readonly tableName: string; - - /** - * A attribute representing the sort key of the item to retrieve. - * - * @default - No sort key - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key - */ - readonly sortKey?: DynamoAttribute; - - /** - * Determines the read consistency model: - * If set to true, then the operation uses strongly consistent reads; - * otherwise, the operation uses eventually consistent reads. - * - * @default false - */ - readonly consistentRead?: boolean; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ExpressionAttributeNames - * - * @default - No expression attributes - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * An array of DynamoProjectionExpression that identifies one or more attributes to retrieve from the table. - * These attributes can include scalars, sets, or elements of a JSON document. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ProjectionExpression - * - * @default - No projection expression - */ - readonly projectionExpression?: DynamoProjectionExpression[]; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; -} - -/** - * Properties for DynamoPutItem Task - */ -export interface DynamoPutItemProps { - /** - * A map of attribute name/value pairs, one for each attribute. - * Only the primary key attributes are required; - * you can optionally provide other attribute name-value pairs for the item. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-Item - */ - readonly item: DynamoAttributeValueMap; - - /** - * The name of the table where the item should be writen . - */ - readonly tableName: string; - - /** - * A condition that must be satisfied in order for a conditional PutItem operation to succeed. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ConditionExpression - * - * @default - No condition expression - */ - readonly conditionExpression?: string; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeNames - * - * @default - No expression attribute names - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * One or more values that can be substituted in an expression. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeValues - * - * @default - No expression attribute values - */ - readonly expressionAttributeValues?: DynamoAttributeValueMap; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; - - /** - * The item collection metrics to returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections - * - * @default DynamoItemCollectionMetrics.NONE - */ - readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; - - /** - * Use ReturnValues if you want to get the item attributes as they appeared - * before they were updated with the PutItem request. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues - * - * @default DynamoReturnValues.NONE - */ - readonly returnValues?: DynamoReturnValues; -} - -/** - * Properties for DynamoDeleteItem Task - */ -export interface DynamoDeleteItemProps { - /** - * An attribute representing the partition key of the item to delete. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-Key - */ - readonly partitionKey: DynamoAttribute; - - /** - * The name of the table containing the requested item. - */ - readonly tableName: string; - - /** - * An attribute representing the sort key of the item to delete. - * - * @default - No sort key - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-Key - */ - readonly sortKey?: DynamoAttribute; - - /** - * A condition that must be satisfied in order for a conditional DeleteItem to succeed. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ConditionExpression - * - * @default - No condition expression - */ - readonly conditionExpression?: string; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeNames - * - * @default - No expression attribute names - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * One or more values that can be substituted in an expression. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeValues - * - * @default - No expression attribute values - */ - readonly expressionAttributeValues?: DynamoAttributeValueMap; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; - - /** - * Determines whether item collection metrics are returned. - * If set to SIZE, the response includes statistics about item collections, if any, - * that were modified during the operation are returned in the response. - * If set to NONE (the default), no statistics are returned. - * - * @default DynamoItemCollectionMetrics.NONE - */ - readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; - - /** - * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues - * - * @default DynamoReturnValues.NONE - */ - readonly returnValues?: DynamoReturnValues; -} - -/** - * Properties for DynamoUpdateItem Task - */ -export interface DynamoUpdateItemProps { - /** - * The partition key of the item to be updated. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-Key - */ - readonly partitionKey: DynamoAttribute; - - /** - * The name of the table containing the requested item. - */ - readonly tableName: string; - - /** - * The sort key of the item to be updated. - * - * @default - No sort key - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-Key - */ - readonly sortKey?: DynamoAttribute; - - /** - * A condition that must be satisfied in order for a conditional DeleteItem to succeed. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ConditionExpression - * - * @default - No condition expression - */ - readonly conditionExpression?: string; - - /** - * One or more substitution tokens for attribute names in an expression - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeNames - * - * @default - No expression attribute names - */ - readonly expressionAttributeNames?: { [key: string]: string }; - - /** - * One or more values that can be substituted in an expression. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeValues - * - * @default - No expression attribute values - */ - readonly expressionAttributeValues?: DynamoAttributeValueMap; - - /** - * Determines the level of detail about provisioned throughput consumption that is returned in the response - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnConsumedCapacity - * - * @default DynamoConsumedCapacity.NONE - */ - readonly returnConsumedCapacity?: DynamoConsumedCapacity; - - /** - * Determines whether item collection metrics are returned. - * If set to SIZE, the response includes statistics about item collections, if any, - * that were modified during the operation are returned in the response. - * If set to NONE (the default), no statistics are returned. - * - * @default DynamoItemCollectionMetrics.NONE - */ - readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; - - /** - * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues - * - * @default DynamoReturnValues.NONE - */ - readonly returnValues?: DynamoReturnValues; - - /** - * An expression that defines one or more attributes to be updated, - * the action to be performed on them, and new values for them. - * - * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-UpdateExpression - * - * @default - No update expression - */ - readonly updateExpression?: string; -} - -/** - * A StepFunctions task to call DynamoGetItem - */ -export class DynamoGetItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoGetItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.GET), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.GET, - ), - parameters: { - Key: configurePrimaryKey(this.props.partitionKey, this.props.sortKey), - TableName: this.props.tableName, - ConsistentRead: this.props.consistentRead ?? false, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ProjectionExpression: this.configureProjectionExpression( - this.props.projectionExpression, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - }, - }; - } - - private configureProjectionExpression( - expressions?: DynamoProjectionExpression[], - ): string | undefined { - return expressions - ? expressions.map(expression => expression.toString()).join(',') - : undefined; - } -} - -/** - * A StepFunctions task to call DynamoPutItem - */ -export class DynamoPutItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoPutItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.PUT), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.PUT, - ), - parameters: { - Item: transformAttributeValueMap(this.props.item), - TableName: this.props.tableName, - ConditionExpression: this.props.conditionExpression, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ExpressionAttributeValues: transformAttributeValueMap( - this.props.expressionAttributeValues, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, - ReturnValues: this.props.returnValues, - }, - }; - } -} - -/** - * A StepFunctions task to call DynamoDeleteItem - */ -export class DynamoDeleteItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoDeleteItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.DELETE), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.DELETE, - ), - parameters: { - Key: configurePrimaryKey(this.props.partitionKey, this.props.sortKey), - TableName: this.props.tableName, - ConditionExpression: this.props.conditionExpression, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ExpressionAttributeValues: transformAttributeValueMap( - this.props.expressionAttributeValues, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, - ReturnValues: this.props.returnValues, - }, - }; - } -} - -/** - * A StepFunctions task to call DynamoUpdateItem - */ -export class DynamoUpdateItem implements sfn.IStepFunctionsTask { - constructor(private readonly props: DynamoUpdateItemProps) { - withResolved(props.tableName, validateTableName); - } - - public bind(_task: sfn.Task): sfn.StepFunctionsTaskConfig { - return { - resourceArn: getDynamoResourceArn(DynamoMethod.UPDATE), - policyStatements: getDynamoPolicyStatements( - _task, - this.props.tableName, - DynamoMethod.UPDATE, - ), - parameters: { - Key: configurePrimaryKey(this.props.partitionKey, this.props.sortKey), - TableName: this.props.tableName, - ConditionExpression: this.props.conditionExpression, - ExpressionAttributeNames: this.props.expressionAttributeNames, - ExpressionAttributeValues: transformAttributeValueMap( - this.props.expressionAttributeValues, - ), - ReturnConsumedCapacity: this.props.returnConsumedCapacity, - ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, - ReturnValues: this.props.returnValues, - UpdateExpression: this.props.updateExpression, - }, - }; - } -} - -/** - * A helper wrapper class to call all DynamoDB APIs - */ -export class CallDynamoDB { - /** - * Method to get DynamoGetItem task - * - * @param props DynamoGetItemProps - */ - public static getItem(props: DynamoGetItemProps) { - return new DynamoGetItem(props); - } - - /** - * Method to get DynamoPutItem task - * - * @param props DynamoPutItemProps - */ - public static putItem(props: DynamoPutItemProps) { - return new DynamoPutItem(props); - } - - /** - * Method to get DynamoDeleteItem task - * - * @param props DynamoDeleteItemProps - */ - public static deleteItem(props: DynamoDeleteItemProps) { - return new DynamoDeleteItem(props); - } - - /** - * Method to get DynamoUpdateItem task - * - * @param props DynamoUpdateItemProps - */ - public static updateItem(props: DynamoUpdateItemProps) { - return new DynamoUpdateItem(props); - } -} - -enum DynamoMethod { - GET = 'Get', - PUT = 'Put', - DELETE = 'Delete', - UPDATE = 'Update' -} - -function validateTableName(tableName: string) { - if ( - tableName.length < 3 || - tableName.length > 255 || - !new RegExp(/[a-zA-Z0-9_.-]+$/).test(tableName) - ) { - throw new Error( - `TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: ${tableName}`, - ); - } -} - -function getDynamoResourceArn(method: DynamoMethod) { - return getResourceArn( - 'dynamodb', - `${method.toLowerCase()}Item`, - sfn.ServiceIntegrationPattern.FIRE_AND_FORGET, - ); -} - -function getDynamoPolicyStatements( - task: sfn.Task, - tableName: string, - method: DynamoMethod, -) { - return [ - new iam.PolicyStatement({ - resources: [ - Stack.of(task).formatArn({ - service: 'dynamodb', - resource: 'table', - resourceName: tableName, - }), - ], - actions: [`dynamodb:${method}Item`], - }), - ]; -} - -function configurePrimaryKey( - partitionKey: DynamoAttribute, - sortKey?: DynamoAttribute, -) { - const key = { - [partitionKey.name]: partitionKey.value.toObject(), - }; - - if (sortKey) { - key[sortKey.name] = sortKey.value.toObject(); - } - - return key; -} - -function transformAttributeValueMap(attrMap?: DynamoAttributeValueMap) { - const transformedValue: any = {}; - for (const key in attrMap) { - if (key) { - transformedValue[key] = attrMap[key].toObject(); - } - } - return attrMap ? transformedValue : undefined; -} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts new file mode 100644 index 0000000000000..174698241a1f8 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/delete-item.ts @@ -0,0 +1,123 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoItemCollectionMetrics, DynamoReturnValues } from './shared-types'; + +/** + * Properties for DynamoDeleteItem Task + */ +export interface DynamoDeleteItemProps extends sfn.TaskStateBaseProps { + /** + * The name of the table containing the requested item. + */ + readonly table: ddb.ITable; + + /** + * Primary key of the item to retrieve. + * + * For the primary key, you must provide all of the attributes. + * For example, with a simple primary key, you only need to provide a value for the partition key. + * For a composite primary key, you must provide values for both the partition key and the sort key. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key + */ + readonly key: { [key: string]: DynamoAttributeValue }; + + /** + * A condition that must be satisfied in order for a conditional DeleteItem to succeed. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ConditionExpression + * + * @default - No condition expression + */ + readonly conditionExpression?: string; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeNames + * + * @default - No expression attribute names + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * One or more values that can be substituted in an expression. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ExpressionAttributeValues + * + * @default - No expression attribute values + */ + readonly expressionAttributeValues?: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; + + /** + * Determines whether item collection metrics are returned. + * If set to SIZE, the response includes statistics about item collections, if any, + * that were modified during the operation are returned in the response. + * If set to NONE (the default), no statistics are returned. + * + * @default DynamoItemCollectionMetrics.NONE + */ + readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; + + /** + * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_DeleteItem.html#DDB-DeleteItem-request-ReturnValues + * + * @default DynamoReturnValues.NONE + */ + readonly returnValues?: DynamoReturnValues; +} + +/** + * A StepFunctions task to call DynamoDeleteItem + */ +export class DynamoDeleteItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoDeleteItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.DELETE}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.DELETE), + Parameters: sfn.FieldUtils.renderObject({ + Key: transformAttributeValueMap(this.props.key), + TableName: this.props.table.tableName, + ConditionExpression: this.props.conditionExpression, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ExpressionAttributeValues: transformAttributeValueMap(this.props.expressionAttributeValues), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, + ReturnValues: this.props.returnValues, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts new file mode 100644 index 0000000000000..3cef6bd994fdc --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/get-item.ts @@ -0,0 +1,107 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoProjectionExpression } from './shared-types'; + +/** + * Properties for DynamoGetItem Task + */ +export interface DynamoGetItemProps extends sfn.TaskStateBaseProps { + /** + * The name of the table containing the requested item. + */ + readonly table: ddb.ITable; + + /** + * Primary key of the item to retrieve. + * + * For the primary key, you must provide all of the attributes. + * For example, with a simple primary key, you only need to provide a value for the partition key. + * For a composite primary key, you must provide values for both the partition key and the sort key. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key + */ + readonly key: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the read consistency model: + * If set to true, then the operation uses strongly consistent reads; + * otherwise, the operation uses eventually consistent reads. + * + * @default false + */ + readonly consistentRead?: boolean; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ExpressionAttributeNames + * + * @default - No expression attributes + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * An array of DynamoProjectionExpression that identifies one or more attributes to retrieve from the table. + * These attributes can include scalars, sets, or elements of a JSON document. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ProjectionExpression + * + * @default - No projection expression + */ + readonly projectionExpression?: DynamoProjectionExpression[]; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; +} + +/** + * A StepFunctions task to call DynamoGetItem + */ +export class DynamoGetItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoGetItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.GET}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.GET), + Parameters: sfn.FieldUtils.renderObject({ + Key: transformAttributeValueMap(this.props.key), + TableName: this.props.table.tableName, + ConsistentRead: this.props.consistentRead ?? false, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ProjectionExpression: this.configureProjectionExpression(this.props.projectionExpression), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + }), + }; + } + + private configureProjectionExpression(expressions?: DynamoProjectionExpression[]): string | undefined { + return expressions ? expressions.map((expression) => expression.toString()).join(',') : undefined; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts new file mode 100644 index 0000000000000..f951b7ed978e1 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/private/utils.ts @@ -0,0 +1,24 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { integrationResourceArn } from '../../private/task-utils'; +import { DynamoAttributeValue } from '../shared-types'; + +export enum DynamoMethod { + GET = 'Get', + PUT = 'Put', + DELETE = 'Delete', + UPDATE = 'Update', +} + +export function getDynamoResourceArn(method: DynamoMethod) { + return integrationResourceArn('dynamodb', `${method.toLowerCase()}Item`, sfn.IntegrationPattern.REQUEST_RESPONSE); +} + +export function transformAttributeValueMap(attrMap?: { [key: string]: DynamoAttributeValue }) { + const transformedValue: any = {}; + for (const key in attrMap) { + if (key) { + transformedValue[key] = attrMap[key].toObject(); + } + } + return attrMap ? transformedValue : undefined; +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts new file mode 100644 index 0000000000000..c23842bf5d98f --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/put-item.ts @@ -0,0 +1,121 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoItemCollectionMetrics, DynamoReturnValues } from './shared-types'; + +/** + * Properties for DynamoPutItem Task + */ +export interface DynamoPutItemProps extends sfn.TaskStateBaseProps { + /** + * A map of attribute name/value pairs, one for each attribute. + * Only the primary key attributes are required; + * you can optionally provide other attribute name-value pairs for the item. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-Item + */ + readonly item: { [key: string]: DynamoAttributeValue }; + + /** + * The name of the table where the item should be written . + */ + readonly table: ddb.ITable; + + /** + * A condition that must be satisfied in order for a conditional PutItem operation to succeed. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ConditionExpression + * + * @default - No condition expression + */ + readonly conditionExpression?: string; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeNames + * + * @default - No expression attribute names + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * One or more values that can be substituted in an expression. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ExpressionAttributeValues + * + * @default - No expression attribute values + */ + readonly expressionAttributeValues?: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; + + /** + * The item collection metrics to returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html#LSI.ItemCollections + * + * @default DynamoItemCollectionMetrics.NONE + */ + readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; + + /** + * Use ReturnValues if you want to get the item attributes as they appeared + * before they were updated with the PutItem request. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_PutItem.html#DDB-PutItem-request-ReturnValues + * + * @default DynamoReturnValues.NONE + */ + readonly returnValues?: DynamoReturnValues; +} + +/** + * A StepFunctions task to call DynamoPutItem + */ +export class DynamoPutItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoPutItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.PUT}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.PUT), + Parameters: sfn.FieldUtils.renderObject({ + Item: transformAttributeValueMap(this.props.item), + TableName: this.props.table.tableName, + ConditionExpression: this.props.conditionExpression, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ExpressionAttributeValues: transformAttributeValueMap(this.props.expressionAttributeValues), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, + ReturnValues: this.props.returnValues, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts new file mode 100644 index 0000000000000..95a1151eb1803 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/shared-types.ts @@ -0,0 +1,251 @@ +import { transformAttributeValueMap } from './private/utils'; + +/** + * Determines the level of detail about provisioned throughput consumption that is returned. + */ +export enum DynamoConsumedCapacity { + /** + * The response includes the aggregate ConsumedCapacity for the operation, + * together with ConsumedCapacity for each table and secondary index that was accessed + */ + INDEXES = 'INDEXES', + + /** + * The response includes only the aggregate ConsumedCapacity for the operation. + */ + TOTAL = 'TOTAL', + + /** + * No ConsumedCapacity details are included in the response. + */ + NONE = 'NONE', +} + +/** + * Determines whether item collection metrics are returned. + */ +export enum DynamoItemCollectionMetrics { + /** + * If set to SIZE, the response includes statistics about item collections, + * if any, that were modified during the operation. + */ + SIZE = 'SIZE', + + /** + * If set to NONE, no statistics are returned. + */ + NONE = 'NONE', +} + +/** + * Use ReturnValues if you want to get the item attributes as they appear before or after they are changed + */ +export enum DynamoReturnValues { + /** + * Nothing is returned + */ + NONE = 'NONE', + + /** + * Returns all of the attributes of the item + */ + ALL_OLD = 'ALL_OLD', + + /** + * Returns only the updated attributes + */ + UPDATED_OLD = 'UPDATED_OLD', + + /** + * Returns all of the attributes of the item + */ + ALL_NEW = 'ALL_NEW', + + /** + * Returns only the updated attributes + */ + UPDATED_NEW = 'UPDATED_NEW', +} + +/** + * Class to generate projection expression + */ +export class DynamoProjectionExpression { + private expression: string[] = []; + + /** + * Adds the passed attribute to the chain + * + * @param attr Attribute name + */ + public withAttribute(attr: string): DynamoProjectionExpression { + if (this.expression.length) { + this.expression.push(`.${attr}`); + } else { + this.expression.push(attr); + } + return this; + } + + /** + * Adds the array literal access for passed index + * + * @param index array index + */ + public atIndex(index: number): DynamoProjectionExpression { + if (!this.expression.length) { + throw new Error('Expression must start with an attribute'); + } + + this.expression.push(`[${index}]`); + return this; + } + + /** + * converts and return the string expression + */ + public toString(): string { + return this.expression.join(''); + } +} + +/** + * Represents the data for an attribute. + * Each attribute value is described as a name-value pair. + * The name is the data type, and the value is the data itself. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_AttributeValue.html + */ +export class DynamoAttributeValue { + /** + * Sets an attribute of type String. For example: "S": "Hello" + * Strings may be literal values or as JsonPath + */ + public static fromString(value: string) { + return new DynamoAttributeValue({ S: value }); + } + + /** + * Sets a literal number. For example: 1234 + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + */ + public static fromNumber(value: number) { + return new DynamoAttributeValue({ N: value.toString() }); + } + + /** + * Sets an attribute of type Number. For example: "N": "123.45" + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + * + * Numbers may be expressed as literal strings or as JsonPath + */ + public static numberFromString(value: string) { + return new DynamoAttributeValue({ N: value.toString() }); + } + + /** + * Sets an attribute of type Binary. For example: "B": "dGhpcyB0ZXh0IGlzIGJhc2U2NC1lbmNvZGVk" + * + * @param value base-64 encoded string + */ + public static fromBinary(value: string) { + return new DynamoAttributeValue({ B: value }); + } + + /** + * Sets an attribute of type String Set. For example: "SS": ["Giraffe", "Hippo" ,"Zebra"] + */ + public static fromStringSet(value: string[]) { + return new DynamoAttributeValue({ SS: value }); + } + + /** + * Sets an attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + */ + public static fromNumberSet(value: number[]) { + return new DynamoAttributeValue({ NS: value.map(String) }); + } + + /** + * Sets an attribute of type Number Set. For example: "NS": ["42.2", "-19", "7.5", "3.14"] + * Numbers are sent across the network to DynamoDB as strings, + * to maximize compatibility across languages and libraries. + * However, DynamoDB treats them as number type attributes for mathematical operations. + * + * Numbers may be expressed as literal strings or as JsonPath + */ + public static numberSetFromStrings(value: string[]) { + return new DynamoAttributeValue({ NS: value }); + } + + /** + * Sets an attribute of type Binary Set. For example: "BS": ["U3Vubnk=", "UmFpbnk=", "U25vd3k="] + */ + public static fromBinarySet(value: string[]) { + return new DynamoAttributeValue({ BS: value }); + } + + /** + * Sets an attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} + */ + public static fromMap(value: { [key: string]: DynamoAttributeValue }) { + return new DynamoAttributeValue({ M: transformAttributeValueMap(value) }); + } + + /** + * Sets an attribute of type Map. For example: "M": {"Name": {"S": "Joe"}, "Age": {"N": "35"}} + * + * @param value Json path that specifies state input to be used + */ + public static mapFromJsonPath(value: string) { + if (!value.startsWith('$')) { + throw new Error("Data JSON path values must either be exactly equal to '$' or start with '$.'"); + } + return new DynamoAttributeValue({ 'M.$': value }); + } + + /** + * Sets an attribute of type List. For example: "L": [ {"S": "Cookies"} , {"S": "Coffee"}, {"N", "3.14159"}] + */ + public static fromList(value: DynamoAttributeValue[]) { + return new DynamoAttributeValue({ L: value.map((val) => val.toObject()) }); + } + + /** + * Sets an attribute of type Null. For example: "NULL": true + */ + public static fromNull(value: boolean) { + return new DynamoAttributeValue({ NULL: value }); + } + + /** + * Sets an attribute of type Boolean. For example: "BOOL": true + */ + public static fromBoolean(value: boolean) { + return new DynamoAttributeValue({ BOOL: value }); + } + + /** + * Represents the data for the attribute. Data can be + * i.e. "S": "Hello" + */ + public readonly attributeValue: any; + + private constructor(value: any) { + this.attributeValue = value; + } + + /** + * Returns the DynamoDB attribute value + */ + public toObject() { + return this.attributeValue; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts new file mode 100644 index 0000000000000..63c424beb7b20 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/dynamodb/update-item.ts @@ -0,0 +1,134 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as iam from '@aws-cdk/aws-iam'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import { Construct, Stack } from '@aws-cdk/core'; +import { DynamoMethod, getDynamoResourceArn, transformAttributeValueMap } from './private/utils'; +import { DynamoAttributeValue, DynamoConsumedCapacity, DynamoItemCollectionMetrics, DynamoReturnValues } from './shared-types'; + +/** + * Properties for DynamoUpdateItem Task + */ +export interface DynamoUpdateItemProps extends sfn.TaskStateBaseProps { + /** + * The name of the table containing the requested item. + */ + readonly table: ddb.ITable; + + /** + * Primary key of the item to retrieve. + * + * For the primary key, you must provide all of the attributes. + * For example, with a simple primary key, you only need to provide a value for the partition key. + * For a composite primary key, you must provide values for both the partition key and the sort key. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_GetItem.html#DDB-GetItem-request-Key + */ + readonly key: { [key: string]: DynamoAttributeValue }; + + /** + * A condition that must be satisfied in order for a conditional DeleteItem to succeed. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ConditionExpression + * + * @default - No condition expression + */ + readonly conditionExpression?: string; + + /** + * One or more substitution tokens for attribute names in an expression + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeNames + * + * @default - No expression attribute names + */ + readonly expressionAttributeNames?: { [key: string]: string }; + + /** + * One or more values that can be substituted in an expression. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ExpressionAttributeValues + * + * @default - No expression attribute values + */ + readonly expressionAttributeValues?: { [key: string]: DynamoAttributeValue }; + + /** + * Determines the level of detail about provisioned throughput consumption that is returned in the response + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnConsumedCapacity + * + * @default DynamoConsumedCapacity.NONE + */ + readonly returnConsumedCapacity?: DynamoConsumedCapacity; + + /** + * Determines whether item collection metrics are returned. + * If set to SIZE, the response includes statistics about item collections, if any, + * that were modified during the operation are returned in the response. + * If set to NONE (the default), no statistics are returned. + * + * @default DynamoItemCollectionMetrics.NONE + */ + readonly returnItemCollectionMetrics?: DynamoItemCollectionMetrics; + + /** + * Use ReturnValues if you want to get the item attributes as they appeared before they were deleted. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-ReturnValues + * + * @default DynamoReturnValues.NONE + */ + readonly returnValues?: DynamoReturnValues; + + /** + * An expression that defines one or more attributes to be updated, + * the action to be performed on them, and new values for them. + * + * @see https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_UpdateItem.html#DDB-UpdateItem-request-UpdateExpression + * + * @default - No update expression + */ + readonly updateExpression?: string; +} + +/** + * A StepFunctions task to call DynamoUpdateItem + */ +export class DynamoUpdateItem extends sfn.TaskStateBase { + protected readonly taskMetrics?: sfn.TaskMetricsConfig; + protected readonly taskPolicies?: iam.PolicyStatement[]; + + constructor(scope: Construct, id: string, private readonly props: DynamoUpdateItemProps) { + super(scope, id, props); + + this.taskPolicies = [ + new iam.PolicyStatement({ + resources: [ + Stack.of(this).formatArn({ + service: 'dynamodb', + resource: 'table', + resourceName: props.table.tableName, + }), + ], + actions: [`dynamodb:${DynamoMethod.UPDATE}Item`], + }), + ]; + } + + protected renderTask(): any { + return { + Resource: getDynamoResourceArn(DynamoMethod.UPDATE), + Parameters: sfn.FieldUtils.renderObject({ + Key: transformAttributeValueMap(this.props.key), + TableName: this.props.table.tableName, + ConditionExpression: this.props.conditionExpression, + ExpressionAttributeNames: this.props.expressionAttributeNames, + ExpressionAttributeValues: transformAttributeValueMap(this.props.expressionAttributeValues), + ReturnConsumedCapacity: this.props.returnConsumedCapacity, + ReturnItemCollectionMetrics: this.props.returnItemCollectionMetrics, + ReturnValues: this.props.returnValues, + UpdateExpression: this.props.updateExpression, + }), + }; + } +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts index 1bc456d7c47c4..fc4d5f6efc448 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/ecs/run-ecs-task-base.ts @@ -83,7 +83,7 @@ export class EcsRunTaskBase implements ec2.IConnectable, sfn.IStepFunctionsTask if (this.integrationPattern === sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.containerOverrides)) { - throw new Error('Task Token is missing in containerOverrides (pass Context.taskToken somewhere in containerOverrides)'); + throw new Error('Task Token is missing in containerOverrides (pass JsonPath.taskToken somewhere in containerOverrides)'); } for (const override of this.props.containerOverrides || []) { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts index b2a630681bb83..0e13d9d90c1e3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/evaluate-expression.ts @@ -61,7 +61,7 @@ export class EvaluateExpression implements sfn.IStepFunctionsTask { expressionAttributeValues = matches.reduce( (acc, m) => ({ ...acc, - [m]: sfn.Data.stringAt(m), // It's okay to always use `stringAt` here + [m]: sfn.JsonPath.stringAt(m), // It's okay to always use `stringAt` here }), {}, ); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts index 4dad4bf2c295c..ec24d7f518d9c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/index.ts @@ -27,4 +27,8 @@ export * from './glue/run-glue-job-task'; export * from './glue/start-job-run'; export * from './batch/run-batch-job'; export * from './batch/submit-job'; -export * from './dynamodb/call-dynamodb'; +export * from './dynamodb/get-item'; +export * from './dynamodb/put-item'; +export * from './dynamodb/update-item'; +export * from './dynamodb/delete-item'; +export * from './dynamodb/shared-types'; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts index ce7cd397f48c8..8345d12a83011 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/invoke.ts @@ -73,7 +73,7 @@ export class LambdaInvoke extends sfn.TaskStateBase { if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.payload)) { - throw new Error('Task Token is required in `payload` for callback. Use Context.taskToken to set the token.'); + throw new Error('Task Token is required in `payload` for callback. Use JsonPath.taskToken to set the token.'); } this.taskMetrics = { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts index c3ed91e724ca2..cf240ccbfa86f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/lambda/run-lambda-task.ts @@ -22,7 +22,7 @@ export interface RunLambdaTaskProps { * The valid value for Lambda is either FIRE_AND_FORGET or WAIT_FOR_TASK_TOKEN, * it determines whether to pause the workflow until a task token is returned. * - * If this is set to WAIT_FOR_TASK_TOKEN, the Context.taskToken value must be included + * If this is set to WAIT_FOR_TASK_TOKEN, the JsonPath.taskToken value must be included * somewhere in the payload and the Lambda must call * `SendTaskSuccess/SendTaskFailure` using that token. * @@ -79,7 +79,7 @@ export class RunLambdaTask implements sfn.IStepFunctionsTask { if (this.integrationPattern === sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.payload)) { - throw new Error('Task Token is missing in payload (pass Context.taskToken somewhere in payload)'); + throw new Error('Task Token is missing in payload (pass JsonPath.taskToken somewhere in payload)'); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts index 6f1c5f03dcc37..f701f9e20d6e9 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/base-types.ts @@ -308,7 +308,7 @@ export abstract class S3Location { * @param expression the JSON expression resolving to an S3 location URI. */ public static fromJsonExpression(expression: string): S3Location { - return new StandardS3Location({ uri: sfn.Data.stringAt(expression) }); + return new StandardS3Location({ uri: sfn.JsonPath.stringAt(expression) }); } /** diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts index f541a0e692a4f..d57b9114dc7e8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sagemaker/create-training-job.ts @@ -374,7 +374,7 @@ export class SageMakerCreateTrainingJob extends sfn.TaskStateBase implements iam service: 'sagemaker', resource: 'training-job', // If the job name comes from input, we cannot target the policy to a particular ARN prefix reliably... - resourceName: sfn.Data.isJsonPathString(this.props.trainingJobName) ? '*' : `${this.props.trainingJobName}*`, + resourceName: sfn.JsonPath.isEncodedJsonPath(this.props.trainingJobName) ? '*' : `${this.props.trainingJobName}*`, }), ], }), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts index 3473984ada2fa..bc07f33b2bc9b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish-to-topic.ts @@ -71,7 +71,7 @@ export class PublishToTopic implements sfn.IStepFunctionsTask { if (this.integrationPattern === sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN) { if (!sfn.FieldUtils.containsTaskToken(props.message)) { - throw new Error('Task Token is missing in message (pass Context.taskToken somewhere in message)'); + throw new Error('Task Token is missing in message (pass JsonPath.taskToken somewhere in message)'); } } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts index 273257f98743c..a9bca94666963 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sns/publish.ts @@ -73,7 +73,7 @@ export class SnsPublish extends sfn.TaskStateBase { if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) { if (!sfn.FieldUtils.containsTaskToken(props.message)) { - throw new Error('Task Token is required in `message` Use Context.taskToken to set the token.'); + throw new Error('Task Token is required in `message` Use JsonPath.taskToken to set the token.'); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts index c7ec4adb18517..c50c41e952c59 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-message.ts @@ -73,7 +73,7 @@ export class SqsSendMessage extends sfn.TaskStateBase { if (props.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN) { if (!sfn.FieldUtils.containsTaskToken(props.messageBody)) { - throw new Error('Task Token is required in `messageBody` Use Context.taskToken to set the token.'); + throw new Error('Task Token is required in `messageBody` Use JsonPath.taskToken to set the token.'); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts index 4d7dce33f9cf6..36c1d1e7ac6be 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/sqs/send-to-queue.ts @@ -77,7 +77,7 @@ export class SendToQueue implements sfn.IStepFunctionsTask { if (props.integrationPattern === sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN) { if (!sfn.FieldUtils.containsTaskToken(props.messageBody)) { - throw new Error('Task Token is missing in messageBody (pass Context.taskToken somewhere in messageBody)'); + throw new Error('Task Token is missing in messageBody (pass JsonPath.taskToken somewhere in messageBody)'); } } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts index 9ec81c367e4df..0572459f1e4a3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/start-execution.ts @@ -62,7 +62,7 @@ export class StartExecution implements sfn.IStepFunctionsTask { if (this.integrationPattern === sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.input)) { - throw new Error('Task Token is missing in input (pass Context.taskToken somewhere in input)'); + throw new Error('Task Token is missing in input (pass JsonPath.taskToken somewhere in input)'); } } diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts index 5677d5d89021f..4bae47036558c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/stepfunctions/start-execution.ts @@ -55,7 +55,7 @@ export class StepFunctionsStartExecution extends sfn.TaskStateBase { validatePatternSupported(this.integrationPattern, StepFunctionsStartExecution.SUPPORTED_INTEGRATION_PATTERNS); if (this.integrationPattern === sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN && !sfn.FieldUtils.containsTaskToken(props.input)) { - throw new Error('Task Token is required in `input` for callback. Use Context.taskToken to set the token.'); + throw new Error('Task Token is required in `input` for callback. Use JsonPath.taskToken to set the token.'); } this.taskPolicies = this.createScopedAccessPolicy(); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index 7a8d6299c4072..f2dcbba5acc77 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -69,8 +69,9 @@ }, "dependencies": { "@aws-cdk/assets": "0.0.0", - "@aws-cdk/aws-cloudwatch": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", + "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", @@ -91,6 +92,7 @@ "@aws-cdk/assets": "0.0.0", "@aws-cdk/aws-batch": "0.0.0", "@aws-cdk/aws-cloudwatch": "0.0.0", + "@aws-cdk/aws-dynamodb": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", "@aws-cdk/aws-ecr-assets": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.ts index b1c53f9a80710..944039ed40bde 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.run-batch-job.ts @@ -51,7 +51,7 @@ class RunBatchStack extends cdk.Stack { vcpus: 1, }, payload: { - foo: sfn.Data.stringAt('$.bar'), + foo: sfn.JsonPath.stringAt('$.bar'), }, attempts: 3, timeout: cdk.Duration.seconds(60), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts index 86e891d4331ed..5a89db1913334 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/integ.submit-job.ts @@ -50,7 +50,7 @@ class RunBatchStack extends cdk.Stack { vcpus: 1, }, payload: sfn.TaskInput.fromObject({ - foo: sfn.Data.stringAt('$.bar'), + foo: sfn.JsonPath.stringAt('$.bar'), }), attempts: 3, timeout: cdk.Duration.seconds(60), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/run-batch-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/run-batch-job.test.ts index f6be966c1cb85..a9b93a3dd2f2b 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/run-batch-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/run-batch-job.test.ts @@ -86,7 +86,7 @@ test('Task with all the parameters', () => { }, dependsOn: [{ jobId: '1234', type: 'some_type' }], payload: { - foo: sfn.Data.stringAt('$.bar'), + foo: sfn.JsonPath.stringAt('$.bar'), }, attempts: 3, timeout: cdk.Duration.seconds(60), @@ -136,11 +136,11 @@ test('supports tokens', () => { const task = new sfn.Task(stack, 'Task', { task: new tasks.RunBatchJob({ jobDefinition: batchJobDefinition, - jobName: sfn.Data.stringAt('$.jobName'), + jobName: sfn.JsonPath.stringAt('$.jobName'), jobQueue: batchJobQueue, - arraySize: sfn.Data.numberAt('$.arraySize'), - timeout: cdk.Duration.seconds(sfn.Data.numberAt('$.timeout')), - attempts: sfn.Data.numberAt('$.attempts'), + arraySize: sfn.JsonPath.numberAt('$.arraySize'), + timeout: cdk.Duration.seconds(sfn.JsonPath.numberAt('$.timeout')), + attempts: sfn.JsonPath.numberAt('$.attempts'), }), }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts index 6538e8bc1733e..92c8ebeb597b8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/batch/submit-job.test.ts @@ -83,7 +83,7 @@ test('Task with all the parameters', () => { }, dependsOn: [{ jobId: '1234', type: 'some_type' }], payload: sfn.TaskInput.fromObject({ - foo: sfn.Data.stringAt('$.bar'), + foo: sfn.JsonPath.stringAt('$.bar'), }), attempts: 3, timeout: cdk.Duration.seconds(60), @@ -131,11 +131,11 @@ test('supports tokens', () => { // WHEN const task = new BatchSubmitJob(stack, 'Task', { jobDefinition: batchJobDefinition, - jobName: sfn.Data.stringAt('$.jobName'), + jobName: sfn.JsonPath.stringAt('$.jobName'), jobQueue: batchJobQueue, - arraySize: sfn.Data.numberAt('$.arraySize'), - timeout: cdk.Duration.seconds(sfn.Data.numberAt('$.timeout')), - attempts: sfn.Data.numberAt('$.attempts'), + arraySize: sfn.JsonPath.numberAt('$.arraySize'), + timeout: cdk.Duration.seconds(sfn.JsonPath.numberAt('$.timeout')), + attempts: sfn.JsonPath.numberAt('$.attempts'), }); // THEN @@ -175,7 +175,7 @@ test('supports passing task input into payload', () => { // WHEN const task = new BatchSubmitJob(stack, 'Task', { jobDefinition: batchJobDefinition, - jobName: sfn.Data.stringAt('$.jobName'), + jobName: sfn.JsonPath.stringAt('$.jobName'), jobQueue: batchJobQueue, payload: sfn.TaskInput.fromDataAt('$.foo'), }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts deleted file mode 100644 index 13837aaff3bbb..0000000000000 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/call-dynamodb.test.ts +++ /dev/null @@ -1,346 +0,0 @@ -import * as sfn from '@aws-cdk/aws-stepfunctions'; -import * as cdk from '@aws-cdk/core'; -import * as tasks from '../../lib'; - -let stack: cdk.Stack; -const TABLE_NAME = 'SOME_TABLE'; - -beforeEach(() => { - // GIVEN - stack = new cdk.Stack(); -}); - -test('GetItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - sortKey: { - name: 'OTHER_KEY', - value: new tasks.DynamoAttributeValue().withN('4321'), - }, - tableName: TABLE_NAME, - consistentRead: true, - expressionAttributeNames: { OTHER_KEY: '#OK' }, - projectionExpression: [ - new tasks.DynamoProjectionExpression() - .withAttribute('Messages') - .atIndex(1) - .withAttribute('Tags'), - new tasks.DynamoProjectionExpression().withAttribute('ID'), - ], - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:getItem', - ], - ], - }, - End: true, - Parameters: { - Key: { SOME_KEY: { S: '1234' }, OTHER_KEY: { N: '4321' } }, - TableName: TABLE_NAME, - ConsistentRead: true, - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ProjectionExpression: 'Messages[1].Tags,ID', - ReturnConsumedCapacity: 'TOTAL', - }, - }); -}); - -test('PutItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'PutItem', { - task: tasks.CallDynamoDB.putItem({ - item: { SOME_KEY: new tasks.DynamoAttributeValue().withS('1234') }, - tableName: TABLE_NAME, - conditionExpression: 'ForumName <> :f and Subject <> :s', - expressionAttributeNames: { OTHER_KEY: '#OK' }, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - }, - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, - returnValues: tasks.DynamoReturnValues.ALL_NEW, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:putItem', - ], - ], - }, - End: true, - Parameters: { - Item: { SOME_KEY: { S: '1234' } }, - TableName: TABLE_NAME, - ConditionExpression: 'ForumName <> :f and Subject <> :s', - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, - ReturnConsumedCapacity: 'TOTAL', - ReturnItemCollectionMetrics: 'SIZE', - ReturnValues: 'ALL_NEW', - }, - }); -}); - -test('DeleteItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'DeleteItem', { - task: tasks.CallDynamoDB.deleteItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: TABLE_NAME, - conditionExpression: 'ForumName <> :f and Subject <> :s', - expressionAttributeNames: { OTHER_KEY: '#OK' }, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - }, - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, - returnValues: tasks.DynamoReturnValues.ALL_NEW, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:deleteItem', - ], - ], - }, - End: true, - Parameters: { - Key: { SOME_KEY: { S: '1234' } }, - TableName: TABLE_NAME, - ConditionExpression: 'ForumName <> :f and Subject <> :s', - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, - ReturnConsumedCapacity: 'TOTAL', - ReturnItemCollectionMetrics: 'SIZE', - ReturnValues: 'ALL_NEW', - }, - }); -}); - -test('UpdateItem task', () => { - // WHEN - const task = new sfn.Task(stack, 'UpdateItem', { - task: tasks.CallDynamoDB.updateItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: TABLE_NAME, - conditionExpression: 'ForumName <> :f and Subject <> :s', - expressionAttributeNames: { OTHER_KEY: '#OK' }, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - }, - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, - returnValues: tasks.DynamoReturnValues.ALL_NEW, - updateExpression: 'SET TotalCount = TotalCount + :val', - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:updateItem', - ], - ], - }, - End: true, - Parameters: { - Key: { SOME_KEY: { S: '1234' } }, - TableName: TABLE_NAME, - ConditionExpression: 'ForumName <> :f and Subject <> :s', - ExpressionAttributeNames: { OTHER_KEY: '#OK' }, - ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, - ReturnConsumedCapacity: 'TOTAL', - ReturnItemCollectionMetrics: 'SIZE', - ReturnValues: 'ALL_NEW', - UpdateExpression: 'SET TotalCount = TotalCount + :val', - }, - }); -}); - -test('supports tokens', () => { - // WHEN - const task = new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS( - sfn.Data.stringAt('$.partitionKey'), - ), - }, - sortKey: { - name: 'OTHER_KEY', - value: new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.sortKey'), - ), - }, - tableName: sfn.Data.stringAt('$.tableName'), - consistentRead: true, - expressionAttributeNames: { OTHER_KEY: sfn.Data.stringAt('$.otherKey') }, - projectionExpression: [ - new tasks.DynamoProjectionExpression() - .withAttribute('Messages') - .atIndex(1) - .withAttribute('Tags'), - new tasks.DynamoProjectionExpression().withAttribute('ID'), - ], - returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, - }), - }); - - // THEN - expect(stack.resolve(task.toStateJson())).toEqual({ - Type: 'Task', - Resource: { - 'Fn::Join': [ - '', - [ - 'arn:', - { - Ref: 'AWS::Partition', - }, - ':states:::dynamodb:getItem', - ], - ], - }, - End: true, - Parameters: { - // tslint:disable:object-literal-key-quotes - Key: { - SOME_KEY: { 'S.$': '$.partitionKey' }, - OTHER_KEY: { 'N.$': '$.sortKey' }, - }, - 'TableName.$': '$.tableName', - ConsistentRead: true, - ExpressionAttributeNames: { 'OTHER_KEY.$': '$.otherKey' }, - ProjectionExpression: 'Messages[1].Tags,ID', - ReturnConsumedCapacity: 'TOTAL', - }, - }); -}); - -test('Invalid value of TableName should throw', () => { - expect(() => { - new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: 'ab', - }), - }); - }).toThrow( - /TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: ab/, - ); - - expect(() => { - new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: - 'abU93s5MTZDv6TYLk3Q3BE3Hj3AMca3NOb5ypSNZv1JZIONg7p8L8LNxuAStavPxYZKcoG36KwXktkuFHf0jJvt7SKofEqwYHmmK0tNJSkGoPe3MofnB7IWu3V48HbrqNGZqW005CMmDHESQWf40JK8qK0CSQtM8Z64zqysB7SZZazDRm7kKr062RXQKL82nvTxnKxTPfCHiG2YJEhuFdUywHCTN2Rjinl3P7TpwyIuPWyYHm6nZodRKLMmWpgUftZ', - }), - }); - }).toThrow( - /TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: abU93s5MTZDv6TYLk3Q3BE3Hj3AMca3NOb5ypSNZv1JZIONg7p8L8LNxuAStavPxYZKcoG36KwXktkuFHf0jJvt7SKofEqwYHmmK0tNJSkGoPe3MofnB7IWu3V48HbrqNGZqW005CMmDHESQWf40JK8qK0CSQtM8Z64zqysB7SZZazDRm7kKr062RXQKL82nvTxnKxTPfCHiG2YJEhuFdUywHCTN2Rjinl3P7TpwyIuPWyYHm6nZodRKLMmWpgUftZ/, - ); - - expect(() => { - new sfn.Task(stack, 'GetItem', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'SOME_KEY', - value: new tasks.DynamoAttributeValue().withS('1234'), - }, - tableName: 'abcd@', - }), - }); - }).toThrow( - /TableName should not contain alphanumeric characters and should be between 3-255 characters long. Received: abcd@/, - ); -}); - -describe('DynamoProjectionExpression', () => { - test('should correctly configure projectionExpression', () => { - expect( - new tasks.DynamoProjectionExpression() - .withAttribute('Messages') - .atIndex(1) - .atIndex(10) - .withAttribute('Tags') - .withAttribute('Items') - .atIndex(0) - .toString(), - ).toEqual('Messages[1][10].Tags.Items[0]'); - }); - - test('should throw if expression starts with atIndex', () => { - expect(() => - new tasks.DynamoProjectionExpression() - .atIndex(1) - .withAttribute('Messages') - .toString(), - ).toThrow(/Expression must start with an attribute/); - }); -}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts new file mode 100644 index 0000000000000..4a278249f4585 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/delete-item.test.ts @@ -0,0 +1,65 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('DeleteItem task', () => { + // WHEN + const task = new tasks.DynamoDeleteItem(stack, 'DeleteItem', { + key: { SOME_KEY: tasks.DynamoAttributeValue.fromString('1234') }, + table, + conditionExpression: 'ForumName <> :f and Subject <> :s', + expressionAttributeNames: { OTHER_KEY: '#OK' }, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), + }, + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, + returnValues: tasks.DynamoReturnValues.ALL_NEW, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:deleteItem', + ], + ], + }, + End: true, + Parameters: { + Key: { SOME_KEY: { S: '1234' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConditionExpression: 'ForumName <> :f and Subject <> :s', + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, + ReturnConsumedCapacity: 'TOTAL', + ReturnItemCollectionMetrics: 'SIZE', + ReturnValues: 'ALL_NEW', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts new file mode 100644 index 0000000000000..2991c762c025d --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/get-item.test.ts @@ -0,0 +1,115 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('GetItem task', () => { + // WHEN + const task = new tasks.DynamoGetItem(stack, 'GetItem', { + key: { + SOME_KEY: tasks.DynamoAttributeValue.fromString('1234'), + OTHER_KEY: tasks.DynamoAttributeValue.fromNumber(4321), + }, + table, + consistentRead: true, + expressionAttributeNames: { OTHER_KEY: '#OK' }, + projectionExpression: [ + new tasks.DynamoProjectionExpression().withAttribute('Messages').atIndex(1).withAttribute('Tags'), + new tasks.DynamoProjectionExpression().withAttribute('ID'), + ], + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:getItem', + ], + ], + }, + End: true, + Parameters: { + Key: { SOME_KEY: { S: '1234' }, OTHER_KEY: { N: '4321' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConsistentRead: true, + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ProjectionExpression: 'Messages[1].Tags,ID', + ReturnConsumedCapacity: 'TOTAL', + }, + }); +}); + +test('supports tokens', () => { + // WHEN + const task = new tasks.DynamoGetItem(stack, 'GetItem', { + key: { + SOME_KEY: tasks.DynamoAttributeValue.fromString(sfn.JsonPath.stringAt('$.partitionKey')), + OTHER_KEY: tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.sortKey')), + }, + table, + consistentRead: true, + expressionAttributeNames: { OTHER_KEY: sfn.JsonPath.stringAt('$.otherKey') }, + projectionExpression: [ + new tasks.DynamoProjectionExpression().withAttribute('Messages').atIndex(1).withAttribute('Tags'), + new tasks.DynamoProjectionExpression().withAttribute('ID'), + ], + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:getItem', + ], + ], + }, + End: true, + Parameters: { + // tslint:disable:object-literal-key-quotes + Key: { + SOME_KEY: { 'S.$': '$.partitionKey' }, + OTHER_KEY: { 'N.$': '$.sortKey' }, + }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConsistentRead: true, + ExpressionAttributeNames: { 'OTHER_KEY.$': '$.otherKey' }, + ProjectionExpression: 'Messages[1].Tags,ID', + ReturnConsumedCapacity: 'TOTAL', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json index 5bc3b566a2d42..e4876ddef2c6a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.expected.json @@ -1,5 +1,29 @@ { "Resources": { + "Messages804FA4EB": { + "Type": "AWS::DynamoDB::Table", + "Properties": { + "KeySchema": [ + { + "AttributeName": "MessageId", + "KeyType": "HASH" + } + ], + "AttributeDefinitions": [ + { + "AttributeName": "MessageId", + "AttributeType": "S" + } + ], + "ProvisionedThroughput": { + "ReadCapacityUnits": 10, + "WriteCapacityUnits": 5 + }, + "TableName": "Messages" + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, "StateMachineRoleB840431D": { "Type": "AWS::IAM::Role", "Properties": { @@ -52,7 +76,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -76,7 +103,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -100,7 +130,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -124,7 +157,10 @@ { "Ref": "AWS::AccountId" }, - ":table/Messages" + ":table/", + { + "Ref": "Messages804FA4EB" + } ] ] } @@ -143,39 +179,59 @@ "StateMachine2E01A3A5": { "Type": "AWS::StepFunctions::StateMachine", "Properties": { + "RoleArn": { + "Fn::GetAtt": [ + "StateMachineRoleB840431D", + "Arn" + ] + }, "DefinitionString": { "Fn::Join": [ "", [ - "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"PutItem\"},\"PutItem\":{\"Next\":\"GetItemAfterPut\",\"Parameters\":{\"Item\":{\"MessageId\":{\"S\":\"1234\"},\"Text\":{\"S.$\":\"$.bar\"},\"TotalCount\":{\"N\":\"18\"}},\"TableName\":\"Messages\"},\"Type\":\"Task\",\"Resource\":\"arn:", + "{\"StartAt\":\"Start\",\"States\":{\"Start\":{\"Type\":\"Pass\",\"Result\":{\"bar\":\"SomeValue\"},\"Next\":\"PutItem\"},\"PutItem\":{\"Next\":\"GetItemAfterPut\",\"Type\":\"Task\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:putItem\"},\"GetItemAfterPut\":{\"Next\":\"UpdateItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\",\"ConsistentRead\":false},\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:putItem\",\"Parameters\":{\"Item\":{\"MessageId\":{\"S\":\"1234\"},\"Text\":{\"S.$\":\"$.bar\"},\"TotalCount\":{\"N\":\"18\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\"}},\"GetItemAfterPut\":{\"Next\":\"UpdateItem\",\"Type\":\"Task\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:getItem\"},\"UpdateItem\":{\"Next\":\"GetItemAfterUpdate\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\",\"ExpressionAttributeValues\":{\":val\":{\"N.$\":\"$.Item.TotalCount.N\"},\":rand\":{\"N\":\"24\"}},\"UpdateExpression\":\"SET TotalCount = :val + :rand\"},\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:getItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\",\"ConsistentRead\":false}},\"UpdateItem\":{\"Next\":\"GetItemAfterUpdate\",\"Type\":\"Task\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:updateItem\"},\"GetItemAfterUpdate\":{\"Next\":\"DeleteItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\",\"ConsistentRead\":false},\"OutputPath\":\"$.Item.TotalCount.N\",\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:updateItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\",\"ExpressionAttributeValues\":{\":val\":{\"N.$\":\"$.Item.TotalCount.N\"},\":rand\":{\"N\":\"24\"}},\"UpdateExpression\":\"SET TotalCount = :val + :rand\"}},\"GetItemAfterUpdate\":{\"Next\":\"DeleteItem\",\"Type\":\"Task\",\"OutputPath\":\"$.Item.TotalCount.N\",\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:getItem\"},\"DeleteItem\":{\"End\":true,\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"Messages\"},\"Type\":\"Task\",\"Resource\":\"arn:", + ":states:::dynamodb:getItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\",\"ConsistentRead\":false}},\"DeleteItem\":{\"End\":true,\"Type\":\"Task\",\"ResultPath\":null,\"Resource\":\"arn:", { "Ref": "AWS::Partition" }, - ":states:::dynamodb:deleteItem\",\"ResultPath\":null}}}" + ":states:::dynamodb:deleteItem\",\"Parameters\":{\"Key\":{\"MessageId\":{\"S\":\"1234\"}},\"TableName\":\"", + { + "Ref": "Messages804FA4EB" + }, + "\"}}}}" ] ] - }, - "RoleArn": { - "Fn::GetAtt": [ - "StateMachineRoleB840431D", - "Arn" - ] } }, "DependsOn": [ diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts index 7204abd930d46..07e05b7414a9f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/integ.call-dynamodb.ts @@ -1,27 +1,16 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import * as tasks from '../../lib'; /** - * Pre verification steps: - * * aws dynamodb create-table --table-name Messages --key-schema AttributeName=MessageId,KeyType=HASH \ - * * --attribute-definitions AttributeName=MessageId,AttributeType=S \ - * * --provisioned-throughput ReadCapacityUnits=10,WriteCapacityUnits=5 - */ - -/* + * * Stack verification steps: * * aws stepfunctions start-execution --state-machine-arn : should return execution arn * * - * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED - * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the number 42 - */ - -/** - * Post verification steps: - * * aws dynamodb delete-table --table-name Messages + * * aws stepfunctions describe-execution --execution-arn --query 'status': should return status as SUCCEEDED + * * aws stepfunctions describe-execution --execution-arn --query 'output': should return the number 42 */ - class CallDynamoDBStack extends cdk.Stack { constructor(scope: cdk.App, id: string, props: cdk.StackProps = {}) { super(scope, id, props); @@ -31,66 +20,51 @@ class CallDynamoDBStack extends cdk.Stack { const firstNumber = 18; const secondNumber = 24; - const putItemTask = new sfn.Task(this, 'PutItem', { - task: tasks.CallDynamoDB.putItem({ - item: { - MessageId: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - Text: new tasks.DynamoAttributeValue().withS( - sfn.Data.stringAt('$.bar'), - ), - TotalCount: new tasks.DynamoAttributeValue().withN(`${firstNumber}`), - }, - tableName: TABLE_NAME, - }), + const table = new ddb.Table(this, 'Messages', { + tableName: TABLE_NAME, + partitionKey: { + name: 'MessageId', + type: ddb.AttributeType.STRING, + }, + readCapacity: 10, + writeCapacity: 5, + removalPolicy: cdk.RemovalPolicy.DESTROY, + }); + + const putItemTask = new tasks.DynamoPutItem(this, 'PutItem', { + item: { + MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID), + Text: tasks.DynamoAttributeValue.fromString(sfn.JsonPath.stringAt('$.bar')), + TotalCount: tasks.DynamoAttributeValue.fromNumber(firstNumber), + }, + table, }); - const getItemTaskAfterPut = new sfn.Task(this, 'GetItemAfterPut', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - }), + const getItemTaskAfterPut = new tasks.DynamoGetItem(this, 'GetItemAfterPut', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, }); - const updateItemTask = new sfn.Task(this, 'UpdateItem', { - task: tasks.CallDynamoDB.updateItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - expressionAttributeValues: { - ':val': new tasks.DynamoAttributeValue().withN( - sfn.Data.stringAt('$.Item.TotalCount.N'), - ), - ':rand': new tasks.DynamoAttributeValue().withN(`${secondNumber}`), - }, - updateExpression: 'SET TotalCount = :val + :rand', - }), + const updateItemTask = new tasks.DynamoUpdateItem(this, 'UpdateItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), + ':rand': tasks.DynamoAttributeValue.fromNumber(secondNumber), + }, + updateExpression: 'SET TotalCount = :val + :rand', }); - const getItemTaskAfterUpdate = new sfn.Task(this, 'GetItemAfterUpdate', { - task: tasks.CallDynamoDB.getItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - }), - outputPath: sfn.Data.stringAt('$.Item.TotalCount.N'), + const getItemTaskAfterUpdate = new tasks.DynamoGetItem(this, 'GetItemAfterUpdate', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, + outputPath: sfn.JsonPath.stringAt('$.Item.TotalCount.N'), }); - const deleteItemTask = new sfn.Task(this, 'DeleteItem', { - task: tasks.CallDynamoDB.deleteItem({ - partitionKey: { - name: 'MessageId', - value: new tasks.DynamoAttributeValue().withS(MESSAGE_ID), - }, - tableName: TABLE_NAME, - }), - resultPath: 'DISCARD', + const deleteItemTask = new tasks.DynamoDeleteItem(this, 'DeleteItem', { + key: { MessageId: tasks.DynamoAttributeValue.fromString(MESSAGE_ID) }, + table, + resultPath: sfn.JsonPath.DISCARD, }); const definition = new sfn.Pass(this, 'Start', { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts new file mode 100644 index 0000000000000..a21a4decc0b30 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/put-item.test.ts @@ -0,0 +1,65 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('PutItem task', () => { + // WHEN + const task = new tasks.DynamoPutItem(stack, 'PutItem', { + item: { SOME_KEY: tasks.DynamoAttributeValue.fromString('1234') }, + table, + conditionExpression: 'ForumName <> :f and Subject <> :s', + expressionAttributeNames: { OTHER_KEY: '#OK' }, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), + }, + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, + returnValues: tasks.DynamoReturnValues.ALL_NEW, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:putItem', + ], + ], + }, + End: true, + Parameters: { + Item: { SOME_KEY: { S: '1234' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConditionExpression: 'ForumName <> :f and Subject <> :s', + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, + ReturnConsumedCapacity: 'TOTAL', + ReturnItemCollectionMetrics: 'SIZE', + ReturnValues: 'ALL_NEW', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts new file mode 100644 index 0000000000000..d848ef9035132 --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/shared-types.test.ts @@ -0,0 +1,248 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as tasks from '../../lib'; + +describe('DynamoProjectionExpression', () => { + test('should correctly configure projectionExpression', () => { + expect( + new tasks.DynamoProjectionExpression() + .withAttribute('Messages') + .atIndex(1) + .atIndex(10) + .withAttribute('Tags') + .withAttribute('Items') + .atIndex(0) + .toString(), + ).toEqual('Messages[1][10].Tags.Items[0]'); + }); + + test('should throw if expression starts with atIndex', () => { + expect(() => new tasks.DynamoProjectionExpression().atIndex(1).withAttribute('Messages').toString()).toThrow( + /Expression must start with an attribute/, + ); + }); +}); + +describe('DynamoAttributeValue', () => { + test('from string with a string literal', () => { + // GIVEN + const s = 'my-string'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromString(s); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + S: s, + }, + }); + }); + + test('from string with a json path', () => { + // GIVEN + const s = '$.string'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromString(sfn.JsonPath.stringAt(s)); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + 'S.$': s, + }, + }); + }); + + test('from number', () => { + // GIVEN + const n = 9; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromNumber(n); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + N: `${n}`, + }, + }); + }); + + test('number from string', () => { + // GIVEN + const n = '9'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.numberFromString(n); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + N: n, + }, + }); + }); + + test('from binary', () => { + // GIVEN + const b = 'ejBtZ3d0ZmJicQ=='; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromBinary(b); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + B: b, + }, + }); + }); + + test('from string set', () => { + // GIVEN + const ss = ['apple', 'banana']; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromStringSet(ss); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + SS: ss, + }, + }); + }); + + test('from number set', () => { + // GIVEN + const ns = [1, 2]; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromNumberSet(ns); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + NS: ['1', '2'], + }, + }); + }); + + test('number set from strings', () => { + // GIVEN + const ns = ['1', '2']; + + // WHEN + const attribute = tasks.DynamoAttributeValue.numberSetFromStrings(ns); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + NS: ns, + }, + }); + }); + + test('from binary set', () => { + // GIVEN + const bs = ['Y2RrIGlzIGF3ZXNvbWU=', 'ejBtZ3d0ZmJicQ==']; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromBinarySet(bs); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + BS: bs, + }, + }); + }); + + test('from map', () => { + // GIVEN + const m = { cdk: tasks.DynamoAttributeValue.fromString('is-cool') }; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromMap(m); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + M: { + cdk: { S: 'is-cool' }, + }, + }, + }); + }); + + test('map from json path', () => { + // GIVEN + const m = '$.path'; + + // WHEN + const attribute = tasks.DynamoAttributeValue.mapFromJsonPath(m); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + 'M.$': m, + }, + }); + }); + + test('map from invalid json path throws', () => { + // GIVEN + const m = 'invalid'; + + // WHEN / THEN + expect(() => { + tasks.DynamoAttributeValue.mapFromJsonPath(m); + }).toThrow("Data JSON path values must either be exactly equal to '$' or start with '$.'"); + }); + + test('from list', () => { + // GIVEN + const l = [tasks.DynamoAttributeValue.fromString('a string'), tasks.DynamoAttributeValue.fromNumber(7)]; + + // WHEN + const attribute = tasks.DynamoAttributeValue.fromList(l); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + L: [ + { + S: 'a string', + }, + { + N: '7', + }, + ], + }, + }); + }); + + test('from null', () => { + // WHEN + const attribute = tasks.DynamoAttributeValue.fromNull(true); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + NULL: true, + }, + }); + }); + + test('from boolean', () => { + // WHEN + const attribute = tasks.DynamoAttributeValue.fromBoolean(true); + + // THEN + expect(sfn.FieldUtils.renderObject(attribute)).toEqual({ + attributeValue: { + BOOL: true, + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts new file mode 100644 index 0000000000000..9a4eb3cbcb66a --- /dev/null +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/dynamodb/update-item.test.ts @@ -0,0 +1,67 @@ +import * as ddb from '@aws-cdk/aws-dynamodb'; +import * as sfn from '@aws-cdk/aws-stepfunctions'; +import * as cdk from '@aws-cdk/core'; +import * as tasks from '../../lib'; + +let stack: cdk.Stack; +let table: ddb.Table; + +beforeEach(() => { + // GIVEN + stack = new cdk.Stack(); + table = new ddb.Table(stack, 'my-table', { + tableName: 'my-table', + partitionKey: { + name: 'name', + type: ddb.AttributeType.STRING, + }, + }); +}); + +test('UpdateItem task', () => { + // WHEN + const task = new tasks.DynamoUpdateItem(stack, 'UpdateItem', { + key: { SOME_KEY: tasks.DynamoAttributeValue.fromString('1234') }, + table, + conditionExpression: 'ForumName <> :f and Subject <> :s', + expressionAttributeNames: { OTHER_KEY: '#OK' }, + expressionAttributeValues: { + ':val': tasks.DynamoAttributeValue.numberFromString(sfn.JsonPath.stringAt('$.Item.TotalCount.N')), + }, + returnConsumedCapacity: tasks.DynamoConsumedCapacity.TOTAL, + returnItemCollectionMetrics: tasks.DynamoItemCollectionMetrics.SIZE, + returnValues: tasks.DynamoReturnValues.ALL_NEW, + updateExpression: 'SET TotalCount = TotalCount + :val', + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::dynamodb:updateItem', + ], + ], + }, + End: true, + Parameters: { + Key: { SOME_KEY: { S: '1234' } }, + TableName: { + Ref: 'mytable0324D45C', + }, + ConditionExpression: 'ForumName <> :f and Subject <> :s', + ExpressionAttributeNames: { OTHER_KEY: '#OK' }, + ExpressionAttributeValues: { ':val': { 'N.$': '$.Item.TotalCount.N' } }, + ReturnConsumedCapacity: 'TOTAL', + ReturnItemCollectionMetrics: 'SIZE', + ReturnValues: 'ALL_NEW', + UpdateExpression: 'SET TotalCount = TotalCount + :val', + }, + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts index 611339b3fe08a..a644363a9c077 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/ecs-tasks.test.ts @@ -64,7 +64,7 @@ test('Running a Fargate Task', () => { { containerName: 'TheContainer', environment: [ - {name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey')}, + {name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey')}, ], }, ], @@ -173,7 +173,7 @@ test('Running an EC2 Task with bridge network', () => { { containerName: 'TheContainer', environment: [ - {name: 'SOME_KEY', value: sfn.Data.stringAt('$.SomeKey')}, + {name: 'SOME_KEY', value: sfn.JsonPath.stringAt('$.SomeKey')}, ], }, ], @@ -331,9 +331,9 @@ test('Running an EC2 Task with overridden number values', () => { containerOverrides: [ { containerName: 'TheContainer', - command: sfn.Data.listAt('$.TheCommand'), + command: sfn.JsonPath.listAt('$.TheCommand'), cpu: 5, - memoryLimit: sfn.Data.numberAt('$.MemoryLimit'), + memoryLimit: sfn.JsonPath.numberAt('$.MemoryLimit'), }, ], }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts index aa4238f85a218..dc0a42c1d7bd8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.ec2-task.ts @@ -44,7 +44,7 @@ const definition = new sfn.Pass(stack, 'Start', { environment: [ { name: 'SOME_KEY', - value: sfn.Data.stringAt('$.SomeKey'), + value: sfn.JsonPath.stringAt('$.SomeKey'), }, ], }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts index 8c05adb3dbcba..4f981dfaa45b8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/ecs/integ.fargate-task.ts @@ -43,7 +43,7 @@ const definition = new sfn.Pass(stack, 'Start', { environment: [ { name: 'SOME_KEY', - value: sfn.Data.stringAt('$.SomeKey'), + value: sfn.JsonPath.stringAt('$.SomeKey'), }, ], }, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts index 63633412a4933..489c5612b5f2f 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-add-step.test.ts @@ -51,7 +51,7 @@ test('Add Step with static ClusterId and Step configuration', () => { test('Terminate cluster with ClusterId from payload and static Step configuration', () => { // WHEN const task = new tasks.EmrAddStep(stack, 'Task', { - clusterId: sfn.Data.stringAt('$.ClusterId'), + clusterId: sfn.JsonPath.stringAt('$.ClusterId'), name: 'StepName', jar: 'Jar', actionOnFailure: tasks.ActionOnFailure.CONTINUE, @@ -91,7 +91,7 @@ test('Add Step with static ClusterId and Step Name from payload', () => { // WHEN const task = new tasks.EmrAddStep(stack, 'Task', { clusterId: 'ClusterId', - name: sfn.Data.stringAt('$.StepName'), + name: sfn.JsonPath.stringAt('$.StepName'), jar: 'Jar', actionOnFailure: tasks.ActionOnFailure.CONTINUE, integrationPattern: sfn.IntegrationPattern.RUN_JOB, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts index 06d1a2da8d34b..54fdf8a83e63d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-create-cluster.test.ts @@ -813,7 +813,7 @@ test('Create Cluster with InstanceGroup', () => { evaluationPeriods: 1, metricName: 'Name', namespace: 'Namespace', - period: cdk.Duration.seconds(sfn.Data.numberAt('$.CloudWatchPeriod')), + period: cdk.Duration.seconds(sfn.JsonPath.numberAt('$.CloudWatchPeriod')), statistic: EmrCreateCluster.CloudWatchAlarmStatistic.AVERAGE, threshold: 1, unit: EmrCreateCluster.CloudWatchAlarmUnit.NONE, diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts index e6612d92b1cf4..bd8fe53e7ff51 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-fleet-by-name.test.ts @@ -48,7 +48,7 @@ test('Modify an InstanceFleet with static ClusterId, InstanceFleetName, and Inst test('Modify an InstanceFleet with ClusterId from payload and static InstanceFleetName and InstanceFleetConfiguration', () => { // WHEN const task = new tasks.EmrModifyInstanceFleetByName(stack, 'Task', { - clusterId: sfn.Data.stringAt('$.ClusterId'), + clusterId: sfn.JsonPath.stringAt('$.ClusterId'), instanceFleetName: 'InstanceFleetName', targetOnDemandCapacity: 2, targetSpotCapacity: 0, @@ -85,7 +85,7 @@ test('Modify an InstanceFleet with static ClusterId and InstanceFleetConfigurate // WHEN const task = new tasks.EmrModifyInstanceFleetByName(stack, 'Task', { clusterId: 'ClusterId', - instanceFleetName: sfn.Data.stringAt('$.InstanceFleetName'), + instanceFleetName: sfn.JsonPath.stringAt('$.InstanceFleetName'), targetOnDemandCapacity: 2, targetSpotCapacity: 0, }); @@ -122,8 +122,8 @@ test('Modify an InstanceFleet with static ClusterId and InstanceFleetName and Ta const task = new tasks.EmrModifyInstanceFleetByName(stack, 'Task', { clusterId: 'ClusterId', instanceFleetName: 'InstanceFleetName', - targetOnDemandCapacity: sfn.Data.numberAt('$.TargetOnDemandCapacity'), - targetSpotCapacity: sfn.Data.numberAt('$.TargetSpotCapacity'), + targetOnDemandCapacity: sfn.JsonPath.numberAt('$.TargetOnDemandCapacity'), + targetSpotCapacity: sfn.JsonPath.numberAt('$.TargetSpotCapacity'), }); // THEN diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts index f9bc8886de0d3..51bd3600147c1 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/emr/emr-modify-instance-group-by-name.test.ts @@ -78,7 +78,7 @@ test('Modify an InstanceGroup with static ClusterId, InstanceGroupName, and Inst test('Modify an InstanceGroup with ClusterId from payload and static InstanceGroupName and InstanceGroupConfiguration', () => { // WHEN const task = new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { - clusterId: sfn.Data.stringAt('$.ClusterId'), + clusterId: sfn.JsonPath.stringAt('$.ClusterId'), instanceGroupName: 'InstanceGroupName', instanceGroup: { instanceCount: 1, @@ -115,7 +115,7 @@ test('Modify an InstanceGroup with static ClusterId and InstanceGroupConfigurate // WHEN const task = new tasks.EmrModifyInstanceGroupByName(stack, 'Task', { clusterId: 'ClusterId', - instanceGroupName: sfn.Data.stringAt('$.InstanceGroupName'), + instanceGroupName: sfn.JsonPath.stringAt('$.InstanceGroupName'), instanceGroup: { instanceCount: 1, }, @@ -153,7 +153,7 @@ test('Modify an InstanceGroup with static ClusterId and InstanceGroupName and In clusterId: 'ClusterId', instanceGroupName: 'InstanceGroupName', instanceGroup: { - instanceCount: sfn.Data.numberAt('$.InstanceCount'), + instanceCount: sfn.JsonPath.numberAt('$.InstanceCount'), }, }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts index 8624c601f8c83..865e42e9033c5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/integ.start-execution.ts @@ -21,7 +21,7 @@ class TestStack extends Stack { definition: new sfn.Task(this, 'Task', { task: new tasks.StartExecution(child, { input: { - hello: sfn.Data.stringAt('$.hello'), + hello: sfn.JsonPath.stringAt('$.hello'), }, integrationPattern: sfn.ServiceIntegrationPattern.SYNC, }), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.ts index d3791f59782ed..7ff38d135a72d 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/integ.invoke-function.ts @@ -27,7 +27,7 @@ const taskTokenHandler = new sfn.Task(stack, 'Invoke Handler with task token', { task: new tasks.RunLambdaTask(callbackHandler, { integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ - token: sfn.Context.taskToken, + token: sfn.JsonPath.taskToken, }), }), inputPath: '$.guid', diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke-function.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke-function.test.ts index 2c5b51d4a347e..eb5341281e6b2 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke-function.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke-function.test.ts @@ -39,7 +39,7 @@ test('Lambda function payload ends up in Parameters', () => { definition: new sfn.Task(stack, 'Task', { task: new tasks.InvokeFunction(fn, { payload: { - foo: sfn.Data.stringAt('$.bar'), + foo: sfn.JsonPath.stringAt('$.bar'), }, }), }), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts index 62a6d4c632e23..5bb39f17385ff 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/invoke.test.ts @@ -106,7 +106,7 @@ describe('LambdaInvoke', () => { lambdaFunction, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ - token: sfn.Context.taskToken, + token: sfn.JsonPath.taskToken, }), qualifier: 'my-alias', }); @@ -183,7 +183,7 @@ describe('LambdaInvoke', () => { lambdaFunction, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, }); - }).toThrow(/Task Token is required in `payload` for callback. Use Context.taskToken to set the token./); + }).toThrow(/Task Token is required in `payload` for callback. Use JsonPath.taskToken to set the token./); }); test('fails when RUN_JOB integration pattern is used', () => { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/run-lambda-task.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/run-lambda-task.test.ts index 6d46eeaf368cb..46538a2b41cd6 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/run-lambda-task.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/lambda/run-lambda-task.test.ts @@ -64,7 +64,7 @@ test('Lambda function can be used in a Task with Task Token', () => { task: new tasks.RunLambdaTask(fn, { integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, payload: sfn.TaskInput.fromObject({ - token: sfn.Context.taskToken, + token: sfn.JsonPath.taskToken, }), }), }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts index 4f02f9ac048a1..5fb84bfee9981 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-training-job.test.ts @@ -294,7 +294,7 @@ test('pass param to training job', () => { }); const task = new SageMakerCreateTrainingJob(stack, 'TrainSagemaker', { - trainingJobName: sfn.Data.stringAt('$.JobName'), + trainingJobName: sfn.JsonPath.stringAt('$.JobName'), role, algorithmSpecification: { algorithmName: 'BlazingText', diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts index c53233523cfa7..7777f31e70074 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sagemaker/create-transform-job.test.ts @@ -184,8 +184,8 @@ test('create complex transform job', () => { test('pass param to transform job', () => { // WHEN const task = new SageMakerCreateTransformJob(stack, 'TransformTask', { - transformJobName: sfn.Data.stringAt('$.TransformJobName'), - modelName: sfn.Data.stringAt('$.ModelName'), + transformJobName: sfn.JsonPath.stringAt('$.TransformJobName'), + modelName: sfn.JsonPath.stringAt('$.ModelName'), role, transformInput: { transformDataSource: { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish-to-topic.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish-to-topic.test.ts index 162b3b3eee1f6..38da096791e54 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish-to-topic.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish-to-topic.test.ts @@ -46,7 +46,7 @@ test('Publish JSON to SNS topic with task token', () => { integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, message: sfn.TaskInput.fromObject({ Input: 'Publish this message', - Token: sfn.Context.taskToken, + Token: sfn.JsonPath.taskToken, }), }) }); @@ -93,7 +93,7 @@ test('Task throws if WAIT_FOR_TASK_TOKEN is supplied but task token is not inclu test('Publish to topic with ARN from payload', () => { // GIVEN const stack = new cdk.Stack(); - const topic = sns.Topic.fromTopicArn(stack, 'Topic', sfn.Data.stringAt('$.topicArn')); + const topic = sns.Topic.fromTopicArn(stack, 'Topic', sfn.JsonPath.stringAt('$.topicArn')); // WHEN const pub = new sfn.Task(stack, 'Publish', { task: new tasks.PublishToTopic(topic, { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts index c63fb72fb719a..384c578641560 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sns/publish.test.ts @@ -50,7 +50,7 @@ describe('Publish', () => { integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, message: sfn.TaskInput.fromObject({ Input: 'Publish this message', - Token: sfn.Context.taskToken, + Token: sfn.JsonPath.taskToken, }), }); @@ -128,7 +128,7 @@ describe('Publish', () => { test('topic ARN supplied through the task input', () => { // GIVEN const stack = new cdk.Stack(); - const topic = sns.Topic.fromTopicArn(stack, 'Topic', sfn.Data.stringAt('$.topicArn')); + const topic = sns.Topic.fromTopicArn(stack, 'Topic', sfn.JsonPath.stringAt('$.topicArn')); // WHEN const task = new SnsPublish(stack, 'Publish', { @@ -172,7 +172,7 @@ describe('Publish', () => { message: sfn.TaskInput.fromText('Publish this message'), }); // THEN - }).toThrow(/Task Token is required in `message` Use Context.taskToken to set the token./); + }).toThrow(/Task Token is required in `message` Use JsonPath.taskToken to set the token./); }); test('fails when RUN_JOB integration pattern is used', () => { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts index 3ce5cd3cc6a1b..ee1ff021571e8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-message.test.ts @@ -48,7 +48,7 @@ describe('SqsSendMessage', () => { const task = new SqsSendMessage(stack, 'Send', { queue, messageBody: sfn.TaskInput.fromText('Send this message'), - messageDeduplicationId: sfn.Data.stringAt('$.deduping'), + messageDeduplicationId: sfn.JsonPath.stringAt('$.deduping'), comment: 'sending a message to my SQS queue', delay: cdk.Duration.seconds(30), }); @@ -86,7 +86,7 @@ describe('SqsSendMessage', () => { integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, messageBody: sfn.TaskInput.fromObject({ Input: 'Send this message', - Token: sfn.Context.taskToken, + Token: sfn.JsonPath.taskToken, }), }); @@ -152,7 +152,7 @@ describe('SqsSendMessage', () => { queue, messageBody: sfn.TaskInput.fromObject({ literal: 'literal', - SomeInput: sfn.Data.stringAt('$.theMessage'), + SomeInput: sfn.JsonPath.stringAt('$.theMessage'), }), }); @@ -223,7 +223,7 @@ describe('SqsSendMessage', () => { integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, messageBody: sfn.TaskInput.fromText('Send this message'), }); - }).toThrow(/Task Token is required in `messageBody` Use Context.taskToken to set the token./); + }).toThrow(/Task Token is required in `messageBody` Use JsonPath.taskToken to set the token./); }); test('fails when RUN_JOB integration pattern is used', () => { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-to-queue.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-to-queue.test.ts index 7b77ba34f2592..69e051c903a55 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-to-queue.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/sqs/send-to-queue.test.ts @@ -16,7 +16,7 @@ test('Send message to queue', () => { // WHEN const task = new sfn.Task(stack, 'Send', { task: new tasks.SendToQueue(queue, { messageBody: sfn.TaskInput.fromText('Send this message'), - messageDeduplicationId: sfn.Data.stringAt('$.deduping'), + messageDeduplicationId: sfn.JsonPath.stringAt('$.deduping'), }) }); // THEN @@ -49,7 +49,7 @@ test('Send message to SQS queue with task token', () => { integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, messageBody: sfn.TaskInput.fromObject({ Input: 'Send this message', - Token: sfn.Context.taskToken, + Token: sfn.JsonPath.taskToken, }), }) }); @@ -127,7 +127,7 @@ test('Message body can be an object', () => { task: new tasks.SendToQueue(queue, { messageBody: sfn.TaskInput.fromObject({ literal: 'literal', - SomeInput: sfn.Data.stringAt('$.theMessage'), + SomeInput: sfn.JsonPath.stringAt('$.theMessage'), }), }), }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts index 5d1ef3171debc..ee6a252ab09c3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/start-execution.test.ts @@ -181,7 +181,7 @@ test('Execute State Machine - Wait For Task Token', () => { task: new tasks.StartExecution(child, { integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, input: { - token: sfn.Context.taskToken, + token: sfn.JsonPath.taskToken, }, }), }); @@ -223,5 +223,5 @@ test('Execute State Machine - Wait For Task Token - Missing Task Token', () => { integrationPattern: sfn.ServiceIntegrationPattern.WAIT_FOR_TASK_TOKEN, }), }); - }).toThrow('Task Token is missing in input (pass Context.taskToken somewhere in input'); + }).toThrow('Task Token is missing in input (pass JsonPath.taskToken somewhere in input'); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts index 012189950cecd..f103d41da4a7c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/integ.start-execution.ts @@ -21,7 +21,7 @@ class TestStack extends Stack { definition: new StepFunctionsStartExecution(this, 'Task', { stateMachine: child, input: sfn.TaskInput.fromObject({ - hello: sfn.Data.stringAt('$.hello'), + hello: sfn.JsonPath.stringAt('$.hello'), }), integrationPattern: sfn.IntegrationPattern.RUN_JOB, }), diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts index 21d2546af8681..99228dc1491d5 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/stepfunctions/start-execution.test.ts @@ -173,7 +173,7 @@ test('Execute State Machine - Wait For Task Token', () => { stateMachine: child, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, input: sfn.TaskInput.fromObject({ - token: sfn.Context.taskToken, + token: sfn.JsonPath.taskToken, }), }); @@ -213,5 +213,5 @@ test('Execute State Machine - Wait For Task Token - Missing Task Token', () => { stateMachine: child, integrationPattern: sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, }); - }).toThrow('Task Token is required in `input` for callback. Use Context.taskToken to set the token.'); + }).toThrow('Task Token is required in `input` for callback. Use JsonPath.taskToken to set the token.'); }); diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index e8ed1e4fffef1..22e847ed058d9 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -277,7 +277,7 @@ execute the same steps for multiple entries of an array in the state input. ```ts const map = new stepfunctions.Map(this, 'Map State', { maxConcurrency: 1, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap') + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap') }); map.iterator(new stepfunctions.Pass(this, 'Pass State')); ``` diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts b/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts index ab4ddfb070483..407f93cd3d162 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/fields.ts @@ -1,8 +1,90 @@ import { Token } from '@aws-cdk/core'; import { findReferencedPaths, jsonPathString, JsonPathToken, renderObject } from './json-path'; +/** + * Extract a field from the State Machine data or context + * that gets passed around between states + * + * @see https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-paths.html + */ +export class JsonPath { + /** + * Special string value to discard state input, output or result + */ + public static readonly DISCARD = 'DISCARD'; + + /** + * Instead of using a literal string, get the value from a JSON path + */ + public static stringAt(path: string): string { + validateJsonPath(path); + return new JsonPathToken(path).toString(); + } + + /** + * Instead of using a literal string list, get the value from a JSON path + */ + public static listAt(path: string): string[] { + // does not apply to task context + validateDataPath(path); + return Token.asList(new JsonPathToken(path)); + } + + /** + * Instead of using a literal number, get the value from a JSON path + */ + public static numberAt(path: string): number { + validateJsonPath(path); + return Token.asNumber(new JsonPathToken(path)); + } + + /** + * Use the entire data structure + * + * Will be an object at invocation time, but is represented in the CDK + * application as a string. + */ + public static get entirePayload(): string { + return new JsonPathToken('$').toString(); + } + + /** + * Determines if the indicated string is an encoded JSON path + * + * @param value string to be evaluated + */ + public static isEncodedJsonPath(value: string): boolean { + return !!jsonPathString(value); + } + + /** + * Return the Task Token field + * + * External actions will need this token to report step completion + * back to StepFunctions using the `SendTaskSuccess` or `SendTaskFailure` + * calls. + */ + public static get taskToken(): string { + return new JsonPathToken('$$.Task.Token').toString(); + } + + /** + * Use the entire context data structure + * + * Will be an object at invocation time, but is represented in the CDK + * application as a string. + */ + public static get entireContext(): string { + return new JsonPathToken('$$').toString(); + } + + private constructor() {} +} + /** * Extract a field from the State Machine data that gets passed around between states + * + * @deprecated replaced by `JsonPath` */ export class Data { /** @@ -48,14 +130,15 @@ export class Data { return !!jsonPathString(value); } - private constructor() { - } + private constructor() {} } /** * Extract a field from the State Machine Context data * * @see https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#wait-token-contextobject + * + * @deprecated replaced by `JsonPath` */ export class Context { /** @@ -95,26 +178,24 @@ export class Context { return new JsonPathToken('$$').toString(); } - private constructor() { - } + private constructor() {} } /** * Helper functions to work with structures containing fields */ export class FieldUtils { - /** * Render a JSON structure containing fields to the right StepFunctions structure */ - public static renderObject(obj?: {[key: string]: any}): {[key: string]: any} | undefined { + public static renderObject(obj?: { [key: string]: any }): { [key: string]: any } | undefined { return renderObject(obj); } /** * Return all JSON paths used in the given structure */ - public static findReferencedPaths(obj?: {[key: string]: any}): string[] { + public static findReferencedPaths(obj?: { [key: string]: any }): string[] { return Array.from(findReferencedPaths(obj)).sort(); } @@ -124,12 +205,17 @@ export class FieldUtils { * The field is considered included if the field itself or one of its containing * fields occurs anywhere in the payload. */ - public static containsTaskToken(obj?: {[key: string]: any}): boolean { + public static containsTaskToken(obj?: { [key: string]: any }): boolean { const paths = findReferencedPaths(obj); return paths.has('$$.Task.Token') || paths.has('$$.Task') || paths.has('$$'); } - private constructor() { + private constructor() {} +} + +function validateJsonPath(path: string) { + if (path !== '$' && !path.startsWith('$.') && path !== '$$' && !path.startsWith('$$.')) { + throw new Error(`JSON path values must be exactly '$', '$$', start with '$.' or start with '$$.' Received: ${path}`); } } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/input.ts b/packages/@aws-cdk/aws-stepfunctions/lib/input.ts index 9fdf42c0ef3f4..c72b1c01670e7 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/input.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/input.ts @@ -1,4 +1,4 @@ -import { Context, Data } from './fields'; +import { JsonPath } from './fields'; /** * Type union for task classes that accept multiple types of payload @@ -19,10 +19,21 @@ export class TaskInput { * This object may contain Data and Context fields * as object values, if desired. */ - public static fromObject(obj: {[key: string]: any}) { + public static fromObject(obj: { [key: string]: any }) { return new TaskInput(InputType.OBJECT, obj); } + /** + * Use a part of the execution data or task context as task input + * + * Use this when you want to use a subobject or string from + * the current state machine execution or the current task context + * as complete payload to a task. + */ + public static fromJsonPathAt(path: string) { + return new TaskInput(InputType.TEXT, JsonPath.stringAt(path)); + } + /** * Use a part of the execution data as task input * @@ -31,7 +42,7 @@ export class TaskInput { * to a task. */ public static fromDataAt(path: string) { - return new TaskInput(InputType.TEXT, Data.stringAt(path)); + return new TaskInput(InputType.TEXT, JsonPath.stringAt(path)); } /** @@ -42,7 +53,7 @@ export class TaskInput { * to a task. */ public static fromContextAt(path: string) { - return new TaskInput(InputType.TEXT, Context.stringAt(path)); + return new TaskInput(InputType.TEXT, JsonPath.stringAt(path)); } /** @@ -51,8 +62,7 @@ export class TaskInput { * @param value payload for the corresponding input type. * It can be a JSON-encoded object, context, data, etc. */ - private constructor(public readonly type: InputType, public readonly value: any) { - } + private constructor(public readonly type: InputType, public readonly value: any) {} } /** @@ -75,11 +85,11 @@ export enum InputType { * example: * { * literal: 'literal', - * SomeInput: sfn.Data.stringAt('$.someField') + * SomeInput: sfn.JsonPath.stringAt('$.someField') * } * * @see https://docs.aws.amazon.com/step-functions/latest/dg/concepts-state-machine-data.html * @see https://docs.aws.amazon.com/step-functions/latest/dg/input-output-contextobject.html */ - OBJECT -} \ No newline at end of file + OBJECT, +} diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/map.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/map.ts index 2721b975d43d0..14d65ccdc541a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/map.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/map.ts @@ -19,7 +19,7 @@ export interface MapProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default $ @@ -29,7 +29,7 @@ export interface MapProps { /** * JSONPath expression to select part of the state to be the output to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default $ @@ -39,7 +39,7 @@ export interface MapProps { /** * JSONPath expression to indicate where to inject the state's output * - * May also be the special value DISCARD, which will cause the state's + * May also be the special value JsonPath.DISCARD, which will cause the state's * input to become its output. * * @default $ diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts index 281660036e4db..4bce7313377cf 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/parallel.ts @@ -19,7 +19,7 @@ export interface ParallelProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default $ @@ -29,7 +29,7 @@ export interface ParallelProps { /** * JSONPath expression to select part of the state to be the output to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default $ @@ -39,7 +39,7 @@ export interface ParallelProps { /** * JSONPath expression to indicate where to inject the state's output * - * May also be the special value DISCARD, which will cause the state's + * May also be the special value JsonPath.DISCARD, which will cause the state's * input to become its output. * * @default $ diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts index 982980456caaa..6d98608853b68 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/pass.ts @@ -66,7 +66,7 @@ export interface PassProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default $ @@ -76,7 +76,7 @@ export interface PassProps { /** * JSONPath expression to select part of the state to be the output to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default $ @@ -86,7 +86,7 @@ export interface PassProps { /** * JSONPath expression to indicate where to inject the state's output * - * May also be the special value DISCARD, which will cause the state's + * May also be the special value JsonPath.DISCARD, which will cause the state's * input to become its output. * * @default $ diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts index 5f69d2af2d80b..8d82eecd398d7 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/state.ts @@ -1,7 +1,8 @@ import * as cdk from '@aws-cdk/core'; import { Condition } from '../condition'; +import { JsonPath } from '../fields'; import { StateGraph } from '../state-graph'; -import { CatchProps, DISCARD, Errors, IChainable, INextable, RetryProps } from '../types'; +import { CatchProps, Errors, IChainable, INextable, RetryProps } from '../types'; /** * Properties shared by all states @@ -17,7 +18,7 @@ export interface StateProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default $ @@ -37,7 +38,7 @@ export interface StateProps { /** * JSONPath expression to select part of the state to be the output to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default $ @@ -47,7 +48,7 @@ export interface StateProps { /** * JSONPath expression to indicate where to inject the state's output * - * May also be the special value DISCARD, which will cause the state's + * May also be the special value JsonPath.DISCARD, which will cause the state's * input to become its output. * * @default $ @@ -512,7 +513,7 @@ export function renderList(xs: T[], fn: (x: T) => any): any { */ export function renderJsonPath(jsonPath?: string): undefined | null | string { if (jsonPath === undefined) { return undefined; } - if (jsonPath === DISCARD) { return null; } + if (jsonPath === JsonPath.DISCARD) { return null; } if (!jsonPath.startsWith('$')) { throw new Error(`Expected JSON path to start with '$', got: ${jsonPath}`); diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts index b4f4da1d38c2b..75d51840e0234 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/succeed.ts @@ -17,7 +17,7 @@ export interface SucceedProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default $ @@ -27,7 +27,7 @@ export interface SucceedProps { /** * JSONPath expression to select part of the state to be the output to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default $ diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts index 4617ae15adb41..e34875a02fbcc 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task-base.ts @@ -21,7 +21,7 @@ export interface TaskStateBaseProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default - The entire task input (JSON path '$') @@ -32,7 +32,7 @@ export interface TaskStateBaseProps { * JSONPath expression to select select a portion of the state output to pass * to the next state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default - The entire JSON node determined by the state input, the task result, @@ -43,7 +43,7 @@ export interface TaskStateBaseProps { /** * JSONPath expression to indicate where to inject the state's output * - * May also be the special value DISCARD, which will cause the state's + * May also be the special value JsonPath.DISCARD, which will cause the state's * input to become its output. * * @default - Replaces the entire input with the result (JSON path '$') diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts index 243f6b520b41c..1d275d3fb25f8 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/states/task.ts @@ -28,7 +28,7 @@ export interface TaskProps { /** * JSONPath expression to select part of the state to be the input to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * input to be the empty object {}. * * @default $ @@ -38,7 +38,7 @@ export interface TaskProps { /** * JSONPath expression to select part of the state to be the output to this state. * - * May also be the special value DISCARD, which will cause the effective + * May also be the special value JsonPath.DISCARD, which will cause the effective * output to be the empty object {}. * * @default $ @@ -48,7 +48,7 @@ export interface TaskProps { /** * JSONPath expression to indicate where to inject the state's output * - * May also be the special value DISCARD, which will cause the state's + * May also be the special value JsonPath.DISCARD, which will cause the state's * input to become its output. * * @default $ diff --git a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts index 618ae4a6f5bb0..ef6d15fd86519 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts @@ -1,5 +1,5 @@ import '@aws-cdk/assert/jest'; -import { Context, Data, FieldUtils } from '../lib'; +import { FieldUtils, JsonPath } from '../lib'; describe('Fields', () => { test('deep replace correctly handles fields in arrays', () => { @@ -8,12 +8,12 @@ describe('Fields', () => { unknown: undefined, bool: true, literal: 'literal', - field: Data.stringAt('$.stringField'), - listField: Data.listAt('$.listField'), + field: JsonPath.stringAt('$.stringField'), + listField: JsonPath.listAt('$.listField'), deep: [ 'literal', { - deepField: Data.numberAt('$.numField'), + deepField: JsonPath.numberAt('$.numField'), }, ], }), @@ -33,10 +33,10 @@ describe('Fields', () => { test('exercise contextpaths', () => { expect( FieldUtils.renderObject({ - str: Context.stringAt('$$.Execution.StartTime'), - count: Context.numberAt('$$.State.RetryCount'), - token: Context.taskToken, - entire: Context.entireContext, + str: JsonPath.stringAt('$$.Execution.StartTime'), + count: JsonPath.numberAt('$$.State.RetryCount'), + token: JsonPath.taskToken, + entire: JsonPath.entireContext, }), ).toStrictEqual({ 'str.$': '$$.Execution.StartTime', @@ -50,13 +50,13 @@ describe('Fields', () => { FieldUtils.findReferencedPaths({ bool: false, literal: 'literal', - field: Data.stringAt('$.stringField'), - listField: Data.listAt('$.listField'), + field: JsonPath.stringAt('$.stringField'), + listField: JsonPath.listAt('$.listField'), deep: [ 'literal', { - field: Data.stringAt('$.stringField'), - deepField: Data.numberAt('$.numField'), + field: JsonPath.stringAt('$.stringField'), + deepField: JsonPath.numberAt('$.numField'), }, ], }), @@ -64,39 +64,39 @@ describe('Fields', () => { }), test('cannot have JsonPath fields in arrays', () => { expect(() => FieldUtils.renderObject({ - deep: [Data.stringAt('$.hello')], + deep: [JsonPath.stringAt('$.hello')], })).toThrowError(/Cannot use JsonPath fields in an array/); }), test('datafield path must be correct', () => { - expect(Data.stringAt('$')).toBeDefined(); + expect(JsonPath.stringAt('$')).toBeDefined(); - expect(() => Data.stringAt('$hello')).toThrowError(/exactly equal to '\$' or start with '\$.'/); + expect(() => JsonPath.stringAt('$hello')).toThrowError(/exactly '\$', '\$\$', start with '\$.' or start with '\$\$.'/); - expect(() => Data.stringAt('hello')).toThrowError(/exactly equal to '\$' or start with '\$.'/); + expect(() => JsonPath.stringAt('hello')).toThrowError(/exactly '\$', '\$\$', start with '\$.' or start with '\$\$.'/); }), test('context path must be correct', () => { - expect(Context.stringAt('$$')).toBeDefined(); + expect(JsonPath.stringAt('$$')).toBeDefined(); - expect(() => Context.stringAt('$$hello')).toThrowError(/exactly equal to '\$\$' or start with '\$\$.'/); + expect(() => JsonPath.stringAt('$$hello')).toThrowError(/exactly '\$', '\$\$', start with '\$.' or start with '\$\$.'/); - expect(() => Context.stringAt('hello')).toThrowError(/exactly equal to '\$\$' or start with '\$\$.'/); + expect(() => JsonPath.stringAt('hello')).toThrowError(/exactly '\$', '\$\$', start with '\$.' or start with '\$\$.'/); }), test('test contains task token', () => { expect(true).toEqual( FieldUtils.containsTaskToken({ - field: Context.taskToken, + field: JsonPath.taskToken, }), ); expect(true).toEqual( FieldUtils.containsTaskToken({ - field: Context.stringAt('$$.Task'), + field: JsonPath.stringAt('$$.Task'), }), ); expect(true).toEqual( FieldUtils.containsTaskToken({ - field: Context.entireContext, + field: JsonPath.entireContext, }), ); @@ -108,7 +108,7 @@ describe('Fields', () => { expect(false).toEqual( FieldUtils.containsTaskToken({ - oops: Context.stringAt('$$.Execution.StartTime'), + oops: JsonPath.stringAt('$$.Execution.StartTime'), }), ); }), @@ -123,7 +123,7 @@ describe('Fields', () => { }), test('fields cannot be used somewhere in a string interpolation', () => { expect(() => FieldUtils.renderObject({ - field: `contains ${Data.stringAt('$.hello')}`, + field: `contains ${JsonPath.stringAt('$.hello')}`, })).toThrowError(/Field references must be the entire string/); }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts index e70765e96c502..c5ad0bb6d5697 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/map.test.ts @@ -10,10 +10,10 @@ describe('Map State', () => { // WHEN const map = new stepfunctions.Map(stack, 'Map State', { maxConcurrency: 1, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap'), + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), parameters: { foo: 'foo', - bar: stepfunctions.Data.stringAt('$.bar'), + bar: stepfunctions.JsonPath.stringAt('$.bar'), }, }); map.iterator(new stepfunctions.Pass(stack, 'Pass State')); @@ -41,13 +41,11 @@ describe('Map State', () => { }, }); }), - test('synth is successful', () => { - const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { maxConcurrency: 1, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap'), + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), }); map.iterator(new stepfunctions.Pass(stack, 'Pass State')); return map; @@ -55,13 +53,11 @@ describe('Map State', () => { app.synth(); }), - test('fails in synthesis if iterator is missing', () => { - const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { maxConcurrency: 1, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap'), + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), }); return map; @@ -69,13 +65,11 @@ describe('Map State', () => { expect(() => app.synth()).toThrow(/Map state must have a non-empty iterator/); }), - test('fails in synthesis when maxConcurrency is a float', () => { - const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { maxConcurrency: 1.2, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap'), + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), }); map.iterator(new stepfunctions.Pass(stack, 'Pass State')); @@ -84,13 +78,11 @@ describe('Map State', () => { expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); }), - test('fails in synthesis when maxConcurrency is a negative integer', () => { - const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { maxConcurrency: -1, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap'), + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), }); map.iterator(new stepfunctions.Pass(stack, 'Pass State')); @@ -99,13 +91,11 @@ describe('Map State', () => { expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); }), - test('fails in synthesis when maxConcurrency is too big to be an integer', () => { - const app = createAppWithMap((stack) => { const map = new stepfunctions.Map(stack, 'Map State', { maxConcurrency: Number.MAX_VALUE, - itemsPath: stepfunctions.Data.stringAt('$.inputForMap'), + itemsPath: stepfunctions.JsonPath.stringAt('$.inputForMap'), }); map.iterator(new stepfunctions.Pass(stack, 'Pass State')); @@ -114,28 +104,22 @@ describe('Map State', () => { expect(() => app.synth()).toThrow(/maxConcurrency has to be a positive integer/); }), - test('isPositiveInteger is false with negative number', () => { expect(stepfunctions.isPositiveInteger(-1)).toEqual(false); }), - test('isPositiveInteger is false with decimal number', () => { expect(stepfunctions.isPositiveInteger(1.2)).toEqual(false); }), - test('isPositiveInteger is false with a value greater than safe integer', () => { const valueToTest = Number.MAX_SAFE_INTEGER + 1; expect(stepfunctions.isPositiveInteger(valueToTest)).toEqual(false); }), - test('isPositiveInteger is true with 0', () => { expect(stepfunctions.isPositiveInteger(0)).toEqual(true); }), - test('isPositiveInteger is true with 10', () => { expect(stepfunctions.isPositiveInteger(10)).toEqual(true); }), - test('isPositiveInteger is true with max integer value', () => { expect(stepfunctions.isPositiveInteger(Number.MAX_SAFE_INTEGER)).toEqual(true); }); 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 67fcd36752f80..dc016b87012df 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 @@ -619,7 +619,7 @@ describe('State Machine Resources', () => { const stack = new cdk.Stack(); const task = new stepfunctions.Pass(stack, 'Pass', { parameters: { - input: stepfunctions.Data.stringAt('$.myField'), + input: stepfunctions.JsonPath.stringAt('$.myField'), }, }); diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index 279b10397a6db..8d65ed30aecd4 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -46,7 +46,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index b39ddd629873b..eac2654ff030b 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -47,7 +47,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/mock-fs": "^4.10.0", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index 4d128ebfc9579..3196b202c7f27 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -29,7 +29,7 @@ "table": "^5.4.6" }, "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/string-width": "^4.0.1", "@types/table": "^4.0.7", "cdk-build-tools": "0.0.0", diff --git a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts index b7f712c23af56..b5cabdb9d9d01 100644 --- a/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts +++ b/packages/@aws-cdk/cloudformation-include/lib/cfn-include.ts @@ -128,7 +128,13 @@ export class CfnInclude extends core.CfnElement { } private createParameter(logicalId: string): void { - const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Parameters[logicalId]); + const expression = new cfn_parse.CfnParser({ + finder: { + findResource() { throw new Error('Using GetAtt expressions in Parameter definitions is not allowed'); }, + findRefTarget() { throw new Error('Using Ref expressions in Parameter definitions is not allowed'); }, + findCondition() { throw new Error('Referring to Conditions in Parameter definitions is not allowed'); }, + }, + }).parseValue(this.template.Parameters[logicalId]); const cfnParameter = new core.CfnParameter(this, logicalId, { type: expression.Type, default: expression.Default, @@ -149,7 +155,14 @@ export class CfnInclude extends core.CfnElement { private createCondition(conditionName: string): void { // ToDo condition expressions can refer to other conditions - // will be important when implementing preserveLogicalIds=false - const expression = cfn_parse.FromCloudFormation.parseValue(this.template.Conditions[conditionName]); + const expression = new cfn_parse.CfnParser({ + finder: { + findResource() { throw new Error('Using GetAtt in Condition definitions is not allowed'); }, + findRefTarget() { throw new Error('Using Ref expressions in Condition definitions is not allowed'); }, + // ToDo handle one Condition referencing another using the { Condition: "ConditionName" } syntax + findCondition() { return undefined; }, + }, + }).parseValue(this.template.Conditions[conditionName]); const cfnCondition = new core.CfnCondition(this, conditionName, { expression, }); @@ -198,6 +211,14 @@ export class CfnInclude extends core.CfnElement { } return self.getOrCreateResource(lId); }, + + findRefTarget(elementName: string): core.CfnElement | undefined { + if (elementName in self.parameters) { + return self.parameters[elementName]; + } + + return this.findResource(elementName); + }, }; const options: core.FromCloudFormationOptions = { finder, diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index cad1c8249b4fe..de407f258d640 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -302,7 +302,7 @@ }, "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/yaml": "1.2.0", "cdk-build-tools": "0.0.0", "jest": "^25.4.0", diff --git a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts index f6e1c2100365a..eb04ef059e15e 100644 --- a/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/invalid-templates.test.ts @@ -64,6 +64,18 @@ describe('CDK Include', () => { includeTestTemplate(stack, 'non-existent-resource-attribute.json'); }).toThrow(/The NonExistentResourceAttribute resource attribute is not supported by cloudformation-include yet/); }); + + test("throws a validation exception when encountering a Ref-erence to a template element that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'ref-ing-a-non-existent-element.json'); + }).toThrow(/Element used in Ref expression with logical ID: 'DoesNotExist' not found/); + }); + + test("throws a validation exception when encountering a GetAtt reference to a resource that doesn't exist", () => { + expect(() => { + includeTestTemplate(stack, 'getting-attribute-of-a-non-existent-resource.json'); + }).toThrow(/Resource used in GetAtt expression with logical ID: 'DoesNotExist' not found/); + }); }); function includeTestTemplate(scope: core.Construct, testTemplate: string): inc.CfnInclude { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json index c665e5f2641b7..56ab850b8736d 100644 --- a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/bucket-policy-without-bucket.json @@ -28,6 +28,9 @@ "Version": "2012-10-17" } } + }, + "Bucket2": { + "Type": "AWS::S3::Bucket" } } } diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json new file mode 100644 index 0000000000000..1b575ce7cbeac --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/getting-attribute-of-a-non-existent-resource.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Fn::GetAtt": ["DoesNotExist", "SomeAttribute"] + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json new file mode 100644 index 0000000000000..f62b346ed25f3 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/invalid/ref-ing-a-non-existent-element.json @@ -0,0 +1,12 @@ +{ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketName": { + "Ref": "DoesNotExist" + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index f09331895f079..6994a77f6bff1 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -328,6 +328,87 @@ describe('CDK Include', () => { ); }); + test("correctly handles referencing the ingested template's resources across Stacks", () => { + // for cross-stack sharing to work, we need an App + const app = new core.App(); + stack = new core.Stack(app, 'MyStack'); + const cfnTemplate = includeTestTemplate(stack, 'only-empty-bucket.json'); + const cfnBucket = cfnTemplate.getResource('Bucket') as s3.CfnBucket; + + const otherStack = new core.Stack(app, 'OtherStack'); + const role = new iam.Role(otherStack, 'Role', { + assumedBy: new iam.AnyPrincipal(), + }); + role.addToPolicy(new iam.PolicyStatement({ + actions: ['s3:*'], + resources: [cfnBucket.attrArn], + })); + + expect(stack).toMatchTemplate({ + ...loadTestFileToJsObject('only-empty-bucket.json'), + "Outputs": { + "ExportsOutputFnGetAttBucketArn436138FE": { + "Value": { + "Fn::GetAtt": ["Bucket", "Arn"], + }, + "Export": { + "Name": "MyStack:ExportsOutputFnGetAttBucketArn436138FE", + }, + }, + }, + }); + + expect(otherStack).toHaveResourceLike('AWS::IAM::Policy', { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:*", + "Resource": { + "Fn::ImportValue": "MyStack:ExportsOutputFnGetAttBucketArn436138FE", + }, + }, + ], + }, + }); + }); + + test('correctly re-names references to resources in the template if their logical IDs have been changed', () => { + const cfnTemplate = includeTestTemplate(stack, 'bucket-with-encryption-key.json'); + const cfnKey = cfnTemplate.getResource('Key'); + cfnKey.overrideLogicalId('TotallyDifferentKey'); + + const originalTemplate = loadTestFileToJsObject('bucket-with-encryption-key.json'); + expect(stack).toMatchTemplate({ + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "KMSMasterKeyID": { + "Fn::GetAtt": ["TotallyDifferentKey", "Arn"], + }, + "SSEAlgorithm": "aws:kms", + }, + }, + ], + }, + }, + "Metadata" : { + "Object1" : "Location1", + "KeyRef": { "Ref": "TotallyDifferentKey" }, + "KeyArn": { "Fn::GetAtt": ["TotallyDifferentKey", "Arn"] }, + }, + "DeletionPolicy": "Retain", + "UpdateReplacePolicy": "Retain", + }, + "TotallyDifferentKey": originalTemplate.Resources.Key, + }, + }); + }); + test("throws an exception when encountering a Resource type it doesn't recognize", () => { expect(() => { includeTestTemplate(stack, 'non-existent-resource-type.json'); diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index e0844da24d22b..5aae33709a406 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -5,27 +5,25 @@ import { CfnCreationPolicy, CfnDeletionPolicy, CfnResourceAutoScalingCreationPolicy, CfnResourceSignal, CfnUpdatePolicy, } from './cfn-resource-policy'; import { CfnTag } from './cfn-tag'; +import { ICfnFinder } from './from-cfn'; +import { CfnReference } from './private/cfn-reference'; import { IResolvable } from './resolvable'; import { isResolvableObject, Token } from './token'; /** - * This class contains functions for translating from a pure CFN value - * (like a JS object { "Ref": "Bucket" }) - * to a form CDK understands - * (like Fn.ref('Bucket')). + * This class contains static methods called when going from + * translated values received from {@link CfnParser.parseValue} + * to the actual L1 properties - + * things like changing IResolvable to the appropriate type + * (string, string array, or number), etc. * * While this file not exported from the module * (to not make it part of the public API), - * it is directly referenced in the generated L1 code, - * so any renames of it need to be reflected in cfn2ts/codegen.ts as well. + * it is directly referenced in the generated L1 code. * * @experimental */ export class FromCloudFormation { - public static parseValue(cfnValue: any): any { - return parseCfnValueToCdkValue(cfnValue); - } - // nothing to for any but return it public static getAny(value: any) { return value; } @@ -110,11 +108,51 @@ export class FromCloudFormation { return ret; } - public static parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { + public static getCfnTag(tag: any): CfnTag { + return tag == null + ? { } as any // break the type system - this should be detected at runtime by a tag validator + : { + key: tag.Key, + value: tag.Value, + }; + } +} + +/** + * The options for {@link FromCloudFormation.parseValue}. + */ +export interface ParseCfnOptions { + /** + * The finder interface used to resolve references in the template. + */ + readonly finder: ICfnFinder; +} + +/** + * This class contains methods for translating from a pure CFN value + * (like a JS object { "Ref": "Bucket" }) + * to a form CDK understands + * (like Fn.ref('Bucket')). + * + * While this file not exported from the module + * (to not make it part of the public API), + * it is directly referenced in the generated L1 code, + * so any renames of it need to be reflected in cfn2ts/codegen.ts as well. + * + * @experimental + */ +export class CfnParser { + private readonly options: ParseCfnOptions; + + constructor(options: ParseCfnOptions) { + this.options = options; + } + + public parseCreationPolicy(policy: any): CfnCreationPolicy | undefined { if (typeof policy !== 'object') { return undefined; } // change simple JS values to their CDK equivalents - policy = FromCloudFormation.parseValue(policy); + policy = this.parseValue(policy); return undefinedIfAllValuesAreEmpty({ autoScalingCreationPolicy: parseAutoScalingCreationPolicy(policy.AutoScalingCreationPolicy), @@ -139,11 +177,11 @@ export class FromCloudFormation { } } - public static parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { + public parseUpdatePolicy(policy: any): CfnUpdatePolicy | undefined { if (typeof policy !== 'object') { return undefined; } // change simple JS values to their CDK equivalents - policy = FromCloudFormation.parseValue(policy); + policy = this.parseValue(policy); return undefinedIfAllValuesAreEmpty({ autoScalingReplacingUpdate: parseAutoScalingReplacingUpdate(policy.AutoScalingReplacingUpdate), @@ -195,7 +233,7 @@ export class FromCloudFormation { } } - public static parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { + public parseDeletionPolicy(policy: any): CfnDeletionPolicy | undefined { switch (policy) { case null: return undefined; case undefined: return undefined; @@ -206,125 +244,128 @@ export class FromCloudFormation { } } - public static getCfnTag(tag: any): CfnTag { - return tag == null - ? { } as any // break the type system - this should be detected at runtime by a tag validator - : { - key: tag.Key, - value: tag.Value, - }; - } -} - -function parseCfnValueToCdkValue(cfnValue: any): any { - // == null captures undefined as well - if (cfnValue == null) { - return undefined; - } - // if we have any late-bound values, - // just return them - if (isResolvableObject(cfnValue)) { - return cfnValue; - } - if (Array.isArray(cfnValue)) { - return cfnValue.map(el => parseCfnValueToCdkValue(el)); - } - if (typeof cfnValue === 'object') { - // an object can be either a CFN intrinsic, or an actual object - const cfnIntrinsic = parseIfCfnIntrinsic(cfnValue); - if (cfnIntrinsic) { - return cfnIntrinsic; - } - const ret: any = {}; - for (const [key, val] of Object.entries(cfnValue)) { - ret[key] = parseCfnValueToCdkValue(val); + public parseValue(cfnValue: any): any { + // == null captures undefined as well + if (cfnValue == null) { + return undefined; } - return ret; + // if we have any late-bound values, + // just return them + if (isResolvableObject(cfnValue)) { + return cfnValue; + } + if (Array.isArray(cfnValue)) { + return cfnValue.map(el => this.parseValue(el)); + } + if (typeof cfnValue === 'object') { + // an object can be either a CFN intrinsic, or an actual object + const cfnIntrinsic = this.parseIfCfnIntrinsic(cfnValue); + if (cfnIntrinsic) { + return cfnIntrinsic; + } + const ret: any = {}; + for (const [key, val] of Object.entries(cfnValue)) { + ret[key] = this.parseValue(val); + } + return ret; + } + // in all other cases, just return the input + return cfnValue; } - // in all other cases, just return the input - return cfnValue; -} -function parseIfCfnIntrinsic(object: any): any { - const key = looksLikeCfnIntrinsic(object); - switch (key) { - case undefined: - return undefined; - case 'Ref': { - // ToDo handle translating logical IDs - return specialCaseRefs(object[key]) ?? Fn._ref(object[key]); - } - case 'Fn::GetAtt': { - // Fn::GetAtt takes a 2-element list as its argument - const value = object[key]; - // ToDo same comment here as in Ref above - return Fn.getAtt((value[0]), value[1]); - } - case 'Fn::Join': { - // Fn::Join takes a 2-element list as its argument, - // where the first element is the delimiter, - // and the second is the list of elements to join - const value = parseCfnValueToCdkValue(object[key]); - return Fn.join(value[0], value[1]); - } - case 'Fn::Cidr': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.cidr(value[0], value[1], value[2]); - } - case 'Fn::FindInMap': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.findInMap(value[0], value[1], value[2]); - } - case 'Fn::Select': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.select(value[0], value[1]); - } - case 'Fn::GetAZs': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.getAzs(value); - } - case 'Fn::ImportValue': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.importValue(value); - } - case 'Fn::Split': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.split(value[0], value[1]); - } - case 'Fn::Transform': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.transform(value.Name, value.Parameters); - } - case 'Fn::Base64': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.base64(value); - } - case 'Fn::If': { - // Fn::If takes a 3-element list as its argument - // ToDo the first argument is the name of the condition, - // so we will need to retrieve the actual object from the template - // when we handle preserveLogicalIds=false - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionIf(value[0], value[1], value[2]); - } - case 'Fn::Equals': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionEquals(value[0], value[1]); - } - case 'Fn::And': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionAnd(...value); - } - case 'Fn::Not': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionNot(value[0]); - } - case 'Fn::Or': { - const value = parseCfnValueToCdkValue(object[key]); - return Fn.conditionOr(...value); + private parseIfCfnIntrinsic(object: any): any { + const key = looksLikeCfnIntrinsic(object); + switch (key) { + case undefined: + return undefined; + case 'Ref': { + const refTarget = object[key]; + const specialRef = specialCaseRefs(refTarget); + if (specialRef) { + return specialRef; + } else { + const refElement = this.options.finder.findRefTarget(refTarget); + if (!refElement) { + throw new Error(`Element used in Ref expression with logical ID: '${refTarget}' not found`); + } + return CfnReference.for(refElement, 'Ref'); + } + } + case 'Fn::GetAtt': { + // Fn::GetAtt takes a 2-element list as its argument + const value = object[key]; + const target = this.options.finder.findResource(value[0]); + if (!target) { + throw new Error(`Resource used in GetAtt expression with logical ID: '${value[0]}' not found`); + } + return target.getAtt(value[1]); + } + case 'Fn::Join': { + // Fn::Join takes a 2-element list as its argument, + // where the first element is the delimiter, + // and the second is the list of elements to join + const value = this.parseValue(object[key]); + return Fn.join(value[0], value[1]); + } + case 'Fn::Cidr': { + const value = this.parseValue(object[key]); + return Fn.cidr(value[0], value[1], value[2]); + } + case 'Fn::FindInMap': { + const value = this.parseValue(object[key]); + return Fn.findInMap(value[0], value[1], value[2]); + } + case 'Fn::Select': { + const value = this.parseValue(object[key]); + return Fn.select(value[0], value[1]); + } + case 'Fn::GetAZs': { + const value = this.parseValue(object[key]); + return Fn.getAzs(value); + } + case 'Fn::ImportValue': { + const value = this.parseValue(object[key]); + return Fn.importValue(value); + } + case 'Fn::Split': { + const value = this.parseValue(object[key]); + return Fn.split(value[0], value[1]); + } + case 'Fn::Transform': { + const value = this.parseValue(object[key]); + return Fn.transform(value.Name, value.Parameters); + } + case 'Fn::Base64': { + const value = this.parseValue(object[key]); + return Fn.base64(value); + } + case 'Fn::If': { + // Fn::If takes a 3-element list as its argument + // ToDo the first argument is the name of the condition, + // so we will need to retrieve the actual object from the template + // when we handle preserveLogicalIds=false + const value = this.parseValue(object[key]); + return Fn.conditionIf(value[0], value[1], value[2]); + } + case 'Fn::Equals': { + const value = this.parseValue(object[key]); + return Fn.conditionEquals(value[0], value[1]); + } + case 'Fn::And': { + const value = this.parseValue(object[key]); + return Fn.conditionAnd(...value); + } + case 'Fn::Not': { + const value = this.parseValue(object[key]); + return Fn.conditionNot(value[0]); + } + case 'Fn::Or': { + const value = this.parseValue(object[key]); + return Fn.conditionOr(...value); + } + default: + throw new Error(`Unsupported CloudFormation function '${key}'`); } - default: - throw new Error(`Unsupported CloudFormation function '${key}'`); } } diff --git a/packages/@aws-cdk/core/lib/from-cfn.ts b/packages/@aws-cdk/core/lib/from-cfn.ts index 9d3b1544526a2..771af7775a74b 100644 --- a/packages/@aws-cdk/core/lib/from-cfn.ts +++ b/packages/@aws-cdk/core/lib/from-cfn.ts @@ -1,4 +1,5 @@ import { CfnCondition } from './cfn-condition'; +import { CfnElement } from './cfn-element'; import { CfnResource } from './cfn-resource'; /** @@ -15,6 +16,13 @@ export interface ICfnFinder { */ findCondition(conditionName: string): CfnCondition | undefined; + /** + * Returns the element referenced using a Ref expression with the given name. + * If there is no element with this name in the template, + * return undefined. + */ + findRefTarget(elementName: string): CfnElement | undefined; + /** * Returns the resource with the given logical ID in the template. * If a resource with that logical ID was not found in the template, diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index bc6acb2a443dc..4b660f4e83c42 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -151,7 +151,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.156", + "@types/lodash": "^4.14.157", "@types/node": "^10.17.26", "@types/nodeunit": "^0.0.31", "@types/minimatch": "^3.0.3", diff --git a/packages/@aws-cdk/core/test/test.cfn-parse.ts b/packages/@aws-cdk/core/test/test.cfn-parse.ts index 47f6bdbb947cc..7000af65e231d 100644 --- a/packages/@aws-cdk/core/test/test.cfn-parse.ts +++ b/packages/@aws-cdk/core/test/test.cfn-parse.ts @@ -1,23 +1,23 @@ import { Test } from 'nodeunit'; -import { FromCloudFormation } from '../lib/cfn-parse'; +import { CfnParser } from '../lib/cfn-parse'; export = { 'FromCloudFormation class': { '#parseCreationPolicy': { 'returns undefined when given a non-object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseCreationPolicy('blah'), undefined); + test.equal(parseCreationPolicy('blah'), undefined); test.done(); }, 'returns undefined when given an empty object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseCreationPolicy({}), undefined); + test.equal(parseCreationPolicy({}), undefined); test.done(); }, 'returns undefined when given empty sub-objects as the argument'(test: Test) { - test.equal(FromCloudFormation.parseCreationPolicy({ + test.equal(parseCreationPolicy({ AutoScalingCreationPolicy: null, ResourceSignal: { Count: undefined, @@ -30,19 +30,19 @@ export = { '#parseUpdatePolicy': { 'returns undefined when given a non-object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseUpdatePolicy('blah'), undefined); + test.equal(parseUpdatePolicy('blah'), undefined); test.done(); }, 'returns undefined when given an empty object as the argument'(test: Test) { - test.equal(FromCloudFormation.parseUpdatePolicy({}), undefined); + test.equal(parseUpdatePolicy({}), undefined); test.done(); }, 'returns undefined when given empty sub-objects as the argument'(test: Test) { - test.equal(FromCloudFormation.parseUpdatePolicy({ + test.equal(parseUpdatePolicy({ AutoScalingReplacingUpdate: null, AutoScalingRollingUpdate: { PauseTime: undefined, @@ -54,3 +54,19 @@ export = { }, }, }; + +function parseCreationPolicy(policy: any) { + return testCfnParser.parseCreationPolicy(policy); +} + +function parseUpdatePolicy(policy: any) { + return testCfnParser.parseUpdatePolicy(policy); +} + +const testCfnParser = new CfnParser({ + finder: { + findCondition() { return undefined; }, + findRefTarget() { return undefined; }, + findResource() { return undefined; }, + }, +}); diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 79b0fd59e5ee6..7907d4568c796 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -73,13 +73,13 @@ "@types/aws-lambda": "^8.10.39", "@types/fs-extra": "^8.1.0", "@types/sinon": "^9.0.4", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "fs-extra": "^9.0.1", - "nock": "^12.0.3", + "nock": "^13.0.0", "pkglint": "0.0.0", "sinon": "^9.0.2" }, diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index 766e671454da6..371aebc1f65fd 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -53,9 +53,9 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/mock-fs": "^4.10.0", - "@types/semver": "^7.2.0", + "@types/semver": "^7.3.1", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", "mock-fs": "^4.12.0", diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index a83382da131d0..6a923bd5971a3 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -36,7 +36,7 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/node": "^10.17.26", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", diff --git a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts index a79f833d5e71c..6193a04f5715e 100644 --- a/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts +++ b/packages/@monocdk-experiment/rewrite-imports/bin/rewrite-imports.ts @@ -18,12 +18,15 @@ async function main() { 'node_modules/**', ]; - const files = await glob(process.argv[2], { ignore, matchBase: true }); - for (const file of files) { - const input = await fs.promises.readFile(file, { encoding: 'utf8' }); - const output = rewriteImports(input, file); - if (output.trim() !== input.trim()) { - await fs.promises.writeFile(file, output); + const args = process.argv.slice(2); + for (const arg of args) { + const files = await glob(arg, { ignore, matchBase: true }); + for (const file of files) { + const input = await fs.promises.readFile(file, { encoding: 'utf8' }); + const output = rewriteImports(input, file); + if (output.trim() !== input.trim()) { + await fs.promises.writeFile(file, output); + } } } } diff --git a/packages/@monocdk-experiment/rewrite-imports/package.json b/packages/@monocdk-experiment/rewrite-imports/package.json index a9932328b681c..a159df11280b2 100644 --- a/packages/@monocdk-experiment/rewrite-imports/package.json +++ b/packages/@monocdk-experiment/rewrite-imports/package.json @@ -36,7 +36,7 @@ }, "devDependencies": { "@types/glob": "^7.1.2", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/node": "^10.17.26", "cdk-build-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index b30b6346ada84..c40c914714187 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -13,7 +13,7 @@ import { execProgram } from '../lib/api/cxapp/exec'; import { CdkToolkit } from '../lib/cdk-toolkit'; import { RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; -import { data, debug, error, setVerbose } from '../lib/logging'; +import { data, debug, error, setLogLevel } from '../lib/logging'; import { PluginHost } from '../lib/plugin'; import { serializeStructure } from '../lib/serialize'; import { Configuration, Settings } from '../lib/settings'; @@ -45,7 +45,8 @@ async function parseCommandLineArguments() { .option('strict', { type: 'boolean', desc: 'Do not construct stacks with warnings' }) .option('ignore-errors', { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' }) .option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false }) - .option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs', default: false }) + .option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false }) + .count('verbose') .option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true }) .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified.', requiresArg: true }) .option('ca-bundle-path', { type: 'string', desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified.', requiresArg: true }) @@ -122,7 +123,7 @@ if (!process.stdout.isTTY) { async function initCommandLine() { const argv = await parseCommandLineArguments(); if (argv.verbose) { - setVerbose(); + setLogLevel(argv.verbose); } debug('CDK toolkit version:', version.DISPLAY_VERSION); debug('Command line arguments:', argv); @@ -203,7 +204,7 @@ async function initCommandLine() { const cli = new CdkToolkit({ cloudExecutable, cloudFormation, - verbose: argv.trace || argv.verbose, + verbose: argv.trace || argv.verbose > 0, ignoreErrors: argv['ignore-errors'], strict: argv.strict, configuration, diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 239f85fef51bc..d735231836e27 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -1,6 +1,6 @@ import * as AWS from 'aws-sdk'; import { ConfigurationOptions } from 'aws-sdk/lib/config'; -import { debug } from '../../logging'; +import { debug, trace } from '../../logging'; import { cached } from '../../util/functions'; import { AccountAccessKeyCache } from './account-cache'; import { Account } from './sdk-provider'; @@ -42,16 +42,17 @@ export class SDK implements ISDK { private readonly config: ConfigurationOptions; /** - * Default retry options for SDK clients - * - * Biggest bottleneck is CloudFormation, with a 1tps call rate. We want to be - * a little more tenacious than the defaults, and with a little more breathing - * room between calls (defaults are {retries=3, base=100}). + * Default retry options for SDK clients. + */ + private readonly retryOptions = { maxRetries: 6, retryDelayOptions: { base: 300 } }; + + /** + * The more generous retry policy for CloudFormation, which has a 1 TPM limit on certain APIs, + * which are abundantly used for deployment tracking, ... * - * I've left this running in a tight loop for an hour and the throttle errors - * haven't escaped the retry mechanism. + * So we're allowing way more retries, but waiting a bit more. */ - private readonly retryOptions = { maxRetries: 6, retryDelayOptions: { base: 300 }}; + private readonly cloudFormationRetryOptions = { maxRetries: 10, retryDelayOptions: { base: 1_000 } }; constructor(private readonly credentials: AWS.Credentials, region: string, httpOptions: ConfigurationOptions = {}) { this.config = { @@ -59,12 +60,16 @@ export class SDK implements ISDK { ...this.retryOptions, credentials, region, + logger: { log: (...messages) => messages.forEach(m => trace('%s', m)) }, }; this.currentRegion = region; } public cloudFormation(): AWS.CloudFormation { - return wrapServiceErrorHandling(new AWS.CloudFormation(this.config)); + return wrapServiceErrorHandling(new AWS.CloudFormation({ + ...this.config, + ...this.cloudFormationRetryOptions, + })); } public ec2(): AWS.EC2 { @@ -212,4 +217,4 @@ function allChainedExceptionMessages(e: Error | undefined) { e = (e as any).originalError; } return ret.join(': '); -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts b/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts index 67bed3406b07a..0b98ea3a3fbe2 100644 --- a/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts +++ b/packages/aws-cdk/lib/api/bootstrap/legacy-template.ts @@ -33,7 +33,7 @@ export function legacyBootstrapTemplate(params: BootstrappingParameters): any { IgnorePublicAcls: true, RestrictPublicBuckets: true, }, - 'AWS::NoValue', + { Ref: 'AWS::NoValue' }, ]}, }, }, diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts index c1ac8a8194120..7df4680c7fb3c 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-activity-monitor.ts @@ -3,7 +3,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as aws from 'aws-sdk'; import * as colors from 'colors/safe'; import * as util from 'util'; -import { error, isVerbose, setVerbose } from '../../../logging'; +import { error, logLevel, LogLevel, setLogLevel } from '../../../logging'; import { RewritableBlock } from '../display'; interface StackActivity { @@ -28,13 +28,13 @@ export interface StackActivityMonitorProps { readonly resourcesTotal?: number; /** - * Whether 'verbose' was requested in the CLI + * The log level that was requested in the CLI * - * If verbose is requested, we'll always use the full history printer. + * If verbose or trace is requested, we'll always use the full history printer. * - * @default - Use value from logging.isVerbose + * @default - Use value from logging.logLevel */ - readonly verbose?: boolean; + readonly logLevel?: LogLevel; } export class StackActivityMonitor { @@ -78,7 +78,7 @@ export class StackActivityMonitor { }; const isWindows = process.platform === 'win32'; - const verbose = options.verbose ?? isVerbose; + const verbose = options.logLevel ?? logLevel; const fancyOutputAvailable = !isWindows && stream.isTTY; this.printer = fancyOutputAvailable && !verbose @@ -479,7 +479,7 @@ export class CurrentActivityPrinter extends ActivityPrinterBase { */ public readonly updateSleep: number = 2_000; - private oldVerbose: boolean = false; + private oldLogLevel: LogLevel = LogLevel.DEFAULT; private block = new RewritableBlock(this.stream); constructor(props: PrinterProps) { @@ -520,12 +520,12 @@ export class CurrentActivityPrinter extends ActivityPrinterBase { public start() { // Need to prevent the waiter from printing 'stack not stable' every 5 seconds, it messes // with the output calculations. - this.oldVerbose = isVerbose; - setVerbose(false); + this.oldLogLevel = logLevel; + setLogLevel(LogLevel.DEFAULT); } public stop() { - setVerbose(this.oldVerbose); + setLogLevel(this.oldLogLevel); // Print failures at the end const lines = new Array(); @@ -623,4 +623,4 @@ function shorten(maxWidth: number, p: string) { } const TIMESTAMP_WIDTH = 12; -const STATUS_WIDTH = 20; \ No newline at end of file +const STATUS_WIDTH = 20; diff --git a/packages/aws-cdk/lib/init.ts b/packages/aws-cdk/lib/init.ts index 07ec97181f551..cff039fbb9cee 100644 --- a/packages/aws-cdk/lib/init.ts +++ b/packages/aws-cdk/lib/init.ts @@ -241,15 +241,16 @@ async function initializeProject(template: InitTemplate, language: string, canUs await assertIsEmptyDirectory(workDir); print(`Applying project template ${colors.green(template.name)} for ${colors.blue(language)}`); await template.install(language, workDir); + if (await fs.pathExists('README.md')) { + print(colors.green(await fs.readFile('README.md', { encoding: 'utf-8' }))); + } + if (!generateOnly) { await initializeGitRepository(workDir); await postInstall(language, canUseNetwork, workDir); } - if (await fs.pathExists('README.md')) { - print(colors.green(await fs.readFile('README.md', { encoding: 'utf-8' }))); - } else { - print('✅ All done!'); - } + + print('✅ All done!'); } async function assertIsEmptyDirectory(workDir: string) { @@ -292,7 +293,7 @@ async function postInstallTypescript(canUseNetwork: boolean, cwd: string) { const command = 'npm'; if (!canUseNetwork) { - print(`Please run ${colors.green(`${command} install`)}!`); + warning(`Please run '${command} install'!`); return; } @@ -300,28 +301,36 @@ async function postInstallTypescript(canUseNetwork: boolean, cwd: string) { try { await execute(command, ['install'], { cwd }); } catch (e) { - throw new Error(`${colors.green(`${command} install`)} failed: ` + e.message); + warning(`${command} install failed: ` + e.message); } } async function postInstallJava(canUseNetwork: boolean, cwd: string) { + const mvnPackageWarning = 'Please run \'mvn package\'!'; if (!canUseNetwork) { - print(`Please run ${colors.green('mvn package')}!`); + warning(mvnPackageWarning); return; } - print(`Executing ${colors.green('mvn package')}...`); - await execute('mvn', ['package'], { cwd }); + print('Executing \'mvn package\''); + try { + await execute('mvn', ['package'], { cwd }); + } catch (e) { + warning('Unable to package compiled code as JAR'); + warning(mvnPackageWarning); + } + } async function postInstallPython(cwd: string) { const python = pythonExecutable(); + warning(`Please run ${python} -m venv .env'!`); print(`Executing ${colors.green('Creating virtualenv...')}`); try { await execute(python, ['-m venv', '.env'], { cwd }); } catch (e) { - print('Unable to create virtualenv automatically'); - print(`Please run ${colors.green(python + ' -m venv .env')}!`); + warning('Unable to create virtualenv automatically'); + warning(`Please run '${python} -m venv .env'!`); } } diff --git a/packages/aws-cdk/lib/logging.ts b/packages/aws-cdk/lib/logging.ts index 5c23131d1a451..b85881886a84f 100644 --- a/packages/aws-cdk/lib/logging.ts +++ b/packages/aws-cdk/lib/logging.ts @@ -13,15 +13,20 @@ const logger = (stream: Writable, styles?: StyleFn[]) => (fmt: string, ...args: stream.write(str + '\n'); }; -export let isVerbose = false; +export let logLevel = LogLevel.DEFAULT; -export function setVerbose(enabled = true) { - isVerbose = enabled; +export function setLogLevel(newLogLevel: LogLevel) { + logLevel = newLogLevel; +} + +export function increaseVerbosity() { + logLevel += 1; } const _debug = logger(stderr, [colors.gray]); -export const debug = (fmt: string, ...args: any[]) => isVerbose && _debug(fmt, ...args); +export const trace = (fmt: string, ...args: any) => logLevel >= LogLevel.TRACE && _debug(fmt, ...args); +export const debug = (fmt: string, ...args: any[]) => logLevel >= LogLevel.DEBUG && _debug(fmt, ...args); export const error = logger(stderr, [colors.red]); export const warning = logger(stderr, [colors.yellow]); export const success = logger(stderr, [colors.green]); @@ -42,3 +47,12 @@ export type LoggerFunction = (fmt: string, ...args: any[]) => void; export function prefix(prefixString: string, fn: LoggerFunction): LoggerFunction { return (fmt: string, ...args: any[]) => fn(`%s ${fmt}`, prefixString, ...args); } + +export const enum LogLevel { + /** Not verbose at all */ + DEFAULT = 0, + /** Pretty verbose */ + DEBUG = 1, + /** Extremely verbose */ + TRACE = 2 +} diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 5895dc62b74e1..886b797adf972 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -44,12 +44,12 @@ "@types/archiver": "^3.1.0", "@types/fs-extra": "^8.1.0", "@types/glob": "^7.1.2", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/minimatch": "^3.0.3", "@types/mockery": "^1.4.29", "@types/node": "^10.17.26", "@types/promptly": "^3.0.0", - "@types/semver": "^7.2.0", + "@types/semver": "^7.3.1", "@types/sinon": "^9.0.4", "@types/table": "^4.0.7", "@types/uuid": "^8.0.0", @@ -71,7 +71,7 @@ "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "camelcase": "^6.0.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/aws-cdk/test/api/exec.test.ts b/packages/aws-cdk/test/api/exec.test.ts index ec653da6d6a77..13c5cf9a5f222 100644 --- a/packages/aws-cdk/test/api/exec.test.ts +++ b/packages/aws-cdk/test/api/exec.test.ts @@ -5,18 +5,18 @@ import * as semver from 'semver'; import * as sinon from 'sinon'; import { ImportMock } from 'ts-mock-imports'; import { execProgram } from '../../lib/api/cxapp/exec'; -import { setVerbose } from '../../lib/logging'; +import { LogLevel, setLogLevel } from '../../lib/logging'; import { Configuration } from '../../lib/settings'; import * as bockfs from '../bockfs'; import { testAssembly } from '../util'; import { mockSpawn } from '../util/mock-child_process'; import { MockSdkProvider } from '../util/mock-sdk'; -setVerbose(true); - let sdkProvider: MockSdkProvider; let config: Configuration; beforeEach(() => { + setLogLevel(LogLevel.DEBUG); + sdkProvider = new MockSdkProvider(); config = new Configuration(); @@ -34,6 +34,8 @@ beforeEach(() => { }); afterEach(() => { + setLogLevel(LogLevel.DEFAULT); + sinon.restore(); bockfs.restore(); }); diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index 6e6a4f91511c4..0d50a3936053c 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -9,7 +9,6 @@ import * as logging from '../../lib/logging'; import * as bockfs from '../bockfs'; SDKMock.setSDKInstance(AWS); -logging.setVerbose(true); type AwsCallback = (err: Error | null, val: T) => void; @@ -26,6 +25,8 @@ let defaultEnv: cxapi.Environment; beforeEach(() => { uid = `(${uuid.v4()})`; + logging.setLogLevel(logging.LogLevel.TRACE); + bockfs({ '/home/me/.bxt/credentials': dedent(` [default] @@ -98,6 +99,8 @@ beforeEach(() => { }); afterEach(() => { + logging.setLogLevel(logging.LogLevel.DEFAULT); + SDKMock.restore(); bockfs.restore(); }); @@ -264,4 +267,4 @@ function commonPrefix(a: string, b: string): string { if (a[i] !== b[i]) { return a.substring(0, i); } } return a.substr(N); -} \ No newline at end of file +} diff --git a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts index 93f9a0974aa2a..7c7947acdcae6 100644 --- a/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts +++ b/packages/aws-cdk/test/integ/cli/bootstrapping.integtest.ts @@ -152,6 +152,17 @@ integTest('deploying new style synthesis to old style bootstrap fails', async () })).rejects.toThrow('exited with error'); }); +integTest('can create a legacy bootstrap stack with --public-access-block-configuration=false', async () => { + const bootstrapStackName = fullStackName('bootstrap-stack-1'); + + await cdk(['bootstrap', '-v', '--toolkit-stack-name', bootstrapStackName, '--public-access-block-configuration', 'false', '--tags', 'Foo=Bar']); + + const response = await cloudFormation('describeStacks', { StackName: bootstrapStackName }); + expect(response.Stacks?.[0].Tags).toEqual([ + { Key: 'Foo', Value: 'Bar' }, + ]); +}); + integTest('can create multiple legacy bootstrap stacks', async () => { const bootstrapStackName1 = fullStackName('bootstrap-stack-1'); const bootstrapStackName2 = fullStackName('bootstrap-stack-2'); diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index 859e64f9df617..138a686cc6afb 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -32,7 +32,7 @@ "devDependencies": { "@types/archiver": "^3.1.0", "@types/glob": "^7.1.2", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/mock-fs": "^4.10.0", "@types/node": "^10.17.26", "@types/yargs": "^15.0.5", @@ -47,7 +47,7 @@ "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^4.0.1", - "aws-sdk": "^2.703.0", + "aws-sdk": "^2.706.0", "glob": "^7.1.6", "yargs": "^15.3.1" }, diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 7554e7bee6180..c5e6b34be451b 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -30,7 +30,7 @@ "yaml": "1.10.0" }, "devDependencies": { - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/yaml": "1.9.7", "jest": "^25.5.4" }, diff --git a/packages/decdk/package.json b/packages/decdk/package.json index 76fb6b76b2ed7..59b0ad777f07e 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -185,7 +185,7 @@ }, "devDependencies": { "@types/fs-extra": "^8.1.0", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/yaml": "1.9.7", "@types/yargs": "^15.0.5", "jest": "^25.5.4", diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 0305be2513a6b..7913098fa397a 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -34,7 +34,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/fs-extra": "^8.1.0", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/yargs": "^15.0.5", "pkglint": "0.0.0" }, @@ -46,7 +46,7 @@ "eslint": "^6.8.0", "eslint-import-resolver-node": "^0.3.4", "eslint-import-resolver-typescript": "^2.0.0", - "eslint-plugin-import": "^2.21.2", + "eslint-plugin-import": "^2.22.0", "fs-extra": "^9.0.1", "jest": "^25.5.4", "jsii": "^1.7.0", diff --git a/tools/cfn2ts/lib/codegen.ts b/tools/cfn2ts/lib/codegen.ts index 6bba39e50abd5..ff87f6a178222 100644 --- a/tools/cfn2ts/lib/codegen.ts +++ b/tools/cfn2ts/lib/codegen.ts @@ -235,9 +235,12 @@ export default class CodeGenerator { this.code.openBlock(`public static fromCloudFormation(scope: ${CONSTRUCT_CLASS}, id: string, resourceAttributes: any, options: ${CORE}.FromCloudFormationOptions): ` + `${resourceName.className}`); this.code.line('resourceAttributes = resourceAttributes || {};'); + this.code.indent('const cfnParser = new cfn_parse.CfnParser({'); + this.code.line('finder: options.finder,'); + this.code.unindent('});'); if (propsType) { // translate the template properties to CDK objects - this.code.line(`const resourceProperties = ${CFN_PARSE}.FromCloudFormation.parseValue(resourceAttributes.Properties);`); + this.code.line('const resourceProperties = cfnParser.parseValue(resourceAttributes.Properties);'); // translate to props, using a (module-private) factory function this.code.line(`const props = ${genspec.fromCfnFactoryName(propsType).fqn}(resourceProperties);`); // finally, instantiate the resource class @@ -249,11 +252,11 @@ export default class CodeGenerator { // handle all non-property attributes // (retention policies, conditions, metadata, etc.) this.code.line('const cfnOptions = ret.cfnOptions;'); - this.code.line(`cfnOptions.creationPolicy = ${CFN_PARSE}.FromCloudFormation.parseCreationPolicy(resourceAttributes.CreationPolicy);`); - this.code.line(`cfnOptions.updatePolicy = ${CFN_PARSE}.FromCloudFormation.parseUpdatePolicy(resourceAttributes.UpdatePolicy);`); - this.code.line(`cfnOptions.deletionPolicy = ${CFN_PARSE}.FromCloudFormation.parseDeletionPolicy(resourceAttributes.DeletionPolicy);`); - this.code.line(`cfnOptions.updateReplacePolicy = ${CFN_PARSE}.FromCloudFormation.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy);`); - this.code.line(`cfnOptions.metadata = ${CFN_PARSE}.FromCloudFormation.parseValue(resourceAttributes.Metadata);`); + this.code.line('cfnOptions.creationPolicy = cfnParser.parseCreationPolicy(resourceAttributes.CreationPolicy);'); + this.code.line('cfnOptions.updatePolicy = cfnParser.parseUpdatePolicy(resourceAttributes.UpdatePolicy);'); + this.code.line('cfnOptions.deletionPolicy = cfnParser.parseDeletionPolicy(resourceAttributes.DeletionPolicy);'); + this.code.line('cfnOptions.updateReplacePolicy = cfnParser.parseDeletionPolicy(resourceAttributes.UpdateReplacePolicy);'); + this.code.line('cfnOptions.metadata = cfnParser.parseValue(resourceAttributes.Metadata);'); // handle DependsOn this.code.line('// handle DependsOn'); @@ -278,10 +281,6 @@ export default class CodeGenerator { this.code.line('cfnOptions.condition = condition;'); this.code.closeBlock(); - // ToDo handle: - // 1. CreationPolicy - // 2. UpdatePolicy - this.code.line('return ret;'); this.code.closeBlock(); diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index 04fa146c2a4ec..d302cd2aa01be 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@types/fs-extra": "^8.1.0", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "@types/yargs": "^15.0.5", "cdk-build-tools": "0.0.0", "jest": "^25.5.4", diff --git a/tools/pkglint/lib/packagejson.ts b/tools/pkglint/lib/packagejson.ts index 3707a2a29a8ab..1ec46e6b2bdc8 100644 --- a/tools/pkglint/lib/packagejson.ts +++ b/tools/pkglint/lib/packagejson.ts @@ -2,6 +2,9 @@ import * as colors from 'colors/safe'; import * as fs from 'fs-extra'; import * as path from 'path'; +// do not descend into these directories when searching for `package.json` files. +export const PKGLINT_IGNORES = ['node_modules', 'cdk.out', '.cdk.staging']; + /** * Return all package JSONs in the root directory */ @@ -24,8 +27,8 @@ export function findPackageJsons(root: string): PackageJson[] { ret.push(new PackageJson(fullPath)); } - // Recurse into all dirs except node_modules - if (file !== 'node_modules' && (fs.lstatSync(fullPath)).isDirectory()) { + // Recurse into all dirs except ignored dirs + if (!PKGLINT_IGNORES.includes(file) && (fs.lstatSync(fullPath)).isDirectory()) { recurse(fullPath); } } diff --git a/tools/pkglint/lib/util.ts b/tools/pkglint/lib/util.ts index 85f33afb2e7b0..35966d3809b22 100644 --- a/tools/pkglint/lib/util.ts +++ b/tools/pkglint/lib/util.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { PackageJson } from "./packagejson"; +import { PackageJson, PKGLINT_IGNORES } from "./packagejson"; /** * Expect a particular JSON key to be a given value @@ -168,7 +168,7 @@ export function* findInnerPackages(dir: string): IterableIterator { if (e.code !== 'ENOENT') { throw e; } continue; } - if (fname === 'node_modules') { continue; } + if (PKGLINT_IGNORES.includes(fname)) { continue; } if (fs.existsSync(path.join(dir, fname, 'package.json'))) { yield path.join(dir, fname); diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index 88d09027159b0..1c2037573215e 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -36,7 +36,7 @@ "license": "Apache-2.0", "devDependencies": { "@types/fs-extra": "^8.1.0", - "@types/semver": "^7.2.0", + "@types/semver": "^7.3.1", "@types/yargs": "^15.0.5", "jest": "^25.5.4", "typescript": "~3.9.5" diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index e372356bec847..fe1b5490e0967 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -39,7 +39,7 @@ }, "devDependencies": { "@types/yarnpkg__lockfile": "^1.1.3", - "@types/jest": "^26.0.0", + "@types/jest": "^26.0.3", "jest": "^25.5.4", "@types/node": "^10.17.26", "typescript": "~3.9.5", diff --git a/yarn.lock b/yarn.lock index dbf2fa0927a2c..3ad380188332a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1476,10 +1476,10 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" -"@types/jest@^26.0.0": - version "26.0.0" - resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.0.tgz#a6d7573dffa9c68cbbdf38f2e0de26f159e11134" - integrity sha512-/yeMsH9HQ1RLORlXAwoLXe8S98xxvhNtUz3yrgrwbaxYjT+6SFPZZRksmRKRA6L5vsUtSHeN71viDOTTyYAD+g== +"@types/jest@^26.0.3": + version "26.0.3" + resolved "https://registry.yarnpkg.com/@types/jest/-/jest-26.0.3.tgz#79534e0e94857171c0edc596db0ebe7cb7863251" + integrity sha512-v89ga1clpVL/Y1+YI0eIu1VMW+KU7Xl8PhylVtDKVWaSUHBHYPLXMQGBdrpHewaKoTvlXkksbYqPgz8b4cmRZg== dependencies: jest-diff "^25.2.1" pretty-format "^25.2.1" @@ -1501,10 +1501,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.156": - version "4.14.156" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.156.tgz#cbe30909c89a1feeb7c60803e785344ea0ec82d1" - integrity sha512-l2AgHXcKUwx2DsvP19wtRPqZ4NkONjmorOdq4sMcxIjqdIuuV/ULo2ftuv4NUpevwfW7Ju/UKLqo0ZXuEt/8lQ== +"@types/lodash@^4.14.157": + version "4.14.157" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.157.tgz#fdac1c52448861dfde1a2e1515dbc46e54926dc8" + integrity sha512-Ft5BNFmv2pHDgxV5JDsndOWTRJ+56zte0ZpYLowp03tW+K+t8u8YMOzAnpuqPgzX6WO1XpDIUm7u04M8vdDiVQ== "@types/md5@^2.2.0": version "2.2.0" @@ -1572,10 +1572,10 @@ resolved "https://registry.yarnpkg.com/@types/proxyquire/-/proxyquire-1.3.28.tgz#05a647bb0d8fe48fc8edcc193e43cc79310faa7d" integrity sha512-SQaNzWQ2YZSr7FqAyPPiA3FYpux2Lqh3HWMZQk47x3xbMCqgC/w0dY3dw9rGqlweDDkrySQBcaScXWeR+Yb11Q== -"@types/semver@^7.2.0": - version "7.2.0" - resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.2.0.tgz#0d72066965e910531e1db4621c15d0ca36b8d83b" - integrity sha512-TbB0A8ACUWZt3Y6bQPstW9QNbhNeebdgLX4T/ZfkrswAfUzRiXrgd9seol+X379Wa589Pu4UEx9Uok0D4RjRCQ== +"@types/semver@^7.3.1": + version "7.3.1" + resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.3.1.tgz#7a9a5d595b6d873f338c867dcef64df289468cfa" + integrity sha512-ooD/FJ8EuwlDKOI6D9HWxgIgJjMg2cuziXm/42npDC8y4NjxplBUn9loewZiBNCt44450lHAU0OSb51/UqXeag== dependencies: "@types/node" "*" @@ -2124,10 +2124,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.637.0, aws-sdk@^2.703.0: - version "2.703.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.703.0.tgz#b22a65094c65109ce52c206e5f46e05247c8aaf4" - integrity sha512-iMJueMVDp2fqopgpjPfejyFaxaksYYdRJ7bxzWEYSxR1UoSf6V9zgcrgkF+SgoxiKJ2rxsbPxhoPu2MV//b9xA== +aws-sdk@^2.637.0, aws-sdk@^2.706.0: + version "2.706.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.706.0.tgz#09f65e9a91ecac5a635daf934082abae30eca953" + integrity sha512-7GT+yrB5Wb/zOReRdv/Pzkb2Qt+hz6B/8FGMVaoysX3NryHvQUdz7EQWi5yhg9CxOjKxdw5lFwYSs69YlSp1KA== dependencies: buffer "4.9.2" events "1.1.1" @@ -3855,10 +3855,10 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-import@^2.21.2: - version "2.21.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.21.2.tgz#8fef77475cc5510801bedc95f84b932f7f334a7c" - integrity sha512-FEmxeGI6yaz+SnEB6YgNHlQK1Bs2DKLM+YF+vuTk5H8J9CLbJLtlPvRFgZZ2+sXiKAlN5dpdlrWOjK8ZoZJpQA== +eslint-plugin-import@^2.22.0: + version "2.22.0" + resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.0.tgz#92f7736fe1fde3e2de77623c838dd992ff5ffb7e" + integrity sha512-66Fpf1Ln6aIS5Gr/55ts19eUuoDhAbZgnr6UxK5hbDx6l/QgQgx61AePq+BV4PP2uXQFClgMVzep5zZ94qqsxg== dependencies: array-includes "^3.1.1" array.prototype.flat "^1.2.3" @@ -6923,14 +6923,14 @@ nise@^4.0.1: just-extend "^4.0.2" path-to-regexp "^1.7.0" -nock@^12.0.3: - version "12.0.3" - resolved "https://registry.yarnpkg.com/nock/-/nock-12.0.3.tgz#83f25076dbc4c9aa82b5cdf54c9604c7a778d1c9" - integrity sha512-QNb/j8kbFnKCiyqi9C5DD0jH/FubFGj5rt9NQFONXwQm3IPB0CULECg/eS3AU1KgZb/6SwUa4/DTRKhVxkGABw== +nock@^13.0.0: + version "13.0.0" + resolved "https://registry.yarnpkg.com/nock/-/nock-13.0.0.tgz#e07276d558245386a2872cebf4d5570583c0d225" + integrity sha512-FiW8t91Je5yG5MVT1r+go1Z9bX3rCYIEjenUYeZrEl2v8aTWdIX336itrmQaKUO8Ske5Z7RHR7OIzr/9p0Ujjg== dependencies: debug "^4.1.0" json-stringify-safe "^5.0.1" - lodash "^4.17.13" + lodash.set "^4.3.2" propagate "^2.0.0" node-fetch-npm@^2.0.2: