diff --git a/.github/workflows/issue-label-assign.yml b/.github/workflows/issue-label-assign.yml index 618de4693571f..9451bd1b960ae 100644 --- a/.github/workflows/issue-label-assign.yml +++ b/.github/workflows/issue-label-assign.yml @@ -21,23 +21,23 @@ jobs: [ {"keywords":["(cli)","(command line)"],"labels":["package/tools"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/alexa-ask)","(alexa-ask)","(alexa ask)"],"labels":["@aws-cdk/alexa-ask"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/app-delivery)","(app-delivery)","(app delivery)"],"labels":["@aws-cdk/app-delivery"],"assignees":["madeline-k"]}, + {"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":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-acmpca)","(aws-acmpca)","(acmpca)"],"labels":["@aws-cdk/aws-acmpca"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-accessanalyzer)","(aws-accessanalyzer)","(accessanalyzer)","(access analyzer)"],"labels":["@aws-cdk/aws-accessanalyzer"],"assignees":["skinny85"]}, + {"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)","(amazon-mq)"],"labels":["@aws-cdk/aws-amazonmq"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-amplify)","(aws-amplify)","(amplify)"],"labels":["@aws-cdk/aws-amplify"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-apigateway)","(aws-apigateway)","(apigateway)","(api gateway)","(api-gateway)"],"labels":["@aws-cdk/aws-apigateway"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2)","(aws-apigatewayv2)","(apigatewayv2)","(apigateway v2)","(api-gateway-v2)"],"labels":["@aws-cdk/aws-apigatewayv2"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2-authorizers)","(aws-apigatewayv2-authorizers)","(apigatewayv2-authorizers)"],"labels":["@aws-cdk/aws-apigatewayv2-authorizers"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-apigatewayv2-integrations)","(aws-apigatewayv2-integrations)","(apigatewayv2-integrations)","(apigateway v2 integrations)","(api-gateway-v2-integrations)"],"labels":["@aws-cdk/aws-apigatewayv2-integrations"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-appconfig)","(aws-appconfig)","(appconfig)","(app config)","(app-config)"],"labels":["@aws-cdk/aws-appconfig"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/aws-appflow)","(aws-appflow)","(appflow)","(app flow)","(app-flow)"],"labels":["@aws-cdk/aws-appflow"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-appflow)","(aws-appflow)","(appflow)","(app flow)","(app-flow)"],"labels":["@aws-cdk/aws-appflow"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-appintegrations)","(aws-appintegrations)","(appintegrations)"],"labels":["@aws-cdk/aws-appintegrations"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-applicationautoscaling)","(aws-applicationautoscaling)","(applicationautoscaling)","(application autoscaling)","(application-autoscaling)"],"labels":["@aws-cdk/aws-applicationautoscaling"],"assignees":["comcalvi"]}, {"keywords":["(@aws-cdk/aws-applicationinsights)","(aws-applicationinsights)","(applicationinsights)","(application insights)","(application-insights)"],"labels":["@aws-cdk/aws-applicationinsights"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-appmesh)","(aws-appmesh)","(appmesh)","(app mesh)","(app-mesh)"],"labels":["@aws-cdk/aws-appmesh"],"assignees":["Seiya6329"]}, {"keywords":["(@aws-cdk/aws-appstream)","(aws-appstream)","(appstream)","(app stream)","(app-stream)"],"labels":["@aws-cdk/aws-appstream"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-appsync)","(aws-appsync)","(appsync)","(app sync)","(app-sync)"],"labels":["@aws-cdk/aws-appsync"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-athena)","(aws-athena)","(athena)"],"labels":["@aws-cdk/aws-athena"],"assignees":["BenChaimberg"]}, @@ -54,7 +54,7 @@ jobs: {"keywords":["(@aws-cdk/aws-ce)","(aws-ce)","(ce)"],"labels":["@aws-cdk/aws-ce"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-certificatemanager)","(aws-certificatemanager)","(certificatemanager)","(certificate manager)","(certificate-manager)","(acm)"],"labels":["@aws-cdk/aws-certificatemanager"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-chatbot)","(aws-chatbot)","(chatbot)"],"labels":["@aws-cdk/aws-chatbot"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-cloud9)","(aws-cloud9)","(cloud9)","(cloud 9)"],"labels":["@aws-cdk/aws-cloud9"],"assignees":["BenChaimberg"]}, + {"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":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-cloudfront)","(aws-cloudfront)","(cloudfront)","(cloud front)"],"labels":["@aws-cdk/aws-cloudfront"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-cloudfront-origins)","(aws-cloudfront-origins)","(cloudfront-origins)","(cloudfront origins)"],"labels":["@aws-cdk/aws-cloudfront-origins"],"assignees":["njlynch"]}, @@ -62,31 +62,31 @@ jobs: {"keywords":["(@aws-cdk/aws-cloudwatch)","(aws-cloudwatch)","(cloudwatch)","(cloud watch)"],"labels":["@aws-cdk/aws-cloudwatch"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-cloudwatch-actions)","(aws-cloudwatch-actions)","(cloudwatch-actions)","(cloudwatch actions)"],"labels":["@aws-cdk/aws-cloudwatch-actions"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-codeartifact)","(aws-codeartifact)","(codeartifact)","(code artifact)","(code-artifact)"],"labels":["@aws-cdk/aws-codeartifact"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(code-pipeline)"],"labels":["@aws-cdk/aws-codepipeline"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codepipeline-actions)","(aws-codepipeline-actions)","(codepipeline-actions)","(codepipeline actions)"],"labels":["@aws-cdk/aws-codepipeline-actions"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestar)","(aws-codestar)","(codestar)"],"labels":["@aws-cdk/aws-codestar"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestarconnections)","(aws-codestarconnections)","(codestarconnections)","(codestar connections)","(codestar-connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)","(codestar-notifications)"],"labels":["@aws-cdk/aws-codestarnotifications"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-codebuild)","(aws-codebuild)","(codebuild)","(code build)","(code-build)"],"labels":["@aws-cdk/aws-codebuild"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codecommit)","(aws-codecommit)","(codecommit)","(code commit)", "(code-commit)"],"labels":["@aws-cdk/aws-codecommit"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codedeploy)","(aws-codedeploy)","(codedeploy)","(code deploy)","(code-deploy)"],"labels":["@aws-cdk/aws-codedeploy"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codeguruprofiler)","(aws-codeguruprofiler)","(codeguruprofiler)","(codeguru profiler)","(codeguru-profiler)"],"labels":["@aws-cdk/aws-codeguruprofiler"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codegurureviewer)","(aws-codegurureviewer)","(codegurureviewer)","(codeguru reviewer)","(codeguru-reviewer)"],"labels":["@aws-cdk/aws-codegurureviewer"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codepipeline)","(aws-codepipeline)","(codepipeline)","(code pipeline)","(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)","(codestar-connections)"],"labels":["@aws-cdk/aws-codestarconnections"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-codestarnotifications)","(aws-codestarnotifications)","(codestarnotifications)","(codestar notifications)","(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":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-customerprofiles)","(aws-customerprofiles)","(customerprofiles)"],"labels":["@aws-cdk/aws-customerprofiles"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-databrew)","(aws-databrew)","(databrew)"],"labels":["@aws-cdk/aws-databrew"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-datapipeline)","(aws-datapipeline)","(datapipeline)","(data pipeline)","(data-pipeline)"],"labels":["@aws-cdk/aws-datapipeline"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-datasync)","(aws-datasync)","(datasync)"],"labels":["@aws-cdk/aws-datasync"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-dax)","(aws-dax)","(dax)"],"labels":["@aws-cdk/aws-dax"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-detective)","(aws-detective)","(detective)"],"labels":["@aws-cdk/aws-detective"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-devopsguru)","(aws-devopsguru)","(devopsguru)"],"labels":["@aws-cdk/aws-devopsguru"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/aws-directoryservice)","(aws-directoryservice)","(directoryservice)","(directory service)","(directory-service)"],"labels":["@aws-cdk/aws-directoryservice"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-dlm)","(aws-dlm)","(dlm)"],"labels":["@aws-cdk/aws-dlm"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-dms)","(aws-dms)","(dms)"],"labels":["@aws-cdk/aws-dms"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-docdb)","(aws-docdb)","(docdb)","(doc db)","(doc-db)"],"labels":["@aws-cdk/aws-docdb"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb)","(aws-dynamodb)","(dynamodb)","(dynamo db)","(dynamo-db)"],"labels":["@aws-cdk/aws-dynamodb"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-dynamodb-global)","(aws-dynamodb-global)","(dynamodb-global)","(dynamodb global)"],"labels":["@aws-cdk/aws-dynamodb-global"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-ec2)","(aws-ec2)","(ec2)","(vpc)"],"labels":["@aws-cdk/aws-ec2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-ecr)","(aws-ecr)","(ecr)"],"labels":["@aws-cdk/aws-ecr"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-ecr-assets)","(aws-ecr-assets)","(ecr-assets)","(ecr assets)","(ecrassets)"],"labels":["@aws-cdk/aws-ecr-assets"],"assignees":["eladb"]}, @@ -96,7 +96,7 @@ jobs: {"keywords":["(@aws-cdk/aws-eks)","(aws-eks)","(eks)"],"labels":["@aws-cdk/aws-eks"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-eks-legacy)","(aws-eks-legacy)","(eks-legacy)"],"labels":["@aws-cdk/aws-eks-legacy"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-elasticache)","(aws-elasticache)","(elasticache)","(elastic cache)","(elastic-cache)"],"labels":["@aws-cdk/aws-elasticache"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["madeline-k"]}, + {"keywords":["(@aws-cdk/aws-elasticbeanstalk)","(aws-elasticbeanstalk)","(elasticbeanstalk)","(elastic beanstalk)","(elastic-beanstalk)"],"labels":["@aws-cdk/aws-elasticbeanstalk"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancing)","(aws-elasticloadbalancing)","(elasticloadbalancing)","(elastic loadbalancing)","(elastic-loadbalancing)","(elb)"],"labels":["@aws-cdk/aws-elasticloadbalancing"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2)","(aws-elasticloadbalancingv2)","(elasticloadbalancingv2)","(elastic-loadbalancing-v2)","(elbv2)","(elb v2)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-elasticloadbalancingv2-actions)","(aws-elasticloadbalancingv2-actions)","(elasticloadbalancingv2-actions)"],"labels":["@aws-cdk/aws-elasticloadbalancingv2-actions"],"assignees":["njlynch"]}, @@ -106,7 +106,7 @@ jobs: {"keywords":["(@aws-cdk/aws-emrcontainers)","(aws-emrcontainers)","(emrcontainers)"],"labels":["@aws-cdk/aws-emrcontainers"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-events)","(aws-events)","(events)","(eventbridge)","(event-bridge)"],"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":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-eventschemas)","(aws-eventschemas)","(eventschemas)","(event schemas)"],"labels":["@aws-cdk/aws-eventschemas"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-finspace)","(aws-finspace)","(finspace)"],"labels":["@aws-cdk/aws-finspace"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-fis)","(aws-fis)","(fis)"],"labels":["@aws-cdk/aws-fis"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-fms)","(aws-fms)","(fms)"],"labels":["@aws-cdk/aws-fms"],"assignees":["rix0rrr"]}, @@ -116,23 +116,23 @@ jobs: {"keywords":["(@aws-cdk/aws-globalaccelerator)","(aws-globalaccelerator)","(globalaccelerator)","(global accelerator)","(global-accelerator)"],"labels":["@aws-cdk/aws-globalaccelerator"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-globalaccelerator-endpoints)","(aws-globalaccelerator-endpoints)","(globalaccelerator-endpoints)"],"labels":["@aws-cdk/aws-globalaccelerator-endpoints"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-glue)","(aws-glue)","(glue)"],"labels":["@aws-cdk/aws-glue"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-greengrassv2)","(aws-greengrassv2)","(greengrassv2)"],"labels":["@aws-cdk/aws-greengrassv2"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-greengrass)","(aws-greengrass)","(greengrass)","(green grass)","(green-grass)"],"labels":["@aws-cdk/aws-greengrass"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-greengrassv2)","(aws-greengrassv2)","(greengrassv2)"],"labels":["@aws-cdk/aws-greengrassv2"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-groundstation)","(aws-groundstation)","(groundstation)"],"labels":["@aws-cdk/aws-groundstation"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-guardduty)","(aws-guardduty)","(guardduty)","(guard duty)","(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)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotfleethub)","(aws-iotfleethub)","(iotfleethub)"],"labels":["@aws-cdk/aws-iotfleethub"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-iotwireless)","(aws-iotwireless)","(iotwireless)"],"labels":["@aws-cdk/aws-iotwireless"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-imagebuilder)","(aws-imagebuilder)","(imagebuilder)","(image builder)","(image-builder)"],"labels":["@aws-cdk/aws-imagebuilder"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-inspector)","(aws-inspector)","(inspector)"],"labels":["@aws-cdk/aws-inspector"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot)","(aws-iot)","(iot)"],"labels":["@aws-cdk/aws-iot"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iot1click)","(aws-iot1click)","(iot1click)","(iot 1click)"],"labels":["@aws-cdk/aws-iot1click"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotanalytics)","(aws-iotanalytics)","(iotanalytics)","(iot analytics)","(iot-analytics)"],"labels":["@aws-cdk/aws-iotanalytics"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotevents)","(aws-iotevents)","(iotevents)","(iot events)","(iot-events)"],"labels":["@aws-cdk/aws-iotevents"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotfleethub)","(aws-iotfleethub)","(iotfleethub)"],"labels":["@aws-cdk/aws-iotfleethub"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotsitewise)","(aws-iotsitewise)","(iotsitewise)","(iot sitewise)","(iot-sitewise)","(iot-site-wise)","(iot site wise)"],"labels":["@aws-cdk/aws-iotsitewise"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotthingsgraph)","(aws-iotthingsgraph)","(iotthingsgraph)","(iot things graph)","(iot-things-graph)"],"labels":["@aws-cdk/aws-iotthingsgraph"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-iotwireless)","(aws-iotwireless)","(iotwireless)"],"labels":["@aws-cdk/aws-iotwireless"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-ivs)","(aws-ivs)","(Interactive Video Service)","(ivs)"],"labels":["@aws-cdk/aws-ivs"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-kendra)","(aws-kendra)","(kendra)"],"labels":["@aws-cdk/aws-kendra"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-kinesis)","(aws-kinesis)","(kinesis)"],"labels":["@aws-cdk/aws-kinesis"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics)","(aws-kinesisanalytics)","(kinesisanalytics)","(kinesis analytics)","(kinesis-analytics)"],"labels":["@aws-cdk/aws-kinesisanalytics"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-kinesisanalytics-flink)","(aws-kinesisanalytics-flink)","(kinesisanalytics-flink)"],"labels":["@aws-cdk/aws-kinesisanalytics-flink"],"assignees":["otaviomacedo"]}, @@ -151,27 +151,27 @@ jobs: {"keywords":["(@aws-cdk/aws-lookoutmetrics)","(aws-lookoutmetrics)","(lookoutmetrics)"],"labels":["@aws-cdk/aws-lookoutmetrics"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-lookoutvision)","(aws-lookoutvision)","(lookoutvision)"],"labels":["@aws-cdk/aws-lookoutvision"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-macie)","(aws-macie)","(macie)"],"labels":["@aws-cdk/aws-macie"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediaconnect)","(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-managedblockchain)","(aws-managedblockchain)","(managedblockchain)","(managed blockchain)","(managed-blockchain)"],"labels":["@aws-cdk/aws-managedblockchain"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconnect)","(aws-mediaconnect)","(mediaconnect)"],"labels":["@aws-cdk/aws-mediaconnect"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediaconvert)","(aws-mediaconvert)","(mediaconvert)","(media convert)","(media-convert)"],"labels":["@aws-cdk/aws-mediaconvert"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-medialive)","(aws-medialive)","(medialive)","(media live)","(media-live)"],"labels":["@aws-cdk/aws-medialive"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediastore)","(aws-mediastore)","(mediastore)","(media store)","(media-store)"],"labels":["@aws-cdk/aws-mediastore"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-mediapackage)","(aws-mediapackage)","(mediapackage)","(media package)","(media-package)"],"labels":["@aws-cdk/aws-mediapackage"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-msk)","(aws-msk)","(msk)"],"labels":["@aws-cdk/aws-msk"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-mwaa)","(aws-mwaa)","(mwaa)"],"labels":["@aws-cdk/aws-mwaa"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-neptune)","(aws-neptune)","(neptune)"],"labels":["@aws-cdk/aws-neptune"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-networkfirewall)","(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-networkfirewall)","(aws-networkfirewall)","(networkfirewall)"],"labels":["@aws-cdk/aws-networkfirewall"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-networkmanager)","(aws-networkmanager)","(networkmanager)","(network manager)","(network-manager)"],"labels":["@aws-cdk/aws-networkmanager"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-nimblestudio)","(aws-nimblestudio)","(nimblestudio)"],"labels":["@aws-cdk/aws-nimblestudio"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-opsworks)","(aws-opsworks)","(opsworks)","(ops works)","(ops-works)"],"labels":["@aws-cdk/aws-opsworks"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-opsworkscm)","(aws-opsworkscm)","(opsworkscm)","(opsworks cm)","(opsworks-cm)"],"labels":["@aws-cdk/aws-opsworkscm"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-personalize)","(aws-personalize)","(personalize)"],"labels":["@aws-cdk/aws-personalize"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-pinpoint)","(aws-pinpoint)","(pinpoint)"],"labels":["@aws-cdk/aws-pinpoint"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-pinpointemail)","(aws-pinpointemail)","(pinpointemail)","(pinpoint email)","(pinpoint-email)"],"labels":["@aws-cdk/aws-pinpointemail"],"assignees":["otaviomacedo"]}, - {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-qldb)","(aws-qldb)","(qldb)"],"labels":["@aws-cdk/aws-qldb"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-quicksight)","(aws-quicksight)","(quicksight)"],"labels":["@aws-cdk/aws-quicksight"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-ram)","(aws-ram)","(ram)"],"labels":["@aws-cdk/aws-ram"],"assignees":["madeline-k"]}, - {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-rds)","(aws-rds)","(rds)"],"labels":["@aws-cdk/aws-rds"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-redshift)","(aws-redshift)","(redshift)","(red shift)","(red-shift)"],"labels":["@aws-cdk/aws-redshift"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-resourcegroups)","(aws-resourcegroups)","(resourcegroups)","(resource groups)","(resource-groups)"],"labels":["@aws-cdk/aws-resourcegroups"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-robomaker)","(aws-robomaker)","(robomaker)","(robo maker)","(robo-maker)"],"labels":["@aws-cdk/aws-robomaker"],"assignees":["njlynch"]}, @@ -189,9 +189,9 @@ jobs: {"keywords":["(@aws-cdk/aws-sam)","(aws-sam)","(sam)"],"labels":["@aws-cdk/aws-sam"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sdb)","(aws-sdb)","(sdb)"],"labels":["@aws-cdk/aws-sdb"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-secretsmanager)","(aws-secretsmanager)","(secretsmanager)","(secrets manager)","(secrets-manager)"],"labels":["@aws-cdk/aws-secretsmanager"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-servicecatalogappregistry)","(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-securityhub)","(aws-securityhub)","(securityhub)","(security hub)","(security-hub)"],"labels":["@aws-cdk/aws-securityhub"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-servicecatalog)","(aws-servicecatalog)","(servicecatalog)","(service catalog)","(service-catalog)"],"labels":["@aws-cdk/aws-servicecatalog"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/aws-servicecatalogappregistry)","(aws-servicecatalogappregistry)","(servicecatalogappregistry)"],"labels":["@aws-cdk/aws-servicecatalogappregistry"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-servicediscovery)","(aws-servicediscovery)","(servicediscovery)","(service discovery)","(service-discovery)"],"labels":["@aws-cdk/aws-servicediscovery"],"assignees":["madeline-k"]}, {"keywords":["(@aws-cdk/aws-ses)","(aws-ses)","(ses)"],"labels":["@aws-cdk/aws-ses"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-ses-actions)","(aws-ses-actions)","(ses-actions)","(ses actions)"],"labels":["@aws-cdk/aws-ses-actions"],"assignees":["otaviomacedo"]}, @@ -200,11 +200,11 @@ jobs: {"keywords":["(@aws-cdk/aws-sns-subscriptions)","(aws-sns-subscriptions)","(sns-subscriptions)","(sns subscriptions)"],"labels":["@aws-cdk/aws-sns-subscriptions"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-sqs)","(aws-sqs)","(sqs)"],"labels":["@aws-cdk/aws-sqs"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-ssm)","(aws-ssm)","(ssm)"],"labels":["@aws-cdk/aws-ssm"],"assignees":["njlynch"]}, - {"keywords":["(@aws-cdk/aws-sso)","(aws-sso)","(sso)"],"labels":["@aws-cdk/aws-sso"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-sso)","(aws-sso)","(sso)"],"labels":["@aws-cdk/aws-sso"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-stepfunctions)","(aws-stepfunctions)","(stepfunctions)","(step functions)","(step-functions)"],"labels":["@aws-cdk/aws-stepfunctions"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-stepfunctions-tasks)","(aws-stepfunctions-tasks)","(stepfunctions-tasks)","(stepfunctions tasks)"],"labels":["@aws-cdk/aws-stepfunctions-tasks"],"assignees":["BenChaimberg"]}, {"keywords":["(@aws-cdk/aws-synthetics)","(aws-synthetics)","(synthetics)"],"labels":["@aws-cdk/aws-synthetics"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/aws-timestream)","(aws-timestream)","(timestream)"],"labels":["@aws-cdk/aws-timestream"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/aws-transfer)","(aws-transfer)","(transfer)"],"labels":["@aws-cdk/aws-transfer"],"assignees":["otaviomacedo"]}, {"keywords":["(@aws-cdk/aws-waf)","(aws-waf)","(waf)"],"labels":["@aws-cdk/aws-waf"],"assignees":["njlynch"]}, {"keywords":["(@aws-cdk/aws-wafregional)","(aws-wafregional)","(wafregional)","(waf regional)","(waf-regional)"],"labels":["@aws-cdk/aws-wafregional"],"assignees":["njlynch"]}, @@ -213,16 +213,16 @@ jobs: {"keywords":["(@aws-cdk/aws-xray)","(aws-xray)","(xray)"],"labels":["@aws-cdk/aws-xray"],"assignees":["nija-at"]}, {"keywords":["(@aws-cdk/cfnspec)","(cfnspec)","(cfn spec)","(cfn-spec)"],"labels":["@aws-cdk/cfnspec"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cloud-assembly-schema)","(cloud-assembly-schema)","(cloud assembly schema)"],"labels":["@aws-cdk/cloud-assembly-schema"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["BenChaimberg"]}, - {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/cloudformation-diff)","(cloudformation-diff)","(cloudformation diff)","(cfn diff)"],"labels":["@aws-cdk/cloudformation-diff"],"assignees":["skinny85"]}, + {"keywords":["(@aws-cdk/cloudformation-include)","(cloudformation-include)","(cloudformation include)","(cfn include)","(cfn-include)"],"labels":["@aws-cdk/cloudformation-include"],"assignees":["skinny85"]}, {"keywords":["(@aws-cdk/core)","(core)"],"labels":["@aws-cdk/core"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/custom-resources)","(custom-resources)","(custom resources)"],"labels":["@aws-cdk/custom-resources"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/cx-api)","(cx-api)","(cx api)"],"labels":["@aws-cdk/cx-api"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-lambda-layer-awscli)","(aws-lambda-layer-awscli)","(lambda-layer-awscli)"],"labels":["@aws-cdk/aws-lambda-layer-awscli"],"assignees":["rix0rrr"]}, {"keywords":["(@aws-cdk/aws-lambda-layer-kubectl)","(aws-lambda-layer-kubectl)","(lambda-layer-kubectl)"],"labels":["@aws-cdk/aws-lambda-layer-kubectl"],"assignees":["eladb"]}, {"keywords":["(@aws-cdk/pipelines)","(pipelines)","(cdk pipelines)","(cdk-pipelines)"],"labels":["@aws-cdk/pipelines"],"assignees":["rix0rrr"]}, - {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["BenChaimberg"]}, + {"keywords":["(@aws-cdk/region-info)","(region-info)","(region info)"],"labels":["@aws-cdk/region-info"],"assignees":["skinny85"]}, {"keywords":["(aws-cdk-lib)","(cdk-v2)", "(v2)", "(ubergen)"],"labels":["aws-cdk-lib"],"assignees":["nija-at"]}, {"keywords":["(monocdk)","(monocdk-experiment)"],"labels":["monocdk"],"assignees":["nija-at"]}, - {"keywords":["(@aws-cdk/yaml-cfn)","(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["BenChaimberg"]} + {"keywords":["(@aws-cdk/yaml-cfn)","(aws-yaml-cfn)","(yaml-cfn)"],"labels":["@aws-cdk/aws-yaml-cfn"],"assignees":["skinny85"]} ] diff --git a/CHANGELOG.md b/CHANGELOG.md index c207fc6012f7c..dd16881355b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,41 @@ 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.120.0](https://github.com/aws/aws-cdk/compare/v1.119.0...v1.120.0) (2021-08-26) + + +### Features + +* **assertions:** queries and assertions against the Outputs and Mappings sections ([#15892](https://github.com/aws/aws-cdk/issues/15892)) ([90f95e1](https://github.com/aws/aws-cdk/commit/90f95e10f4dd9e4992731d6262dcfc767b65ab3f)) +* **stepfunctions:** add support to heartbeat error inside catch block ([#16078](https://github.com/aws/aws-cdk/issues/16078)) ([2372b3c](https://github.com/aws/aws-cdk/commit/2372b3c360d13fb0224fc981a7bb1ae318581265)), closes [#16084](https://github.com/aws/aws-cdk/issues/16084) +* **cfnspec:** cloudformation spec v40.0.0 ([#16183](https://github.com/aws/aws-cdk/issues/16183)) ([b059124](https://github.com/aws/aws-cdk/commit/b059124b238e27751217cbdaaa01c38b00e80fc9)) +* **cloudwatch:** add support for cross-account alarms ([#16007](https://github.com/aws/aws-cdk/issues/16007)) ([e547ba0](https://github.com/aws/aws-cdk/commit/e547ba0d1491af0abe703132fa06fe786ffd7070)), closes [#15959](https://github.com/aws/aws-cdk/issues/15959) +* **codecommit:** make Repository a source for CodeStar Notifications ([#15739](https://github.com/aws/aws-cdk/issues/15739)) ([ae34d4a](https://github.com/aws/aws-cdk/commit/ae34d4a69a5073d8f0175b5282fa8bf92139fab5)) +* **cognito:** user pools - device tracking ([#16055](https://github.com/aws/aws-cdk/issues/16055)) ([64019bb](https://github.com/aws/aws-cdk/commit/64019bbf090e156261feb626a5a4bd7ff4f26545)), closes [#15013](https://github.com/aws/aws-cdk/issues/15013) +* **docdb:** cluster - deletion protection ([#15216](https://github.com/aws/aws-cdk/issues/15216)) ([0f7beb2](https://github.com/aws/aws-cdk/commit/0f7beb29be18d809052f4d46e415a0394c9299ab)) +* **ecs:** add support for Bottlerocket on ARM64 ([#15454](https://github.com/aws/aws-cdk/issues/15454)) ([cd280a8](https://github.com/aws/aws-cdk/commit/cd280a8f4f46eb50be3a25d80c00a807881832c4)), closes [#14466](https://github.com/aws/aws-cdk/issues/14466) +* **lambda:** nodejs14.x supports inline code ([#16131](https://github.com/aws/aws-cdk/issues/16131)) ([305f683](https://github.com/aws/aws-cdk/commit/305f683e86cca221705c0138572faa38043396eb)) +* **rds:** support 's3export' for Postgres database instances ([#16124](https://github.com/aws/aws-cdk/issues/16124)) ([1d54a45](https://github.com/aws/aws-cdk/commit/1d54a456cd5e2ff65251097f9a684e1ac200cc52)), closes [#14546](https://github.com/aws/aws-cdk/issues/14546) [#10370](https://github.com/aws/aws-cdk/issues/10370) [#14546](https://github.com/aws/aws-cdk/issues/14546) +* **s3-deployment:** exclude and include filters ([#16054](https://github.com/aws/aws-cdk/issues/16054)) ([d42e89e](https://github.com/aws/aws-cdk/commit/d42e89e01034dcba08c8f8ac0390a743143c4531)), closes [#14362](https://github.com/aws/aws-cdk/issues/14362) [#14362](https://github.com/aws/aws-cdk/issues/14362) + + +### Bug Fixes + +* **apigatewayv2:** http api - disallow empty string as domain name ([#16044](https://github.com/aws/aws-cdk/issues/16044)) ([9c39bcb](https://github.com/aws/aws-cdk/commit/9c39bcb970fc791e94d199b962cc006fca1a3320)) +* **appsync:** addSubscription only allows for field type ([#16097](https://github.com/aws/aws-cdk/issues/16097)) ([000d151](https://github.com/aws/aws-cdk/commit/000d151bec2215aa530819c3cf2c8c432352fec3)), closes [#10078](https://github.com/aws/aws-cdk/issues/10078) [#16071](https://github.com/aws/aws-cdk/issues/16071) +* **cfnspec:** changes to resource-level documentation not supported ([#16170](https://github.com/aws/aws-cdk/issues/16170)) ([82e4b4f](https://github.com/aws/aws-cdk/commit/82e4b4f07be202e2d6c6afa4f9ed0d9d6146f0a8)) +* **cli:** Python init template does not work in directory with '-' ([#15939](https://github.com/aws/aws-cdk/issues/15939)) ([3b2c790](https://github.com/aws/aws-cdk/commit/3b2c790c2b7d210868576540feab4e088376ab6c)), closes [#15938](https://github.com/aws/aws-cdk/issues/15938) +* **cli:** unknown command pytest in build container fails integration tests ([#16134](https://github.com/aws/aws-cdk/issues/16134)) ([0f7c0b4](https://github.com/aws/aws-cdk/commit/0f7c0b421327f1ffed28de79692191af187f23ca)), closes [#15939](https://github.com/aws/aws-cdk/issues/15939) +* **resourcegroups:** ResourceGroup not using TagType.STANDARD, causes deploy failure ([#16211](https://github.com/aws/aws-cdk/issues/16211)) ([cdee1af](https://github.com/aws/aws-cdk/commit/cdee1af03c34a1c08988e672bae6edc2538a8877)), closes [#12986](https://github.com/aws/aws-cdk/issues/12986) +* **s3:** bucket is not emptied before update when the name changes ([#16203](https://github.com/aws/aws-cdk/issues/16203)) ([b1d69d7](https://github.com/aws/aws-cdk/commit/b1d69d7b06cd2a2ae8f578e217bdf7fef50a0163)), closes [#14011](https://github.com/aws/aws-cdk/issues/14011) +* **ses:** drop spam rule appears in the incorrect order ([#16146](https://github.com/aws/aws-cdk/issues/16146)) ([677fedc](https://github.com/aws/aws-cdk/commit/677fedcc5351b8b5346970fac03e5e342f36265b)), closes [#16091](https://github.com/aws/aws-cdk/issues/16091) +* **sqs:** unable to import a FIFO queue when the queue ARN is a token ([#15976](https://github.com/aws/aws-cdk/issues/15976)) ([a1a65bc](https://github.com/aws/aws-cdk/commit/a1a65bc9a38b06ec51dff462e52b1beb8d421a56)), closes [#12466](https://github.com/aws/aws-cdk/issues/12466) +* **ssm:** StringParameter.fromStringParameterAttributes cannot accept version as a numeric Token ([#16048](https://github.com/aws/aws-cdk/issues/16048)) ([eb54cd4](https://github.com/aws/aws-cdk/commit/eb54cd416a48708898e30986058491e21125b2f7)), closes [#11913](https://github.com/aws/aws-cdk/issues/11913) +* **ec2:** fix vpc endpoint incorrect issue in China region ([#16139](https://github.com/aws/aws-cdk/issues/16139)) ([0d0db38](https://github.com/aws/aws-cdk/commit/0d0db38e3cdb557b4a641c5993068400847cc7df)), closes [#9864](https://github.com/aws/aws-cdk/issues/9864) +* **eks:** insecure kubeconfig warning ([#16063](https://github.com/aws/aws-cdk/issues/16063)) ([82dd282](https://github.com/aws/aws-cdk/commit/82dd2822a86431d0aa0be896550d421810b80c67)), closes [#14560](https://github.com/aws/aws-cdk/issues/14560) + + + ## [1.119.0](https://github.com/aws/aws-cdk/compare/v1.118.0...v1.119.0) (2021-08-17) diff --git a/package.json b/package.json index 882a0a8808bce..c6047729c7b1b 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ "fs-extra": "^9.1.0", "graceful-fs": "^4.2.6", "jest-junit": "^12.2.0", - "jsii-diff": "^1.31.0", - "jsii-pacmak": "^1.31.0", - "jsii-reflect": "^1.31.0", - "jsii-rosetta": "^1.31.0", + "jsii-diff": "^1.34.0", + "jsii-pacmak": "^1.34.0", + "jsii-reflect": "^1.34.0", + "jsii-rosetta": "^1.34.0", "lerna": "^4.0.0", "patch-package": "^6.4.7", "standard-version": "^9.3.1", diff --git a/packages/@aws-cdk/app-delivery/.gitignore b/packages/@aws-cdk/app-delivery/.gitignore index 1ed331de29bb6..8e88c5db7a762 100644 --- a/packages/@aws-cdk/app-delivery/.gitignore +++ b/packages/@aws-cdk/app-delivery/.gitignore @@ -11,4 +11,5 @@ tsconfig.json coverage !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/app-delivery/.npmignore b/packages/@aws-cdk/app-delivery/.npmignore index b0d9cb5dd2512..a11c52cc9c6cb 100644 --- a/packages/@aws-cdk/app-delivery/.npmignore +++ b/packages/@aws-cdk/app-delivery/.npmignore @@ -22,4 +22,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/app-delivery/jest.config.js b/packages/@aws-cdk/app-delivery/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/app-delivery/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index e267f2dec2ddc..a9332da3d77b2 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -62,11 +62,12 @@ }, "devDependencies": { "@aws-cdk/aws-s3": "0.0.0", + "@types/jest": "^26.0.24", "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "fast-check": "^2.17.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, @@ -82,6 +83,9 @@ "url": "https://aws.amazon.com", "organization": true }, + "cdk-build": { + "jest": true + }, "keywords": [ "aws", "cdk" diff --git a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts b/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts similarity index 87% rename from packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts rename to packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts index e4611adf570f6..e065330c152d3 100644 --- a/packages/@aws-cdk/app-delivery/test/test.pipeline-deploy-stack-action.ts +++ b/packages/@aws-cdk/app-delivery/test/pipeline-deploy-stack-action.test.ts @@ -1,4 +1,5 @@ -import { expect, haveResource, haveResourceLike, isSuperObject } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { isSuperObject } from '@aws-cdk/assert-internal'; import * as cfn from '@aws-cdk/aws-cloudformation'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -10,7 +11,6 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; import * as fc from 'fast-check'; -import * as nodeunit from 'nodeunit'; import { PipelineDeployStackAction } from '../lib/pipeline-deploy-stack-action'; interface SelfUpdatingPipeline { @@ -19,14 +19,14 @@ interface SelfUpdatingPipeline { } const accountId = fc.array(fc.integer(0, 9), 12, 12).map(arr => arr.join()); -export = nodeunit.testCase({ - 'rejects cross-environment deployment'(test: nodeunit.Test) { +describe('pipeline deploy stack action', () => { + test('rejects cross-environment deployment', () => { fc.assert( fc.property( accountId, accountId, (pipelineAccount, stackAccount) => { fc.pre(pipelineAccount !== stackAccount); - test.throws(() => { + expect(() => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Test', { env: { account: pipelineAccount } }); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -43,20 +43,20 @@ export = nodeunit.testCase({ stack: new cdk.Stack(app, 'DeployedStack', { env: { account: stackAccount } }), adminPermissions: false, })); - }, 'Cross-environment deployment is not supported'); + }).toThrow('Cross-environment deployment is not supported'); }, ), ); - test.done(); - }, - 'rejects createRunOrder >= executeRunOrder'(test: nodeunit.Test) { + }); + + test('rejects createRunOrder >= executeRunOrder', () => { fc.assert( fc.property( fc.integer(1, 999), fc.integer(1, 999), (createRunOrder, executeRunOrder) => { fc.pre(createRunOrder >= executeRunOrder); - test.throws(() => { + expect(() => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Test'); const pipeline = new codepipeline.Pipeline(stack, 'Pipeline'); @@ -74,13 +74,13 @@ export = nodeunit.testCase({ stack: new cdk.Stack(app, 'DeployedStack'), adminPermissions: false, })); - }, 'createChangeSetRunOrder must be < executeChangeSetRunOrder'); + }).toThrow(/createChangeSetRunOrder .* must be < executeChangeSetRunOrder/); }, ), ); - test.done(); - }, - 'users can supply CloudFormation capabilities'(test: nodeunit.Test) { + + }); + test('users can supply CloudFormation capabilities', () => { const pipelineStack = getTestStack(); const stackWithNoCapability = new cdk.Stack(undefined, 'NoCapStack', { env: { account: '123456789012', region: 'us-east-1' } }); @@ -134,57 +134,57 @@ export = nodeunit.testCase({ capabilities: [cfn.CloudFormationCapabilities.ANONYMOUS_IAM, cfn.CloudFormationCapabilities.AUTO_EXPAND], adminPermissions: false, })); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'TestStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_NAMED_IAM', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'AnonymousIAM', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_IAM', }, - }))); - expect(pipelineStack).notTo(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).not.toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'NoCapStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_NAMED_IAM', }, - }))); - expect(pipelineStack).notTo(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).not.toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'NoCapStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_IAM', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'NoCapStack', ActionMode: 'CHANGE_SET_REPLACE', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'AutoExpand', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_AUTO_EXPAND', }, - }))); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + })); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'AnonymousIAMAndAutoExpand', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_IAM,CAPABILITY_AUTO_EXPAND', }, - }))); - test.done(); - }, - 'users can use admin permissions'(test: nodeunit.Test) { + })); + + }); + test('users can use admin permissions', () => { const pipelineStack = getTestStack(); const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); @@ -195,7 +195,7 @@ export = nodeunit.testCase({ input: selfUpdatingStack.synthesizedApp, adminPermissions: true, })); - expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -249,17 +249,17 @@ export = nodeunit.testCase({ }, ], }, - })); - expect(pipelineStack).to(haveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ + }); + expect(pipelineStack).toHaveResource('AWS::CodePipeline::Pipeline', hasPipelineAction({ Configuration: { StackName: 'TestStack', ActionMode: 'CHANGE_SET_REPLACE', Capabilities: 'CAPABILITY_NAMED_IAM,CAPABILITY_AUTO_EXPAND', }, - }))); - test.done(); - }, - 'users can supply a role for deploy action'(test: nodeunit.Test) { + })); + + }); + test('users can supply a role for deploy action', () => { const pipelineStack = getTestStack(); const selfUpdatingStack = createSelfUpdatingStack(pipelineStack); @@ -275,10 +275,10 @@ export = nodeunit.testCase({ role, }); selfUpdateStage.addAction(deployAction); - test.same(deployAction.deploymentRole, role); - test.done(); - }, - 'users can specify IAM permissions for the deploy action'(test: nodeunit.Test) { + expect(deployAction.deploymentRole).toEqual(role); + + }); + test('users can specify IAM permissions for the deploy action', () => { // GIVEN // const pipelineStack = getTestStack(); @@ -312,7 +312,7 @@ export = nodeunit.testCase({ })); // THEN // - expect(pipelineStack).to(haveResource('AWS::IAM::Policy', { + expect(pipelineStack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -379,10 +379,10 @@ export = nodeunit.testCase({ Ref: 'CodePipelineDeployChangeSetRoleF9F2B343', }, ], - })); - test.done(); - }, - 'rejects stacks with assets'(test: nodeunit.Test) { + }); + + }); + test('rejects stacks with assets', () => { fc.assert( fc.property( fc.integer(1, 5), @@ -394,21 +394,21 @@ export = nodeunit.testCase({ deployedStack.node.addMetadata(cxschema.ArtifactMetadataEntryType.ASSET, {}); } - test.throws(() => { + expect(() => { new PipelineDeployStackAction({ changeSetName: 'ChangeSet', input: new codepipeline.Artifact(), stack: deployedStack, adminPermissions: false, }); - }, /Cannot deploy the stack DeployedStack because it references/); + }).toThrow(/Cannot deploy the stack DeployedStack because it references/); }, ), ); - test.done(); - }, - 'allows overriding the ChangeSet and Execute action names'(test: nodeunit.Test) { + }); + + test('allows overriding the ChangeSet and Execute action names', () => { const stack = getTestStack(); const selfUpdatingPipeline = createSelfUpdatingStack(stack); selfUpdatingPipeline.pipeline.addStage({ @@ -424,7 +424,7 @@ export = nodeunit.testCase({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, {}, @@ -440,10 +440,10 @@ export = nodeunit.testCase({ ], }, ], - })); + }); + - test.done(); - }, + }); }); class FakeAction implements codepipeline.IAction { diff --git a/packages/@aws-cdk/assertions/README.md b/packages/@aws-cdk/assertions/README.md index 67109b1156964..d651ab72c1bc3 100644 --- a/packages/@aws-cdk/assertions/README.md +++ b/packages/@aws-cdk/assertions/README.md @@ -238,6 +238,35 @@ target array. Out of order will be recorded as a match failure. Alternatively, the `Match.arrayEquals()` API can be used to assert that the target is exactly equal to the pattern array. +### Not Matcher + +The not matcher inverts the search pattern and matches all patterns in the path that does +not match the pattern specified. + +```ts +// Given a template - +// { +// "Resources": { +// "MyBar": { +// "Type": "Foo::Bar", +// "Properties": { +// "Fred": ["Flob", "Cat"] +// } +// } +// } +// } + +// The following will NOT throw an assertion error +assert.hasResourceProperties('Foo::Bar', { + Fred: Match.not(['Flob']), +}); + +// The following will throw an assertion error +assert.hasResourceProperties('Foo::Bar', Match.objectLike({ + Fred: Match.not(['Flob', 'Cat']); +}}); +``` + ## Strongly typed languages Some of the APIs documented above, such as `templateMatches()` and diff --git a/packages/@aws-cdk/assertions/lib/match.ts b/packages/@aws-cdk/assertions/lib/match.ts index 2a88bada59bac..802acdc603e70 100644 --- a/packages/@aws-cdk/assertions/lib/match.ts +++ b/packages/@aws-cdk/assertions/lib/match.ts @@ -55,6 +55,14 @@ export abstract class Match { public static objectEquals(pattern: {[key: string]: any}): Matcher { return new ObjectMatch('objectEquals', pattern, { partial: false }); } + + /** + * Matches any target which does NOT follow the specified pattern. + * @param pattern the pattern to NOT match + */ + public static not(pattern: any): Matcher { + return new NotMatch('not', pattern); + } } /** @@ -82,7 +90,6 @@ class LiteralMatch extends Matcher { super(); this.partialObjects = options.partialObjects ?? false; - this.name = 'exact'; if (Matcher.isMatcher(this.pattern)) { throw new Error('LiteralMatch cannot directly contain another matcher. ' + @@ -143,11 +150,6 @@ class ArrayMatch extends Matcher { super(); this.partial = options.subsequence ?? true; - if (this.partial) { - this.name = 'arrayWith'; - } else { - this.name = 'arrayEquals'; - } } public test(actual: any): MatchResult { @@ -211,11 +213,6 @@ class ObjectMatch extends Matcher { super(); this.partial = options.partial ?? true; - if (this.partial) { - this.name = 'objectLike'; - } else { - this.name = 'objectEquals'; - } } public test(actual: any): MatchResult { @@ -254,6 +251,26 @@ class ObjectMatch extends Matcher { } } +class NotMatch extends Matcher { + constructor( + public readonly name: string, + private readonly pattern: {[key: string]: any}) { + + super(); + } + + public test(actual: any): MatchResult { + const matcher = Matcher.isMatcher(this.pattern) ? this.pattern : new LiteralMatch(this.name, this.pattern); + + const innerResult = matcher.test(actual); + const result = new MatchResult(actual); + if (innerResult.failCount === 0) { + result.push(this, [], `Found unexpected match: ${JSON.stringify(actual, undefined, 2)}`); + } + return result; + } +} + function getType(obj: any): string { return Array.isArray(obj) ? 'array' : typeof obj; } \ No newline at end of file diff --git a/packages/@aws-cdk/assertions/test/match.test.ts b/packages/@aws-cdk/assertions/test/match.test.ts index 697c46e139c11..b46eb0d53d204 100644 --- a/packages/@aws-cdk/assertions/test/match.test.ts +++ b/packages/@aws-cdk/assertions/test/match.test.ts @@ -194,17 +194,112 @@ describe('Matchers', () => { expectFailure(matcher, { foo: 'bar', baz: 'qux' }, [/Unexpected key at \/baz/]); }); }); + + describe('not()', () => { + let matcher: Matcher; + + test('literal', () => { + matcher = Match.not('foo'); + expectPass(matcher, 'bar'); + expectPass(matcher, 3); + + expectFailure(matcher, 'foo', ['Found unexpected match: "foo"']); + }); + + test('object', () => { + matcher = Match.not({ foo: 'bar' }); + expectPass(matcher, 'bar'); + expectPass(matcher, 3); + expectPass(matcher, { foo: 'baz' }); + expectPass(matcher, { bar: 'foo' }); + + const msg = [ + 'Found unexpected match: {', + ' "foo": "bar"', + '}', + ].join('\n'); + expectFailure(matcher, { foo: 'bar' }, [msg]); + }); + + test('array', () => { + matcher = Match.not(['foo', 'bar']); + expectPass(matcher, 'foo'); + expectPass(matcher, []); + expectPass(matcher, ['bar']); + expectPass(matcher, ['foo', 3]); + + const msg = [ + 'Found unexpected match: [', + ' "foo",', + ' "bar"', + ']', + ].join('\n'); + expectFailure(matcher, ['foo', 'bar'], [msg]); + }); + + test('as a nested matcher', () => { + matcher = Match.exact({ + foo: { bar: Match.not([1, 2]) }, + }); + + expectPass(matcher, { + foo: { bar: [1] }, + }); + expectPass(matcher, { + foo: { bar: ['baz'] }, + }); + + const msg = [ + 'Found unexpected match: [', + ' 1,', + ' 2', + '] at /foo/bar', + ].join('\n'); + expectFailure(matcher, { + foo: { bar: [1, 2] }, + }, [msg]); + }); + + test('with nested matcher', () => { + matcher = Match.not({ + foo: { bar: Match.arrayWith([1]) }, + }); + + expectPass(matcher, { + foo: { bar: [2] }, + }); + expectPass(matcher, 'foo'); + + const msg = [ + 'Found unexpected match: {', + ' "foo": {', + ' "bar": [', + ' 1,', + ' 2', + ' ]', + ' }', + '}', + ].join('\n'); + expectFailure(matcher, { + foo: { bar: [1, 2] }, + }, [msg]); + }); + }); }); function expectPass(matcher: Matcher, target: any): void { expect(matcher.test(target).hasFailed()).toEqual(false); } -function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[]): void { - const actual = matcher.test(target).toHumanStrings(); +function expectFailure(matcher: Matcher, target: any, expected: (string | RegExp)[] = []): void { + const result = matcher.test(target); + expect(result.failCount).toBeGreaterThan(0); + const actual = result.toHumanStrings(); + if (expected.length > 0) { + expect(actual.length).toEqual(expected.length); + } for (let i = 0; i < expected.length; i++) { const e = expected[i]; expect(actual[i]).toMatch(e); } - expect(expected.length).toEqual(actual.length); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts index f9ebfc89b52ca..a4031ddf783e2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/common/api-mapping.ts @@ -103,11 +103,6 @@ export class ApiMapping extends Resource implements IApiMapping { } } - const paramRe = '^[a-zA-Z0-9]*[-_.+!,$]?[a-zA-Z0-9]*$'; - if (props.apiMappingKey && !new RegExp(paramRe).test(props.apiMappingKey)) { - throw new Error('An ApiMapping key may contain only letters, numbers and one of $-_.+!*\'(),'); - } - if (props.apiMappingKey === '') { throw new Error('empty string for api mapping key not allowed'); } diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts index 607afb5f8238f..ff53d3dad11fc 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/common/api-mapping.test.ts @@ -83,120 +83,6 @@ describe('ApiMapping', () => { }).toThrow(/empty string for api mapping key not allowed/); }); - test('apiMappingKey validation - single slash not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '/', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - prefix slash not allowd', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '/foo', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - slash in the middle not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo/bar', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - trailing slash not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo/', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - special character in the prefix not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: '^foo', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - - test('apiMappingKey validation - multiple special character not allowed', () => { - - const stack = new Stack(); - const api = new HttpApi(stack, 'Api'); - - const dn = new DomainName(stack, 'DomainName', { - domainName, - certificate: Certificate.fromCertificateArn(stack, 'cert', certArn), - }); - - expect(() => { - new ApiMapping(stack, 'Mapping', { - api, - domainName: dn, - apiMappingKey: 'foo.*$', - }); - }).toThrow(/An ApiMapping key may contain only letters, numbers and one of/); - }); - test('import mapping', () => { const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/.gitignore b/packages/@aws-cdk/aws-applicationautoscaling/.gitignore index 018c65919d67c..7e6fdd4d423db 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/.gitignore +++ b/packages/@aws-cdk/aws-applicationautoscaling/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/.npmignore b/packages/@aws-cdk/aws-applicationautoscaling/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/.npmignore +++ b/packages/@aws-cdk/aws-applicationautoscaling/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-applicationautoscaling/jest.config.js b/packages/@aws-cdk/aws-applicationautoscaling/jest.config.js new file mode 100644 index 0000000000000..3eef7ba398c07 --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/jest.config.js @@ -0,0 +1,9 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 75, + }, + }, +}; diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 0b40e6f2eb95b..cc4b7b63a61f1 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -56,6 +56,7 @@ }, "cdk-build": { "cloudformation": "AWS::ApplicationAutoScaling", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": true } @@ -73,11 +74,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", "fast-check": "^2.17.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts new file mode 100644 index 0000000000000..b3c337883146c --- /dev/null +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/cron.test.ts @@ -0,0 +1,55 @@ +import { Duration, Stack, Lazy } from '@aws-cdk/core'; +import * as appscaling from '../lib'; + +describe('cron', () => { + test('test utc cron, hour only', () => { + expect(appscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString).toEqual('cron(0 18 * * ? *)'); + + }); + + test('test utc cron, hour and minute', () => { + expect(appscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString).toEqual('cron(24 18 * * ? *)'); + + }); + + test('rate must be whole number of minutes', () => { + expect(() => { + appscaling.Schedule.rate(Duration.minutes(0.13456)); + }).toThrow(/'0.13456 minutes' cannot be converted into a whole number of seconds/); + + }); + + test('rate can be in seconds', () => { + const duration = appscaling.Schedule.rate(Duration.seconds(120)); + expect('rate(2 minutes)').toEqual(duration.expressionString); + + }); + + test('rate must not be in seconds when specified as a token', () => { + expect(() => { + appscaling.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); + }).toThrow(/Allowed units for scheduling/); + + }); + + test('rate cannot be 0', () => { + expect(() => { + appscaling.Schedule.rate(Duration.days(0)); + }).toThrow(/Duration cannot be 0/); + + }); + + test('rate can be token', () => { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = appscaling.Schedule.rate(lazyDuration); + expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString); + + }); + + test('rate can be in allowed type hours', () => { + expect('rate(1 hour)').toEqual(appscaling.Schedule.rate(Duration.hours(1)) + .expressionString); + + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts similarity index 64% rename from packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts rename to packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts index 8c48f8b844a84..8628a8624468c 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.scalable-target.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/scalable-target.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as appscaling from '../lib'; import { createScalableTarget } from './util'; -export = { - 'test scalable target creation'(test: Test) { +describe('scalable target', () => { + test('test scalable target creation', () => { // GIVEN const stack = new cdk.Stack(); @@ -20,18 +19,18 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', MinCapacity: 1, MaxCapacity: 20, - })); + }); + - test.done(); - }, + }); - 'validation does not fail when using Tokens'(test: Test) { + test('validation does not fail when using Tokens', () => { // GIVEN const stack = new cdk.Stack(); @@ -45,18 +44,18 @@ export = { }); // THEN: no exception - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ServiceNamespace: 'dynamodb', ScalableDimension: 'test:TestCount', ResourceId: 'test:this/test', MinCapacity: 10, MaxCapacity: 1, - })); + }); - test.done(); - }, - 'add scheduled scaling'(test: Test) { + }); + + test('add scheduled scaling', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -69,7 +68,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { @@ -80,12 +79,12 @@ export = { ScheduledActionName: 'ScaleUp', }, ], - })); + }); + - test.done(); - }, + }); - 'step scaling on MathExpression'(test: Test) { + test('step scaling on MathExpression', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -110,11 +109,11 @@ export = { }); // THEN - expect(stack).notTo(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { Period: 60, - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, Metrics: [ @@ -136,22 +135,22 @@ export = { }, ], Threshold: 49, - })); - - test.done(); - }, - - 'test service namespace enum'(test: Test) { - test.equals(appscaling.ServiceNamespace.APPSTREAM, 'appstream'); - test.equals(appscaling.ServiceNamespace.COMPREHEND, 'comprehend'); - test.equals(appscaling.ServiceNamespace.CUSTOM_RESOURCE, 'custom-resource'); - test.equals(appscaling.ServiceNamespace.DYNAMODB, 'dynamodb'); - test.equals(appscaling.ServiceNamespace.EC2, 'ec2'); - test.equals(appscaling.ServiceNamespace.ECS, 'ecs'); - test.equals(appscaling.ServiceNamespace.ELASTIC_MAP_REDUCE, 'elasticmapreduce'); - test.equals(appscaling.ServiceNamespace.LAMBDA, 'lambda'); - test.equals(appscaling.ServiceNamespace.RDS, 'rds'); - test.equals(appscaling.ServiceNamespace.SAGEMAKER, 'sagemaker'); - test.done(); - }, -}; + }); + + + }); + + test('test service namespace enum', () => { + expect(appscaling.ServiceNamespace.APPSTREAM).toEqual( 'appstream'); + expect(appscaling.ServiceNamespace.COMPREHEND).toEqual( 'comprehend'); + expect(appscaling.ServiceNamespace.CUSTOM_RESOURCE).toEqual( 'custom-resource'); + expect(appscaling.ServiceNamespace.DYNAMODB).toEqual( 'dynamodb'); + expect(appscaling.ServiceNamespace.EC2).toEqual( 'ec2'); + expect(appscaling.ServiceNamespace.ECS).toEqual( 'ecs'); + expect(appscaling.ServiceNamespace.ELASTIC_MAP_REDUCE).toEqual( 'elasticmapreduce'); + expect(appscaling.ServiceNamespace.LAMBDA).toEqual( 'lambda'); + expect(appscaling.ServiceNamespace.RDS).toEqual( 'rds'); + expect(appscaling.ServiceNamespace.SAGEMAKER).toEqual( 'sagemaker'); + + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts similarity index 87% rename from packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts rename to packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts index bb585daeb6eae..528a84b997306 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.step-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/step-scaling-policy.test.ts @@ -1,13 +1,13 @@ -import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; import * as fc from 'fast-check'; -import { Test } from 'nodeunit'; import * as appscaling from '../lib'; import { arbitrary_input_intervals, createScalableTarget } from './util'; -export = { - 'alarm thresholds are valid numbers'(test: Test) { +describe('step scaling policy', () => { + test('alarm thresholds are valid numbers', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -24,10 +24,10 @@ export = { }, )); - test.done(); - }, - 'generated step intervals are valid intervals'(test: Test) { + }); + + test('generated step intervals are valid intervals', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -40,10 +40,10 @@ export = { }, )); - test.done(); - }, - 'generated step intervals are nonoverlapping'(test: Test) { + }); + + test('generated step intervals are nonoverlapping', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -61,10 +61,10 @@ export = { }, ), { verbose: true }); - test.done(); - }, - 'all template intervals occur in input array'(test: Test) { + }); + + test('all template intervals occur in input array', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -83,10 +83,10 @@ export = { }, )); - test.done(); - }, - 'lower alarm uses lower policy'(test: Test) { + }); + + test('lower alarm uses lower policy', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -98,10 +98,10 @@ export = { }, )); - test.done(); - }, - 'upper alarm uses upper policy'(test: Test) { + }); + + test('upper alarm uses upper policy', () => { fc.assert(fc.property( arbitrary_input_intervals(), (intervals) => { @@ -113,10 +113,10 @@ export = { }, )); - test.done(); - }, - 'test step scaling on metric'(test: Test) { + }); + + test('test step scaling on metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -132,7 +132,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', ScalingTargetId: { Ref: 'Target3191CF44', @@ -148,12 +148,12 @@ export = { ], }, - })); + }); + - test.done(); - }, + }); - 'step scaling from percentile metric'(test: Test) { + test('step scaling from percentile metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -169,14 +169,14 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Average', }, - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 1, AlarmActions: [ @@ -186,12 +186,12 @@ export = { MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); - test.done(); - }, - 'step scaling with evaluation period configured'(test: Test) { + }); + + test('step scaling with evaluation period configured', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -209,25 +209,25 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResourceLike('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', StepScalingPolicyConfiguration: { AdjustmentType: 'ChangeInCapacity', MetricAggregationType: 'Maximum', }, - })); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + }); + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'GreaterThanOrEqualToThreshold', EvaluationPeriods: 10, ExtendedStatistic: 'p99', MetricName: 'Metric', Namespace: 'Test', Threshold: 100, - })); + }); - test.done(); - }, -}; + + }); +}); /** * Synthesize the given step scaling setup to a template diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.target-tracking.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts similarity index 73% rename from packages/@aws-cdk/aws-applicationautoscaling/test/test.target-tracking.ts rename to packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts index cbd67cbe9c330..2ec9f8dd07a26 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.target-tracking.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/test/target-tracking.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as appscaling from '../lib'; import { createScalableTarget } from './util'; -export = { - 'test setup target tracking on predefined metric'(test: Test) { +describe('target tracking', () => { + test('test setup target tracking on predefined metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -18,19 +17,19 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'EC2SpotFleetRequestAverageCPUUtilization' }, TargetValue: 30, }, - })); + }); + - test.done(); - }, + }); - 'test setup target tracking on predefined metric for lambda'(test: Test) { + test('test setup target tracking on predefined metric for lambda', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -42,19 +41,19 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'LambdaProvisionedConcurrencyUtilization' }, TargetValue: 0.9, }, - })); + }); + - test.done(); - }, + }); - 'test setup target tracking on custom metric'(test: Test) { + test('test setup target tracking on custom metric', () => { // GIVEN const stack = new cdk.Stack(); const target = createScalableTarget(stack); @@ -66,7 +65,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -77,8 +76,8 @@ export = { TargetValue: 30, }, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts b/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts deleted file mode 100644 index 324b2f8243c97..0000000000000 --- a/packages/@aws-cdk/aws-applicationautoscaling/test/test.cron.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Duration, Stack, Lazy } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as appscaling from '../lib'; - -export = { - 'test utc cron, hour only'(test: Test) { - test.equals(appscaling.Schedule.cron({ hour: '18', minute: '0' }).expressionString, 'cron(0 18 * * ? *)'); - test.done(); - }, - - 'test utc cron, hour and minute'(test: Test) { - test.equals(appscaling.Schedule.cron({ hour: '18', minute: '24' }).expressionString, 'cron(24 18 * * ? *)'); - test.done(); - }, - - 'rate must be whole number of minutes'(test: Test) { - test.throws(() => { - appscaling.Schedule.rate(Duration.minutes(0.13456)); - }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); - test.done(); - }, - - 'rate can be in seconds'(test: Test) { - const duration = appscaling.Schedule.rate(Duration.seconds(120)); - test.equal('rate(2 minutes)', duration.expressionString); - test.done(); - }, - - 'rate must not be in seconds when specified as a token'(test: Test) { - test.throws(() => { - appscaling.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); - }, /Allowed units for scheduling/); - test.done(); - }, - - 'rate cannot be 0'(test: Test) { - test.throws(() => { - appscaling.Schedule.rate(Duration.days(0)); - }, /Duration cannot be 0/); - test.done(); - }, - - 'rate can be token'(test: Test) { - const stack = new Stack(); - const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); - const rate = appscaling.Schedule.rate(lazyDuration); - test.equal('rate(5 minutes)', stack.resolve(rate).expressionString); - test.done(); - }, - - 'rate can be in allowed type hours'(test: Test) { - test.equal('rate(1 hour)', appscaling.Schedule.rate(Duration.hours(1)) - .expressionString); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-autoscaling/package.json b/packages/@aws-cdk/aws-autoscaling/package.json index fe06a90f81963..100b56d4b7587 100644 --- a/packages/@aws-cdk/aws-autoscaling/package.json +++ b/packages/@aws-cdk/aws-autoscaling/package.json @@ -79,7 +79,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit-shim": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" diff --git a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts index 9fb5da7b46ce8..64795593e8ec4 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/auto-scaling-group.test.ts @@ -1,17 +1,17 @@ -import { ABSENT, expect, haveResource, haveResourceLike, InspectionFailure, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, InspectionFailure, ResourcePart } from '@aws-cdk/assert-internal'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'default fleet'(test: Test) { +describe('auto scaling group', () => { + test('default fleet', () => { const stack = getTestStack(); const vpc = mockVpc(stack); @@ -21,7 +21,7 @@ nodeunitShim({ vpc, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Parameters': { 'SsmParameterValueawsserviceamiamazonlinuxlatestamznamihvmx8664gp2C96584B6F00A464EAD1953AFF4B05118Parameter': { 'Type': 'AWS::SSM::Parameter::Value', @@ -136,10 +136,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'can set minCapacity, maxCapacity, desiredCapacity to 0'(test: Test) { + }); + + test('can set minCapacity, maxCapacity, desiredCapacity to 0', () => { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -152,17 +152,17 @@ nodeunitShim({ desiredCapacity: 0, }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '0', MaxSize: '0', DesiredCapacity: '0', }, - )); + ); - test.done(); - }, - 'validation is not performed when using Tokens'(test: Test) { + }); + + test('validation is not performed when using Tokens', () => { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -176,17 +176,17 @@ nodeunitShim({ }); // THEN: no exception - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '5', MaxSize: '1', DesiredCapacity: '20', }, - )); + ); - test.done(); - }, - 'userdata can be overridden by image'(test: Test) { + }); + + test('userdata can be overridden by image', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -204,12 +204,12 @@ nodeunitShim({ }); // THEN - test.equals(asg.userData.render(), '#!/bin/bash\nit me!'); + expect(asg.userData.render()).toEqual('#!/bin/bash\nit me!'); + - test.done(); - }, + }); - 'userdata can be overridden at ASG directly'(test: Test) { + test('userdata can be overridden at ASG directly', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -231,12 +231,12 @@ nodeunitShim({ }); // THEN - test.equals(asg.userData.render(), '#!/bin/bash\nno me!'); + expect(asg.userData.render()).toEqual('#!/bin/bash\nno me!'); - test.done(); - }, - 'can specify only min capacity'(test: Test) { + }); + + test('can specify only min capacity', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -250,16 +250,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '10', MaxSize: '10', }, - )); + ); + - test.done(); - }, + }); - 'can specify only max capacity'(test: Test) { + test('can specify only max capacity', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -273,16 +273,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', }, - )); + ); + - test.done(); - }, + }); - 'can specify only desiredCount'(test: Test) { + test('can specify only desiredCount', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -296,17 +296,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MinSize: '1', MaxSize: '10', DesiredCapacity: '10', }, - )); + ); + - test.done(); - }, + }); - 'addToRolePolicy can be used to add statements to the role policy'(test: Test) { + test('addToRolePolicy can be used to add statements to the role policy', () => { const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -321,7 +321,7 @@ nodeunitShim({ resources: ['*'], })); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -332,11 +332,11 @@ nodeunitShim({ }, ], }, - })); - test.done(); - }, + }); + + }); - 'can configure replacing update'(test: Test) { + test('can configure replacing update', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -351,7 +351,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { AutoScalingReplacingUpdate: { WillReplace: true, @@ -362,12 +362,12 @@ nodeunitShim({ MinSuccessfulInstancesPercent: 50, }, }, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can configure rolling update'(test: Test) { + }); + + test('can configure rolling update', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -385,7 +385,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { UpdatePolicy: { 'AutoScalingRollingUpdate': { 'MinSuccessfulInstancesPercent': 50, @@ -394,12 +394,12 @@ nodeunitShim({ 'SuspendProcesses': ['HealthCheck', 'ReplaceUnhealthy', 'AZRebalance', 'AlarmNotification', 'ScheduledActions'], }, }, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can configure resource signals'(test: Test) { + }); + + test('can configure resource signals', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -414,19 +414,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { CreationPolicy: { ResourceSignal: { Count: 5, Timeout: 'PT11M6S', }, }, - }, ResourcePart.CompleteDefinition)); + }, ResourcePart.CompleteDefinition); - test.done(); - }, - 'can configure EC2 health check'(test: Test) { + }); + + test('can configure EC2 health check', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -440,14 +440,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'EC2', - })); + }); - test.done(); - }, - 'can configure EBS health check'(test: Test) { + }); + + test('can configure EBS health check', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -461,15 +461,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { HealthCheckType: 'ELB', HealthCheckGracePeriod: 900, - })); + }); - test.done(); - }, - 'can add Security Group to Fleet'(test: Test) { + }); + + test('can add Security Group to Fleet', () => { // GIVEN const stack = new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' } }); const vpc = mockVpc(stack); @@ -481,7 +481,7 @@ nodeunitShim({ vpc, }); asg.addSecurityGroup(mockSecurityGroup(stack)); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: [ { 'Fn::GetAtt': [ @@ -491,11 +491,11 @@ nodeunitShim({ }, 'most-secure', ], - })); - test.done(); - }, + }); - 'can set tags'(test: Test) { + }); + + test('can set tags', () => { // GIVEN const stack = getTestStack(); // new cdk.Stack(undefined, 'MyStack', { env: { region: 'us-east-1', account: '1234' }}); @@ -517,7 +517,7 @@ nodeunitShim({ cdk.Tags.of(asg).add('notsuper', 'caramel', { applyToLaunchedInstances: false }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: 'Name', @@ -535,11 +535,11 @@ nodeunitShim({ Value: 'acai', }, ], - })); - test.done(); - }, + }); + + }); - 'allows setting spot price'(test: Test) { + test('allows setting spot price', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -554,15 +554,15 @@ nodeunitShim({ }); // THEN - test.deepEqual(asg.spotPrice, '0.05'); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(asg.spotPrice).toEqual('0.05'); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.05', - })); + }); + - test.done(); - }, + }); - 'allows association of public IP address'(test: Test) { + test('allows association of public IP address', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -581,20 +581,20 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: true, }, - )); - test.done(); - }, + ); + + }); - 'association of public IP address requires public subnet'(test: Test) { + test('association of public IP address requires public subnet', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // WHEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -604,11 +604,11 @@ nodeunitShim({ desiredCapacity: 0, associatePublicIpAddress: true, }); - }); - test.done(); - }, + }).toThrow(); + + }); - 'allows disassociation of public IP address'(test: Test) { + test('allows disassociation of public IP address', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -625,14 +625,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { AssociatePublicIpAddress: false, }, - )); - test.done(); - }, + ); - 'does not specify public IP address association by default'(test: Test) { + }); + + test('does not specify public IP address association by default', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -648,7 +648,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', (resource: any, errors: InspectionFailure) => { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', (resource: any, errors: InspectionFailure) => { for (const key of Object.keys(resource)) { if (key === 'AssociatePublicIpAddress') { errors.failureReason = 'Has AssociatePublicIpAddress'; @@ -656,11 +656,11 @@ nodeunitShim({ } } return true; - })); - test.done(); - }, + }); - 'an existing security group can be specified instead of auto-created'(test: Test) { + }); + + test('an existing security group can be specified instead of auto-created', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -675,14 +675,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SecurityGroups: ['most-secure'], }, - )); - test.done(); - }, + ); + + }); - 'an existing role can be specified instead of auto-created'(test: Test) { + test('an existing role can be specified instead of auto-created', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -697,14 +697,14 @@ nodeunitShim({ }); // THEN - test.equal(asg.role, importedRole); - expect(stack).to(haveResource('AWS::IAM::InstanceProfile', { + expect(asg.role).toEqual(importedRole); + expect(stack).toHaveResource('AWS::IAM::InstanceProfile', { 'Roles': ['HelloDude'], - })); - test.done(); - }, + }); + + }); - 'defaultChild is available on an ASG'(test: Test) { + test('defaultChild is available on an ASG', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -715,12 +715,12 @@ nodeunitShim({ }); // THEN - test.ok(asg.node.defaultChild instanceof autoscaling.CfnAutoScalingGroup); + expect(asg.node.defaultChild instanceof autoscaling.CfnAutoScalingGroup).toEqual(true); + - test.done(); - }, + }); - 'can set blockDeviceMappings'(test: Test) { + test('can set blockDeviceMappings', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -758,7 +758,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { BlockDeviceMappings: [ { DeviceName: 'ebs', @@ -795,12 +795,12 @@ nodeunitShim({ NoDevice: true, }, ], - })); + }); - test.done(); - }, - 'can configure maxInstanceLifetime'(test: Test) { + }); + + test('can configure maxInstanceLifetime', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -812,50 +812,50 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { 'MaxInstanceLifetime': 604800, - })); + }); - test.done(); - }, - 'throws if maxInstanceLifetime < 7 days'(test: Test) { + }); + + test('throws if maxInstanceLifetime < 7 days', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), vpc, maxInstanceLifetime: cdk.Duration.days(6), }); - }, /maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); + }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - test.done(); - }, - 'throws if maxInstanceLifetime > 365 days'(test: Test) { + }); + + test('throws if maxInstanceLifetime > 365 days', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), vpc, maxInstanceLifetime: cdk.Duration.days(366), }); - }, /maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); + }).toThrow(/maxInstanceLifetime must be between 7 and 365 days \(inclusive\)/); - test.done(); - }, - 'can configure instance monitoring'(test: Test) { + }); + + test('can configure instance monitoring', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -869,13 +869,13 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceMonitoring: false, - })); - test.done(); - }, + }); + + }); - 'instance monitoring defaults to absent'(test: Test) { + test('instance monitoring defaults to absent', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -888,19 +888,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceMonitoring: ABSENT, - })); - test.done(); - }, + }); + + }); - 'throws if ephemeral volumeIndex < 0'(test: Test) { + test('throws if ephemeral volumeIndex < 0', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -910,18 +910,18 @@ nodeunitShim({ volume: autoscaling.BlockDeviceVolume.ephemeral(-1), }], }); - }, /volumeIndex must be a number starting from 0/); + }).toThrow(/volumeIndex must be a number starting from 0/); - test.done(); - }, - 'throws if volumeType === IO1 without iops'(test: Test) { + }); + + test('throws if volumeType === IO1 without iops', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyStack', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -935,12 +935,12 @@ nodeunitShim({ }), }], }); - }, /ops property is required with volumeType: EbsDeviceVolumeType.IO1/); + }).toThrow(/ops property is required with volumeType: EbsDeviceVolumeType.IO1/); - test.done(); - }, - 'warning if iops without volumeType'(test: Test) { + }); + + test('warning if iops without volumeType', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -960,13 +960,13 @@ nodeunitShim({ }); // THEN - test.deepEqual(asg.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN); - test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + expect(asg.node.metadata[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); + expect(asg.node.metadata[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); - test.done(); - }, - 'warning if iops and volumeType !== IO1'(test: Test) { + }); + + test('warning if iops and volumeType !== IO1', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -987,13 +987,13 @@ nodeunitShim({ }); // THEN - test.deepEqual(asg.node.metadata[0].type, cxschema.ArtifactMetadataEntryType.WARN); - test.deepEqual(asg.node.metadata[0].data, 'iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + expect(asg.node.metadata[0].type).toEqual(cxschema.ArtifactMetadataEntryType.WARN); + expect(asg.node.metadata[0].data).toEqual('iops will be ignored without volumeType: EbsDeviceVolumeType.IO1'); + - test.done(); - }, + }); - 'step scaling on metric'(test: Test) { + test('step scaling on metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1018,18 +1018,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { ComparisonOperator: 'LessThanOrEqualToThreshold', EvaluationPeriods: 1, MetricName: 'Metric', Namespace: 'Test', Period: 300, - })); + }); + - test.done(); - }, + }); - 'step scaling on MathExpression'(test: Test) { + test('step scaling on MathExpression', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1059,11 +1059,11 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).not.toHaveResource('AWS::CloudWatch::Alarm', { Period: 60, - })); + }); - expect(stack).to(haveResource('AWS::CloudWatch::Alarm', { + expect(stack).toHaveResource('AWS::CloudWatch::Alarm', { 'ComparisonOperator': 'LessThanOrEqualToThreshold', 'EvaluationPeriods': 1, 'Metrics': [ @@ -1085,12 +1085,12 @@ nodeunitShim({ }, ], 'Threshold': 49, - })); + }); - test.done(); - }, - 'test GroupMetrics.all(), adds a single MetricsCollection with no Metrics specified'(test: Test) { + }); + + test('test GroupMetrics.all(), adds a single MetricsCollection with no Metrics specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1103,18 +1103,18 @@ nodeunitShim({ }); // Then - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', Metrics: ABSENT, }, ], - })); - test.done(); - }, + }); + + }); - 'test can specify a subset of group metrics'(test: Test) { + test('test can specify a subset of group metrics', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1138,7 +1138,7 @@ nodeunitShim({ }); // Then - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', @@ -1148,11 +1148,11 @@ nodeunitShim({ Metrics: ['GroupPendingInstances', 'GroupStandbyInstances', 'GroupTotalInstances', 'GroupTerminatingInstances'], }, ], - })); - test.done(); - }, + }); + + }); - 'test deduplication of group metrics '(test: Test) { + test('test deduplication of group metrics ', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1168,18 +1168,18 @@ nodeunitShim({ }); // Then - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MetricsCollection: [ { Granularity: '1Minute', Metrics: ['GroupMinSize', 'GroupMaxSize'], }, ], - })); - test.done(); - }, + }); + + }); - 'allow configuring notifications'(test: Test) { + test('allow configuring notifications', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1203,7 +1203,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1220,19 +1220,19 @@ nodeunitShim({ }, ], }, - )); + ); - test.done(); - }, - 'throw if notification and notificationsTopics are both configured'(test: Test) { + }); + + test('throw if notification and notificationsTopics are both configured', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); const topic = new sns.Topic(stack, 'MyTopic'); // THEN - test.throws(() => { + expect(() => { new autoscaling.AutoScalingGroup(stack, 'MyASG', { instanceType: ec2.InstanceType.of(ec2.InstanceClass.M4, ec2.InstanceSize.MICRO), machineImage: new ec2.AmazonLinuxImage(), @@ -1242,11 +1242,11 @@ nodeunitShim({ topic, }], }); - }, 'Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); - test.done(); - }, + }).toThrow('Cannot set \'notificationsTopic\' and \'notifications\', \'notificationsTopic\' is deprecated use \'notifications\' instead'); + + }); - 'notificationTypes default includes all non test NotificationType'(test: Test) { + test('notificationTypes default includes all non test NotificationType', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1265,7 +1265,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1278,12 +1278,12 @@ nodeunitShim({ }, ], }, - )); + ); - test.done(); - }, - 'setting notificationTopic configures all non test NotificationType'(test: Test) { + }); + + test('setting notificationTopic configures all non test NotificationType', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1298,7 +1298,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NotificationConfigurations: [ { TopicARN: { Ref: 'MyTopic86869434' }, @@ -1311,17 +1311,17 @@ nodeunitShim({ }, ], }, - )); + ); + + + }); - test.done(); - }, + test('NotificationTypes.ALL includes all non test NotificationType', () => { + expect(Object.values(autoscaling.ScalingEvent).length - 1).toEqual(autoscaling.ScalingEvents.ALL._types.length); - 'NotificationTypes.ALL includes all non test NotificationType'(test: Test) { - test.deepEqual(Object.values(autoscaling.ScalingEvent).length - 1, autoscaling.ScalingEvents.ALL._types.length); - test.done(); - }, + }); - 'Can protect new instances from scale-in via constructor property'(test: Test) { + test('Can protect new instances from scale-in via constructor property', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1335,15 +1335,15 @@ nodeunitShim({ }); // THEN - test.strictEqual(asg.areNewInstancesProtectedFromScaleIn(), true); - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); + }); - test.done(); - }, - 'Can protect new instances from scale-in via setter'(test: Test) { + }); + + test('Can protect new instances from scale-in via setter', () => { // GIVEN const stack = new cdk.Stack(); const vpc = mockVpc(stack); @@ -1357,13 +1357,13 @@ nodeunitShim({ asg.protectNewInstancesFromScaleIn(); // THEN - test.strictEqual(asg.areNewInstancesProtectedFromScaleIn(), true); - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(asg.areNewInstancesProtectedFromScaleIn()).toEqual(true); + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); + }); - test.done(); - }, + + }); }); function mockVpc(stack: cdk.Stack) { @@ -1390,9 +1390,9 @@ test('Can set autoScalingGroupName', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { AutoScalingGroupName: 'MyAsg', - })); + }); }); test('can use Vpc imported from unparseable list tokens', () => { @@ -1427,11 +1427,11 @@ test('can use Vpc imported from unparseable list tokens', () => { }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::AutoScalingGroup', { VPCZoneIdentifier: { 'Fn::Split': [',', { 'Fn::ImportValue': 'myPrivateSubnetIds' }], }, - })); + }); }); function mockSecurityGroup(stack: cdk.Stack) { diff --git a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts index 58d9bc6ec152c..2fd252e5e459d 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/cfn-init.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert-internal/jest'; import { anything, arrayWith, expect, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import { Duration, Stack } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts index 24bebc1a605d0..7ebf6d18cbe6e 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/lifecyclehooks.test.ts @@ -1,13 +1,13 @@ +import '@aws-cdk/assert-internal/jest'; import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; 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 constructs from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; -nodeunitShim({ - 'we can add a lifecycle hook to an ASG'(test: Test) { +describe('lifecycle hooks', () => { + test('we can add a lifecycle hook to an ASG', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -67,8 +67,8 @@ nodeunitShim({ }, })); - test.done(); - }, + + }); }); class FakeNotificationTarget implements autoscaling.ILifecycleHookTarget { diff --git a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts index 25d75edfdff98..31e171179b076 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scaling.test.ts @@ -1,15 +1,15 @@ +import '@aws-cdk/assert-internal/jest'; import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; -nodeunitShim({ - 'target tracking policies': { - 'cpu utilization'(test: Test) { +describe('scaling', () => { + describe('target tracking policies', () => { + test('cpu utilization', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -28,10 +28,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'network ingress'(test: Test) { + }); + + test('network ingress', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -50,10 +50,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'network egress'(test: Test) { + }); + + test('network egress', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -72,10 +72,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'request count per second'(test: Test) { + }); + + test('request count per second', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -120,10 +120,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'request count per minute'(test: Test) { + }); + + test('request count per minute', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -168,10 +168,10 @@ nodeunitShim({ }, })); - test.done(); - }, - 'custom metric'(test: Test) { + }); + + test('custom metric', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -202,11 +202,11 @@ nodeunitShim({ }, })); - test.done(); - }, - }, - 'step scaling'(test: Test) { + }); + }); + + test('step scaling', () => { // GIVEN const stack = new cdk.Stack(); const fixture = new ASGFixture(stack, 'Fixture'); @@ -269,8 +269,8 @@ nodeunitShim({ AlarmDescription: 'Lower threshold scaling alarm', })); - test.done(); - }, + + }); }); test('step scaling from percentile metric', () => { diff --git a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts index e5044af33543e..3afbd887b45ae 100644 --- a/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts +++ b/packages/@aws-cdk/aws-autoscaling/test/scheduled-action.test.ts @@ -1,12 +1,12 @@ +import '@aws-cdk/assert-internal/jest'; import { expect, haveResource, MatchStyle } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; import * as constructs from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as autoscaling from '../lib'; -nodeunitShim({ - 'can schedule an action'(test: Test) { +describe('scheduled action', () => { + test('can schedule an action', () => { // GIVEN const stack = new cdk.Stack(); const asg = makeAutoScalingGroup(stack); @@ -23,10 +23,10 @@ nodeunitShim({ MinSize: 10, })); - test.done(); - }, - 'correctly formats date objects'(test: Test) { + }); + + test('correctly formats date objects', () => { // GIVEN const stack = new cdk.Stack(); const asg = makeAutoScalingGroup(stack); @@ -43,10 +43,10 @@ nodeunitShim({ StartTime: '2033-09-10T12:00:00Z', })); - test.done(); - }, - 'autoscaling group has recommended updatepolicy for scheduled actions'(test: Test) { + }); + + test('autoscaling group has recommended updatepolicy for scheduled actions', () => { // GIVEN const stack = new cdk.Stack(); const asg = makeAutoScalingGroup(stack); @@ -104,8 +104,8 @@ nodeunitShim({ }, }, MatchStyle.SUPERSET); - test.done(); - }, + + }); }); function makeAutoScalingGroup(scope: constructs.Construct) { diff --git a/packages/@aws-cdk/aws-cloudfront-origins/README.md b/packages/@aws-cdk/aws-cloudfront-origins/README.md index 05d12bc1ae6f4..cb7af64ff8618 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/README.md +++ b/packages/@aws-cdk/aws-cloudfront-origins/README.md @@ -33,6 +33,24 @@ CloudFront's redirect and error handling will be used. In the latter case, the O underlying bucket. This can be used in conjunction with a bucket that is not public to require that your users access your content using CloudFront URLs and not S3 URLs directly. Alternatively, a custom origin access identity can be passed to the S3 origin in the properties. +### Adding Custom Headers + +You can configure CloudFront to add custom headers to the requests that it sends to your origin. These custom headers enable you to send and gather information from your origin that you don’t get with typical viewer requests. These headers can even be customized for each origin. CloudFront supports custom headers for both for custom and Amazon S3 origins. + +```ts +import * as cloudfront from '@aws-cdk/aws-cloudfront'; +import * as origins from '@aws-cdk/aws-cloudfront-origins'; + +const myBucket = new s3.Bucket(this, 'myBucket'); +new cloudfront.Distribution(this, 'myDist', { + defaultBehavior: { origin: new origins.S3Origin(myBucket, { + customHeaders: { + Foo: 'bar', + }, + })}, +}); +``` + ## ELBv2 Load Balancer An Elastic Load Balancing (ELB) v2 load balancer may be used as an origin. In order for a load balancer to serve as an origin, it must be publicly diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts index b55493e974fed..d2ca1d46ba1eb 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts @@ -8,14 +8,7 @@ import { HttpOrigin } from './http-origin'; /** * Properties to use to customize an S3 Origin. */ -export interface S3OriginProps { - /** - * An optional path that CloudFront appends to the origin domain name when CloudFront requests content from the origin. - * Must begin, but not end, with '/' (e.g., '/production/images'). - * - * @default '/' - */ - readonly originPath?: string; +export interface S3OriginProps extends cloudfront.OriginProps { /** * An optional Origin Access Identity of the origin identity cloudfront will use when calling your s3 bucket. * diff --git a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts index c9e0ecb797464..e8e2c4b2c41b9 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/test/s3-origin.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert-internal/jest'; import * as cloudfront from '@aws-cdk/aws-cloudfront'; import * as s3 from '@aws-cdk/aws-s3'; -import { App, Stack } from '@aws-cdk/core'; +import { App, Duration, Stack } from '@aws-cdk/core'; import { S3Origin } from '../lib'; let app: App; @@ -34,16 +34,27 @@ describe('With bucket', () => { }); }); - test('can customize originPath property', () => { + test('can customize base origin properties', () => { const bucket = new s3.Bucket(stack, 'Bucket'); - const origin = new S3Origin(bucket, { originPath: '/assets' }); + const origin = new S3Origin(bucket, { + originPath: '/assets', + connectionTimeout: Duration.seconds(5), + connectionAttempts: 2, + customHeaders: { AUTH: 'NONE' }, + }); const originBindConfig = origin.bind(stack, { originId: 'StackOrigin029E19582' }); expect(stack.resolve(originBindConfig.originProperty)).toEqual({ id: 'StackOrigin029E19582', domainName: { 'Fn::GetAtt': ['Bucket83908E77', 'RegionalDomainName'] }, originPath: '/assets', + connectionTimeout: 5, + connectionAttempts: 2, + originCustomHeaders: [{ + headerName: 'AUTH', + headerValue: 'NONE', + }], s3OriginConfig: { originAccessIdentity: { 'Fn::Join': ['', diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts index 41592b51786ca..210ffb87648f7 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin.ts @@ -120,6 +120,7 @@ export abstract class OriginBase implements IOrigin { protected constructor(domainName: string, props: OriginProps = {}) { validateIntInRangeOrUndefined('connectionTimeout', 1, 10, props.connectionTimeout?.toSeconds()); validateIntInRangeOrUndefined('connectionAttempts', 1, 3, props.connectionAttempts, false); + validateCustomHeaders(props.customHeaders); this.domainName = domainName; this.originPath = this.validateOriginPath(props.originPath); @@ -205,3 +206,34 @@ function validateIntInRangeOrUndefined(name: string, min: number, max: number, v throw new Error(`${name}: Must be an int between ${min} and ${max}${seconds} (inclusive); received ${value}.`); } } + +/** + * Throws an error if custom header assignment is prohibited by CloudFront. + * @link: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/add-origin-custom-headers.html#add-origin-custom-headers-denylist + */ +function validateCustomHeaders(customHeaders?: Record) { + if (!customHeaders || Object.entries(customHeaders).length === 0) { return; } + const customHeaderKeys = Object.keys(customHeaders); + const prohibitedHeaderKeys = [ + 'Cache-Control', 'Connection', 'Content-Length', 'Cookie', 'Host', 'If-Match', 'If-Modified-Since', 'If-None-Match', 'If-Range', 'If-Unmodified-Since', + 'Max-Forwards', 'Pragma', 'Proxy-Authorization', 'Proxy-Connection', 'Range', 'Request-Range', 'TE', 'Trailer', 'Transfer-Encoding', 'Upgrade', 'Via', + 'X-Real-Ip', + ]; + const prohibitedHeaderKeyPrefixes = [ + 'X-Amz-', 'X-Edge-', + ]; + + const prohibitedHeadersKeysMatches = customHeaderKeys.filter(customKey => { + return prohibitedHeaderKeys.map((prohibitedKey) => prohibitedKey.toLowerCase()).includes(customKey.toLowerCase()); + }); + const prohibitedHeaderPrefixMatches = customHeaderKeys.filter(customKey => { + return prohibitedHeaderKeyPrefixes.some(prohibitedKeyPrefix => customKey.toLowerCase().startsWith(prohibitedKeyPrefix.toLowerCase())); + }); + + if (prohibitedHeadersKeysMatches.length !== 0) { + throw new Error(`The following headers cannot be configured as custom origin headers: ${prohibitedHeadersKeysMatches.join(', ')}`); + } + if (prohibitedHeaderPrefixMatches.length !== 0) { + throw new Error(`The following headers cannot be used as prefixes for custom origin headers: ${prohibitedHeaderPrefixMatches.join(', ')}`); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts index b30362c7fe652..e6a59ff6179d6 100644 --- a/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts +++ b/packages/@aws-cdk/aws-cloudfront/test/origin.test.ts @@ -57,4 +57,52 @@ test.each(['us-east-1', 'ap-southeast-2', 'eu-west-3', 'me-south-1']) enabled: true, originShieldRegion, }); -}); \ No newline at end of file +}); + +test('throw an error if Custom Headers keys are not permitted', () => { + // case sensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + Host: 'bad', + Cookie: 'bad', + Connection: 'bad', + TS: 'bad', + }, + }); + }).toThrow(/The following headers cannot be configured as custom origin headers: (.*?)/); + + // case insensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + hOst: 'bad', + cOOkIe: 'bad', + Connection: 'bad', + Ts: 'bad', + }, + }); + }).toThrow(/The following headers cannot be configured as custom origin headers: (.*?)/); +}); + +test('throw an error if Custom Headers are pre-fixed with non-permitted keys', () => { + // case sensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + 'X-Amz-dummy': 'bad', + 'X-Edge-dummy': 'bad', + }, + }); + }).toThrow(/The following headers cannot be used as prefixes for custom origin headers: (.*?)/); + + // case insensitive + expect(() => { + new TestOrigin('example.com', { + customHeaders: { + 'x-amZ-dummy': 'bad', + 'x-eDgE-dummy': 'bad', + }, + }); + }).toThrow(/The following headers cannot be used as prefixes for custom origin headers: (.*?)/); +}); diff --git a/packages/@aws-cdk/aws-codecommit/README.md b/packages/@aws-cdk/aws-codecommit/README.md index 3d4ef1c3ba46c..7dd08eef3bb82 100644 --- a/packages/@aws-cdk/aws-codecommit/README.md +++ b/packages/@aws-cdk/aws-codecommit/README.md @@ -55,3 +55,16 @@ const rule = repo.onCommentOnPullRequest('CommentOnPullRequest', { target: new targets.SnsTopic(myTopic), }); ``` + +## CodeStar Notifications + +To define CodeStar Notification rules for Repositories, use one of the `notifyOnXxx()` methods. +They are very similar to `onXxx()` methods for CloudWatch events: + +```ts +const target = new chatbot.SlackChannelConfiguration(stack, 'MySlackChannel', { + slackChannelConfigurationName: 'YOUR_CHANNEL_NAME', + slackWorkspaceId: 'YOUR_SLACK_WORKSPACE_ID', + slackChannelId: 'YOUR_SLACK_CHANNEL_ID', +}); +const rule = repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); diff --git a/packages/@aws-cdk/aws-codecommit/lib/repository.ts b/packages/@aws-cdk/aws-codecommit/lib/repository.ts index 495be2f981d86..e55053c0249be 100644 --- a/packages/@aws-cdk/aws-codecommit/lib/repository.ts +++ b/packages/@aws-cdk/aws-codecommit/lib/repository.ts @@ -1,10 +1,23 @@ +import * as notifications from '@aws-cdk/aws-codestarnotifications'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import { IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnRepository } from './codecommit.generated'; -export interface IRepository extends IResource { +/** + * Additional options to pass to the notification rule. + */ +export interface RepositoryNotifyOnOptions extends notifications.NotificationRuleOptions { + /** + * A list of event types associated with this notification rule for CodeCommit repositories. + * For a complete list of event types and IDs, see Notification concepts in the Developer Tools Console User Guide. + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/concepts.html#concepts-api + */ + readonly events: RepositoryNotificationEvents[]; +} + +export interface IRepository extends IResource, notifications.INotificationRuleSource { /** * The ARN of this Repository. * @attribute @@ -18,19 +31,19 @@ export interface IRepository extends IResource { readonly repositoryName: string; /** - * The HTTP clone URL + * The HTTP clone URL. * @attribute */ readonly repositoryCloneUrlHttp: string; /** - * The SSH clone URL + * The SSH clone URL. * @attribute */ readonly repositoryCloneUrlSsh: string; /** - * The HTTPS (GRC) clone URL + * The HTTPS (GRC) clone URL. * * HTTPS (GRC) is the protocol to use with git-remote-codecommit (GRC). * @@ -92,7 +105,7 @@ export interface IRepository extends IResource { onCommit(id: string, options?: OnCommitOptions): events.Rule; /** - * Grant the given principal identity permissions to perform the actions on this repository + * Grant the given principal identity permissions to perform the actions on this repository. */ grant(grantee: iam.IGrantable, ...actions: string[]): iam.Grant; @@ -110,13 +123,90 @@ export interface IRepository extends IResource { * Grant the given identity permissions to read this repository. */ grantRead(grantee: iam.IGrantable): iam.Grant; + + /** + * Defines a CodeStar Notification rule triggered when the project + * events specified by you are emitted. Similar to `onEvent` API. + * + * You can also use the methods to define rules for the specific event emitted. + * eg: `notifyOnPullRequstCreated`. + * + * @returns CodeStar Notifications rule associated with this repository. + */ + notifyOn( + id: string, + target: notifications.INotificationRuleTarget, + options: RepositoryNotifyOnOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a comment is made on a pull request. + */ + notifyOnPullRequestComment( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when an approval status is changed. + */ + notifyOnApprovalStatusChanged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when an approval rule is overridden. + */ + notifyOnApprovalRuleOverridden( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a pull request is created. + */ + notifyOnPullRequestCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a pull request is merged. + */ + notifiyOnPullRequestMerged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a new branch or tag is created. + */ + notifyOnBranchOrTagCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; + + /** + * Defines a CodeStar Notification rule which triggers when a branch or tag is deleted. + */ + notifyOnBranchOrTagDeleted( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule; } /** - * Options for the onCommit() method + * Options for the onCommit() method. */ export interface OnCommitOptions extends events.OnEventOptions { - /** * The branch to monitor. * @@ -268,6 +358,101 @@ abstract class RepositoryBase extends Resource implements IRepository { 'codecommit:Describe*', ); } + + public notifyOn( + id: string, + target: notifications.INotificationRuleTarget, + options: RepositoryNotifyOnOptions, + ): notifications.INotificationRule { + return new notifications.NotificationRule(this, id, { + ...options, + source: this, + targets: [target], + }); + } + + public notifyOnPullRequestComment( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_COMMENT], + }); + } + + public notifyOnApprovalStatusChanged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.APPROVAL_STATUS_CHANGED], + }); + } + + public notifyOnApprovalRuleOverridden( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.APPROVAL_RULE_OVERRIDDEN], + }); + } + + public notifyOnPullRequestCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_CREATED], + }); + } + + public notifiyOnPullRequestMerged( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.PULL_REQUEST_MERGED], + }); + } + + public notifyOnBranchOrTagCreated( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.BRANCH_OR_TAG_CREATED], + }); + } + + public notifyOnBranchOrTagDeleted( + id: string, + target: notifications.INotificationRuleTarget, + options?: notifications.NotificationRuleOptions, + ): notifications.INotificationRule { + return this.notifyOn(id, target, { + ...options, + events: [RepositoryNotificationEvents.BRANCH_OR_TAG_DELETED], + }); + } + + public bindAsNotificationRuleSource(_scope: Construct): notifications.NotificationRuleSourceConfig { + return { + sourceArn: this.repositoryArn, + }; + } } export interface RepositoryProps { @@ -288,7 +473,7 @@ export interface RepositoryProps { } /** - * Provides a CodeCommit Repository + * Provides a CodeCommit Repository. */ export class Repository extends RepositoryBase { @@ -401,7 +586,7 @@ export class Repository extends RepositoryBase { */ export interface RepositoryTriggerOptions { /** - * A name for the trigger.Triggers on a repository must have unique names + * A name for the trigger.Triggers on a repository must have unique names. */ readonly name?: string; @@ -437,7 +622,7 @@ export enum RepositoryEventTrigger { } /** - * Returns the clone URL for a protocol + * Returns the clone URL for a protocol. */ function makeCloneUrl(stack: Stack, repositoryName: string, protocol: 'https' | 'ssh' | 'grc', region?: string) { switch (protocol) { @@ -448,3 +633,64 @@ function makeCloneUrl(stack: Stack, repositoryName: string, protocol: 'https' | return `codecommit::${region ?? stack.region}://${repositoryName}`; } } + +/** + * List of event types for AWS CodeCommit + * @see https://docs.aws.amazon.com/dtconsole/latest/userguide/concepts.html#events-ref-repositories + */ +export enum RepositoryNotificationEvents { + /** + * Trigger notication when comment made on commit. + */ + COMMIT_COMMENT = 'codecommit-repository-comments-on-commits', + + /** + * Trigger notification when comment made on pull request. + */ + PULL_REQUEST_COMMENT = 'codecommit-repository-comments-on-pull-requests', + + /** + * Trigger notification when approval status changed. + */ + APPROVAL_STATUS_CHANGED = 'codecommit-repository-approvals-status-changed', + + /** + * Trigger notifications when approval rule is overridden. + */ + APPROVAL_RULE_OVERRIDDEN = 'codecommit-repository-approvals-rule-override', + + /** + * Trigger notification when pull request created. + */ + PULL_REQUEST_CREATED = 'codecommit-repository-pull-request-created', + + /** + * Trigger notification when pull request source updated. + */ + PULL_REQUEST_SOURCE_UPDATED = 'codecommit-repository-pull-request-source-updated', + + /** + * Trigger notification when pull request status is changed. + */ + PULL_REQUEST_STATUS_CHANGED = 'codecommit-repository-pull-request-status-changed', + + /** + * Trigger notification when pull requset is merged. + */ + PULL_REQUEST_MERGED = 'codecommit-repository-pull-request-merged', + + /** + * Trigger notification when a branch or tag is created. + */ + BRANCH_OR_TAG_CREATED = 'codecommit-repository-branches-and-tags-created', + + /** + * Trigger notification when a branch or tag is deleted. + */ + BRANCH_OR_TAG_DELETED = 'codecommit-repository-branches-and-tags-deleted', + + /** + * Trigger notification when a branch or tag is updated. + */ + BRANCH_OR_TAG_UPDATED = 'codecommit-repository-branches-and-tags-updated', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 5c687257ec899..9f9554735a846 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -89,6 +89,7 @@ "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -96,6 +97,7 @@ }, "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { + "@aws-cdk/aws-codestarnotifications": "0.0.0", "@aws-cdk/aws-events": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json new file mode 100644 index 0000000000000..c2f386ccebe0e --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.expected.json @@ -0,0 +1,87 @@ +{ + "Resources": { + "MyCodecommitRepository26DB372B": { + "Type": "AWS::CodeCommit::Repository", + "Properties": { + "RepositoryName": "my-test-repository" + } + }, + "MyCodecommitRepositoryNotifyOnPullRequestCreated4CAB0621": { + "Type": "AWS::CodeStarNotifications::NotificationRule", + "Properties": { + "DetailType": "FULL", + "EventTypeIds": [ + "codecommit-repository-pull-request-created" + ], + "Name": "decommitMyCodecommitRepositoryNotifyOnPullRequestCreated65969BCB", + "Resource": { + "Fn::GetAtt": [ + "MyCodecommitRepository26DB372B", + "Arn" + ] + }, + "Targets": [ + { + "TargetAddress": { + "Ref": "MyTopic86869434" + }, + "TargetType": "SNS" + } + ] + } + }, + "MyCodecommitRepositoryNotifyOnPullRequestMerged80574FED": { + "Type": "AWS::CodeStarNotifications::NotificationRule", + "Properties": { + "DetailType": "FULL", + "EventTypeIds": [ + "codecommit-repository-pull-request-merged" + ], + "Name": "odecommitMyCodecommitRepositoryNotifyOnPullRequestMergedF426197C", + "Resource": { + "Fn::GetAtt": [ + "MyCodecommitRepository26DB372B", + "Arn" + ] + }, + "Targets": [ + { + "TargetAddress": { + "Ref": "MyTopic86869434" + }, + "TargetType": "SNS" + } + ] + } + }, + "MyTopic86869434": { + "Type": "AWS::SNS::Topic" + }, + "MyTopicPolicy12A5EC17": { + "Type": "AWS::SNS::TopicPolicy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": "sns:Publish", + "Effect": "Allow", + "Principal": { + "Service": "codestar-notifications.amazonaws.com" + }, + "Resource": { + "Ref": "MyTopic86869434" + }, + "Sid": "0" + } + ], + "Version": "2012-10-17" + }, + "Topics": [ + { + "Ref": "MyTopic86869434" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts new file mode 100644 index 0000000000000..4f7a5926a47ac --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/integ.repository-notification.ts @@ -0,0 +1,19 @@ +#!/usr/bin/env node +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import * as codecommit from '../lib'; + +const app = new cdk.App(); + +const stack = new cdk.Stack(app, 'aws-cdk-codecommit'); + +const repository = new codecommit.Repository(stack, 'MyCodecommitRepository', { + repositoryName: 'my-test-repository', +}); + +const target = new sns.Topic(stack, 'MyTopic'); + +repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); +repository.notifiyOnPullRequestMerged('NotifyOnPullRequestMerged', target); + +app.synth(); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts b/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts new file mode 100644 index 0000000000000..aa0e48a51a3ac --- /dev/null +++ b/packages/@aws-cdk/aws-codecommit/test/test.notification-rule.ts @@ -0,0 +1,66 @@ +import { expect, haveResource } from '@aws-cdk/assert-internal'; +import * as sns from '@aws-cdk/aws-sns'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; +import * as codecommit from '../lib'; + +export = { + 'CodeCommit Repositories - can create notification rule'(test: Test) { + const stack = new cdk.Stack(); + const repository = new codecommit.Repository(stack, 'MyCodecommitRepository', { + repositoryName: 'my-test-repository', + }); + + const target = new sns.Topic(stack, 'MyTopic'); + + repository.notifyOnPullRequestCreated('NotifyOnPullRequestCreated', target); + + repository.notifiyOnPullRequestMerged('NotifyOnPullRequestMerged', target); + + expect(stack).to(haveResource('AWS::CodeStarNotifications::NotificationRule', { + Name: 'MyCodecommitRepositoryNotifyOnPullRequestCreatedBB14EA32', + DetailType: 'FULL', + EventTypeIds: [ + 'codecommit-repository-pull-request-created', + ], + Resource: { + 'Fn::GetAtt': [ + 'MyCodecommitRepository26DB372B', + 'Arn', + ], + }, + Targets: [ + { + TargetAddress: { + Ref: 'MyTopic86869434', + }, + TargetType: 'SNS', + }, + ], + })); + + expect(stack).to(haveResource('AWS::CodeStarNotifications::NotificationRule', { + Name: 'MyCodecommitRepositoryNotifyOnPullRequestMerged34A7EDF1', + DetailType: 'FULL', + EventTypeIds: [ + 'codecommit-repository-pull-request-merged', + ], + Resource: { + 'Fn::GetAtt': [ + 'MyCodecommitRepository26DB372B', + 'Arn', + ], + }, + Targets: [ + { + TargetAddress: { + Ref: 'MyTopic86869434', + }, + TargetType: 'SNS', + }, + ], + })); + + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index 9ab1167da0c8d..dc1aa633cde21 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -76,8 +76,8 @@ "@types/lodash": "^4.14.171", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", + "jest": "^26.6.3", "lodash": "^4.17.21", - "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts index 74df86e395548..a4767fa1a61bc 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/bitbucket/bitbucket-source-action.test.ts @@ -1,22 +1,22 @@ -import { arrayWith, expect, haveResourceLike, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'BitBucket source Action': { - 'produces the correct configuration when added to a pipeline'(test: Test) { +describe('BitBucket source Action', () => { + describe('BitBucket source Action', () => { + test('produces the correct configuration when added to a pipeline', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -44,20 +44,20 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + test('setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -78,16 +78,16 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, - 'grant s3 putObjectACL to the following CodeBuild Project'(test: Test) { + }); + test('grant s3 putObjectACL to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -110,17 +110,17 @@ nodeunitShim({ }), ), }, - })); - test.done(); - }, - 'setting triggerOnPush=false reflects in the configuration'(test: Test) { + }); + + }); + test('setting triggerOnPush=false reflects in the configuration', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { triggerOnPush: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -149,10 +149,10 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); }); function createBitBucketAndCodeBuildPipeline(stack: Stack, props: Partial): void { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts index e478ad7a97d21..955e54107789a 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/cloudformation-pipeline-actions.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CreateChangeSetAction can be used to make a change set from a CodePipeline'(test: Test) { +describe('CloudFormation Pipeline Actions', () => { + test('CreateChangeSetAction can be used to make a change set from a CodePipeline', () => { const stack = new cdk.Stack(); const pipeline = new codepipeline.Pipeline(stack, 'MagicPipeline'); @@ -79,7 +78,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'MagicPipelineArtifactsBucket212FE7BF', @@ -193,13 +192,12 @@ nodeunitShim({ ], 'Name': 'prod', }], - })); + }); - test.done(); - }, + }); - 'fullPermissions leads to admin role and full IAM capabilities with pipeline bucket+key read permissions'(test: Test) { + test('fullPermissions leads to admin role and full IAM capabilities with pipeline bucket+key read permissions', () => { // GIVEN const stack = new TestFixture(); @@ -214,7 +212,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -234,10 +232,10 @@ nodeunitShim({ ], }, ], - })); + }); // THEN: Role is created with full permissions - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Version: '2012-10-17', Statement: [ @@ -264,12 +262,12 @@ nodeunitShim({ ], }, Roles: [{ Ref: roleId }], - })); + }); + - test.done(); - }, + }); - 'outputFileName leads to creation of output artifact'(test: Test) { + test('outputFileName leads to creation of output artifact', () => { // GIVEN const stack = new TestFixture(); @@ -283,7 +281,7 @@ nodeunitShim({ })); // THEN: Action has output artifacts - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -296,12 +294,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'replaceOnFailure switches action type'(test: Test) { + test('replaceOnFailure switches action type', () => { // GIVEN const stack = new TestFixture(); @@ -315,7 +313,7 @@ nodeunitShim({ })); // THEN: Action has output artifacts - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -330,12 +328,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'parameterOverrides are serialized as a string'(test: Test) { + test('parameterOverrides are serialized as a string', () => { // GIVEN const stack = new TestFixture(); @@ -351,7 +349,7 @@ nodeunitShim({ })); // THEN - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -372,12 +370,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'Action service role is passed to template'(test: Test) { + }); + + test('Action service role is passed to template', () => { const stack = new TestFixture(); const importedRole = Role.fromRoleArn(stack, 'ImportedRole', 'arn:aws:iam::000000000000:role/action-role'); @@ -399,7 +397,7 @@ nodeunitShim({ stackName: 'magicStack', })); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', /* don't care about the rest */ @@ -423,12 +421,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'Single capability is passed to template'(test: Test) { + test('Single capability is passed to template', () => { // GIVEN const stack = new TestFixture(); @@ -446,7 +444,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -466,12 +464,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'Multiple capabilities are passed to template'(test: Test) { + test('Multiple capabilities are passed to template', () => { // GIVEN const stack = new TestFixture(); @@ -490,7 +488,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -510,12 +508,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'Empty capabilities is not passed to template'(test: Test) { + }); + + test('Empty capabilities is not passed to template', () => { // GIVEN const stack = new TestFixture(); @@ -533,7 +531,7 @@ nodeunitShim({ const roleId = 'PipelineDeployCreateUpdateRole515CB7D4'; // THEN: Action in Pipeline has no capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -552,12 +550,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'can use CfnCapabilities from the core module'(test: Test) { + test('can use CfnCapabilities from the core module', () => { // GIVEN const stack = new TestFixture(); @@ -574,7 +572,7 @@ nodeunitShim({ })); // THEN: Action in Pipeline has named IAM and AUTOEXPAND capabilities - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -594,13 +592,13 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'cross-account CFN Pipeline': { - 'correctly creates the deployment Role in the other account'(test: Test) { + describe('cross-account CFN Pipeline', () => { + test('correctly creates the deployment Role in the other account', () => { const app = new cdk.App(); const pipelineStack = new cdk.Stack(app, 'PipelineStack', { @@ -638,7 +636,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -662,10 +660,10 @@ nodeunitShim({ ], }, ], - })); + }); // the pipeline's BucketPolicy should trust both CFN roles - expect(pipelineStack).to(haveResourceLike('AWS::S3::BucketPolicy', { + expect(pipelineStack).toHaveResourceLike('AWS::S3::BucketPolicy', { 'PolicyDocument': { 'Statement': [ { @@ -698,19 +696,19 @@ nodeunitShim({ }, ], }, - })); + }); const otherStack = app.node.findChild('cross-account-support-stack-123456789012') as cdk.Stack; - expect(otherStack).to(haveResourceLike('AWS::IAM::Role', { + expect(otherStack).toHaveResourceLike('AWS::IAM::Role', { 'RoleName': 'pipelinestack-support-123loycfnactionrole56af64af3590f311bc50', - })); - expect(otherStack).to(haveResourceLike('AWS::IAM::Role', { + }); + expect(otherStack).toHaveResourceLike('AWS::IAM::Role', { 'RoleName': 'pipelinestack-support-123fndeploymentrole4668d9b5a30ce3dc4508', - })); + }); - test.done(); - }, - }, + + }); + }); }); /** diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts index ed7b5345ba641..cdbf078b58aff 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/cloudformation/pipeline-actions.test.ts @@ -1,3 +1,4 @@ +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as notifications from '@aws-cdk/aws-codestarnotifications'; import * as events from '@aws-cdk/aws-events'; @@ -6,12 +7,11 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Construct, IConstruct, Node } from 'constructs'; import * as _ from 'lodash'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - CreateReplaceChangeSet: { - 'works'(test: Test) { +describe('Pipeline Actions', () => { + describe('CreateReplaceChangeSet', () => { + test('works', (done) => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'Stack'); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); @@ -30,29 +30,29 @@ nodeunitShim({ app.synth(); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); const stackArn = _stackArn('MyStack', stack); const changeSetCondition = { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }; - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn, changeSetCondition); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:CreateChangeSet', stackArn, changeSetCondition); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DeleteChangeSet', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeStacks', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeChangeSet', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:CreateChangeSet', stackArn, changeSetCondition); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DeleteChangeSet', stackArn, changeSetCondition); // TODO: revert "as any" once we move all actions into a single package. - test.deepEqual(stage.fullActions[0].actionProperties.inputs, [artifact], - 'The input was correctly registered'); + expect(stage.fullActions[0].actionProperties.inputs).toEqual([artifact]); - _assertActionMatches(test, stack, stage.fullActions, 'CloudFormation', 'Deploy', { + _assertActionMatches(done, stack, stage.fullActions, 'CloudFormation', 'Deploy', { ActionMode: 'CHANGE_SET_CREATE_REPLACE', StackName: 'MyStack', ChangeSetName: 'MyChangeSet', }); - test.done(); - }, + done(); - 'uses a single permission statement if the same ChangeSet name is used'(test: Test) { + }); + + test('uses a single permission statement if the same ChangeSet name is used', () => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const artifact = new codepipeline.Artifact('TestArtifact'); @@ -76,8 +76,9 @@ nodeunitShim({ ], }); - test.deepEqual( + expect( stack.resolve(pipelineRole.statements.map(s => s.toStatementJson())), + ).toEqual( [ { Action: 'iam:PassRole', @@ -106,12 +107,12 @@ nodeunitShim({ ], ); - test.done(); - }, - }, - ExecuteChangeSet: { - 'works'(test: Test) { + }); + }); + + describe('ExecuteChangeSet', () => { + test('works', (done) => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const stage = new StageDouble({ @@ -126,19 +127,19 @@ nodeunitShim({ }); const stackArn = _stackArn('MyStack', stack); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:ExecuteChangeSet', stackArn, { StringEqualsIfExists: { 'cloudformation:ChangeSetName': 'MyChangeSet' } }); - _assertActionMatches(test, stack, stage.fullActions, 'CloudFormation', 'Deploy', { + _assertActionMatches(done, stack, stage.fullActions, 'CloudFormation', 'Deploy', { ActionMode: 'CHANGE_SET_EXECUTE', StackName: 'MyStack', ChangeSetName: 'MyChangeSet', }); - test.done(); - }, + done(); + }); - 'uses a single permission statement if the same ChangeSet name is used'(test: Test) { + test('uses a single permission statement if the same ChangeSet name is used', () => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); new StageDouble({ @@ -157,8 +158,9 @@ nodeunitShim({ ], }); - test.deepEqual( + expect( stack.resolve(pipelineRole.statements.map(s => s.toStatementJson())), + ).toEqual( [ { Action: [ @@ -178,11 +180,11 @@ nodeunitShim({ ], ); - test.done(); - }, - }, - 'the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions'(test: Test) { + }); + }); + + test('the CreateUpdateStack Action sets the DescribeStack*, Create/Update/DeleteStack & PassRole permissions', (done) => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cpactions.CloudFormationCreateUpdateStackAction({ @@ -198,17 +200,17 @@ nodeunitShim({ }); const stackArn = _stackArn('MyStack', stack); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:CreateStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:CreateStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:UpdateStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); - test.done(); - }, + done(); + }); - 'the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions'(test: Test) { + test('the DeleteStack Action sets the DescribeStack*, DeleteStack & PassRole permissions', (done) => { const stack = new cdk.Stack(); const pipelineRole = new RoleDouble(stack, 'PipelineRole'); const action = new cpactions.CloudFormationDeleteStackAction({ @@ -222,13 +224,13 @@ nodeunitShim({ }); const stackArn = _stackArn('MyStack', stack); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DescribeStack*', stackArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'cloudformation:DeleteStack', stackArn); - _assertPermissionGranted(test, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); + _assertPermissionGranted(done, stack, pipelineRole.statements, 'iam:PassRole', action.deploymentRole.roleArn); - test.done(); - }, + done(); + }); }); interface PolicyStatementJson { @@ -239,7 +241,7 @@ interface PolicyStatementJson { } function _assertActionMatches( - test: Test, + done: jest.DoneCallback, stack: cdk.Stack, actions: FullAction[], provider: string, @@ -256,8 +258,9 @@ function _assertActionMatches( configuration: stack.resolve(a.actionConfig.configuration), }), ), null, 2); - test.ok(_hasAction(stack, actions, provider, category, configuration), - `Expected to find an action with provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); + if (!_hasAction(stack, actions, provider, category, configuration)) { + done.fail(`Expected to find an action with provider ${provider}, category ${category}${configurationStr}, but found ${actionsStr}`); + } } function _hasAction( @@ -280,7 +283,7 @@ function _hasAction( } function _assertPermissionGranted( - test: Test, + done: jest.DoneCallback, stack: cdk.Stack, statements: iam.PolicyStatement[], action: string, @@ -291,8 +294,9 @@ function _assertPermissionGranted( : ''; const resolvedStatements = stack.resolve(statements.map(s => s.toStatementJson())); const statementsStr = JSON.stringify(resolvedStatements, null, 2); - test.ok(_grantsPermission(stack, resolvedStatements, action, resource, conditions), - `Expected to find a statement granting ${action} on ${JSON.stringify(stack.resolve(resource))}${conditionStr}, found:\n${statementsStr}`); + if (!_grantsPermission(stack, resolvedStatements, action, resource, conditions)) { + done.fail(`Expected to find a statement granting ${action} on ${JSON.stringify(stack.resolve(resource))}${conditionStr}, found:\n${statementsStr}`); + } } function _grantsPermission(stack: cdk.Stack, statements: PolicyStatementJson[], action: string, resource: string, conditions?: any) { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts index 09b583c011513..c8ddc211a813d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codebuild/codebuild-action.test.ts @@ -1,19 +1,18 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CodeBuild action': { - 'that is cross-account and has outputs': { - 'causes an error'(test: Test) { +describe('CodeBuild Action', () => { + describe('CodeBuild action', () => { + describe('that is cross-account and has outputs', () => { + test('causes an error', () => { const app = new App(); const projectStack = new Stack(app, 'ProjectStack', { @@ -61,15 +60,15 @@ nodeunitShim({ outputs: [new codepipeline.Artifact()], }); - test.throws(() => { + expect(() => { buildStage.addAction(buildAction2); - }, /https:\/\/github\.com\/aws\/aws-cdk\/issues\/4169/); + }).toThrow(/https:\/\/github\.com\/aws\/aws-cdk\/issues\/4169/); - test.done(); - }, - }, - 'can be backed by an imported project'(test: Test) { + }); + }); + + test('can be backed by an imported project', () => { const stack = new Stack(); const codeBuildProject = codebuild.PipelineProject.fromProjectName(stack, 'CodeBuild', @@ -102,7 +101,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -119,12 +118,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'exposes variables for other actions to consume'(test: Test) { + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -177,7 +176,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -198,12 +197,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'sets the BatchEnabled configuration'(test: Test) { + test('sets the BatchEnabled configuration', () => { const stack = new Stack(); const codeBuildProject = new codebuild.PipelineProject(stack, 'CodeBuild'); @@ -236,7 +235,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -253,12 +252,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'sets the CombineArtifacts configuration'(test: Test) { + }); + + test('sets the CombineArtifacts configuration', () => { const stack = new Stack(); const codeBuildProject = new codebuild.PipelineProject(stack, 'CodeBuild'); @@ -292,7 +291,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -310,13 +309,13 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'environment variables': { - 'should fail by default when added to a Pipeline while using a secret value in a plaintext variable'(test: Test) { + }); + + describe('environment variables', () => { + test('should fail by default when added to a Pipeline while using a secret value in a plaintext variable', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -350,14 +349,14 @@ nodeunitShim({ }, }); - test.throws(() => { + expect(() => { buildStage.addAction(buildAction); - }, /Plaintext environment variable 'X' contains a secret value!/); + }).toThrow(/Plaintext environment variable 'X' contains a secret value!/); - test.done(); - }, - "should allow opting out of the 'secret value in a plaintext variable' validation"(test: Test) { + }); + + test("should allow opting out of the 'secret value in a plaintext variable' validation", () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -390,8 +389,8 @@ nodeunitShim({ ], }); - test.done(); - }, - }, - }, + + }); + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts index da226dd37a74d..648c113ce2155 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codecommit/codecommit-source-action.test.ts @@ -1,4 +1,5 @@ -import { ABSENT, arrayWith, countResources, expect, haveResourceLike, not, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, arrayWith, objectLike } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -6,19 +7,18 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import { Stack, Lazy, App } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CodeCommit Source Action': { - 'by default does not poll for source changes and uses Events'(test: Test) { +describe('CodeCommit Source Action', () => { + describe('CodeCommit Source Action', () => { + test('by default does not poll for source changes and uses Events', () => { const stack = new Stack(); minimalPipeline(stack, undefined); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -31,14 +31,14 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).toCountResources('AWS::Events::Rule', 1); - expect(stack).to(countResources('AWS::Events::Rule', 1)); - test.done(); - }, + }); - 'cross-account CodeCommit Repository Source does not use target role in source stack'(test: Test) { + test('cross-account CodeCommit Repository Source does not use target role in source stack', () => { // Test for https://github.com/aws/aws-cdk/issues/15639 const app = new App(); const sourceStack = new Stack(app, 'SourceStack', { env: { account: '1234', region: 'north-pole' } }); @@ -67,7 +67,7 @@ nodeunitShim({ }); // THEN - creates a Rule in the source stack targeting the pipeline stack's event bus using a generated role - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { EventPattern: { source: ['aws.codecommit'], resources: [ @@ -84,10 +84,10 @@ nodeunitShim({ ]], }, }], - })); + }); // THEN - creates a Rule in the pipeline stack using the role to start the pipeline - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'aws.codecommit', @@ -118,17 +118,17 @@ nodeunitShim({ 'RoleArn': { 'Fn::GetAtt': ['MyPipelineEventsRoleFAB99F32', 'Arn'] }, }, ], - })); + }); + - test.done(); - }, + }); - 'does not poll for source changes and uses Events for CodeCommitTrigger.EVENTS'(test: Test) { + test('does not poll for source changes and uses Events for CodeCommitTrigger.EVENTS', () => { const stack = new Stack(); minimalPipeline(stack, cpactions.CodeCommitTrigger.EVENTS); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -141,19 +141,19 @@ nodeunitShim({ }, {}, ], - })); + }); - expect(stack).to(countResources('AWS::Events::Rule', 1)); + expect(stack).toCountResources('AWS::Events::Rule', 1); - test.done(); - }, - 'polls for source changes and does not use Events for CodeCommitTrigger.POLL'(test: Test) { + }); + + test('polls for source changes and does not use Events for CodeCommitTrigger.POLL', () => { const stack = new Stack(); minimalPipeline(stack, cpactions.CodeCommitTrigger.POLL); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -166,19 +166,19 @@ nodeunitShim({ }, {}, ], - })); + }); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - test.done(); - }, - 'does not poll for source changes and does not use Events for CodeCommitTrigger.NONE'(test: Test) { + }); + + test('does not poll for source changes and does not use Events for CodeCommitTrigger.NONE', () => { const stack = new Stack(); minimalPipeline(stack, cpactions.CodeCommitTrigger.NONE); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -191,32 +191,32 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); - test.done(); - }, + }); - 'cannot be created with an empty branch'(test: Test) { + test('cannot be created with an empty branch', () => { const stack = new Stack(); const repo = new codecommit.Repository(stack, 'MyRepo', { repositoryName: 'my-repo', }); - test.throws(() => { + expect(() => { new cpactions.CodeCommitSourceAction({ actionName: 'Source2', repository: repo, output: new codepipeline.Artifact(), branch: '', }); - }, /'branch' parameter cannot be an empty string/); + }).toThrow(/'branch' parameter cannot be an empty string/); + - test.done(); - }, + }); - 'allows using the same repository multiple times with different branches when trigger=EVENTS'(test: Test) { + test('allows using the same repository multiple times with different branches when trigger=EVENTS', () => { const stack = new Stack(); const repo = new codecommit.Repository(stack, 'MyRepo', { @@ -255,10 +255,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'exposes variables for other actions to consume'(test: Test) { + }); + + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -291,7 +291,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -308,12 +308,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'allows using a Token for the branch name'(test: Test) { + test('allows using a Token for the branch name', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -345,18 +345,18 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { EventPattern: { detail: { referenceName: ['my-branch'], }, }, - })); + }); + - test.done(); - }, + }); - 'allows to enable full clone'(test: Test) { + test('allows to enable full clone', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -389,7 +389,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -408,9 +408,9 @@ nodeunitShim({ ], }, ], - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -432,9 +432,9 @@ nodeunitShim({ }), ), }, - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -456,12 +456,12 @@ nodeunitShim({ }), ), }, - })); + }); + - test.done(); - }, + }); - 'uses the role when passed'(test: Test) { + test('uses the role when passed', () => { const stack = new Stack(); const pipeline = new codepipeline.Pipeline(stack, 'P', { @@ -504,7 +504,7 @@ nodeunitShim({ actions: [buildAction], }); - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Arn: stack.resolve(pipeline.pipelineArn), @@ -512,12 +512,12 @@ nodeunitShim({ RoleArn: stack.resolve(triggerEventTestRole.roleArn), }, ], - })); + }); + - test.done(); - }, + }); - 'grants explicit s3:PutObjectAcl permissions when the Actions is cross-account'(test: Test) { + test('grants explicit s3:PutObjectAcl permissions when the Actions is cross-account', () => { const app = new App(); const repoStack = new Stack(app, 'RepoStack', { @@ -552,7 +552,7 @@ nodeunitShim({ ], }); - expect(repoStack).to(haveResourceLike('AWS::IAM::Policy', { + expect(repoStack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: arrayWith({ 'Action': 's3:PutObjectAcl', @@ -566,11 +566,11 @@ nodeunitShim({ }, }), }, - })); + }); - test.done(); - }, - }, + + }); + }); }); function minimalPipeline(stack: Stack, trigger: cpactions.CodeCommitTrigger | undefined): codepipeline.Pipeline { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts index 30a1d4827b091..18a0c714f4d5b 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codedeploy/ecs-deploy-action.test.ts @@ -1,13 +1,12 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codedeploy from '@aws-cdk/aws-codedeploy'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - 'CodeDeploy ECS Deploy Action': { - 'throws an exception if more than 4 container image inputs are provided'(test: Test) { +describe('CodeDeploy ECS Deploy Action', () => { + describe('CodeDeploy ECS Deploy Action', () => { + test('throws an exception if more than 4 container image inputs are provided', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); @@ -19,7 +18,7 @@ nodeunitShim({ }); } - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, @@ -27,18 +26,18 @@ nodeunitShim({ appSpecTemplateInput: artifact, containerImageInputs, }); - }, /Action cannot have more than 4 container image inputs, got: 5/); + }).toThrow(/Action cannot have more than 4 container image inputs, got: 5/); - test.done(); - }, - 'throws an exception if both appspec artifact input and file are specified'(test: Test) { + }); + + test('throws an exception if both appspec artifact input and file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); const artifactPath = new codepipeline.ArtifactPath(artifact, 'hello'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, @@ -46,34 +45,34 @@ nodeunitShim({ appSpecTemplateInput: artifact, appSpecTemplateFile: artifactPath, }); - }, /Exactly one of 'appSpecTemplateInput' or 'appSpecTemplateFile' can be provided in the ECS CodeDeploy Action/); + }).toThrow(/Exactly one of 'appSpecTemplateInput' or 'appSpecTemplateFile' can be provided in the ECS CodeDeploy Action/); - test.done(); - }, - 'throws an exception if neither appspec artifact input nor file are specified'(test: Test) { + }); + + test('throws an exception if neither appspec artifact input nor file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, taskDefinitionTemplateInput: artifact, }); - }, /Specifying one of 'appSpecTemplateInput' or 'appSpecTemplateFile' is required for the ECS CodeDeploy Action/); + }).toThrow(/Specifying one of 'appSpecTemplateInput' or 'appSpecTemplateFile' is required for the ECS CodeDeploy Action/); - test.done(); - }, - 'throws an exception if both task definition artifact input and file are specified'(test: Test) { + }); + + test('throws an exception if both task definition artifact input and file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); const artifactPath = new codepipeline.ArtifactPath(artifact, 'hello'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, @@ -81,28 +80,28 @@ nodeunitShim({ taskDefinitionTemplateFile: artifactPath, appSpecTemplateInput: artifact, }); - }, /Exactly one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' can be provided in the ECS CodeDeploy Action/); + }).toThrow(/Exactly one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' can be provided in the ECS CodeDeploy Action/); + - test.done(); - }, + }); - 'throws an exception if neither task definition artifact input nor file are specified'(test: Test) { + test('throws an exception if neither task definition artifact input nor file are specified', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.CodeDeployEcsDeployAction({ actionName: 'DeployToECS', deploymentGroup, appSpecTemplateInput: artifact, }); - }, /Specifying one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' is required for the ECS CodeDeploy Action/); + }).toThrow(/Specifying one of 'taskDefinitionTemplateInput' or 'taskDefinitionTemplateFile' is required for the ECS CodeDeploy Action/); - test.done(); - }, - 'defaults task definition and appspec template paths'(test: Test) { + }); + + test('defaults task definition and appspec template paths', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); addCodeDeployECSCodePipeline(stack, { @@ -112,7 +111,7 @@ nodeunitShim({ appSpecTemplateInput: new codepipeline.Artifact('AppSpecArtifact'), }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -138,12 +137,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'defaults task definition placeholder string'(test: Test) { + }); + + test('defaults task definition placeholder string', () => { const stack = new cdk.Stack(); const deploymentGroup = addEcsDeploymentGroup(stack); const artifact1 = new codepipeline.Artifact(); @@ -163,7 +162,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -193,11 +192,11 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); function addEcsDeploymentGroup(stack: cdk.Stack): codedeploy.IEcsDeploymentGroup { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts index 367da1e11ed3f..75fe764109b25 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/codestar-connections/codestar-connections-source-action.test.ts @@ -1,22 +1,22 @@ -import { arrayWith, expect, haveResourceLike, objectLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { arrayWith, objectLike } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'CodeStar Connections source Action': { - 'produces the correct configuration when added to a pipeline'(test: Test) { +describe('CodeStar Connections source Action', () => { + describe('CodeStar Connections source Action', () => { + test('produces the correct configuration when added to a pipeline', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -44,20 +44,20 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project'(test: Test) { + test('setting codeBuildCloneOutput=true adds permission to use the connection to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -78,19 +78,19 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'grant s3 putObjectACL to the following CodeBuild Project'(test: Test) { + test('grant s3 putObjectACL to the following CodeBuild Project', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { codeBuildCloneOutput: true, }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': arrayWith( objectLike({ @@ -105,19 +105,19 @@ nodeunitShim({ }), ), }, - })); + }); + - test.done(); - }, + }); - 'setting triggerOnPush=false reflects in the configuration'(test: Test) { + test('setting triggerOnPush=false reflects in the configuration', () => { const stack = new Stack(); createBitBucketAndCodeBuildPipeline(stack, { triggerOnPush: false, }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -146,10 +146,10 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); }); function createBitBucketAndCodeBuildPipeline(stack: Stack, props: Partial): void { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts index aecf6cb915f04..b51a566d4ef14 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecr/ecr-source-action.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ecr from '@aws-cdk/aws-ecr'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'ECR source Action': { - 'exposes variables for other actions to consume'(test: Test) { +describe('ecr source action', () => { + describe('ECR source Action', () => { + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -41,7 +40,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -58,9 +57,9 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts index 3cdc12554d501..63927d5832ec8 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/ecs/ecs-deploy-action.test.ts @@ -1,124 +1,123 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - 'ECS deploy Action': { - 'throws an exception if neither inputArtifact nor imageFile were provided'(test: Test) { +describe('ecs deploy action', () => { + describe('ECS deploy Action', () => { + test('throws an exception if neither inputArtifact nor imageFile were provided', () => { const service = anyEcsService(); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, }); - }, /one of 'input' or 'imageFile' is required/); + }).toThrow(/one of 'input' or 'imageFile' is required/); - test.done(); - }, - 'can be created just by specifying the inputArtifact'(test: Test) { + }); + + test('can be created just by specifying the inputArtifact', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.doesNotThrow(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, }); - }); + }).not.toThrow(); - test.done(); - }, - 'can be created just by specifying the imageFile'(test: Test) { + }); + + test('can be created just by specifying the imageFile', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.doesNotThrow(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, imageFile: artifact.atPath('imageFile.json'), }); - }); + }).not.toThrow(); - test.done(); - }, - 'throws an exception if both inputArtifact and imageFile were provided'(test: Test) { + }); + + test('throws an exception if both inputArtifact and imageFile were provided', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, imageFile: artifact.atPath('file.json'), }); - }, /one of 'input' or 'imageFile' can be provided/); + }).toThrow(/one of 'input' or 'imageFile' can be provided/); + - test.done(); - }, + }); - 'can be created with deploymentTimeout between 1-60 minutes'(test: Test) { + test('can be created with deploymentTimeout between 1-60 minutes', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.doesNotThrow(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.minutes(30), }); - }); + }).not.toThrow(); - test.done(); - }, - 'throws an exception if deploymentTimeout is out of bounds'(test: Test) { + }); + + test('throws an exception if deploymentTimeout is out of bounds', () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.minutes(61), }); - }, /timeout must be between 1 and 60 minutes/); + }).toThrow(/timeout must be between 1 and 60 minutes/); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.minutes(0), }); - }, /timeout must be between 1 and 60 minutes/); + }).toThrow(/timeout must be between 1 and 60 minutes/); - test.throws(() => { + expect(() => { new cpactions.EcsDeployAction({ actionName: 'ECS', service, input: artifact, deploymentTimeout: cdk.Duration.seconds(30), }); - }, /cannot be converted into a whole number/); + }).toThrow(/cannot be converted into a whole number/); + - test.done(); - }, + }); - "sets the target service as the action's backing resource"(test: Test) { + test("sets the target service as the action's backing resource", () => { const service = anyEcsService(); const artifact = new codepipeline.Artifact('Artifact'); @@ -128,12 +127,12 @@ nodeunitShim({ input: artifact, }); - test.equal(action.actionProperties.resource, service); + expect(action.actionProperties.resource).toEqual(service); + - test.done(); - }, + }); - 'can be created by existing service'(test: Test) { + test('can be created by existing service', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); const service = ecs.FargateService.fromFargateServiceAttributes(stack, 'FargateService', { @@ -173,7 +172,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ {}, { @@ -193,11 +192,11 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); function anyEcsService(): ecs.FargateService { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts index 635f7ac67780f..6a1342888042e 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/github/github-source-action.test.ts @@ -1,15 +1,15 @@ -import { expect, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'GitHub source Action': { - 'exposes variables for other actions to consume'(test: Test) { +describe('Github source action', () => { + describe('GitHub source Action', () => { + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -42,7 +42,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -59,12 +59,12 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); - 'always renders the customer-supplied namespace, even if none of the variables are used'(test: Test) { + test('always renders the customer-supplied namespace, even if none of the variables are used', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -96,7 +96,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -110,12 +110,12 @@ nodeunitShim({ { }, ], - })); + }); - test.done(); - }, - 'fails if a variable from an action without a namespace set that is not part of a pipeline is referenced'(test: Test) { + }); + + test('fails if a variable from an action without a namespace set that is not part of a pipeline is referenced', () => { const stack = new Stack(); const unusedSourceAction = new cpactions.GitHubSourceAction({ @@ -154,14 +154,14 @@ nodeunitShim({ ], }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + }).toThrow(/Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + - test.done(); - }, + }); - 'fails if a variable from an action with a namespace set that is not part of a pipeline is referenced'(test: Test) { + test('fails if a variable from an action with a namespace set that is not part of a pipeline is referenced', () => { const stack = new Stack(); const unusedSourceAction = new cpactions.GitHubSourceAction({ @@ -201,11 +201,11 @@ nodeunitShim({ ], }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + }).toThrow(/Cannot reference variables of action 'Source2', as that action was never added to a pipeline/); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts index 93b4dc1134160..bf894a5db1c88 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/manual-approval.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import { SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'manual approval Action': { - 'allows passing an SNS Topic when constructing it'(test: Test) { +describe('manual approval', () => { + describe('manual approval Action', () => { + test('allows passing an SNS Topic when constructing it', () => { const stack = new Stack(); const topic = new sns.Topic(stack, 'Topic'); const manualApprovalAction = new cpactions.ManualApprovalAction({ @@ -21,12 +20,12 @@ nodeunitShim({ const stage = pipeline.addStage({ stageName: 'stage' }); stage.addAction(manualApprovalAction); - test.equal(manualApprovalAction.notificationTopic, topic); + expect(manualApprovalAction.notificationTopic).toEqual(topic); - test.done(); - }, - 'allows granting manual approval permissions to role'(test: Test) { + }); + + test('allows granting manual approval permissions to role', () => { const stack = new Stack(); const role = new iam.Role(stack, 'Human', { assumedBy: new iam.AnyPrincipal() }); const pipeline = new codepipeline.Pipeline(stack, 'pipeline'); @@ -49,7 +48,7 @@ nodeunitShim({ manualApprovalAction.grantManualApproval(role); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -109,26 +108,26 @@ nodeunitShim({ 'Ref': 'HumanD337C84C', }, ], - })); + }); + - test.done(); - }, + }); - 'rejects granting manual approval permissions before binding action to stage'(test: Test) { + test('rejects granting manual approval permissions before binding action to stage', () => { const stack = new Stack(); const role = new iam.Role(stack, 'Human', { assumedBy: new iam.AnyPrincipal() }); const manualApprovalAction = new cpactions.ManualApprovalAction({ actionName: 'Approve', }); - test.throws(() => { + expect(() => { manualApprovalAction.grantManualApproval(role); - }, 'Cannot grant permissions before binding action to a stage'); + }).toThrow('Cannot grant permissions before binding action to a stage'); - test.done(); - }, - 'renders CustomData and ExternalEntityLink even if notificationTopic was not passed'(test: Test) { + }); + + test('renders CustomData and ExternalEntityLink even if notificationTopic was not passed', () => { const stack = new Stack(); new codepipeline.Pipeline(stack, 'pipeline', { stages: [ @@ -155,7 +154,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -173,9 +172,9 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts index 746eaa84c66d9..67e714409040f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/pipeline.test.ts @@ -1,4 +1,5 @@ -import { countResources, expect, haveResource, haveResourceLike, not, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; @@ -8,13 +9,12 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; import * as sns from '@aws-cdk/aws-sns'; import { App, Aws, CfnParameter, SecretValue, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'basic pipeline'(test: Test) { +describe('pipeline', () => { + test('basic pipeline', () => { const stack = new Stack(); const repository = new codecommit.Repository(stack, 'MyRepo', { @@ -45,28 +45,49 @@ nodeunitShim({ ], }); - test.notDeepEqual(SynthUtils.toCloudFormation(stack), {}); - test.deepEqual([], pipeline.node.validate()); - test.done(); - }, + expect(SynthUtils.toCloudFormation(stack)).not.toEqual({}); + expect([]).toEqual(pipeline.node.validate()); + }); - 'Tokens can be used as physical names of the Pipeline'(test: Test) { + test('Tokens can be used as physical names of the Pipeline', () => { const stack = new Stack(undefined, 'StackName'); - new codepipeline.Pipeline(stack, 'Pipeline', { + const p = new codepipeline.Pipeline(stack, 'Pipeline', { pipelineName: Aws.STACK_NAME, }); + p.addStage({ + stageName: 'Source', + actions: [ + new cpactions.GitHubSourceAction({ + actionName: 'GH', + runOrder: 8, + output: new codepipeline.Artifact('A'), + branch: 'branch', + oauthToken: SecretValue.plainText('secret'), + owner: 'foo', + repo: 'bar', + trigger: cpactions.GitHubTrigger.POLL, + }), + ], + }); + + p.addStage({ + stageName: 'Two', + actions: [ + new cpactions.ManualApprovalAction({ actionName: 'Boo' }), + ], + }); - expect(stack, true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Name': { 'Ref': 'AWS::StackName', }, - })); + }); + - test.done(); - }, + }); - 'pipeline with GitHub source with poll trigger'(test: Test) { + test('pipeline with GitHub source with poll trigger', () => { const stack = new Stack(); const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); @@ -96,9 +117,9 @@ nodeunitShim({ ], }); - expect(stack).to(not(haveResourceLike('AWS::CodePipeline::Webhook'))); + expect(stack).not.toHaveResourceLike('AWS::CodePipeline::Webhook'); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -120,12 +141,12 @@ nodeunitShim({ 'Name': 'Two', }, ], - })); + }); + - test.done(); - }, + }); - 'pipeline with GitHub source without triggers'(test: Test) { + test('pipeline with GitHub source without triggers', () => { const stack = new Stack(); const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); @@ -155,9 +176,9 @@ nodeunitShim({ ], }); - expect(stack).to(not(haveResourceLike('AWS::CodePipeline::Webhook'))); + expect(stack).not.toHaveResourceLike('AWS::CodePipeline::Webhook'); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -179,12 +200,12 @@ nodeunitShim({ 'Name': 'Two', }, ], - })); + }); + - test.done(); - }, + }); - 'github action uses ThirdParty owner'(test: Test) { + test('github action uses ThirdParty owner', () => { const stack = new Stack(); const secret = new CfnParameter(stack, 'GitHubToken', { type: 'String', default: 'my-token' }); @@ -213,9 +234,9 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Webhook')); + expect(stack).toHaveResourceLike('AWS::CodePipeline::Webhook'); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'PArtifactsBucket5E711C12', @@ -274,13 +295,12 @@ nodeunitShim({ 'Name': 'Two', }, ], - })); + }); - test.deepEqual([], p.node.validate()); - test.done(); - }, + expect([]).toEqual(p.node.validate()); + }); - 'onStateChange'(test: Test) { + test('onStateChange', () => { const stack = new Stack(); const topic = new sns.Topic(stack, 'Topic'); @@ -316,7 +336,7 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { 'Description': 'desc', 'EventPattern': { 'detail': { @@ -365,22 +385,21 @@ nodeunitShim({ 'Id': 'Target0', }, ], - })); + }); - test.deepEqual([], pipeline.node.validate()); - test.done(); - }, + expect([]).toEqual(pipeline.node.validate()); + }); - 'PipelineProject': { - 'with a custom Project Name': { - 'sets the source and artifacts to CodePipeline'(test: Test) { + describe('PipelineProject', () => { + describe('with a custom Project Name', () => { + test('sets the source and artifacts to CodePipeline', () => { const stack = new Stack(); new codebuild.PipelineProject(stack, 'MyProject', { projectName: 'MyProject', }); - expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + expect(stack).toHaveResourceLike('AWS::CodeBuild::Project', { 'Name': 'MyProject', 'Source': { 'Type': 'CODEPIPELINE', @@ -400,14 +419,14 @@ nodeunitShim({ 'Image': 'aws/codebuild/standard:1.0', 'ComputeType': 'BUILD_GENERAL1_SMALL', }, - })); + }); - test.done(); - }, - }, - }, - 'Lambda PipelineInvokeAction can be used to invoke Lambda functions from a CodePipeline'(test: Test) { + }); + }); + }); + + test('Lambda PipelineInvokeAction can be used to invoke Lambda functions from a CodePipeline', () => { const stack = new Stack(); const lambdaFun = new lambda.Function(stack, 'Function', { @@ -459,7 +478,7 @@ nodeunitShim({ actions: [lambdaAction], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStore': { 'Location': { 'Ref': 'PipelineArtifactsBucket22248F97', @@ -506,11 +525,11 @@ nodeunitShim({ 'Name': 'Stage', }, ], - })); + }); - test.equal((lambdaAction.actionProperties.outputs || []).length, 3); + expect((lambdaAction.actionProperties.outputs || []).length).toEqual(3); - expect(stack, /* skip validation */ true).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -530,13 +549,13 @@ nodeunitShim({ 'Ref': 'FunctionServiceRole675BB04A', }, ], - })); + }); - test.done(); - }, - 'cross-region Pipeline': { - 'generates the required Action & ArtifactStores properties in the template'(test: Test) { + }); + + describe('cross-region Pipeline', () => { + test('generates the required Action & ArtifactStores properties in the template', () => { const pipelineRegion = 'us-west-2'; const pipelineAccount = '123'; @@ -594,7 +613,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': 'us-west-1', @@ -644,36 +663,35 @@ nodeunitShim({ ], }, ], - })); + }); - test.notEqual(pipeline.crossRegionSupport[pipelineRegion], undefined); - test.notEqual(pipeline.crossRegionSupport['us-west-1'], undefined); + expect(pipeline.crossRegionSupport[pipelineRegion]).toBeDefined(); + expect(pipeline.crossRegionSupport['us-west-1']).toBeDefined(); const usEast1Support = pipeline.crossRegionSupport['us-east-1']; - test.notEqual(usEast1Support, undefined); - test.equal(usEast1Support.stack.region, 'us-east-1'); - test.equal(usEast1Support.stack.account, pipelineAccount); - test.ok(usEast1Support.stack.node.id.indexOf('us-east-1') !== -1, - `expected '${usEast1Support.stack.node.id}' to contain 'us-east-1'`); + expect(usEast1Support).toBeDefined(); + expect(usEast1Support.stack.region).toEqual('us-east-1'); + expect(usEast1Support.stack.account).toEqual(pipelineAccount); + expect(usEast1Support.stack.node.id.indexOf('us-east-1')).not.toEqual(-1); - test.done(); - }, - 'allows specifying only one of artifactBucket and crossRegionReplicationBuckets'(test: Test) { + }); + + test('allows specifying only one of artifactBucket and crossRegionReplicationBuckets', () => { const stack = new Stack(); - test.throws(() => { + expect(() => { new codepipeline.Pipeline(stack, 'Pipeline', { artifactBucket: new s3.Bucket(stack, 'Bucket'), crossRegionReplicationBuckets: { // even an empty map should trigger this validation... }, }); - }, /Only one of artifactBucket and crossRegionReplicationBuckets can be specified!/); - test.done(); - }, + }).toThrow(/Only one of artifactBucket and crossRegionReplicationBuckets can be specified!/); + + }); - 'does not create a new artifact Bucket if one was provided in the cross-region Buckets for the Pipeline region'(test: Test) { + test('does not create a new artifact Bucket if one was provided in the cross-region Buckets for the Pipeline region', () => { const pipelineRegion = 'us-west-2'; const stack = new Stack(undefined, undefined, { @@ -712,9 +730,9 @@ nodeunitShim({ ], }); - expect(stack).to(countResources('AWS::S3::Bucket', 1)); + expect(stack).toCountResources('AWS::S3::Bucket', 1); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': pipelineRegion, @@ -726,12 +744,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'allows providing a resource-backed action from a different region directly'(test: Test) { + test('allows providing a resource-backed action from a different region directly', () => { const account = '123456789012'; const app = new App(); @@ -765,7 +783,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'ArtifactStores': [ { 'Region': replicationRegion, @@ -810,18 +828,18 @@ nodeunitShim({ ], }, ], - })); + }); - expect(replicationStack).to(haveResourceLike('AWS::S3::Bucket', { + expect(replicationStack).toHaveResourceLike('AWS::S3::Bucket', { 'BucketName': 'replicationstackeplicationbucket2464cd5c33b386483b66', - })); + }); - test.done(); - }, - }, - 'cross-account Pipeline': { - 'with a CodeBuild Project in a different account works correctly'(test: Test) { + }); + }); + + describe('cross-account Pipeline', () => { + test('with a CodeBuild Project in a different account works correctly', () => { const app = new App(); const buildAccount = '901234567890'; @@ -874,7 +892,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -903,9 +921,9 @@ nodeunitShim({ ], }, ], - })); + }); - expect(buildStack).to(haveResourceLike('AWS::IAM::Policy', { + expect(buildStack).toHaveResourceLike('AWS::IAM::Policy', { 'PolicyDocument': { 'Statement': [ { @@ -958,12 +976,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'adds a dependency on the Stack containing a new action Role'(test: Test) { + test('adds a dependency on the Stack containing a new action Role', () => { const region = 'us-west-2'; const pipelineAccount = '123456789012'; const buildAccount = '901234567890'; @@ -1017,7 +1035,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -1041,14 +1059,14 @@ nodeunitShim({ ], }, ], - })); + }); - test.equal(pipelineStack.dependencies.length, 1); + expect(pipelineStack.dependencies.length).toEqual(1); - test.done(); - }, - 'does not add a dependency on the Stack containing an imported action Role'(test: Test) { + }); + + test('does not add a dependency on the Stack containing an imported action Role', () => { const region = 'us-west-2'; const pipelineAccount = '123456789012'; const buildAccount = '901234567890'; @@ -1101,7 +1119,7 @@ nodeunitShim({ ], }); - expect(pipelineStack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -1119,11 +1137,11 @@ nodeunitShim({ ], }, ], - })); + }); + + expect(pipelineStack.dependencies.length).toEqual(0); - test.equal(pipelineStack.dependencies.length, 0); - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts index 0fb227a9246e8..65553aee81be5 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/s3/s3-source-action.test.ts @@ -1,21 +1,20 @@ -import { countResources, expect, haveResourceLike, not } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import { Lazy, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'S3 Source Action': { - 'by default polls for source changes and does not use Events'(test: Test) { +describe('S3 source Action', () => { + describe('S3 Source Action', () => { + test('by default polls for source changes and does not use Events', () => { const stack = new Stack(); minimalPipeline(stack, undefined); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -27,19 +26,19 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); - test.done(); - }, + }); - 'does not poll for source changes and uses Events for S3Trigger.EVENTS'(test: Test) { + test('does not poll for source changes and uses Events for S3Trigger.EVENTS', () => { const stack = new Stack(); minimalPipeline(stack, { trigger: cpactions.S3Trigger.EVENTS }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -52,19 +51,19 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).toCountResources('AWS::Events::Rule', 1); - expect(stack).to(countResources('AWS::Events::Rule', 1)); - test.done(); - }, + }); - 'polls for source changes and does not use Events for S3Trigger.POLL'(test: Test) { + test('polls for source changes and does not use Events for S3Trigger.POLL', () => { const stack = new Stack(); minimalPipeline(stack, { trigger: cpactions.S3Trigger.POLL }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -77,19 +76,19 @@ nodeunitShim({ }, {}, ], - })); + }); + + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); - test.done(); - }, + }); - 'does not poll for source changes and does not use Events for S3Trigger.NONE'(test: Test) { + test('does not poll for source changes and does not use Events for S3Trigger.NONE', () => { const stack = new Stack(); minimalPipeline(stack, { trigger: cpactions.S3Trigger.NONE }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -102,29 +101,29 @@ nodeunitShim({ }, {}, ], - })); + }); - expect(stack).to(not(haveResourceLike('AWS::Events::Rule'))); + expect(stack).not.toHaveResourceLike('AWS::Events::Rule'); - test.done(); - }, - 'does not allow passing an empty string for the bucketKey property'(test: Test) { + }); + + test('does not allow passing an empty string for the bucketKey property', () => { const stack = new Stack(); - test.throws(() => { + expect(() => { new cpactions.S3SourceAction({ actionName: 'Source', bucket: new s3.Bucket(stack, 'MyBucket'), bucketKey: '', output: new codepipeline.Artifact(), }); - }, /Property bucketKey cannot be an empty string/); + }).toThrow(/Property bucketKey cannot be an empty string/); + - test.done(); - }, + }); - 'allows using the same bucket with events trigger mutliple times with different bucket paths'(test: Test) { + test('allows using the same bucket with events trigger mutliple times with different bucket paths', () => { const stack = new Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -141,10 +140,10 @@ nodeunitShim({ output: new codepipeline.Artifact(), })); - test.done(); - }, - 'throws an error if the same bucket and path with trigger = Events are added to the same pipeline twice'(test: Test) { + }); + + test('throws an error if the same bucket and path with trigger = Events are added to the same pipeline twice', () => { const stack = new Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -169,14 +168,14 @@ nodeunitShim({ output: new codepipeline.Artifact(), }); - test.throws(() => { + expect(() => { sourceStage.addAction(duplicateBucketAndPath); - }, /S3 source action with path 'my\/other\/path' is already present in the pipeline for this source bucket/); + }).toThrow(/S3 source action with path 'my\/other\/path' is already present in the pipeline for this source bucket/); - test.done(); - }, - 'allows using a Token bucketKey with trigger = Events, multiple times'(test: Test) { + }); + + test('allows using a Token bucketKey with trigger = Events, multiple times', () => { const stack = new Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -193,7 +192,7 @@ nodeunitShim({ output: new codepipeline.Artifact(), })); - expect(stack, /* skipValidation = */ true).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Actions': [ @@ -210,12 +209,12 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'exposes variables for other actions to consume'(test: Test) { + }); + + test('exposes variables for other actions to consume', () => { const stack = new Stack(); const sourceOutput = new codepipeline.Artifact(); @@ -247,7 +246,7 @@ nodeunitShim({ ], }); - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source', @@ -264,11 +263,11 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - }, + + }); + }); }); interface MinimalPipelineOptions { diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts index ef96441ca585e..cf490f21fc206 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/servicecatalog/servicecatalog-deploy-action-beta1.test.ts @@ -1,14 +1,13 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; /* eslint-disable quote-props */ -nodeunitShim({ - 'addAction succesfully leads to creation of codepipeline service catalog action with properly formatted TemplateFilePath'(test: Test) { +describe('ServiceCatalog Deploy Action', () => { + test('addAction succesfully leads to creation of codepipeline service catalog action with properly formatted TemplateFilePath', () => { // GIVEN const stack = new TestFixture(); // WHEN @@ -20,7 +19,7 @@ nodeunitShim({ productId: 'prod-xxxxxxxxx', })); // THEN - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -50,11 +49,11 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, - 'deployment without a description works successfully'(test: Test) { + }); + test('deployment without a description works successfully', () => { // GIVEN const stack = new TestFixture(); // WHEN @@ -65,7 +64,7 @@ nodeunitShim({ productId: 'prod-xxxxxxxxx', })); // THEN - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { 'Stages': [ { 'Name': 'Source' /* don't care about the rest */ }, { @@ -94,10 +93,10 @@ nodeunitShim({ ], }, ], - })); + }); + - test.done(); - }, + }); }); /** diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts b/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts index 50f8788e8823c..0ad5d8b2213a1 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/stepfunctions/stepfunctions-invoke-actions.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as s3 from '@aws-cdk/aws-s3'; import * as stepfunction from '@aws-cdk/aws-stepfunctions'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as cpactions from '../../lib'; -nodeunitShim({ - 'StepFunctions Invoke Action': { - 'Verify stepfunction configuration properties are set to specific values'(test: Test) { +describe('StepFunctions Invoke Action', () => { + describe('StepFunctions Invoke Action', () => { + test('Verify stepfunction configuration properties are set to specific values', () => { const stack = new Stack(); // when minimalPipeline(stack); // then - expect(stack).to(haveResourceLike('AWS::CodePipeline::Pipeline', { + expect(stack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { Stages: [ // Must have a source stage { @@ -58,17 +57,17 @@ nodeunitShim({ ], }, ], - })); + }); - test.done(); - }, - 'Allows the pipeline to invoke this stepfunction'(test: Test) { + }); + + test('Allows the pipeline to invoke this stepfunction', () => { const stack = new Stack(); minimalPipeline(stack); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -80,19 +79,19 @@ nodeunitShim({ }, ], }, - })); + }); + + expect(stack).toHaveResourceLike('AWS::IAM::Role'); - expect(stack).to(haveResourceLike('AWS::IAM::Role')); - test.done(); - }, + }); - 'Allows the pipeline to describe this stepfunction execution'(test: Test) { + test('Allows the pipeline to describe this stepfunction execution', () => { const stack = new Stack(); minimalPipeline(stack); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ {}, @@ -136,14 +135,14 @@ nodeunitShim({ }, ], }, - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Role')); + expect(stack).toHaveResourceLike('AWS::IAM::Role'); - test.done(); - }, - }, + }); + + }); }); function minimalPipeline(stack: Stack): codepipeline.IStage { diff --git a/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts b/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts index 0796557e38a77..f30cf11f58a99 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/lib/notification-rule.ts @@ -62,7 +62,7 @@ export interface NotificationRuleProps extends NotificationRuleOptions { /** * The Amazon Resource Name (ARN) of the resource to associate with the notification rule. - * Currently, Supported sources include pipelines in AWS CodePipeline and build projects in AWS CodeBuild in this L2 constructor. + * Currently, Supported sources include pipelines in AWS CodePipeline, build projects in AWS CodeBuild, and repositories in AWS CodeCommit in this L2 constructor. * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-codestarnotifications-notificationrule.html#cfn-codestarnotifications-notificationrule-resource */ readonly source: INotificationRuleSource; diff --git a/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts b/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts index bf1d335c94d9f..3acf5e33ca334 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/helpers.ts @@ -22,6 +22,17 @@ export class FakeCodePipeline implements notifications.INotificationRuleSource { } } +export class FakeCodeCommit implements notifications.INotificationRuleSource { + readonly repositoryArn = 'arn:aws:codecommit::1234567890:MyCodecommitProject'; + readonly repositoryName = 'test-repository'; + + bindAsNotificationRuleSource(): notifications.NotificationRuleSourceConfig { + return { + sourceArn: this.repositoryArn, + }; + } +} + export class FakeSnsTopicTarget implements notifications.INotificationRuleTarget { readonly topicArn = 'arn:aws:sns::1234567890:MyTopic'; diff --git a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts index a155b3583a524..332e808ca3176 100644 --- a/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts +++ b/packages/@aws-cdk/aws-codestarnotifications/test/notification-rule.test.ts @@ -4,6 +4,7 @@ import * as notifications from '../lib'; import { FakeCodeBuild, FakeCodePipeline, + FakeCodeCommit, FakeSlackTarget, FakeSnsTopicTarget, } from './helpers'; @@ -29,6 +30,26 @@ describe('NotificationRule', () => { }); }); + test('created new notification rule from repository source', () => { + const repository = new FakeCodeCommit(); + + new notifications.NotificationRule(stack, 'MyNotificationRule', { + source: repository, + events: [ + 'codecommit-repository-pull-request-created', + 'codecommit-repository-pull-request-merged', + ], + }); + + expect(stack).toHaveResourceLike('AWS::CodeStarNotifications::NotificationRule', { + Resource: repository.repositoryArn, + EventTypeIds: [ + 'codecommit-repository-pull-request-created', + 'codecommit-repository-pull-request-merged', + ], + }); + }); + test('created new notification rule with all parameters in constructor props', () => { const project = new FakeCodeBuild(); const slack = new FakeSlackTarget(); diff --git a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts index a12dfb92061c6..013f7d3389f3c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/instance-types.ts +++ b/packages/@aws-cdk/aws-ec2/lib/instance-types.ts @@ -442,6 +442,16 @@ export enum InstanceClass { */ M6G = 'm6g', + /** + * Standard instances based on Intel (Ice Lake), 6th generation. + */ + STANDARD6_INTEL = 'm6i', + + /** + * Standard instances based on Intel (Ice Lake), 6th generation. + */ + M6I = 'm6i', + /** * Standard instances, 6th generation with Graviton2 processors and local NVME drive */ diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index 58dba9f6786cb..00212e704e294 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -330,6 +330,31 @@ const scheduledFargateTask = new ScheduledFargateTask(stack, 'ScheduledFargateTa In addition to using the constructs, users can also add logic to customize these constructs: +### Configure HTTPS on an ApplicationLoadBalancedFargateService + +```ts +import { ApplicationLoadBalancedFargateService } from './application-load-balanced-fargate-service'; +import { HostedZone } from '@aws-cdk/aws-route53'; +import { Certificate } from '@aws-cdk/aws-certificatemanager'; +import { SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; + +const domainZone = HostedZone.fromLookup(this, 'Zone', { domainName: 'example.com' }); +const certificate = Certificate.fromCertificateArn(this, 'Cert', 'arn:aws:acm:us-east-1:123456:certificate/abcdefg'); + +const loadBalancedFargateService = new ApplicationLoadBalancedFargateService(stack, 'Service', { + vpc + cluster, + certificate, + sslPolicy: SslPolicy.RECOMMENDED, + domainName: 'api.example.com', + domainZone, + redirectHTTP: true, + taskImageOptions: { + image: ecs.ContainerImage.fromRegistry("amazon/amazon-ecs-sample"), + }, +}); +``` + ### Add Schedule-Based Auto-Scaling to an ApplicationLoadBalancedFargateService ```ts @@ -484,6 +509,57 @@ const queueProcessingFargateService = new QueueProcessingFargateService(stack, ' }); ``` +### Set capacityProviderStrategies for QueueProcessingFargateService + +```ts +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +cluster.enableFargateCapacityProviders(); + +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], +}); +``` + +### Set capacityProviderStrategies for QueueProcessingEc2Service + +```ts +const vpc = new ec2.Vpc(stack, 'Vpc', { maxAzs: 1 }); +const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); +const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.MICRO), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), +}); +const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, +}); +cluster.addAsgCapacityProvider(capacityProvider); + +const queueProcessingFargateService = new QueueProcessingFargateService(stack, 'Service', { + cluster, + memoryLimitMiB: 512, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + }, + ], +}); +``` + ### Select specific vpc subnets for ApplicationLoadBalancedFargateService ```ts diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index 4f7cd20220387..8839b39a09369 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -6,7 +6,7 @@ import { } from '@aws-cdk/aws-ecs'; import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, ApplicationTargetGroup, - IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, + IApplicationLoadBalancer, ListenerCertificate, ListenerAction, AddApplicationTargetsProps, SslPolicy, } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget, CnameRecord } from '@aws-cdk/aws-route53'; @@ -186,6 +186,13 @@ export interface ApplicationLoadBalancedServiceBaseProps { */ readonly listenerPort?: number; + /** + * The security policy that defines which ciphers and protocols are supported by the ALB Listener. + * + * @default - The recommended elastic load balancing security policy + */ + readonly sslPolicy?: SslPolicy; + /** * Specifies whether to propagate the tags from the task definition or the service to the tasks in the service. * Tags can only be propagated to the tasks within the service during service creation. @@ -438,6 +445,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends Construct { protocol, port: props.listenerPort, open: props.openListener ?? true, + sslPolicy: props.sslPolicy, }); this.targetGroup = this.listener.addTargets('ECS', targetProps); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts index aae6421c621c9..7c29a46741aaa 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-multiple-target-groups-service-base.ts @@ -4,7 +4,7 @@ import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerDefinition, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Protocol, Secret, } from '@aws-cdk/aws-ecs'; -import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationListener, ApplicationLoadBalancer, ApplicationProtocol, ApplicationTargetGroup, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; @@ -328,6 +328,13 @@ export interface ApplicationListenerProps { * created for the load balancer's specified domain name. */ readonly certificate?: ICertificate; + + /** + * The security policy that defines which ciphers and protocols are supported by the ALB Listener. + * + * @default - The recommended elastic load balancing security policy + */ + readonly sslPolicy?: SslPolicy; } /** @@ -404,6 +411,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru listenerName: listenerProps.name, loadBalancer: lb, port: listenerProps.port, + sslPolicy: listenerProps.sslPolicy, }); this.listeners.push(listener); } @@ -500,7 +508,7 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru } private configListener(protocol: ApplicationProtocol, props: ListenerConfig): ApplicationListener { - const listener = this.createListener(props.listenerName, props.loadBalancer, protocol, props.port); + const listener = this.createListener(props, protocol); let certificate; if (protocol === ApplicationProtocol.HTTPS) { certificate = this.createListenerCertificate(props.listenerName, props.certificate, props.domainName, props.domainZone); @@ -564,11 +572,12 @@ export abstract class ApplicationMultipleTargetGroupsServiceBase extends Constru } } - private createListener(name: string, lb: ApplicationLoadBalancer, protocol?: ApplicationProtocol, port?: number): ApplicationListener { - return lb.addListener(name, { + private createListener({ loadBalancer, listenerName, port, sslPolicy }: ListenerConfig, protocol?: ApplicationProtocol): ApplicationListener { + return loadBalancer.addListener(listenerName, { protocol, open: true, port, + sslPolicy, }); } @@ -619,6 +628,13 @@ interface ListenerConfig { */ readonly certificate?: ICertificate; + /** + * SSL Policy for the listener + * + * @default null + */ + readonly sslPolicy?: SslPolicy; + /** * The domain name for the service, e.g. "api.example.com." * diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts index 14def26120f5e..a4520e745536d 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/queue-processing-service-base.ts @@ -1,7 +1,7 @@ import { ScalingInterval } from '@aws-cdk/aws-applicationautoscaling'; import { IVpc } from '@aws-cdk/aws-ec2'; import { - AwsLogDriver, BaseService, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, + AwsLogDriver, BaseService, CapacityProviderStrategy, Cluster, ContainerImage, DeploymentController, DeploymentCircuitBreaker, ICluster, LogDriver, PropagatedTagSource, Secret, } from '@aws-cdk/aws-ecs'; import { IQueue, Queue } from '@aws-cdk/aws-sqs'; @@ -203,6 +203,14 @@ export interface QueueProcessingServiceBaseProps { * @default - disabled */ readonly circuitBreaker?: DeploymentCircuitBreaker; + + /** + * A list of Capacity Provider strategies used to place a service. + * + * @default - undefined + * + */ + readonly capacityProviderStrategies?: CapacityProviderStrategy[]; } /** diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts index d927284797b5b..0d9f612abfb76 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/ecs/queue-processing-ecs-service.ts @@ -123,6 +123,7 @@ export class QueueProcessingEc2Service extends QueueProcessingServiceBase { enableECSManagedTags: props.enableECSManagedTags, deploymentController: props.deploymentController, circuitBreaker: props.circuitBreaker, + capacityProviderStrategies: props.capacityProviderStrategies, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts index cb4b8d77a8188..e6f8b89c736b3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/fargate/queue-processing-fargate-service.ts @@ -149,6 +149,7 @@ export class QueueProcessingFargateService extends QueueProcessingServiceBase { vpcSubnets: props.taskSubnets, assignPublicIp: props.assignPublicIp, circuitBreaker: props.circuitBreaker, + capacityProviderStrategies: props.capacityProviderStrategies, }); this.configureAutoscalingForService(this.service); diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 550cf20262152..33d9ef58d0a7c 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -77,6 +77,7 @@ }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", @@ -95,6 +96,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", + "@aws-cdk/aws-autoscaling": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-ecs": "0.0.0", diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts index 0946123907fe2..5c3c5889c62b6 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s-v2.ts @@ -2,7 +2,7 @@ import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/ass import { Certificate } from '@aws-cdk/aws-certificatemanager'; import { InstanceType, Vpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, Cluster, ContainerImage, Ec2TaskDefinition, PropagatedTagSource, Protocol } from '@aws-cdk/aws-ecs'; -import { ApplicationProtocol } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationProtocol, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { CompositePrincipal, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import { NamespaceType } from '@aws-cdk/aws-servicediscovery'; @@ -124,6 +124,7 @@ export = { name: 'listener', protocol: ApplicationProtocol.HTTPS, certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), + sslPolicy: SslPolicy.TLS12_EXT, }, ], }, @@ -240,6 +241,15 @@ export = { }, })); + expect(stack).to(haveResourceLike('AWS::ElasticLoadBalancingV2::Listener', { + Port: 443, + Protocol: 'HTTPS', + Certificates: [{ + CertificateArn: 'helloworld', + }], + SslPolicy: SslPolicy.TLS12_EXT, + })); + test.done(); }, 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 5ef253d71bdb1..8e2f527fbe509 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 @@ -2,7 +2,7 @@ import { ABSENT, arrayWith, expect, haveResource, haveResourceLike, objectLike } import { Certificate } from '@aws-cdk/aws-certificatemanager'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; -import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, NetworkLoadBalancer } from '@aws-cdk/aws-elasticloadbalancingv2'; +import { ApplicationLoadBalancer, ApplicationProtocol, ApplicationProtocolVersion, NetworkLoadBalancer, SslPolicy } from '@aws-cdk/aws-elasticloadbalancingv2'; import { PublicHostedZone } from '@aws-cdk/aws-route53'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; @@ -508,6 +508,7 @@ export = { domainName: 'api.example.com', domainZone: zone, certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), + sslPolicy: SslPolicy.TLS12_EXT, }); // THEN - stack contains a load balancer and a service @@ -519,6 +520,7 @@ export = { Certificates: [{ CertificateArn: 'helloworld', }], + SslPolicy: SslPolicy.TLS12_EXT, })); expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts index 763cef9f33114..97bdb47c7b5b3 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.queue-processing-ecs-service.ts @@ -1,4 +1,5 @@ import { ABSENT, expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as sqs from '@aws-cdk/aws-sqs'; @@ -351,4 +352,48 @@ export = { test.done(); }, + + 'can set capacity provider strategies'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + const autoScalingGroup = new autoscaling.AutoScalingGroup(stack, 'asg', { + vpc, + instanceType: new ec2.InstanceType('bogus'), + machineImage: ecs.EcsOptimizedImage.amazonLinux2(), + }); + const capacityProvider = new ecs.AsgCapacityProvider(stack, 'provider', { + autoScalingGroup, + }); + cluster.addAsgCapacityProvider(capacityProvider); + + // WHEN + new ecsPatterns.QueueProcessingEc2Service(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromRegistry('test'), + memoryLimitMiB: 512, + capacityProviderStrategies: [ + { + capacityProvider: capacityProvider.capacityProviderName, + }, + ], + }); + + // THEN + expect(stack).to( + haveResource('AWS::ECS::Service', { + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: { + Ref: 'providerD3FF4D3A', + }, + }, + ], + }), + ); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts index 7a0f33e3a0a0c..6b5a05ccaaf7b 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.queue-processing-fargate-service.ts @@ -529,4 +529,49 @@ export = { test.done(); }, + + 'can set capacity provider strategies'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'MyVpc', {}); + const cluster = new ecs.Cluster(stack, 'EcsCluster', { + vpc, + }); + cluster.enableFargateCapacityProviders(); + + // WHEN + new ecsPatterns.QueueProcessingFargateService(stack, 'Service', { + cluster, + image: ecs.ContainerImage.fromRegistry('test'), + capacityProviderStrategies: [ + { + capacityProvider: 'FARGATE_SPOT', + weight: 2, + }, + { + capacityProvider: 'FARGATE', + weight: 1, + }, + ], + }); + + // THEN + expect(stack).to( + haveResource('AWS::ECS::Service', { + LaunchType: ABSENT, + CapacityProviderStrategy: [ + { + CapacityProvider: 'FARGATE_SPOT', + Weight: 2, + }, + { + CapacityProvider: 'FARGATE', + Weight: 1, + }, + ], + }), + ); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-ecs/README.md b/packages/@aws-cdk/aws-ecs/README.md index 1d8a10f4deb8c..5454d93e61095 100644 --- a/packages/@aws-cdk/aws-ecs/README.md +++ b/packages/@aws-cdk/aws-ecs/README.md @@ -231,6 +231,17 @@ const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { }); ``` +On Fargate Platform Version 1.4.0 or later, you may specify up to 200GiB of +[ephemeral storage](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/fargate-task-storage.html#fargate-task-storage-pv14): + +```ts +const fargateTaskDefinition = new ecs.FargateTaskDefinition(this, 'TaskDef', { + memoryLimitMiB: 512, + cpu: 256, + ephemeralStorageGiB: 100 +}); +``` + To add containers to a task definition, call `addContainer()`: ```ts diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 4cc66decb7933..9875097fa11cf 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -199,6 +199,15 @@ export interface TaskDefinitionProps extends CommonTaskDefinitionProps { * @default - No inference accelerators. */ readonly inferenceAccelerators?: InferenceAccelerator[]; + + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. + * + * Only supported in Fargate platform version 1.4.0 or later. + * + * @default - Undefined, in which case, the task will receive 20GiB ephemeral storage. + */ + readonly ephemeralStorageGiB?: number; } /** @@ -329,6 +338,13 @@ export class TaskDefinition extends TaskDefinitionBase { */ public readonly compatibility: Compatibility; + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. + * + * Only supported in Fargate platform version 1.4.0 or later. + */ + public readonly ephemeralStorageGiB?: number; + /** * The container definitions. */ @@ -399,6 +415,8 @@ export class TaskDefinition extends TaskDefinitionBase { props.inferenceAccelerators.forEach(ia => this.addInferenceAccelerator(ia)); } + this.ephemeralStorageGiB = props.ephemeralStorageGiB; + const taskDef = new CfnTaskDefinition(this, 'Resource', { containerDefinitions: Lazy.any({ produce: () => this.renderContainers() }, { omitEmptyArray: true }), volumes: Lazy.any({ produce: () => this.renderVolumes() }, { omitEmptyArray: true }), @@ -424,6 +442,9 @@ export class TaskDefinition extends TaskDefinitionBase { produce: () => !isFargateCompatible(this.compatibility) ? this.renderInferenceAccelerators() : undefined, }, { omitEmptyArray: true }), + ephemeralStorage: this.ephemeralStorageGiB ? { + sizeInGiB: this.ephemeralStorageGiB, + } : undefined, }); if (props.placementConstraints) { diff --git a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts index ccf3291859708..803ec6a449969 100644 --- a/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/fargate/fargate-task-definition.ts @@ -50,6 +50,15 @@ export interface FargateTaskDefinitionProps extends CommonTaskDefinitionProps { * @default 512 */ readonly memoryLimitMiB?: number; + + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. The maximum supported value is 200 GiB. + * + * NOTE: This parameter is only supported for tasks hosted on AWS Fargate using platform version 1.4.0 or later. + * + * @default 20 + */ + readonly ephemeralStorageGiB?: number; } /** @@ -104,6 +113,11 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas // we need to explicitly write the type here, as type deduction for enums won't lead to // the import being generated in the .d.ts file. + /** + * The amount (in GiB) of ephemeral storage to be allocated to the task. + */ + public readonly ephemeralStorageGiB?: number; + /** * Constructs a new instance of the FargateTaskDefinition class. */ @@ -115,5 +129,11 @@ export class FargateTaskDefinition extends TaskDefinition implements IFargateTas compatibility: Compatibility.FARGATE, networkMode: NetworkMode.AWS_VPC, }); + + if (props.ephemeralStorageGiB && (props.ephemeralStorageGiB < 21 || props.ephemeralStorageGiB > 200)) { + throw new Error('Ephemeral storage size must be between 21GiB and 200GiB'); + } + + this.ephemeralStorageGiB = props.ephemeralStorageGiB; } } diff --git a/packages/@aws-cdk/aws-ecs/package.json b/packages/@aws-cdk/aws-ecs/package.json index c715f93c72f99..92ea16797b095 100644 --- a/packages/@aws-cdk/aws-ecs/package.json +++ b/packages/@aws-cdk/aws-ecs/package.json @@ -82,7 +82,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit-shim": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", "proxyquire": "^2.1.3", "@aws-cdk/assert-internal": "0.0.0" diff --git a/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts b/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts index 7865809011a32..7c8bd5f880f77 100644 --- a/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/app-mesh-proxy-configuration.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; -nodeunitShim({ - 'correctly sets all appMeshProxyConfiguration'(test: Test) { +describe('app mesh proxy configuration', () => { + test('correctly sets all appMeshProxyConfiguration', () => { // GIVEN const stack = new cdk.Stack(); @@ -34,7 +33,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -69,11 +68,11 @@ nodeunitShim({ ], Type: 'APPMESH', }, - })); - test.done(); - }, + }); + + }); - 'correctly sets appMeshProxyConfiguration with default properties set'(test: Test) { + test('correctly sets appMeshProxyConfiguration with default properties set', () => { // GIVEN const stack = new cdk.Stack(); @@ -100,7 +99,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -123,11 +122,11 @@ nodeunitShim({ ], Type: 'APPMESH', }, - })); - test.done(); - }, + }); + + }); - 'correctly sets appMeshProxyConfiguration with empty egressIgnoredPorts and egressIgnoredIPs'(test: Test) { + test('correctly sets appMeshProxyConfiguration with empty egressIgnoredPorts and egressIgnoredIPs', () => { // GIVEN const stack = new cdk.Stack(); @@ -156,7 +155,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ProxyConfiguration: { ContainerName: 'envoy', ProxyConfigurationProperties: [ @@ -179,16 +178,16 @@ nodeunitShim({ ], Type: 'APPMESH', }, - })); - test.done(); - }, + }); + + }); - 'throws when neither of IgnoredUID and IgnoredGID is set'(test: Test) { + test('throws when neither of IgnoredUID and IgnoredGID is set', () => { // GIVEN const stack = new cdk.Stack(); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2TaskDefinition(stack, 'Ec2TaskDef', { networkMode: ecs.NetworkMode.AWS_VPC, proxyConfiguration: ecs.ProxyConfigurations.appMeshProxyConfiguration({ @@ -200,8 +199,8 @@ nodeunitShim({ }, }), }); - }, /At least one of ignoredUID or ignoredGID should be specified./); + }).toThrow(/At least one of ignoredUID or ignoredGID should be specified./); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts index 8a802ac72014b..5e570a7d39eeb 100644 --- a/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/aws-log-driver.test.ts @@ -1,22 +1,21 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as logs from '@aws-cdk/aws-logs'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('aws log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.FargateTaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create an aws log driver'(test: Test) { + }); + + test('create an aws log driver', () => { // WHEN td.addContainer('Container', { image, @@ -30,11 +29,11 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', { + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.ONE_MONTH, - })); + }); - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -50,12 +49,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create an aws log driver using awsLogs'(test: Test) { + }); + + test('create an aws log driver using awsLogs', () => { // WHEN td.addContainer('Container', { image, @@ -68,11 +67,11 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', { + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.ONE_MONTH, - })); + }); - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -87,12 +86,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'with a defined log group'(test: Test) { + }); + + test('with a defined log group', () => { // GIVEN const logGroup = new logs.LogGroup(stack, 'LogGroup'); @@ -106,11 +105,11 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', { + expect(stack).toHaveResource('AWS::Logs::LogGroup', { RetentionInDays: logs.RetentionDays.TWO_YEARS, - })); + }); - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -123,12 +122,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'without a defined log group: creates one anyway'(test: Test) { + }); + + test('without a defined log group: creates one anyway', () => { // GIVEN td.addContainer('Container', { image, @@ -138,22 +137,22 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Logs::LogGroup', {})); + expect(stack).toHaveResource('AWS::Logs::LogGroup', {}); + - test.done(); - }, + }); - 'throws when specifying log retention and log group'(test: Test) { + test('throws when specifying log retention and log group', () => { // GIVEN const logGroup = new logs.LogGroup(stack, 'LogGroup'); // THEN - test.throws(() => new ecs.AwsLogDriver({ + expect(() => new ecs.AwsLogDriver({ logGroup, logRetention: logs.RetentionDays.FIVE_DAYS, streamPrefix: 'hello', - }), /`logGroup`.*`logRetentionDays`/); + })).toThrow(/`logGroup`.*`logRetentionDays`/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts index 223f5dd3d27ae..e7649fbdae4d5 100644 --- a/packages/@aws-cdk/aws-ecs/test/cluster.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/cluster.test.ts @@ -1,11 +1,5 @@ -import { - countResources, - expect, - haveResource, - haveResourceLike, - ResourcePart, - ABSENT, -} from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT } from '@aws-cdk/assert-internal'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as kms from '@aws-cdk/aws-kms'; @@ -14,12 +8,11 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; -nodeunitShim({ - 'When creating an ECS Cluster': { - 'with no properties set, it correctly sets default properties'(test: Test) { +describe('cluster', () => { + describe('When creating an ECS Cluster', () => { + test('with no properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -28,9 +21,9 @@ nodeunitShim({ instanceType: new ec2.InstanceType('t2.micro'), }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -41,9 +34,9 @@ nodeunitShim({ Value: 'Default/EcsCluster/Vpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -74,9 +67,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -97,9 +90,9 @@ nodeunitShim({ Ref: 'EcsClusterVpcPrivateSubnet2SubnetC2B7B1BA', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -117,9 +110,9 @@ nodeunitShim({ VpcId: { Ref: 'EcsClusterVpc779914AB', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -132,9 +125,9 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -182,12 +175,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'with only vpc set, it correctly sets default properties'(test: Test) { + }); + + test('with only vpc set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -199,9 +192,9 @@ nodeunitShim({ instanceType: new ec2.InstanceType('t2.micro'), }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -212,9 +205,9 @@ nodeunitShim({ Value: 'Default/MyVpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -245,9 +238,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -268,9 +261,9 @@ nodeunitShim({ Ref: 'MyVpcPrivateSubnet2Subnet0040C983', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -288,9 +281,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -303,9 +296,9 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -353,12 +346,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'multiple clusters with default capacity'(test: Test) { + }); + + test('multiple clusters with default capacity', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -371,10 +364,10 @@ nodeunitShim({ }); } - test.done(); - }, - 'lifecycle hook is automatically added'(test: Test) { + }); + + test('lifecycle hook is automatically added', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -388,16 +381,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LifecycleHook', { + expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { AutoScalingGroupName: { Ref: 'EcsClusterDefaultAutoScalingGroupASGC1A785DB' }, LifecycleTransition: 'autoscaling:EC2_INSTANCE_TERMINATING', DefaultResult: 'CONTINUE', HeartbeatTimeout: 300, NotificationTargetARN: { Ref: 'EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookTopicACD2D4A4' }, RoleARN: { 'Fn::GetAtt': ['EcsClusterDefaultAutoScalingGroupLifecycleHookDrainHookRoleA38EC83B', 'Arn'] }, - })); + }); - expect(stack).to(haveResource('AWS::Lambda::Function', { + expect(stack).toHaveResource('AWS::Lambda::Function', { Timeout: 310, Environment: { Variables: { @@ -407,9 +400,9 @@ nodeunitShim({ }, }, Handler: 'index.lambda_handler', - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -508,12 +501,12 @@ nodeunitShim({ Ref: 'EcsClusterDefaultAutoScalingGroupDrainECSHookFunctionServiceRole94543EDA', }, ], - })); + }); + - test.done(); - }, + }); - 'lifecycle hook with encrypted SNS is added correctly'(test: Test) { + test('lifecycle hook with encrypted SNS is added correctly', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -529,19 +522,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::SNS::Topic', { + expect(stack).toHaveResourceLike('AWS::SNS::Topic', { KmsMasterKeyId: { 'Fn::GetAtt': [ 'Key961B73FD', 'Arn', ], }, - })); + }); - test.done(); - }, - 'with capacity and cloudmap namespace properties set'(test: Test) { + }); + + test('with capacity and cloudmap namespace properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -556,16 +549,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foo.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -576,9 +569,9 @@ nodeunitShim({ Value: 'Default/MyVpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -609,9 +602,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -632,9 +625,9 @@ nodeunitShim({ Ref: 'MyVpcPrivateSubnet2Subnet0040C983', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -652,9 +645,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Role', { + expect(stack).toHaveResource('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -667,9 +660,9 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -717,13 +710,13 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, - }, + }); + }); - 'allows specifying instance type'(test: Test) { + test('allows specifying instance type', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -734,14 +727,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm3.large', - })); + }); - test.done(); - }, - 'allows specifying cluster size'(test: Test) { + }); + + test('allows specifying cluster size', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -753,14 +746,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '3', - })); + }); - test.done(); - }, - 'configures userdata with powershell if windows machine image is specified'(test: Test) { + }); + + test('configures userdata with powershell if windows machine image is specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -774,7 +767,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -808,15 +801,15 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, + + }); /* * TODO:v2.0.0 BEGINNING OF OBSOLETE BLOCK */ - 'allows specifying special HW AMI Type'(test: Test) { + test('allows specifying special HW AMI Type', () => { // GIVEN const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); const stack = new cdk.Stack(app, 'test'); @@ -833,23 +826,23 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id', }, }); - test.done(); - }, - 'errors if amazon linux given with special HW type'(test: Test) { + }); + + test('errors if amazon linux given with special HW type', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -857,7 +850,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.throws(() => { + expect(() => { cluster.addCapacity('GpuAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), machineImage: new ecs.EcsOptimizedAmi({ @@ -865,12 +858,12 @@ nodeunitShim({ hardwareType: ecs.AmiHardwareType.GPU, }), }); - }, /Amazon Linux does not support special hardware type/); + }).toThrow(/Amazon Linux does not support special hardware type/); + - test.done(); - }, + }); - 'allows specifying windows image'(test: Test) { + test('allows specifying windows image', () => { // GIVEN const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); const stack = new cdk.Stack(app, 'test'); @@ -887,17 +880,17 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/windows_server/2019/english/full/recommended/image_id', }, }); - test.done(); - }, - 'errors if windows given with special HW type'(test: Test) { + }); + + test('errors if windows given with special HW type', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -905,7 +898,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.throws(() => { + expect(() => { cluster.addCapacity('WindowsGpuAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), machineImage: new ecs.EcsOptimizedAmi({ @@ -913,12 +906,12 @@ nodeunitShim({ hardwareType: ecs.AmiHardwareType.GPU, }), }); - }, /Windows Server does not support special hardware type/); + }).toThrow(/Windows Server does not support special hardware type/); + - test.done(); - }, + }); - 'errors if windowsVersion and linux generation are set'(test: Test) { + test('errors if windowsVersion and linux generation are set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -926,7 +919,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.throws(() => { + expect(() => { cluster.addCapacity('WindowsScalingGroup', { instanceType: new ec2.InstanceType('t2.micro'), machineImage: new ecs.EcsOptimizedAmi({ @@ -934,93 +927,93 @@ nodeunitShim({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX, }), }); - }, /"windowsVersion" and Linux image "generation" cannot be both set/); + }).toThrow(/"windowsVersion" and Linux image "generation" cannot be both set/); - test.done(); - }, - 'allows returning the correct image for windows for EcsOptimizedAmi'(test: Test) { + }); + + test('allows returning the correct image for windows for EcsOptimizedAmi', () => { // GIVEN const stack = new cdk.Stack(); const ami = new ecs.EcsOptimizedAmi({ windowsVersion: ecs.WindowsOptimizedVersion.SERVER_2019, }); - test.equal(ami.getImage(stack).osType, ec2.OperatingSystemType.WINDOWS); + expect(ami.getImage(stack).osType).toEqual(ec2.OperatingSystemType.WINDOWS); + - test.done(); - }, + }); - 'allows returning the correct image for linux for EcsOptimizedAmi'(test: Test) { + test('allows returning the correct image for linux for EcsOptimizedAmi', () => { // GIVEN const stack = new cdk.Stack(); const ami = new ecs.EcsOptimizedAmi({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX, }); - test.equal(ami.getImage(stack).osType, ec2.OperatingSystemType.LINUX); + expect(ami.getImage(stack).osType).toEqual(ec2.OperatingSystemType.LINUX); - test.done(); - }, - 'allows returning the correct image for linux 2 for EcsOptimizedAmi'(test: Test) { + }); + + test('allows returning the correct image for linux 2 for EcsOptimizedAmi', () => { // GIVEN const stack = new cdk.Stack(); const ami = new ecs.EcsOptimizedAmi({ generation: ec2.AmazonLinuxGeneration.AMAZON_LINUX_2, }); - test.equal(ami.getImage(stack).osType, ec2.OperatingSystemType.LINUX); + expect(ami.getImage(stack).osType).toEqual(ec2.OperatingSystemType.LINUX); + - test.done(); - }, + }); - 'allows returning the correct image for linux for EcsOptimizedImage'(test: Test) { + test('allows returning the correct image for linux for EcsOptimizedImage', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.amazonLinux().getImage(stack).osType, + expect(ecs.EcsOptimizedImage.amazonLinux().getImage(stack).osType).toEqual( ec2.OperatingSystemType.LINUX); - test.done(); - }, - 'allows returning the correct image for linux 2 for EcsOptimizedImage'(test: Test) { + }); + + test('allows returning the correct image for linux 2 for EcsOptimizedImage', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.amazonLinux2().getImage(stack).osType, + expect(ecs.EcsOptimizedImage.amazonLinux2().getImage(stack).osType).toEqual( ec2.OperatingSystemType.LINUX); - test.done(); - }, - 'allows returning the correct image for linux 2 for EcsOptimizedImage with ARM hardware'(test: Test) { + }); + + test('allows returning the correct image for linux 2 for EcsOptimizedImage with ARM hardware', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.ARM).getImage(stack).osType, + expect(ecs.EcsOptimizedImage.amazonLinux2(ecs.AmiHardwareType.ARM).getImage(stack).osType).toEqual( ec2.OperatingSystemType.LINUX); - test.done(); - }, + }); - 'allows returning the correct image for windows for EcsOptimizedImage'(test: Test) { + + test('allows returning the correct image for windows for EcsOptimizedImage', () => { // GIVEN const stack = new cdk.Stack(); - test.equal(ecs.EcsOptimizedImage.windows(ecs.WindowsOptimizedVersion.SERVER_2019).getImage(stack).osType, + expect(ecs.EcsOptimizedImage.windows(ecs.WindowsOptimizedVersion.SERVER_2019).getImage(stack).osType).toEqual( ec2.OperatingSystemType.WINDOWS); - test.done(); - }, + + }); /* * TODO:v2.0.0 END OF OBSOLETE BLOCK */ - 'allows specifying special HW AMI Type v2'(test: Test) { + test('allows specifying special HW AMI Type v2', () => { // GIVEN const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); const stack = new cdk.Stack(app, 'test'); @@ -1035,23 +1028,23 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiamazonlinux2gpurecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/amazon-linux-2/gpu/recommended/image_id', }, }); - test.done(); - }, - 'allows specifying Amazon Linux v1 AMI'(test: Test) { + }); + + test('allows specifying Amazon Linux v1 AMI', () => { // GIVEN const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); const stack = new cdk.Stack(app, 'test'); @@ -1066,23 +1059,23 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiamazonlinuxrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/amazon-linux/recommended/image_id', }, }); - test.done(); - }, - 'allows specifying windows image v2'(test: Test) { + }); + + test('allows specifying windows image v2', () => { // GIVEN const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); const stack = new cdk.Stack(app, 'test'); @@ -1097,17 +1090,17 @@ nodeunitShim({ // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsserviceecsoptimizedamiwindowsserver2019englishfullrecommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/ecs/optimized-ami/windows_server/2019/english/full/recommended/image_id', }, }); - test.done(); - }, - 'allows specifying spot fleet'(test: Test) { + }); + + test('allows specifying spot fleet', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1119,14 +1112,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { SpotPrice: '0.31', - })); + }); - test.done(); - }, - 'allows specifying drain time'(test: Test) { + }); + + test('allows specifying drain time', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1138,14 +1131,14 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LifecycleHook', { + expect(stack).toHaveResource('AWS::AutoScaling::LifecycleHook', { HeartbeatTimeout: 60, - })); + }); + - test.done(); - }, + }); - 'allows specifying automated spot draining'(test: Test) { + test('allows specifying automated spot draining', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1158,7 +1151,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1173,12 +1166,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'allows containers access to instance metadata service'(test: Test) { + test('allows containers access to instance metadata service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1190,7 +1183,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1205,12 +1198,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'allows adding default service discovery namespace'(test: Test) { + test('allows adding default service discovery namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1226,17 +1219,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'foo.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); + - test.done(); - }, + }); - 'allows adding public service discovery namespace'(test: Test) { + test('allows adding public service discovery namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1253,16 +1246,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::PublicDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PublicDnsNamespace', { Name: 'foo.com', - })); + }); + + expect(cluster.defaultCloudMapNamespace!.type).toEqual(cloudmap.NamespaceType.DNS_PUBLIC); - test.equal(cluster.defaultCloudMapNamespace!.type, cloudmap.NamespaceType.DNS_PUBLIC); - test.done(); - }, + }); - 'throws if default service discovery namespace added more than once'(test: Test) { + test('throws if default service discovery namespace added more than once', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1278,16 +1271,16 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { cluster.addDefaultCloudMapNamespace({ name: 'foo.com', }); - }, /Can only add default namespace once./); + }).toThrow(/Can only add default namespace once./); - test.done(); - }, - 'export/import of a cluster with a namespace'(test: Test) { + }); + + test('export/import of a cluster with a namespace', () => { // GIVEN const stack1 = new cdk.Stack(); const vpc1 = new ec2.Vpc(stack1, 'Vpc'); @@ -1311,16 +1304,16 @@ nodeunitShim({ }); // THEN - test.equal(cluster2.defaultCloudMapNamespace!.type, cloudmap.NamespaceType.DNS_PRIVATE); - test.deepEqual(stack2.resolve(cluster2.defaultCloudMapNamespace!.namespaceId), 'import-namespace-id'); + expect(cluster2.defaultCloudMapNamespace!.type).toEqual(cloudmap.NamespaceType.DNS_PRIVATE); + expect(stack2.resolve(cluster2.defaultCloudMapNamespace!.namespaceId)).toEqual('import-namespace-id'); // Can retrieve subnets from VPC - will throw 'There are no 'Private' subnets in this VPC. Use a different VPC subnet selection.' if broken. cluster2.vpc.selectSubnets(); - test.done(); - }, - 'imported cluster with imported security groups honors allowAllOutbound'(test: Test) { + }); + + test('imported cluster with imported security groups honors allowAllOutbound', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -1338,16 +1331,16 @@ nodeunitShim({ cluster.connections.allowToAnyIpv4(ec2.Port.tcp(443)); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { GroupId: 'sg-1', - })); + }); + + expect(stack).toCountResources('AWS::EC2::SecurityGroupEgress', 1); - expect(stack).to(countResources('AWS::EC2::SecurityGroupEgress', 1)); - test.done(); - }, + }); - 'Metric'(test: Test) { + test('Metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1355,7 +1348,7 @@ nodeunitShim({ const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); // THEN - test.deepEqual(stack.resolve(cluster.metricCpuReservation()), { + expect(stack.resolve(cluster.metricCpuReservation())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, }, @@ -1365,7 +1358,7 @@ nodeunitShim({ statistic: 'Average', }); - test.deepEqual(stack.resolve(cluster.metricMemoryReservation()), { + expect(stack.resolve(cluster.metricMemoryReservation())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, }, @@ -1375,7 +1368,7 @@ nodeunitShim({ statistic: 'Average', }); - test.deepEqual(stack.resolve(cluster.metric('myMetric')), { + expect(stack.resolve(cluster.metric('myMetric'))).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, }, @@ -1385,10 +1378,10 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'ASG with a public VPC without NAT Gateways'(test: Test) { + }); + + test('ASG with a public VPC without NAT Gateways', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyPublicVpc', { @@ -1410,9 +1403,9 @@ nodeunitShim({ }, }); - expect(stack).to(haveResource('AWS::ECS::Cluster')); + expect(stack).toHaveResource('AWS::ECS::Cluster'); - expect(stack).to(haveResource('AWS::EC2::VPC', { + expect(stack).toHaveResource('AWS::EC2::VPC', { CidrBlock: '10.0.0.0/16', EnableDnsHostnames: true, EnableDnsSupport: true, @@ -1423,9 +1416,9 @@ nodeunitShim({ Value: 'Default/MyPublicVpc', }, ], - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsserviceecsoptimizedamiamazonlinux2recommendedimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1457,9 +1450,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { MaxSize: '1', MinSize: '1', LaunchConfigurationName: { @@ -1480,9 +1473,9 @@ nodeunitShim({ Ref: 'MyPublicVpcingressSubnet2SubnetD2F2E034', }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/EcsCluster/DefaultAutoScalingGroup/InstanceSecurityGroup', SecurityGroupEgress: [ { @@ -1500,13 +1493,13 @@ nodeunitShim({ VpcId: { Ref: 'MyPublicVpcA2BF6CDA', }, - })); + }); // THEN - test.done(); - }, - 'enable container insights'(test: Test) { + }); + + test('enable container insights', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1514,19 +1507,19 @@ nodeunitShim({ new ecs.Cluster(stack, 'EcsCluster', { containerInsights: true }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { ClusterSettings: [ { Name: 'containerInsights', Value: 'enabled', }, ], - }, ResourcePart.Properties)); + }); + - test.done(); - }, + }); - 'disable container insights'(test: Test) { + test('disable container insights', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1534,19 +1527,19 @@ nodeunitShim({ new ecs.Cluster(stack, 'EcsCluster', { containerInsights: false }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { ClusterSettings: [ { Name: 'containerInsights', Value: 'disabled', }, ], - }, ResourcePart.Properties)); + }); + - test.done(); - }, + }); - 'default container insights is undefined'(test: Test) { + test('default container insights is undefined', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1558,17 +1551,15 @@ nodeunitShim({ const stackAssembly = assembly.getStackByName(stack.stackName); const template = stackAssembly.template; - test.equal( + expect( template.Resources.EcsCluster97242B84.Properties === undefined || template.Resources.EcsCluster97242B84.Properties.ClusterSettings === undefined, - true, - 'ClusterSettings should not be defined', - ); + ).toEqual(true); - test.done(); - }, - 'BottleRocketImage() returns correct AMI'(test: Test) { + }); + + test('BottleRocketImage() returns correct AMI', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1579,18 +1570,18 @@ nodeunitShim({ // THEN const assembly = app.synth(); const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( + expect(Object.entries(parameters).some( ([k, v]) => k.startsWith('SsmParameterValueawsservicebottlerocketawsecs') && (v as any).Default.includes('/bottlerocket/'), - ), 'Bottlerocket AMI should be in ssm parameters'); - test.ok(Object.entries(parameters).some( + )).toEqual(true); + expect(Object.entries(parameters).some( ([k, v]) => k.startsWith('SsmParameterValueawsservicebottlerocketawsecs') && (v as any).Default.includes('/aws-ecs-1/'), - ), 'ecs variant should be in ssm parameters'); - test.done(); - }, + )).toEqual(true); + + }); - 'cluster capacity with bottlerocket AMI, by setting machineImageType'(test: Test) { + test('cluster capacity with bottlerocket AMI, by setting machineImageType', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1602,9 +1593,9 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster')); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup')); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::ECS::Cluster'); + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup'); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1x8664latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, @@ -1622,8 +1613,8 @@ nodeunitShim({ ], }, }, - })); - expect(stack).to(haveResourceLike('AWS::IAM::Role', { + }); + expect(stack).toHaveResourceLike('AWS::IAM::Role', { AssumeRolePolicyDocument: { Statement: [ { @@ -1678,12 +1669,11 @@ nodeunitShim({ Value: 'test/EcsCluster/bottlerocket-asg', }, ], - }), - ); - test.done(); - }, + }); - 'correct bottlerocket AMI for ARM64 architecture'(test: Test) { + }); + + test('correct bottlerocket AMI for ARM64 architecture', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1695,24 +1685,24 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { ImageId: { Ref: 'SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter', }, - })); + }); const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Parameters, { + expect(template.Parameters).toEqual({ SsmParameterValueawsservicebottlerocketawsecs1arm64latestimageidC96584B6F00A464EAD1953AFF4B05118Parameter: { Type: 'AWS::SSM::Parameter::Value', Default: '/aws/service/bottlerocket/aws-ecs-1/arm64/latest/image_id', }, }); - test.done(); - }, - 'throws when machineImage and machineImageType both specified'(test: Test) { + }); + + test('throws when machineImage and machineImageType both specified', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1724,7 +1714,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::AutoScaling::LaunchConfiguration', { + expect(stack).toHaveResourceLike('AWS::AutoScaling::LaunchConfiguration', { UserData: { 'Fn::Base64': { 'Fn::Join': [ @@ -1739,11 +1729,11 @@ nodeunitShim({ ], }, }, - })); - test.done(); - }, + }); - 'allows specifying capacityProviders (deprecated)'(test: Test) { + }); + + test('allows specifying capacityProviders (deprecated)', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1752,18 +1742,18 @@ nodeunitShim({ new ecs.Cluster(stack, 'EcsCluster', { capacityProviders: ['FARGATE_SPOT'] }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE_SPOT'], - })); + }); + - test.done(); - }, + }); - 'allows specifying Fargate capacityProviders'(test: Test) { + test('allows specifying Fargate capacityProviders', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1774,18 +1764,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); - test.done(); - }, - 'allows specifying capacityProviders (alternate method)'(test: Test) { + }); + + test('allows specifying capacityProviders (alternate method)', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1795,18 +1785,18 @@ nodeunitShim({ cluster.enableFargateCapacityProviders(); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); + - test.done(); - }, + }); - 'allows adding capacityProviders post-construction (deprecated)'(test: Test) { + test('allows adding capacityProviders post-construction (deprecated)', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1817,18 +1807,18 @@ nodeunitShim({ cluster.addCapacityProvider('FARGATE'); // does not add twice // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], - })); + }); - test.done(); - }, - 'allows adding capacityProviders post-construction'(test: Test) { + }); + + test('allows adding capacityProviders post-construction', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1839,32 +1829,32 @@ nodeunitShim({ cluster.addCapacityProvider('FARGATE'); // does not add twice // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE'], - })); + }); + - test.done(); - }, + }); - 'throws for unsupported capacity providers'(test: Test) { + test('throws for unsupported capacity providers', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); const cluster = new ecs.Cluster(stack, 'EcsCluster'); // THEN - test.throws(() => { + expect(() => { cluster.addCapacityProvider('HONK'); - }, /CapacityProvider not supported/); + }).toThrow(/CapacityProvider not supported/); - test.done(); - }, - 'creates ASG capacity providers with expected defaults'(test: Test) { + }); + + test('creates ASG capacity providers with expected defaults', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1881,7 +1871,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::CapacityProvider', { + expect(stack).toHaveResource('AWS::ECS::CapacityProvider', { AutoScalingGroupProvider: { AutoScalingGroupArn: { Ref: 'asgASG4D014670', @@ -1892,11 +1882,11 @@ nodeunitShim({ }, ManagedTerminationProtection: 'ENABLED', }, - })); - test.done(); - }, + }); - 'can disable managed scaling for ASG capacity provider'(test: Test) { + }); + + test('can disable managed scaling for ASG capacity provider', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1914,7 +1904,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::CapacityProvider', { + expect(stack).toHaveResource('AWS::ECS::CapacityProvider', { AutoScalingGroupProvider: { AutoScalingGroupArn: { Ref: 'asgASG4D014670', @@ -1922,11 +1912,11 @@ nodeunitShim({ ManagedScaling: ABSENT, ManagedTerminationProtection: 'ENABLED', }, - })); - test.done(); - }, + }); - 'capacity provider enables ASG new instance scale-in protection by default'(test: Test) { + }); + + test('capacity provider enables ASG new instance scale-in protection by default', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1943,13 +1933,13 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); - test.done(); - }, + }); - 'capacity provider disables ASG new instance scale-in protection'(test: Test) { + }); + + test('capacity provider disables ASG new instance scale-in protection', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1967,13 +1957,13 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup', { NewInstancesProtectedFromScaleIn: true, - })); - test.done(); - }, + }); - 'can add ASG capacity via Capacity Provider'(test: Test) { + }); + + test('can add ASG capacity via Capacity Provider', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -1999,7 +1989,7 @@ nodeunitShim({ cluster.addAsgCapacityProvider(capacityProvider); // THEN - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -2011,11 +2001,11 @@ nodeunitShim({ }, ], DefaultCapacityProviderStrategy: [], - })); - test.done(); - }, + }); - 'correctly sets log configuration for execute command'(test: Test) { + }); + + test('correctly sets log configuration for execute command', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -2046,7 +2036,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { Configuration: { ExecuteCommandConfiguration: { KmsKeyId: { @@ -2069,29 +2059,29 @@ nodeunitShim({ Logging: 'OVERRIDE', }, }, - })); + }); - test.done(); - }, - 'throws when no log configuration is provided when logging is set to OVERRIDE'(test: Test) { + }); + + test('throws when no log configuration is provided when logging is set to OVERRIDE', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logging: ecs.ExecuteCommandLogging.OVERRIDE, }, }); - }, /Execute command log configuration must only be specified when logging is OVERRIDE./); + }).toThrow(/Execute command log configuration must only be specified when logging is OVERRIDE./); + - test.done(); - }, + }); - 'throws when log configuration provided but logging is set to DEFAULT'(test: Test) { + test('throws when log configuration provided but logging is set to DEFAULT', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); @@ -2099,7 +2089,7 @@ nodeunitShim({ const logGroup = new logs.LogGroup(stack, 'LogGroup'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logConfiguration: { @@ -2108,18 +2098,18 @@ nodeunitShim({ logging: ecs.ExecuteCommandLogging.DEFAULT, }, }); - }, /Execute command log configuration must only be specified when logging is OVERRIDE./); + }).toThrow(/Execute command log configuration must only be specified when logging is OVERRIDE./); - test.done(); - }, - 'throws when CloudWatchEncryptionEnabled without providing CloudWatch Logs log group name'(test: Test) { + }); + + test('throws when CloudWatchEncryptionEnabled without providing CloudWatch Logs log group name', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logConfiguration: { @@ -2128,18 +2118,18 @@ nodeunitShim({ logging: ecs.ExecuteCommandLogging.OVERRIDE, }, }); - }, /You must specify a CloudWatch log group in the execute command log configuration to enable CloudWatch encryption./); + }).toThrow(/You must specify a CloudWatch log group in the execute command log configuration to enable CloudWatch encryption./); + - test.done(); - }, + }); - 'throws when S3EncryptionEnabled without providing S3 Bucket name'(test: Test) { + test('throws when S3EncryptionEnabled without providing S3 Bucket name', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'test'); // THEN - test.throws(() => { + expect(() => { new ecs.Cluster(stack, 'EcsCluster', { executeCommandConfiguration: { logConfiguration: { @@ -2148,8 +2138,8 @@ nodeunitShim({ logging: ecs.ExecuteCommandLogging.OVERRIDE, }, }); - }, /You must specify an S3 bucket name in the execute command log configuration to enable S3 encryption./); + }).toThrow(/You must specify an S3 bucket name in the execute command log configuration to enable S3 encryption./); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts index 7e6fce7a4f301..7decd0047975e 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/cross-stack.test.ts @@ -1,8 +1,7 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import { App, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; // Test various cross-stack Cluster/Service/ALB scenario's @@ -13,8 +12,8 @@ let stack2: Stack; let cluster: ecs.Cluster; let service: ecs.Ec2Service; -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('cross stack', () => { + beforeEach(() => { app = new App(); stack1 = new Stack(app, 'Stack1'); @@ -37,10 +36,10 @@ nodeunitShim({ taskDefinition, }); - cb(); - }, - 'ALB next to Service'(test: Test) { + }); + + test('ALB next to Service', () => { // WHEN const lb = new elbv2.ApplicationLoadBalancer(stack2, 'ALB', { vpc: cluster.vpc }); const listener = lb.addListener('listener', { port: 80 }); @@ -50,14 +49,14 @@ nodeunitShim({ }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).to(haveResource('AWS::ECS::Service')); + expect(stack2).toHaveResource('AWS::ECS::Service'); expectIngress(stack2); - test.done(); - }, - 'ALB next to Cluster'(test: Test) { + }); + + test('ALB next to Cluster', () => { // WHEN const lb = new elbv2.ApplicationLoadBalancer(stack1, 'ALB', { vpc: cluster.vpc }); const listener = lb.addListener('listener', { port: 80 }); @@ -67,13 +66,13 @@ nodeunitShim({ }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).to(haveResource('AWS::ECS::Service')); + expect(stack2).toHaveResource('AWS::ECS::Service'); expectIngress(stack2); - test.done(); - }, - 'ALB in its own stack'(test: Test) { + }); + + test('ALB in its own stack', () => { // WHEN const stack3 = new Stack(app, 'Stack3'); const lb = new elbv2.ApplicationLoadBalancer(stack3, 'ALB', { vpc: cluster.vpc }); @@ -84,17 +83,17 @@ nodeunitShim({ }); // THEN: it shouldn't throw due to cyclic dependencies - expect(stack2).to(haveResource('AWS::ECS::Service')); + expect(stack2).toHaveResource('AWS::ECS::Service'); expectIngress(stack2); - test.done(); - }, + + }); }); function expectIngress(stack: Stack) { - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { FromPort: 32768, ToPort: 65535, GroupId: { 'Fn::ImportValue': 'Stack1:ExportsOutputFnGetAttClusterDefaultAutoScalingGroupInstanceSecurityGroup1D15236AGroupIdEAB9C5E1' }, - })); + }); } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts index 7f28047ccdb81..705223aeb2748 100644 --- a/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/ec2/ec2-service.test.ts @@ -1,4 +1,5 @@ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import { SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as elb from '@aws-cdk/aws-elasticloadbalancing'; @@ -8,14 +9,13 @@ import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource } from '../../lib/base/base-service'; import { PlacementConstraint, PlacementStrategy } from '../../lib/placement'; -nodeunitShim({ - 'When creating an EC2 Service': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('ec2 service', () => { + describe('When creating an EC2 Service', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -34,7 +34,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -48,14 +48,14 @@ nodeunitShim({ LaunchType: LaunchType.EC2, SchedulingStrategy: 'REPLICA', EnableECSManagedTags: false, - })); + }); - test.notEqual(service.node.defaultChild, undefined); + expect(service.node.defaultChild).toBeDefined(); - test.done(); - }, - 'allows setting enable execute command'(test: Test) { + }); + + test('allows setting enable execute command', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -75,7 +75,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -90,9 +90,9 @@ nodeunitShim({ SchedulingStrategy: 'REPLICA', EnableECSManagedTags: false, EnableExecuteCommand: true, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -128,12 +128,12 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - test.done(); - }, - 'no logging enabled when logging field is set to NONE'(test: Test) { + }); + + test('no logging enabled when logging field is set to NONE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -166,7 +166,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -188,12 +188,12 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); + - test.done(); - }, + }); - 'enables execute command logging when logging field is set to OVERRIDE'(test: Test) { + test('enables execute command logging when logging field is set to OVERRIDE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -229,7 +229,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -305,12 +305,12 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - test.done(); - }, - 'enables only execute command session encryption'(test: Test) { + }); + + test('enables only execute command session encryption', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -349,7 +349,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -438,9 +438,9 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -488,12 +488,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'enables encryption for execute command logging'(test: Test) { + test('enables encryption for execute command logging', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -539,7 +539,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -643,9 +643,9 @@ nodeunitShim({ Ref: 'Ec2TaskDefTaskRole400FA349', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -738,12 +738,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'with custom cloudmap namespace'(test: Test) { + test('with custom cloudmap namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -773,7 +773,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -799,19 +799,19 @@ nodeunitShim({ 'Id', ], }, - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'scorekeep.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); + - test.done(); - }, + }); - 'with all properties set'(test: Test) { + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -864,7 +864,7 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -925,12 +925,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'with autoscaling group capacity provider'(test: Test) { + }); + + test('with autoscaling group capacity provider', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'Vpc'); @@ -970,7 +970,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { CapacityProviderStrategy: [ { CapacityProvider: { @@ -978,11 +978,11 @@ nodeunitShim({ }, }, ], - })); - test.done(); - }, + }); - 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + }); + + test('with multiple security groups, it correctly updates the cfn template', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1021,7 +1021,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'Ec2TaskDef0226F28C', }, @@ -1059,9 +1059,9 @@ nodeunitShim({ }, SchedulingStrategy: 'REPLICA', ServiceName: 'bonjour', - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -1074,9 +1074,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -1091,12 +1091,12 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - test.done(); - }, - 'throws when both securityGroup and securityGroups are supplied'(test: Test) { + }); + + test('throws when both securityGroup and securityGroups are supplied', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1123,7 +1123,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1136,12 +1136,12 @@ nodeunitShim({ serviceName: 'bonjour', vpcSubnets: { subnetType: ec2.SubnetType.PUBLIC }, }); - }, /Only one of SecurityGroup or SecurityGroups can be populated./); + }).toThrow(/Only one of SecurityGroup or SecurityGroups can be populated./); - test.done(); - }, - 'throws when task definition is not EC2 compatible'(test: Test) { + }); + + test('throws when task definition is not EC2 compatible', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -1156,17 +1156,17 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, }); - }, /Supplied TaskDefinition is not configured for compatibility with EC2/); + }).toThrow(/Supplied TaskDefinition is not configured for compatibility with EC2/); - test.done(); - }, - 'ignore task definition and launch type if deployment controller is set to be EXTERNAL'(test: Test) { + }); + + test('ignore task definition and launch type if deployment controller is set to be EXTERNAL', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1188,8 +1188,8 @@ nodeunitShim({ }); // THEN - test.deepEqual(service.node.metadata[0].data, 'taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); + expect(stack).toHaveResource('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -1199,12 +1199,12 @@ nodeunitShim({ }, SchedulingStrategy: 'REPLICA', EnableECSManagedTags: false, - })); + }); - test.done(); - }, - 'errors if daemon and desiredCount both specified'(test: Test) { + }); + + test('errors if daemon and desiredCount both specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1217,19 +1217,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, daemon: true, desiredCount: 2, }); - }, /Don't supply desiredCount/); + }).toThrow(/Don't supply desiredCount/); - test.done(); - }, - 'errors if daemon and maximumPercent not 100'(test: Test) { + }); + + test('errors if daemon and maximumPercent not 100', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1242,19 +1242,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, daemon: true, maxHealthyPercent: 300, }); - }, /Maximum percent must be 100 for daemon mode./); + }).toThrow(/Maximum percent must be 100 for daemon mode./); - test.done(); - }, - 'errors if minimum not less than maximum'(test: Test) { + }); + + test('errors if minimum not less than maximum', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1267,7 +1267,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1275,12 +1275,12 @@ nodeunitShim({ minHealthyPercent: 100, maxHealthyPercent: 100, }); - }, /Minimum healthy percent must be less than maximum healthy percent./); + }).toThrow(/Minimum healthy percent must be less than maximum healthy percent./); - test.done(); - }, - 'errors if no container definitions'(test: Test) { + }); + + test('errors if no container definitions', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1294,14 +1294,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { - expect(stack); - }, /one essential container/); + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/one essential container/); - test.done(); - }, - 'allows adding the default container after creating the service'(test: Test) { + }); + + test('allows adding the default container after creating the service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1321,18 +1321,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Name: 'main', }, ], - })); + }); - test.done(); - }, - 'sets daemon scheduling strategy'(test: Test) { + }); + + test('sets daemon scheduling strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1352,19 +1352,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { SchedulingStrategy: 'DAEMON', DeploymentConfiguration: { MaximumPercent: 100, MinimumHealthyPercent: 0, }, - })); + }); - test.done(); - }, - 'with a TaskDefinition with Bridge network mode': { - 'it errors if vpcSubnets is specified'(test: Test) { + }); + + describe('with a TaskDefinition with Bridge network mode', () => { + test('it errors if vpcSubnets is specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1380,7 +1380,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1391,10 +1391,10 @@ nodeunitShim({ }); // THEN - test.done(); - }, - 'it errors if assignPublicIp is true'(test: Test) { + }); + + test('it errors if assignPublicIp is true', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1410,19 +1410,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, assignPublicIp: true, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - 'it errors if vpc subnets is provided'(test: Test) { + }); + + test('it errors if vpc subnets is provided', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1442,7 +1442,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, @@ -1450,13 +1450,13 @@ nodeunitShim({ subnets: [subnet], }, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - 'it errors if security group is provided'(test: Test) { + }); + + test('it errors if security group is provided', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1472,19 +1472,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, securityGroup, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - 'it errors if multiple security groups is provided'(test: Test) { + }); + + test('it errors if multiple security groups is provided', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1503,21 +1503,21 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, securityGroups, }); - }, /vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); + }).toThrow(/vpcSubnets, securityGroup\(s\) and assignPublicIp can only be used in AwsVpc networking mode/); // THEN - test.done(); - }, - }, - 'with a TaskDefinition with AwsVpc network mode': { - 'it creates a security group for the service'(test: Test) { + }); + }); + + describe('with a TaskDefinition with AwsVpc network mode', () => { + test('it creates a security group for the service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1538,7 +1538,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'DISABLED', @@ -1560,12 +1560,12 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, - 'it allows vpcSubnets'(test: Test) { + }); + + test('it allows vpcSubnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1589,11 +1589,11 @@ nodeunitShim({ }); // THEN - test.done(); - }, - }, - 'with distinctInstance placement constraint'(test: Test) { + }); + }); + + test('with distinctInstance placement constraint', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1613,16 +1613,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementConstraints: [{ Type: 'distinctInstance', }], - })); + }); + - test.done(); - }, + }); - 'with memberOf placement constraints'(test: Test) { + test('with memberOf placement constraints', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1643,17 +1643,17 @@ nodeunitShim({ service.addPlacementConstraints(PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementConstraints: [{ Expression: 'attribute:ecs.instance-type =~ t2.*', Type: 'memberOf', }], - })); + }); + - test.done(); - }, + }); - 'with spreadAcross container instances strategy'(test: Test) { + test('with spreadAcross container instances strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1675,17 +1675,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.spreadAcrossInstances()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'instanceId', Type: 'spread', }], - })); + }); + - test.done(); - }, + }); - 'with spreadAcross placement strategy'(test: Test) { + test('with spreadAcross placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1706,36 +1706,36 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'attribute:ecs.availability-zone', Type: 'spread', }], - })); + }); + - test.done(); - }, + }); - 'can turn PlacementStrategy into json format'(test: Test) { + test('can turn PlacementStrategy into json format', () => { // THEN - test.deepEqual(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE).toJson(), [{ + expect(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE).toJson()).toEqual([{ type: 'spread', field: 'attribute:ecs.availability-zone', }]); - test.done(); - }, - 'can turn PlacementConstraints into json format'(test: Test) { + }); + + test('can turn PlacementConstraints into json format', () => { // THEN - test.deepEqual(PlacementConstraint.distinctInstances().toJson(), [{ + expect(PlacementConstraint.distinctInstances().toJson()).toEqual([{ type: 'distinctInstance', }]); - test.done(); - }, - 'errors when spreadAcross with no input'(test: Test) { + }); + + test('errors when spreadAcross with no input', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1754,14 +1754,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.spreadAcross()); - }, 'spreadAcross: give at least one field to spread by'); + }).toThrow('spreadAcross: give at least one field to spread by'); + - test.done(); - }, + }); - 'errors with spreadAcross placement strategy if daemon specified'(test: Test) { + test('errors with spreadAcross placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1781,14 +1781,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.spreadAcross(ecs.BuiltInAttributes.AVAILABILITY_ZONE)); }); - test.done(); - }, - 'with no placement constraints'(test: Test) { + }); + + test('with no placement constraints', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1807,14 +1807,14 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::ECS::Service', { + expect(stack).not.toHaveResource('AWS::ECS::Service', { PlacementConstraints: undefined, - })); + }); + - test.done(); - }, + }); - 'with both propagateTags and propagateTaskTagsFrom defined'(test: Test) { + test('with both propagateTags and propagateTaskTagsFrom defined', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1827,18 +1827,18 @@ nodeunitShim({ memoryLimitMiB: 512, }); - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Ec2Service', { cluster, taskDefinition, propagateTags: PropagatedTagSource.SERVICE, propagateTaskTagsFrom: PropagatedTagSource.SERVICE, }); - }, /You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); - test.done(); - }, + }).toThrow(/You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); + + }); - 'with no placement strategy if daemon specified'(test: Test) { + test('with no placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1858,14 +1858,14 @@ nodeunitShim({ }); // THEN - expect(stack).notTo(haveResource('AWS::ECS::Service', { + expect(stack).not.toHaveResource('AWS::ECS::Service', { PlacementStrategies: undefined, - })); + }); + - test.done(); - }, + }); - 'with random placement strategy'(test: Test) { + test('with random placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc'); @@ -1886,16 +1886,16 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.randomly()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Type: 'random', }], - })); + }); - test.done(); - }, - 'errors with random placement strategy if daemon specified'(test: Test) { + }); + + test('errors with random placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc'); @@ -1915,14 +1915,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.randomly()); - }); + }).toThrow(); - test.done(); - }, - 'with packedbyCpu placement strategy'(test: Test) { + }); + + test('with packedbyCpu placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1943,17 +1943,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.packedByCpu()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'cpu', Type: 'binpack', }], - })); + }); + - test.done(); - }, + }); - 'with packedbyMemory placement strategy'(test: Test) { + test('with packedbyMemory placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1974,17 +1974,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.packedByMemory()); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'memory', Type: 'binpack', }], - })); + }); - test.done(); - }, - 'with packedBy placement strategy'(test: Test) { + }); + + test('with packedBy placement strategy', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2005,17 +2005,17 @@ nodeunitShim({ service.addPlacementStrategies(PlacementStrategy.packedBy(ecs.BinPackResource.MEMORY)); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { PlacementStrategies: [{ Field: 'memory', Type: 'binpack', }], - })); + }); + - test.done(); - }, + }); - 'errors with packedBy placement strategy if daemon specified'(test: Test) { + test('errors with packedBy placement strategy if daemon specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2035,16 +2035,16 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.addPlacementStrategies(PlacementStrategy.packedBy(ecs.BinPackResource.MEMORY)); - }); + }).toThrow(); - test.done(); - }, - }, - 'attachToClassicLB': { - 'allows network mode of task definition to be host'(test: Test) { + }); + }); + + describe('attachToClassicLB', () => { + test('allows network mode of task definition to be host', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2065,10 +2065,10 @@ nodeunitShim({ const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); service.attachToClassicLB(lb); - test.done(); - }, - 'allows network mode of task definition to be bridge'(test: Test) { + }); + + test('allows network mode of task definition to be bridge', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2089,10 +2089,10 @@ nodeunitShim({ const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); service.attachToClassicLB(lb); - test.done(); - }, - 'throws when network mode of task definition is AwsVpc'(test: Test) { + }); + + test('throws when network mode of task definition is AwsVpc', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2111,14 +2111,14 @@ nodeunitShim({ // THEN const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); - test.throws(() => { + expect(() => { service.attachToClassicLB(lb); - }, /Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead./); + }).toThrow(/Cannot use a Classic Load Balancer if NetworkMode is AwsVpc. Use Host or Bridge instead./); - test.done(); - }, - 'throws when network mode of task definition is none'(test: Test) { + }); + + test('throws when network mode of task definition is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2137,16 +2137,16 @@ nodeunitShim({ // THEN const lb = new elb.LoadBalancer(stack, 'LB', { vpc }); - test.throws(() => { + expect(() => { service.attachToClassicLB(lb); - }, /Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead./); + }).toThrow(/Cannot use a Classic Load Balancer if NetworkMode is None. Use Host or Bridge instead./); - test.done(); - }, - }, - 'attachToApplicationTargetGroup': { - 'allows network mode of task definition to be other than none'(test: Test) { + }); + }); + + describe('attachToApplicationTargetGroup', () => { + test('allows network mode of task definition to be other than none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2171,10 +2171,10 @@ nodeunitShim({ // THEN service.attachToApplicationTargetGroup(targetGroup); - test.done(); - }, - 'throws when network mode of task definition is none'(test: Test) { + }); + + test('throws when network mode of task definition is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2197,15 +2197,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.attachToApplicationTargetGroup(targetGroup); - }, /Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + }).toThrow(/Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); - test.done(); - }, - 'correctly setting ingress and egress port': { - 'with bridge/NAT network mode and 0 host port'(test: Test) { + }); + + describe('correctly setting ingress and egress port', () => { + test('with bridge/NAT network mode and 0 host port', () => { [ecs.NetworkMode.BRIDGE, ecs.NetworkMode.NAT].forEach((networkMode: ecs.NetworkMode) => { // GIVEN const stack = new cdk.Stack(); @@ -2237,23 +2237,23 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 32768, ToPort: 65535, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 32768, ToPort: 65535, - })); + }); }); - test.done(); - }, - 'with bridge/NAT network mode and host port other than 0'(test: Test) { + }); + + test('with bridge/NAT network mode and host port other than 0', () => { [ecs.NetworkMode.BRIDGE, ecs.NetworkMode.NAT].forEach((networkMode: ecs.NetworkMode) => { // GIVEN const stack = new cdk.Stack(); @@ -2285,23 +2285,23 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 80, ToPort: 80, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 80, ToPort: 80, - })); + }); }); - test.done(); - }, - 'with host network mode'(test: Test) { + }); + + test('with host network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2332,22 +2332,22 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - test.done(); - }, - 'with aws_vpc network mode'(test: Test) { + }); + + test('with aws_vpc network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2378,25 +2378,25 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8001, ToPort: 8001, - })); + }); - test.done(); - }, - }, - }, - 'attachToNetworkTargetGroup': { - 'allows network mode of task definition to be other than none'(test: Test) { + }); + }); + }); + + describe('attachToNetworkTargetGroup', () => { + test('allows network mode of task definition to be other than none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2421,10 +2421,10 @@ nodeunitShim({ // THEN service.attachToNetworkTargetGroup(targetGroup); - test.done(); - }, - 'throws when network mode of task definition is none'(test: Test) { + }); + + test('throws when network mode of task definition is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2447,16 +2447,16 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { service.attachToNetworkTargetGroup(targetGroup); - }, /Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + }).toThrow(/Cannot use a load balancer if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); - test.done(); - }, - }, - 'classic ELB': { - 'can attach to classic ELB'(test: Test) { + }); + }); + + describe('classic ELB', () => { + test('can attach to classic ELB', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2478,7 +2478,7 @@ nodeunitShim({ lb.addTarget(service); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'web', @@ -2486,18 +2486,18 @@ nodeunitShim({ LoadBalancerName: { Ref: 'LB8A12904C' }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { // if any load balancer is configured and healthCheckGracePeriodSeconds is not // set, then it should default to 60 seconds. HealthCheckGracePeriodSeconds: 60, - })); + }); - test.done(); - }, - 'can attach any container and port as a target'(test: Test) { + }); + + test('can attach any container and port as a target', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'VPC'); @@ -2523,7 +2523,7 @@ nodeunitShim({ })); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'web', @@ -2531,14 +2531,14 @@ nodeunitShim({ LoadBalancerName: { Ref: 'LB8A12904C' }, }, ], - })); + }); - test.done(); - }, - }, - 'When enabling service discovery': { - 'throws if namespace has not been added to cluster'(test: Test) { + }); + }); + + describe('When enabling service discovery', () => { + test('throws if namespace has not been added to cluster', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2554,7 +2554,7 @@ nodeunitShim({ container.addPortMappings({ containerPort: 8000 }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -2562,12 +2562,12 @@ nodeunitShim({ name: 'myApp', }, }); - }, /Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + }).toThrow(/Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + - test.done(); - }, + }); - 'throws if network mode is none'(test: Test) { + test('throws if network mode is none', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2585,7 +2585,7 @@ nodeunitShim({ cluster.addDefaultCloudMapNamespace({ name: 'foo.com' }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -2593,12 +2593,12 @@ nodeunitShim({ name: 'myApp', }, }); - }, /Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); + }).toThrow(/Cannot use a service discovery if NetworkMode is None. Use Bridge, Host or AwsVpc instead./); - test.done(); - }, - 'creates AWS Cloud Map service for Private DNS namespace with bridge network mode'(test: Test) { + }); + + test('creates AWS Cloud Map service for Private DNS namespace with bridge network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2628,7 +2628,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2641,9 +2641,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2669,12 +2669,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with host network mode'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with host network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2705,7 +2705,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2718,9 +2718,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2746,12 +2746,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'throws if wrong DNS record type specified with bridge network mode'(test: Test) { + test('throws if wrong DNS record type specified with bridge network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2771,7 +2771,7 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -2780,12 +2780,12 @@ nodeunitShim({ dnsRecordType: cloudmap.DnsRecordType.A, }, }); - }, /SRV records must be used when network mode is Bridge or Host./); + }).toThrow(/SRV records must be used when network mode is Bridge or Host./); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2816,7 +2816,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { @@ -2827,9 +2827,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2855,12 +2855,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode with SRV records'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with AwsVpc network mode with SRV records', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2892,7 +2892,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'MainContainer', @@ -2905,9 +2905,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2933,12 +2933,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'user can select any container and port'(test: Test) { + test('user can select any container and port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2976,7 +2976,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, @@ -2984,12 +2984,12 @@ nodeunitShim({ ContainerPort: 8001, }, ], - })); + }); + - test.done(); - }, + }); - 'By default, the container name is the default'(test: Test) { + test('By default, the container name is the default', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3021,17 +3021,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [{ ContainerName: 'main', ContainerPort: undefined, }], - })); + }); + - test.done(); - }, + }); - 'For SRV, by default, container name is default container and port is the default container port'(test: Test) { + test('For SRV, by default, container name is default container and port is the default container port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3065,17 +3065,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [{ ContainerName: 'main', ContainerPort: 1234, }], - })); + }); + - test.done(); - }, + }); - 'allows SRV service discovery to select the container and port'(test: Test) { + test('allows SRV service discovery to select the container and port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3112,17 +3112,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [{ ContainerName: 'second', ContainerPort: 4321, }], - })); + }); + - test.done(); - }, + }); - 'throws if SRV and container is not part of task definition'(test: Test) { + test('throws if SRV and container is not part of task definition', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3150,7 +3150,7 @@ nodeunitShim({ }); // WHEN - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -3160,12 +3160,12 @@ nodeunitShim({ containerPort: 4321, }, }); - }, /another task definition/i); + }).toThrow(/another task definition/i); + - test.done(); - }, + }); - 'throws if SRV and the container port is not mapped'(test: Test) { + test('throws if SRV and the container port is not mapped', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -3185,7 +3185,7 @@ nodeunitShim({ container.addPortMappings({ containerPort: 8000 }); - test.throws(() => { + expect(() => { new ecs.Ec2Service(stack, 'Service', { cluster, taskDefinition, @@ -3195,13 +3195,13 @@ nodeunitShim({ containerPort: 4321, }, }); - }, /container port.*not.*mapped/i); + }).toThrow(/container port.*not.*mapped/i); + - test.done(); - }, - }, + }); + }); - 'Metric'(test: Test) { + test('Metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -3219,7 +3219,7 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.metricMemoryUtilization()), { + expect(stack.resolve(service.metricMemoryUtilization())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, ServiceName: { 'Fn::GetAtt': ['ServiceD69D759B', 'Name'] }, @@ -3230,7 +3230,7 @@ nodeunitShim({ statistic: 'Average', }); - test.deepEqual(stack.resolve(service.metricCpuUtilization()), { + expect(stack.resolve(service.metricCpuUtilization())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, ServiceName: { 'Fn::GetAtt': ['ServiceD69D759B', 'Name'] }, @@ -3241,11 +3241,11 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'When import an EC2 Service': { - 'with serviceArn'(test: Test) { + }); + + describe('When import an EC2 Service', () => { + test('with serviceArn', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -3257,13 +3257,13 @@ nodeunitShim({ }); // THEN - test.equal(service.serviceArn, 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); - test.equal(service.serviceName, 'my-http-service'); + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); - test.done(); - }, - 'with serviceName'(test: Test) { + }); + + test('with serviceName', () => { // GIVEN const stack = new cdk.Stack(); const pseudo = new cdk.ScopedAws(stack); @@ -3276,40 +3276,40 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.serviceArn), stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); - test.equal(service.serviceName, 'my-http-service'); + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); - test.done(); - }, - 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + }); + + test('throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', serviceName: 'my-http-service', cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - 'throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + }); + + test('throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.Ec2Service.fromEc2ServiceAttributes(stack, 'EcsService', { cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts b/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts index d4a182b12d7f9..f791369f0d60e 100644 --- a/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/environment-file.test.ts @@ -1,24 +1,24 @@ +import '@aws-cdk/assert-internal/jest'; import * as path from 'path'; import * as cdk from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; /* eslint-disable dot-notation */ -nodeunitShim({ - 'ecs.EnvironmentFile.fromAsset': { - 'fails if asset is not a single file'(test: Test) { +describe('environment file', () => { + describe('ecs.EnvironmentFile.fromAsset', () => { + test('fails if asset is not a single file', () => { // GIVEN const stack = new cdk.Stack(); const fileAsset = ecs.EnvironmentFile.fromAsset(path.join(__dirname, 'demo-envfiles')); // THEN - test.throws(() => defineContainerDefinition(stack, fileAsset), /Asset must be a single file/); - test.done(); - }, + expect(() => defineContainerDefinition(stack, fileAsset)).toThrow(/Asset must be a single file/); - 'only one environment file asset object is created even if multiple container definitions use the same file'(test: Test) { + }); + + test('only one environment file asset object is created even if multiple container definitions use the same file', () => { // GIVEN const app = new cdk.App({ context: { [cxapi.NEW_STYLE_STACK_SYNTHESIS_CONTEXT]: false } }); const stack = new cdk.Stack(app); @@ -42,10 +42,10 @@ nodeunitShim({ const synthesized = assembly.stacks[0]; // container one has an asset, container two does not - test.deepEqual(synthesized.assets.length, 1); - test.done(); - }, - }, + expect(synthesized.assets.length).toEqual(1); + + }); + }); }); function defineContainerDefinition(stack: cdk.Stack, environmentFile: ecs.EnvironmentFile) { diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts index d01eaa14f11b9..94384a464bb47 100644 --- a/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/external/external-service.test.ts @@ -4,13 +4,12 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; import { LaunchType } from '../../lib/base/base-service'; -nodeunitShim({ - 'When creating an External Service': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('external service', () => { + describe('When creating an External Service', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -44,13 +43,13 @@ nodeunitShim({ LaunchType: LaunchType.EXTERNAL, }); - test.notEqual(service.node.defaultChild, undefined); + expect(service.node.defaultChild).toBeDefined(); - test.done(); - }, - }, - 'with all properties set'(test: Test) { + }); + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -97,10 +96,10 @@ nodeunitShim({ ServiceName: 'bonjour', }); - test.done(); - }, - 'with cloudmap set on cluster, throw error'(test: Test) { + }); + + test('with cloudmap set on cluster, throw error', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -130,10 +129,10 @@ nodeunitShim({ serviceName: 'bonjour', })).toThrow('Cloud map integration is not supported for External service' ); - test.done(); - }, - 'with multiple security groups, it correctly updates the cfn template'(test: Test) { + }); + + test('with multiple security groups, it correctly updates the cfn template', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -205,10 +204,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'throws when task definition is not External compatible'(test: Test) { + }); + + test('throws when task definition is not External compatible', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -227,10 +226,10 @@ nodeunitShim({ taskDefinition, })).toThrow('Supplied TaskDefinition is not configured for compatibility with ECS Anywhere cluster'); - test.done(); - }, - 'errors if minimum not less than maximum'(test: Test) { + }); + + test('errors if minimum not less than maximum', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -250,10 +249,10 @@ nodeunitShim({ maxHealthyPercent: 100, })).toThrow('Minimum healthy percent must be less than maximum healthy percent.'); - test.done(); - }, - 'error if cloudmap options provided with external service'(test: Test) { + }); + + test('error if cloudmap options provided with external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -276,10 +275,10 @@ nodeunitShim({ })).toThrow('Cloud map options are not supported for External service'); // THEN - test.done(); - }, - 'error if enableExecuteCommand options provided with external service'(test: Test) { + }); + + test('error if enableExecuteCommand options provided with external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -300,10 +299,10 @@ nodeunitShim({ })).toThrow('Enable Execute Command options are not supported for External service'); // THEN - test.done(); - }, - 'error if capacityProviderStrategies options provided with external service'(test: Test) { + }); + + test('error if capacityProviderStrategies options provided with external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -338,10 +337,10 @@ nodeunitShim({ })).toThrow('Capacity Providers are not supported for External service'); // THEN - test.done(); - }, - 'error when performing attachToApplicationTargetGroup to an external service'(test: Test) { + }); + + test('error when performing attachToApplicationTargetGroup to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -369,10 +368,10 @@ nodeunitShim({ expect(() => service.attachToApplicationTargetGroup(targetGroup)).toThrow('Application load balancer cannot be attached to an external service'); // THEN - test.done(); - }, - 'error when performing loadBalancerTarget to an external service'(test: Test) { + }); + + test('error when performing loadBalancerTarget to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -396,10 +395,10 @@ nodeunitShim({ })).toThrow('External service cannot be attached as load balancer targets'); // THEN - test.done(); - }, - 'error when performing registerLoadBalancerTargets to an external service'(test: Test) { + }); + + test('error when performing registerLoadBalancerTargets to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -430,10 +429,10 @@ nodeunitShim({ )).toThrow('External service cannot be registered as load balancer targets'); // THEN - test.done(); - }, - 'error when performing autoScaleTaskCount to an external service'(test: Test) { + }); + + test('error when performing autoScaleTaskCount to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -458,10 +457,10 @@ nodeunitShim({ })).toThrow('Autoscaling not supported for external service'); // THEN - test.done(); - }, - 'error when performing enableCloudMap to an external service'(test: Test) { + }); + + test('error when performing enableCloudMap to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -483,10 +482,10 @@ nodeunitShim({ expect(() => service.enableCloudMap({})).toThrow('Cloud map integration not supported for an external service'); // THEN - test.done(); - }, - 'error when performing associateCloudMapService to an external service'(test: Test) { + }); + + test('error when performing associateCloudMapService to an external service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -523,6 +522,6 @@ nodeunitShim({ })).toThrow('Cloud map service association is not supported for an external service'); // THEN - test.done(); - }, + + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts index 6cd9032fb29b4..1fb353a19f1d2 100644 --- a/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/external/external-task-definition.test.ts @@ -6,12 +6,11 @@ import * as iam from '@aws-cdk/aws-iam'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; -nodeunitShim({ - 'When creating an External TaskDefinition': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('external task definition', () => { + describe('When creating an External TaskDefinition', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -23,10 +22,10 @@ nodeunitShim({ RequiresCompatibilities: ['EXTERNAL'], }); - test.done(); - }, - 'with all properties set'(test: Test) { + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', { @@ -64,10 +63,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'correctly sets containers'(test: Test) { + }); + + test('correctly sets containers', () => { // GIVEN const stack = new cdk.Stack(); @@ -131,10 +130,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'all container definition options defined'(test: Test) { + }); + + test('all container definition options defined', () => { const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -304,10 +303,10 @@ nodeunitShim({ ], }); - test.done(); - }, - 'correctly sets containers from ECR repository using all props'(test: Test) { + }); + + test('correctly sets containers from ECR repository using all props', () => { // GIVEN const stack = new cdk.Stack(); @@ -397,11 +396,11 @@ nodeunitShim({ }], }); - test.done(); - }, - }, - 'correctly sets containers from ECR repository using an image tag'(test: Test) { + }); + }); + + test('correctly sets containers from ECR repository using an image tag', () => { // GIVEN const stack = new cdk.Stack(); @@ -472,10 +471,10 @@ nodeunitShim({ }], }); - test.done(); - }, - 'correctly sets containers from ECR repository using an image digest'(test: Test) { + }); + + test('correctly sets containers from ECR repository using an image digest', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -545,10 +544,10 @@ nodeunitShim({ }], }); - test.done(); - }, - 'correctly sets containers from ECR repository using default props'(test: Test) { + }); + + test('correctly sets containers from ECR repository using default props', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef'); @@ -562,10 +561,10 @@ nodeunitShim({ // THEN expect(stack).toHaveResource('AWS::ECR::Repository', {}); - test.done(); - }, - 'warns when setting containers from ECR repository using fromRegistry method'(test: Test) { + }); + + test('warns when setting containers from ECR repository using fromRegistry method', () => { // GIVEN const stack = new cdk.Stack(); @@ -579,10 +578,10 @@ nodeunitShim({ // THEN expect(container.node.metadata[0].data).toEqual("Proper policies need to be attached before pulling from ECR repository, or use 'fromEcrRepository'."); - test.done(); - }, - 'correctly sets volumes from'(test: Test) { + }); + + test('correctly sets volumes from', () => { const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); @@ -594,10 +593,10 @@ nodeunitShim({ name: 'scratch', })).toThrow('External task definitions doesnt support volumes' ); - test.done(); - }, - 'error when interferenceAccelerators set'(test: Test) { + }); + + test('error when interferenceAccelerators set', () => { const stack = new cdk.Stack(); const taskDefinition = new ecs.ExternalTaskDefinition(stack, 'ExternalTaskDef', {}); @@ -607,6 +606,6 @@ nodeunitShim({ deviceType: 'eia2.medium', })).toThrow('Cannot use inference accelerators on tasks that run on External service'); - test.done(); - }, + + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts index dcb57e3c5c188..94a790b757c21 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-service.test.ts @@ -1,4 +1,5 @@ -import { expect, haveResource, haveResourceLike, ABSENT } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ABSENT, SynthUtils } from '@aws-cdk/assert-internal'; import * as appscaling from '@aws-cdk/aws-applicationautoscaling'; import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; @@ -9,13 +10,12 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; import { DeploymentControllerType, LaunchType, PropagatedTagSource } from '../../lib/base/base-service'; -nodeunitShim({ - 'When creating a Fargate Service': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('fargate service', () => { + describe('When creating a Fargate Service', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -32,7 +32,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -66,9 +66,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Default/FargateService/SecurityGroup', SecurityGroupEgress: [ { @@ -80,14 +80,14 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); + + expect(service.node.defaultChild).toBeDefined(); - test.notEqual(service.node.defaultChild, undefined); - test.done(); - }, + }); - 'can create service with default settings if VPC only has public subnets'(test: Test) { + test('can create service with default settings if VPC only has public subnets', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', { @@ -112,10 +112,10 @@ nodeunitShim({ }); // THEN -- did not throw - test.done(); - }, - 'does not set launchType when capacity provider strategies specified (deprecated)'(test: Test) { + }); + + test('does not set launchType when capacity provider strategies specified (deprecated)', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -147,15 +147,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -199,12 +199,12 @@ nodeunitShim({ ], }, }, - })); + }); - test.done(); - }, - 'does not set launchType when capacity provider strategies specified'(test: Test) { + }); + + test('does not set launchType when capacity provider strategies specified', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -237,15 +237,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Cluster', { + expect(stack).toHaveResource('AWS::ECS::Cluster', { CapacityProviders: ABSENT, - })); + }); - expect(stack).to(haveResource('AWS::ECS::ClusterCapacityProviderAssociations', { + expect(stack).toHaveResource('AWS::ECS::ClusterCapacityProviderAssociations', { CapacityProviders: ['FARGATE', 'FARGATE_SPOT'], - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -290,12 +290,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'with custom cloudmap namespace'(test: Test) { + test('with custom cloudmap namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -324,7 +324,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -350,19 +350,19 @@ nodeunitShim({ 'Id', ], }, - })); + }); - expect(stack).to(haveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::PrivateDnsNamespace', { Name: 'scorekeep.com', Vpc: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - test.done(); - }, - 'with user-provided cloudmap service'(test: Test) { + }); + + test('with user-provided cloudmap service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -399,7 +399,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { ServiceRegistries: [ { ContainerName: 'web', @@ -407,12 +407,12 @@ nodeunitShim({ RegistryArn: { 'Fn::GetAtt': ['ServiceDBC79909', 'Arn'] }, }, ], - })); + }); - test.done(); - }, - 'errors when more than one service registry used'(test: Test) { + }); + + test('errors when more than one service registry used', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -446,18 +446,18 @@ nodeunitShim({ }); // WHEN / THEN - test.throws(() => { + expect(() => { ecsService.associateCloudMapService({ service: cloudMapService, container: container, containerPort: 8000, }); - }, /at most one service registry/i); + }).toThrow(/at most one service registry/i); - test.done(); - }, - 'with all properties set'(test: Test) { + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -503,9 +503,9 @@ nodeunitShim({ }); // THEN - test.ok(svc.cloudMapService !== undefined); + expect(svc.cloudMapService).toBeDefined(); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -558,12 +558,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'throws when task definition is not Fargate compatible'(test: Test) { + }); + + test('throws when task definition is not Fargate compatible', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -576,17 +576,17 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, }); - }, /Supplied TaskDefinition is not configured for compatibility with Fargate/); + }).toThrow(/Supplied TaskDefinition is not configured for compatibility with Fargate/); - test.done(); - }, - 'throws whith secret json field on unsupported platform version'(test: Test) { + }); + + test('throws whith secret json field on unsupported platform version', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -600,18 +600,18 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, platformVersion: ecs.FargatePlatformVersion.VERSION1_3, }); - }, new RegExp(`uses at least one container that references a secret JSON field.+platform version ${ecs.FargatePlatformVersion.VERSION1_4} or later`)); + }).toThrow(new RegExp(`uses at least one container that references a secret JSON field.+platform version ${ecs.FargatePlatformVersion.VERSION1_4} or later`)); + - test.done(); - }, + }); - 'ignore task definition and launch type if deployment controller is set to be EXTERNAL'(test: Test) { + test('ignore task definition and launch type if deployment controller is set to be EXTERNAL', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -631,8 +631,8 @@ nodeunitShim({ }); // THEN - test.deepEqual(service.node.metadata[0].data, 'taskDefinition and launchType are blanked out when using external deployment controller.'); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(service.node.metadata[0].data).toEqual('taskDefinition and launchType are blanked out when using external deployment controller.'); + expect(stack).toHaveResource('AWS::ECS::Service', { Cluster: { Ref: 'EcsCluster97242B84', }, @@ -665,12 +665,12 @@ nodeunitShim({ ], }, }, - })); + }); + - test.done(); - }, + }); - 'errors when no container specified on task definition'(test: Test) { + test('errors when no container specified on task definition', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -684,14 +684,14 @@ nodeunitShim({ }); // THEN - test.throws(() => { - expect(stack); - }, /one essential container/); + expect(() => { + SynthUtils.synthesize(stack); + }).toThrow(/one essential container/); + - test.done(); - }, + }); - 'allows adding the default container after creating the service'(test: Test) { + test('allows adding the default container after creating the service', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -709,18 +709,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Name: 'main', }, ], - })); + }); + - test.done(); - }, + }); - 'allows specifying assignPublicIP as enabled'(test: Test) { + test('allows specifying assignPublicIP as enabled', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -738,18 +738,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { NetworkConfiguration: { AwsvpcConfiguration: { AssignPublicIp: 'ENABLED', }, }, - })); + }); + - test.done(); - }, + }); - 'allows specifying 0 for minimumHealthyPercent'(test: Test) { + test('allows specifying 0 for minimumHealthyPercent', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -767,16 +767,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { DeploymentConfiguration: { MinimumHealthyPercent: 0, }, - })); + }); - test.done(); - }, - 'throws when securityGroup and securityGroups are supplied'(test: Test) { + }); + + test('throws when securityGroup and securityGroups are supplied', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -800,19 +800,19 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, securityGroup: securityGroup1, securityGroups: [securityGroup2], }); - }, /Only one of SecurityGroup or SecurityGroups can be populated./); + }).toThrow(/Only one of SecurityGroup or SecurityGroups can be populated./); + - test.done(); - }, + }); - 'with multiple securty groups, it correctly updates cloudformation template'(test: Test) { + test('with multiple securty groups, it correctly updates cloudformation template', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -842,7 +842,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -882,9 +882,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Bingo', SecurityGroupEgress: [ @@ -897,9 +897,9 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroup', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroup', { GroupDescription: 'Example', GroupName: 'Rolly', SecurityGroupEgress: [ @@ -914,15 +914,15 @@ nodeunitShim({ VpcId: { Ref: 'MyVpcF9F0CA6F', }, - })); + }); - test.done(); - }, - }, + }); + + }); - 'When setting up a health check': { - 'grace period is respected'(test: Test) { + describe('When setting up a health check', () => { + test('grace period is respected', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -940,16 +940,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { HealthCheckGracePeriodSeconds: 10, - })); + }); - test.done(); - }, - }, - 'When adding an app load balancer': { - 'allows auto scaling by ALB request per target'(test: Test) { + }); + }); + + describe('When adding an app load balancer', () => { + test('allows auto scaling by ALB request per target', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -976,7 +976,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, ResourceId: { @@ -997,9 +997,9 @@ nodeunitShim({ ], ], }, - })); + }); - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ALBRequestCountPerTarget', @@ -1014,18 +1014,18 @@ nodeunitShim({ }, TargetValue: 1000, }, - })); + }); - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { // if any load balancer is configured and healthCheckGracePeriodSeconds is not // set, then it should default to 60 seconds. HealthCheckGracePeriodSeconds: 60, - })); + }); - test.done(); - }, - 'allows auto scaling by ALB with new service arn format'(test: Test) { + }); + + test('allows auto scaling by ALB with new service arn format', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1056,7 +1056,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { MaxCapacity: 10, MinCapacity: 1, ResourceId: { @@ -1077,13 +1077,13 @@ nodeunitShim({ ], ], }, - })); + }); - test.done(); - }, - 'allows specify any existing container name and port in a service': { - 'with default setting'(test: Test) { + }); + + describe('allows specify any existing container name and port in a service', () => { + test('with default setting', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1111,7 +1111,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1121,24 +1121,24 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupIngress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupIngress', { Description: 'Load balancer to target', FromPort: 8000, ToPort: 8000, - })); + }); - expect(stack).to(haveResource('AWS::EC2::SecurityGroupEgress', { + expect(stack).toHaveResource('AWS::EC2::SecurityGroupEgress', { Description: 'Load balancer to target', FromPort: 8000, ToPort: 8000, - })); + }); - test.done(); - }, - 'with TCP protocol'(test: Test) { + }); + + test('with TCP protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1169,10 +1169,10 @@ nodeunitShim({ })], }); - test.done(); - }, - 'with UDP protocol'(test: Test) { + }); + + test('with UDP protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1203,10 +1203,10 @@ nodeunitShim({ })], }); - test.done(); - }, - 'throws when protocol does not match'(test: Test) { + }); + + test('throws when protocol does not match', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1228,7 +1228,7 @@ nodeunitShim({ const listener = lb.addListener('listener', { port: 80 }); // THEN - test.throws(() => { + expect(() => { listener.addTargets('target', { port: 80, targets: [service.loadBalancerTarget({ @@ -1237,12 +1237,12 @@ nodeunitShim({ protocol: ecs.Protocol.TCP, })], }); - }, /Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8001 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); + }).toThrow(/Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8001 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); - test.done(); - }, - 'throws when port does not match'(test: Test) { + }); + + test('throws when port does not match', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1264,7 +1264,7 @@ nodeunitShim({ const listener = lb.addListener('listener', { port: 80 }); // THEN - test.throws(() => { + expect(() => { listener.addTargets('target', { port: 80, targets: [service.loadBalancerTarget({ @@ -1272,12 +1272,12 @@ nodeunitShim({ containerPort: 8002, })], }); - }, /Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8002 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); + }).toThrow(/Container 'Default\/FargateTaskDef\/MainContainer' has no mapping for port 8002 and protocol tcp. Did you call "container.addPortMappings\(\)"\?/); - test.done(); - }, - 'throws when container does not exist'(test: Test) { + }); + + test('throws when container does not exist', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1299,7 +1299,7 @@ nodeunitShim({ const listener = lb.addListener('listener', { port: 80 }); // THEN - test.throws(() => { + expect(() => { listener.addTargets('target', { port: 80, targets: [service.loadBalancerTarget({ @@ -1307,15 +1307,15 @@ nodeunitShim({ containerPort: 8001, })], }); - }, /No container named 'SideContainer'. Did you call "addContainer()"?/); + }).toThrow(/No container named 'SideContainer'. Did you call "addContainer()"?/); - test.done(); - }, - }, - 'allows load balancing to any container and port of service': { - 'with application load balancers': { - 'with default target group port and protocol'(test: Test) { + }); + }); + + describe('allows load balancing to any container and port of service', () => { + describe('with application load balancers', () => { + test('with default target group port and protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1345,7 +1345,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1355,17 +1355,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', - })); + }); - test.done(); - }, - 'with default target group port and HTTP protocol'(test: Test) { + }); + + test('with default target group port and HTTP protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1397,7 +1397,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1407,17 +1407,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'HTTP', - })); + }); - test.done(); - }, - 'with default target group port and HTTPS protocol'(test: Test) { + }); + + test('with default target group port and HTTPS protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1449,7 +1449,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1459,17 +1459,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 443, Protocol: 'HTTPS', - })); + }); - test.done(); - }, - 'with any target group port and protocol'(test: Test) { + }); + + test('with any target group port and protocol', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1502,7 +1502,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1512,19 +1512,19 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 83, Protocol: 'HTTP', - })); + }); - test.done(); - }, - }, - 'with network load balancers': { - 'with default target group port'(test: Test) { + }); + }); + + describe('with network load balancers', () => { + test('with default target group port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1554,7 +1554,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1564,17 +1564,17 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 80, Protocol: 'TCP', - })); + }); - test.done(); - }, - 'with any target group port'(test: Test) { + }); + + test('with any target group port', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1606,7 +1606,7 @@ nodeunitShim({ ); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { LoadBalancers: [ { ContainerName: 'MainContainer', @@ -1616,20 +1616,20 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { Port: 81, Protocol: 'TCP', - })); + }); - test.done(); - }, - }, - }, - }, - 'allows scaling on a specified scheduled time'(test: Test) { + }); + }); + }); + }); + + test('allows scaling on a specified scheduled time', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1653,7 +1653,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalableTarget', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalableTarget', { ScheduledActions: [ { ScalableTargetAction: { @@ -1663,12 +1663,12 @@ nodeunitShim({ ScheduledActionName: 'ScaleOnSchedule', }, ], - })); + }); - test.done(); - }, - 'allows scaling on a specified metric value'(test: Test) { + }); + + test('allows scaling on a specified metric value', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1696,7 +1696,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'StepScaling', ScalingTargetId: { Ref: 'ServiceTaskCountTarget23E25614', @@ -1711,12 +1711,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'allows scaling on a target CPU utilization'(test: Test) { + test('allows scaling on a target CPU utilization', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1739,18 +1739,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageCPUUtilization' }, TargetValue: 30, }, - })); + }); + - test.done(); - }, + }); - 'allows scaling on memory utilization'(test: Test) { + test('allows scaling on memory utilization', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1773,18 +1773,18 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { PredefinedMetricSpecification: { PredefinedMetricType: 'ECSServiceAverageMemoryUtilization' }, TargetValue: 30, }, - })); + }); - test.done(); - }, - 'allows scaling on custom CloudWatch metric'(test: Test) { + }); + + test('allows scaling on custom CloudWatch metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1808,7 +1808,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { + expect(stack).toHaveResource('AWS::ApplicationAutoScaling::ScalingPolicy', { PolicyType: 'TargetTrackingScaling', TargetTrackingScalingPolicyConfiguration: { CustomizedMetricSpecification: { @@ -1818,13 +1818,13 @@ nodeunitShim({ }, TargetValue: 5, }, - })); + }); + - test.done(); - }, + }); - 'When enabling service discovery': { - 'throws if namespace has not been added to cluster'(test: Test) { + describe('When enabling service discovery', () => { + test('throws if namespace has not been added to cluster', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1837,7 +1837,7 @@ nodeunitShim({ container.addPortMappings({ containerPort: 8000 }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'Service', { cluster, taskDefinition, @@ -1845,12 +1845,12 @@ nodeunitShim({ name: 'myApp', }, }); - }, /Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); + }).toThrow(/Cannot enable service discovery if a Cloudmap Namespace has not been created in the cluster./); - test.done(); - }, - 'creates cloud map service for Private DNS namespace'(test: Test) { + }); + + test('creates cloud map service for Private DNS namespace', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1876,7 +1876,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -1902,12 +1902,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with SRV records with proper defaults'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with SRV records with proper defaults', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1937,7 +1937,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -1963,12 +1963,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'creates AWS Cloud Map service for Private DNS namespace with SRV records with overriden defaults'(test: Test) { + test('creates AWS Cloud Map service for Private DNS namespace with SRV records with overriden defaults', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -1999,7 +1999,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ServiceDiscovery::Service', { + expect(stack).toHaveResource('AWS::ServiceDiscovery::Service', { DnsConfig: { DnsRecords: [ { @@ -2025,12 +2025,12 @@ nodeunitShim({ 'Id', ], }, - })); + }); + - test.done(); - }, + }); - 'user can select any container and port'(test: Test) { + test('user can select any container and port', () => { const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); const cluster = new ecs.Cluster(stack, 'EcsCluster', { vpc }); @@ -2063,7 +2063,7 @@ nodeunitShim({ }, }); - expect(stack).to(haveResourceLike('AWS::ECS::Service', { + expect(stack).toHaveResourceLike('AWS::ECS::Service', { ServiceRegistries: [ { RegistryArn: { 'Fn::GetAtt': ['ServiceCloudmapService046058A4', 'Arn'] }, @@ -2071,13 +2071,13 @@ nodeunitShim({ ContainerPort: 8001, }, ], - })); + }); + - test.done(); - }, - }, + }); + }); - 'Metric'(test: Test) { + test('Metric', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2094,7 +2094,7 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.metricCpuUtilization()), { + expect(stack.resolve(service.metricCpuUtilization())).toEqual({ dimensions: { ClusterName: { Ref: 'EcsCluster97242B84' }, ServiceName: { 'Fn::GetAtt': ['ServiceD69D759B', 'Name'] }, @@ -2105,11 +2105,11 @@ nodeunitShim({ statistic: 'Average', }); - test.done(); - }, - 'When import a Fargate Service': { - 'with serviceArn'(test: Test) { + }); + + describe('When import a Fargate Service', () => { + test('with serviceArn', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -2121,13 +2121,13 @@ nodeunitShim({ }); // THEN - test.equal(service.serviceArn, 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); - test.equal(service.serviceName, 'my-http-service'); + expect(service.serviceArn).toEqual('arn:aws:ecs:us-west-2:123456789012:service/my-http-service'); + expect(service.serviceName).toEqual('my-http-service'); + - test.done(); - }, + }); - 'with serviceName'(test: Test) { + test('with serviceName', () => { // GIVEN const stack = new cdk.Stack(); const pseudo = new cdk.ScopedAws(stack); @@ -2140,13 +2140,13 @@ nodeunitShim({ }); // THEN - test.deepEqual(stack.resolve(service.serviceArn), stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); - test.equal(service.serviceName, 'my-http-service'); + expect(stack.resolve(service.serviceArn)).toEqual(stack.resolve(`arn:${pseudo.partition}:ecs:${pseudo.region}:${pseudo.accountId}:service/my-http-service`)); + expect(service.serviceName).toEqual('my-http-service'); + - test.done(); - }, + }); - 'with circuit breaker'(test: Test) { + test('with circuit breaker', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); @@ -2164,7 +2164,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { DeploymentConfiguration: { MaximumPercent: 200, MinimumHealthyPercent: 50, @@ -2176,42 +2176,42 @@ nodeunitShim({ DeploymentController: { Type: ecs.DeploymentControllerType.ECS, }, - })); + }); + - test.done(); - }, + }); - 'throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + test('throws an exception if both serviceArn and serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { serviceArn: 'arn:aws:ecs:us-west-2:123456789012:service/my-http-service', serviceName: 'my-http-service', cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - 'throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes'(test: Test) { + }); + + test('throws an exception if neither serviceArn nor serviceName were provided for fromEc2ServiceAttributes', () => { // GIVEN const stack = new cdk.Stack(); const cluster = new ecs.Cluster(stack, 'EcsCluster'); - test.throws(() => { + expect(() => { ecs.FargateService.fromFargateServiceAttributes(stack, 'EcsService', { cluster, }); - }, /only specify either serviceArn or serviceName/); + }).toThrow(/only specify either serviceArn or serviceName/); - test.done(); - }, - 'allows setting enable execute command'(test: Test) { + }); + + test('allows setting enable execute command', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2229,7 +2229,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::Service', { + expect(stack).toHaveResource('AWS::ECS::Service', { TaskDefinition: { Ref: 'FargateTaskDefC6FB60B4', }, @@ -2264,9 +2264,9 @@ nodeunitShim({ ], }, }, - })); + }); - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2302,12 +2302,12 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); + - test.done(); - }, + }); - 'no logging enabled when logging field is set to NONE'(test: Test) { + test('no logging enabled when logging field is set to NONE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2340,7 +2340,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2362,12 +2362,12 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); + - test.done(); - }, + }); - 'enables execute command logging with logging field set to OVERRIDE'(test: Test) { + test('enables execute command logging with logging field set to OVERRIDE', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2403,7 +2403,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2479,12 +2479,12 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); + - test.done(); - }, + }); - 'enables only execute command session encryption'(test: Test) { + test('enables only execute command session encryption', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2523,7 +2523,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2612,9 +2612,9 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -2662,12 +2662,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'enables encryption for execute command logging'(test: Test) { + }); + + test('enables encryption for execute command logging', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2713,7 +2713,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -2817,9 +2817,9 @@ nodeunitShim({ Ref: 'FargateTaskDefTaskRole0B257552', }, ], - })); + }); - expect(stack).to(haveResource('AWS::KMS::Key', { + expect(stack).toHaveResource('AWS::KMS::Key', { KeyPolicy: { Statement: [ { @@ -2912,12 +2912,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); + - test.done(); - }, + }); - 'with both propagateTags and propagateTaskTagsFrom defined'(test: Test) { + test('with both propagateTags and propagateTaskTagsFrom defined', () => { // GIVEN const stack = new cdk.Stack(); const vpc = new ec2.Vpc(stack, 'MyVpc', {}); @@ -2930,15 +2930,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { new ecs.FargateService(stack, 'FargateService', { cluster, taskDefinition, propagateTags: PropagatedTagSource.SERVICE, propagateTaskTagsFrom: PropagatedTagSource.SERVICE, }); - }, /You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); - test.done(); - }, - }, + }).toThrow(/You can only specify either propagateTags or propagateTaskTagsFrom. Alternatively, you can leave both blank/); + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts index 9cd5d994c9555..3ff15ac22057e 100644 --- a/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fargate/fargate-task-definition.test.ts @@ -1,29 +1,28 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; -nodeunitShim({ - 'When creating a Fargate TaskDefinition': { - 'with only required properties set, it correctly sets default properties'(test: Test) { +describe('fargate task definition', () => { + describe('When creating a Fargate TaskDefinition', () => { + test('with only required properties set, it correctly sets default properties', () => { // GIVEN const stack = new cdk.Stack(); new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { Family: 'FargateTaskDef', NetworkMode: ecs.NetworkMode.AWS_VPC, RequiresCompatibilities: ['FARGATE'], Cpu: '256', Memory: '512', - })); + }); + - test.done(); - }, + }); - 'support lazy cpu and memory values'(test: Test) { + test('support lazy cpu and memory values', () => { // GIVEN const stack = new cdk.Stack(); @@ -33,15 +32,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { Cpu: '128', Memory: '1024', - })); + }); - test.done(); - }, - 'with all properties set'(test: Test) { + }); + + test('with all properties set', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { @@ -58,6 +57,7 @@ nodeunitShim({ taskRole: new iam.Role(stack, 'TaskRole', { assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com'), }), + ephemeralStorageGiB: 21, }); taskDefinition.addVolume({ @@ -68,7 +68,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { Cpu: '128', ExecutionRoleArn: { 'Fn::GetAtt': [ @@ -76,6 +76,9 @@ nodeunitShim({ 'Arn', ], }, + EphemeralStorage: { + SizeInGiB: 21, + }, Family: 'myApp', Memory: '1024', NetworkMode: 'awsvpc', @@ -96,25 +99,25 @@ nodeunitShim({ Name: 'scratch', }, ], - })); + }); - test.done(); - }, - 'throws when adding placement constraint'(test: Test) { + }); + + test('throws when adding placement constraint', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); // THEN - test.throws(() => { + expect(() => { taskDefinition.addPlacementConstraint(ecs.PlacementConstraint.memberOf('attribute:ecs.instance-type =~ t2.*')); - }, /Cannot set placement constraints on tasks that run on Fargate/); + }).toThrow(/Cannot set placement constraints on tasks that run on Fargate/); + - test.done(); - }, + }); - 'throws when adding inference accelerators'(test: Test) { + test('throws when adding inference accelerators', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinition = new ecs.FargateTaskDefinition(stack, 'FargateTaskDef'); @@ -125,16 +128,40 @@ nodeunitShim({ }; // THEN - test.throws(() => { + expect(() => { taskDefinition.addInferenceAccelerator(inferenceAccelerator); - }, /Cannot use inference accelerators on tasks that run on Fargate/); + }).toThrow(/Cannot use inference accelerators on tasks that run on Fargate/); + - test.done(); - }, - }, + }); - 'When importing from an existing Fargate TaskDefinition': { - 'can succeed using TaskDefinition Arn'(test: Test) { + test('throws when ephemeral storage request is too high', () => { + // GIVEN + const stack = new cdk.Stack(); + expect(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + ephemeralStorageGiB: 201, + }); + }).toThrow(/Ephemeral storage size must be between 21GiB and 200GiB/); + + // THEN + }); + + test('throws when ephemeral storage request is too low', () => { + // GIVEN + const stack = new cdk.Stack(); + expect(() => { + new ecs.FargateTaskDefinition(stack, 'FargateTaskDef', { + ephemeralStorageGiB: 20, + }); + }).toThrow(/Ephemeral storage size must be between 21GiB and 200GiB/); + + // THEN + }); + }); + + describe('When importing from an existing Fargate TaskDefinition', () => { + test('can succeed using TaskDefinition Arn', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -143,11 +170,11 @@ nodeunitShim({ const taskDefinition = ecs.FargateTaskDefinition.fromFargateTaskDefinitionArn(stack, 'FARGATE_TD_ID', expectTaskDefinitionArn); // THEN - test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); - test.done(); - }, + expect(taskDefinition.taskDefinitionArn).toEqual(expectTaskDefinitionArn); + + }); - 'can succeed using attributes'(test: Test) { + test('can succeed using attributes', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -164,17 +191,17 @@ nodeunitShim({ }); // THEN - test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); - test.equal(taskDefinition.compatibility, ecs.Compatibility.FARGATE); - test.ok(taskDefinition.isFargateCompatible); - test.equal(taskDefinition.isEc2Compatible, false); - test.equal(taskDefinition.networkMode, expectNetworkMode); - test.equal(taskDefinition.taskRole, expectTaskRole); + expect(taskDefinition.taskDefinitionArn).toEqual(expectTaskDefinitionArn); + expect(taskDefinition.compatibility).toEqual(ecs.Compatibility.FARGATE); + expect(taskDefinition.isFargateCompatible).toEqual(true); + expect(taskDefinition.isEc2Compatible).toEqual(false); + expect(taskDefinition.networkMode).toEqual(expectNetworkMode); + expect(taskDefinition.taskRole).toEqual(expectTaskRole); + - test.done(); - }, + }); - 'returns a Fargate TaskDefinition that will throw an error when trying to access its networkMode but its networkMode is undefined'(test: Test) { + test('returns a Fargate TaskDefinition that will throw an error when trying to access its networkMode but its networkMode is undefined', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -189,15 +216,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.networkMode; - }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - 'returns a Fargate TaskDefinition that will throw an error when trying to access its taskRole but its taskRole is undefined'(test: Test) { + }); + + test('returns a Fargate TaskDefinition that will throw an error when trying to access its taskRole but its taskRole is undefined', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -210,12 +237,12 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.taskRole; - }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts index aff395c8598d2..6ab96b05df364 100644 --- a/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/firelens-log-driver.test.ts @@ -1,22 +1,21 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('firelens log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a firelens log driver with default options'(test: Test) { + + }); + test('create a firelens log driver with default options', () => { // WHEN td.addContainer('Container', { image, @@ -25,7 +24,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -39,12 +38,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a firelens log driver with secret options'(test: Test) { + test('create a firelens log driver with secret options', () => { const secret = new secretsmanager.Secret(stack, 'Secret'); const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { parameterName: '/host', @@ -72,7 +71,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -125,9 +124,9 @@ nodeunitShim({ }, }, ], - })); + }); - expect(stack).to(haveResourceLike('AWS::IAM::Policy', { + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -172,12 +171,12 @@ nodeunitShim({ ], Version: '2012-10-17', }, - })); + }); - test.done(); - }, - 'create a firelens log driver to route logs to CloudWatch Logs with Fluent Bit'(test: Test) { + }); + + test('create a firelens log driver to route logs to CloudWatch Logs with Fluent Bit', () => { // WHEN td.addContainer('Container', { image, @@ -194,7 +193,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -215,12 +214,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a firelens log driver to route logs to kinesis firehose Logs with Fluent Bit'(test: Test) { + test('create a firelens log driver to route logs to kinesis firehose Logs with Fluent Bit', () => { // WHEN td.addContainer('Container', { image, @@ -235,7 +234,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -254,13 +253,13 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'Firelens Configuration': { - 'fluentd log router container'(test: Test) { + describe('Firelens Configuration', () => { + test('fluentd log router container', () => { // GIVEN td.addFirelensLogRouter('log_router', { image: ecs.ContainerImage.fromRegistry('fluent/fluentd'), @@ -271,7 +270,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -283,11 +282,11 @@ nodeunitShim({ }, }, ], - })); - test.done(); - }, + }); + + }); - 'fluent-bit log router container with options'(test: Test) { + test('fluent-bit log router container with options', () => { // GIVEN const stack2 = new cdk.Stack(undefined, 'Stack2', { env: { region: 'us-east-1' } }); const td2 = new ecs.Ec2TaskDefinition(stack2, 'TaskDefinition'); @@ -305,7 +304,7 @@ nodeunitShim({ }); // THEN - expect(stack2).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack2).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -321,12 +320,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'fluent-bit log router with file config type'(test: Test) { + }); + + test('fluent-bit log router with file config type', () => { // GIVEN td.addFirelensLogRouter('log_router', { image: ecs.obtainDefaultFluentBitECRImage(td, undefined, '2.1.0'), @@ -343,7 +342,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Essential: true, @@ -359,9 +358,9 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts index 54ac824fb6c47..81c17b8c0b76f 100644 --- a/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/fluentd-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('fluentd log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a fluentd log driver with options'(test: Test) { + }); + + test('create a fluentd log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a fluentd log driver without options'(test: Test) { + test('create a fluentd log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a fluentd log driver with all possible options'(test: Test) { + test('create a fluentd log driver with all possible options', () => { // WHEN td.addContainer('Container', { image, @@ -92,7 +91,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -112,12 +111,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a fluentd log driver using fluentd'(test: Test) { + test('create a fluentd log driver using fluentd', () => { // WHEN td.addContainer('Container', { image, @@ -126,7 +125,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -134,8 +133,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts index 9f6663304d353..e8cdce10736e1 100644 --- a/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/gelf-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('gelf log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a gelf log driver with minimum options'(test: Test) { + }); + + test('create a gelf log driver with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a gelf log driver using gelf with minimum options'(test: Test) { + test('create a gelf log driver using gelf with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -53,7 +52,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -64,8 +63,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts b/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts index d782d39d874cb..3f409739d76ed 100644 --- a/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/images/tag-parameter-container-image.test.ts @@ -1,12 +1,12 @@ -import { expect, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { SynthUtils } from '@aws-cdk/assert-internal'; import * as ecr from '@aws-cdk/aws-ecr'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../../lib'; -nodeunitShim({ - 'TagParameter container image': { - 'throws an error when tagParameterName() is used without binding the image'(test: Test) { +describe('tag parameter container image', () => { + describe('TagParameter container image', () => { + test('throws an error when tagParameterName() is used without binding the image', () => { // GIVEN const stack = new cdk.Stack(); const repository = new ecr.Repository(stack, 'Repository'); @@ -15,14 +15,14 @@ nodeunitShim({ value: tagParameterContainerImage.tagParameterName, }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /TagParameterContainerImage must be used in a container definition when using tagParameterName/); + }).toThrow(/TagParameterContainerImage must be used in a container definition when using tagParameterName/); - test.done(); - }, - 'throws an error when tagParameterValue() is used without binding the image'(test: Test) { + }); + + test('throws an error when tagParameterValue() is used without binding the image', () => { // GIVEN const stack = new cdk.Stack(); const repository = new ecr.Repository(stack, 'Repository'); @@ -31,14 +31,14 @@ nodeunitShim({ value: tagParameterContainerImage.tagParameterValue, }); - test.throws(() => { + expect(() => { SynthUtils.synthesize(stack); - }, /TagParameterContainerImage must be used in a container definition when using tagParameterValue/); + }).toThrow(/TagParameterContainerImage must be used in a container definition when using tagParameterValue/); + - test.done(); - }, + }); - 'can be used in a cross-account manner'(test: Test) { + test('can be used in a cross-account manner', () => { // GIVEN const app = new cdk.App(); const pipelineStack = new cdk.Stack(app, 'PipelineStack', { @@ -67,7 +67,7 @@ nodeunitShim({ }); // THEN - expect(pipelineStack).to(haveResourceLike('AWS::ECR::Repository', { + expect(pipelineStack).toHaveResourceLike('AWS::ECR::Repository', { RepositoryName: repositoryName, RepositoryPolicyText: { Statement: [{ @@ -88,11 +88,11 @@ nodeunitShim({ }, }], }, - })); - expect(serviceStack).to(haveResourceLike('AWS::IAM::Role', { + }); + expect(serviceStack).toHaveResourceLike('AWS::IAM::Role', { RoleName: 'servicestackionexecutionrolee7e2d9a783a54eb795f4', - })); - expect(serviceStack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + }); + expect(serviceStack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { Image: { @@ -128,9 +128,9 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, - }, + }); + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts index aeffbc2073629..fdf67efc3bb4f 100644 --- a/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/journald-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('journald log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a journald log driver with options'(test: Test) { + }); + + test('create a journald log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a journald log driver without options'(test: Test) { + }); + + test('create a journald log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a journald log driver using journald'(test: Test) { + }); + + test('create a journald log driver using journald', () => { // WHEN td.addContainer('Container', { image, @@ -73,7 +72,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -82,8 +81,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts index b282a61c29f88..0c21ff2d0b5f1 100644 --- a/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/json-file-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('json file log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a json-file log driver with options'(test: Test) { + }); + + test('create a json-file log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a json-file log driver without options'(test: Test) { + }); + + test('create a json-file log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a json-file log driver using json-file'(test: Test) { + }); + + test('create a json-file log driver using json-file', () => { // WHEN td.addContainer('Container', { image, @@ -73,7 +72,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -82,8 +81,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts index ee7d3f4b68ee8..d9902cb9728e3 100644 --- a/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/splunk-log-driver.test.ts @@ -1,23 +1,22 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; -import * as cdk from '@aws-cdk/core'; +import '@aws-cdk/assert-internal/jest'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as ssm from '@aws-cdk/aws-ssm'; -import { nodeunitShim, Test } from 'nodeunit-shim'; +import * as cdk from '@aws-cdk/core'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('splunk log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a splunk log driver with minimum options'(test: Test) { + }); + + test('create a splunk log driver with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -29,7 +28,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -41,12 +40,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a splunk log driver using splunk with minimum options'(test: Test) { + }); + + test('create a splunk log driver using splunk with minimum options', () => { // WHEN td.addContainer('Container', { image, @@ -58,7 +57,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -70,12 +69,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a splunk log driver using splunk with sourcetype defined'(test: Test) { + test('create a splunk log driver using splunk with sourcetype defined', () => { // WHEN td.addContainer('Container', { image, @@ -88,7 +87,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -101,12 +100,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a splunk log driver using secret splunk token from secrets manager'(test: Test) { + test('create a splunk log driver using secret splunk token from secrets manager', () => { const secret = new secretsmanager.Secret(stack, 'Secret'); // WHEN td.addContainer('Container', { @@ -119,7 +118,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -138,12 +137,12 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'create a splunk log driver using secret splunk token from systems manager parameter store'(test: Test) { + test('create a splunk log driver using secret splunk token from systems manager parameter store', () => { const parameter = ssm.StringParameter.fromSecureStringParameterAttributes(stack, 'Parameter', { parameterName: '/token', version: 1, @@ -159,7 +158,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -195,13 +194,13 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); - 'throws when neither token nor secret token are provided'(test: Test) { - test.throws(() => { + test('throws when neither token nor secret token are provided', () => { + expect(() => { td.addContainer('Container', { image, logging: ecs.LogDrivers.splunk({ @@ -209,8 +208,8 @@ nodeunitShim({ }), memoryLimitMiB: 128, }); - }, 'Please provide either token or secretToken.'); + }).toThrow('Please provide either token or secretToken.'); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts b/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts index db328772d7548..ae32f55ecd863 100644 --- a/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/syslog-log-driver.test.ts @@ -1,21 +1,20 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; let stack: cdk.Stack; let td: ecs.TaskDefinition; const image = ecs.ContainerImage.fromRegistry('test-image'); -nodeunitShim({ - 'setUp'(cb: () => void) { +describe('syslog log driver', () => { + beforeEach(() => { stack = new cdk.Stack(); td = new ecs.Ec2TaskDefinition(stack, 'TaskDefinition'); - cb(); - }, - 'create a syslog log driver with options'(test: Test) { + }); + + test('create a syslog log driver with options', () => { // WHEN td.addContainer('Container', { image, @@ -26,7 +25,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -37,12 +36,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a syslog log driver without options'(test: Test) { + }); + + test('create a syslog log driver without options', () => { // WHEN td.addContainer('Container', { image, @@ -51,7 +50,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -59,12 +58,12 @@ nodeunitShim({ }, }, ], - })); + }); - test.done(); - }, - 'create a syslog log driver using syslog'(test: Test) { + }); + + test('create a syslog log driver using syslog', () => { // WHEN td.addContainer('Container', { image, @@ -73,7 +72,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResourceLike('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResourceLike('AWS::ECS::TaskDefinition', { ContainerDefinitions: [ { LogConfiguration: { @@ -82,8 +81,8 @@ nodeunitShim({ }, }, ], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts index 6e578952a5765..07b3a8211da06 100644 --- a/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts +++ b/packages/@aws-cdk/aws-ecs/test/task-definition.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as ecs from '../lib'; -nodeunitShim({ - 'When creating a new TaskDefinition': { - 'A task definition with both compatibilities defaults to networkmode AwsVpc'(test: Test) { +describe('task definition', () => { + describe('When creating a new TaskDefinition', () => { + test('A task definition with both compatibilities defaults to networkmode AwsVpc', () => { // GIVEN const stack = new cdk.Stack(); @@ -18,16 +17,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::ECS::TaskDefinition', { + expect(stack).toHaveResource('AWS::ECS::TaskDefinition', { NetworkMode: 'awsvpc', - })); + }); + - test.done(); - }, - }, + }); + }); - 'When importing from an existing Task definition': { - 'can import using a task definition arn'(test: Test) { + describe('When importing from an existing Task definition', () => { + test('can import using a task definition arn', () => { // GIVEN const stack = new cdk.Stack(); const taskDefinitionArn = 'TDArn'; @@ -36,14 +35,14 @@ nodeunitShim({ const taskDefinition = ecs.TaskDefinition.fromTaskDefinitionArn(stack, 'TD_ID', taskDefinitionArn); // THEN - test.equal(taskDefinition.taskDefinitionArn, taskDefinitionArn); - test.equal(taskDefinition.compatibility, ecs.Compatibility.EC2_AND_FARGATE); - test.equal(taskDefinition.executionRole, undefined); + expect(taskDefinition.taskDefinitionArn).toEqual(taskDefinitionArn); + expect(taskDefinition.compatibility).toEqual(ecs.Compatibility.EC2_AND_FARGATE); + expect(taskDefinition.executionRole).toEqual(undefined); + - test.done(); - }, + }); - 'can import a Task Definition using attributes'(test: Test) { + test('can import a Task Definition using attributes', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -62,16 +61,16 @@ nodeunitShim({ }); // THEN - test.equal(taskDefinition.taskDefinitionArn, expectTaskDefinitionArn); - test.equal(taskDefinition.compatibility, expectCompatibility); - test.equal(taskDefinition.executionRole, undefined); - test.equal(taskDefinition.networkMode, expectNetworkMode); - test.equal(taskDefinition.taskRole, expectTaskRole); + expect(taskDefinition.taskDefinitionArn).toEqual(expectTaskDefinitionArn); + expect(taskDefinition.compatibility).toEqual(expectCompatibility); + expect(taskDefinition.executionRole).toEqual(undefined); + expect(taskDefinition.networkMode).toEqual(expectNetworkMode); + expect(taskDefinition.taskRole).toEqual(expectTaskRole); - test.done(); - }, - 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined networkMode'(test: Test) { + }); + + test('returns an imported TaskDefinition that will throw an error when trying to access its yet to defined networkMode', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -88,15 +87,15 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.networkMode; - }, 'This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the networkMode in ImportedTaskDefinition to be defined. ' + 'Add the \'networkMode\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - 'returns an imported TaskDefinition that will throw an error when trying to access its yet to defined taskRole'(test: Test) { + }); + + test('returns an imported TaskDefinition that will throw an error when trying to access its yet to defined taskRole', () => { // GIVEN const stack = new cdk.Stack(); const expectTaskDefinitionArn = 'TD_ARN'; @@ -111,12 +110,12 @@ nodeunitShim({ }); // THEN - test.throws(() => { + expect(() => { taskDefinition.taskRole; - }, 'This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + + }).toThrow('This operation requires the taskRole in ImportedTaskDefinition to be defined. ' + 'Add the \'taskRole\' in ImportedTaskDefinitionProps to instantiate ImportedTaskDefinition'); - test.done(); - }, - }, + + }); + }); }); diff --git a/packages/@aws-cdk/aws-eks-legacy/.gitignore b/packages/@aws-cdk/aws-eks-legacy/.gitignore index 752d096cc6b12..633c903ceefc7 100644 --- a/packages/@aws-cdk/aws-eks-legacy/.gitignore +++ b/packages/@aws-cdk/aws-eks-legacy/.gitignore @@ -16,4 +16,5 @@ coverage nyc.config.js !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/.npmignore b/packages/@aws-cdk/aws-eks-legacy/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-eks-legacy/.npmignore +++ b/packages/@aws-cdk/aws-eks-legacy/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/jest.config.js b/packages/@aws-cdk/aws-eks-legacy/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-eks-legacy/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-eks-legacy/package.json b/packages/@aws-cdk/aws-eks-legacy/package.json index 75440be746b2d..5a7b6542a8892 100644 --- a/packages/@aws-cdk/aws-eks-legacy/package.json +++ b/packages/@aws-cdk/aws-eks-legacy/package.json @@ -56,7 +56,8 @@ "build+test+extract": "yarn build+test && yarn rosetta:extract" }, "cdk-build": { - "cloudformation": "AWS::EKS" + "cloudformation": "AWS::EKS", + "jest": true }, "keywords": [ "aws", @@ -71,11 +72,11 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/nodeunit": "^0.0.32", + "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.awsauth.ts b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts similarity index 85% rename from packages/@aws-cdk/aws-eks-legacy/test/test.awsauth.ts rename to packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts index 2ebdcde32dbcf..d9eacb3f183d1 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.awsauth.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/awsauth.test.ts @@ -1,14 +1,13 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; -import { Test } from 'nodeunit'; import { Cluster, KubernetesResource } from '../lib'; import { AwsAuth } from '../lib/aws-auth'; import { testFixtureNoVpc } from './util'; /* eslint-disable max-len */ -export = { - 'empty aws-auth'(test: Test) { +describe('awsauth', () => { + test('empty aws-auth', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'cluster'); @@ -17,18 +16,18 @@ export = { new AwsAuth(stack, 'AwsAuth', { cluster }); // THEN - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify([{ apiVersion: 'v1', kind: 'ConfigMap', metadata: { name: 'aws-auth', namespace: 'kube-system' }, data: { mapRoles: '[]', mapUsers: '[]', mapAccounts: '[]' }, }]), - })); - test.done(); - }, + }); - 'addRoleMapping and addUserMapping can be used to define the aws-auth ConfigMap'(test: Test) { + }); + + test('addRoleMapping and addUserMapping can be used to define the aws-auth ConfigMap', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'Cluster'); @@ -44,8 +43,8 @@ export = { cluster.awsAuth.addAccount('5566776655'); // THEN - expect(stack).to(countResources(KubernetesResource.RESOURCE_TYPE, 1)); - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toCountResources(KubernetesResource.RESOURCE_TYPE, 1); + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -89,12 +88,12 @@ export = { ], ], }, - })); + }); + - test.done(); - }, + }); - 'imported users and roles can be also be used'(test: Test) { + test('imported users and roles can be also be used', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'Cluster'); @@ -106,7 +105,7 @@ export = { cluster.awsAuth.addUserMapping(user, { groups: ['group2'] }); // THEN - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -130,8 +129,8 @@ export = { ], ], }, - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts similarity index 70% rename from packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts rename to packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts index cfcc21a8ce50e..f395348cc6f1e 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/cluster.test.ts @@ -1,16 +1,15 @@ -import { expect, haveResource, haveResourceLike, not } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import * as eks from '../lib'; import { spotInterruptHandler } from '../lib/spot-interrupt-handler'; import { testFixture, testFixtureNoVpc } from './util'; /* eslint-disable max-len */ -export = { - 'a default cluster spans all subnets'(test: Test) { +describe('cluster', () => { + test('a default cluster spans all subnets', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -18,7 +17,7 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResourceLike('AWS::EKS::Cluster', { + expect(stack).toHaveResourceLike('AWS::EKS::Cluster', { ResourcesVpcConfig: { SubnetIds: [ { Ref: 'VPCPublicSubnet1SubnetB4246D30' }, @@ -27,12 +26,12 @@ export = { { Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A' }, ], }, - })); + }); + - test.done(); - }, + }); - 'if "vpc" is not specified, vpc with default configuration will be created'(test: Test) { + test('if "vpc" is not specified, vpc with default configuration will be created', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -40,13 +39,13 @@ export = { new eks.Cluster(stack, 'cluster'); // THEN - expect(stack).to(haveResource('AWS::EC2::VPC')); - test.done(); - }, + expect(stack).toHaveResource('AWS::EC2::VPC'); - 'default capacity': { + }); - 'x2 m5.large by default'(test: Test) { + describe('default capacity', () => { + + test('x2 m5.large by default', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -54,13 +53,13 @@ export = { const cluster = new eks.Cluster(stack, 'cluster'); // THEN - test.ok(cluster.defaultCapacity); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' })); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' })); - test.done(); - }, + expect(cluster.defaultCapacity).toBeDefined(); + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '2' }); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm5.large' }); + + }); - 'quantity and type can be customized'(test: Test) { + test('quantity and type can be customized', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -71,13 +70,13 @@ export = { }); // THEN - test.ok(cluster.defaultCapacity); - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' })); - expect(stack).to(haveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' })); - test.done(); - }, + expect(cluster.defaultCapacity).toBeDefined(); + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { DesiredCapacity: '10' }); + expect(stack).toHaveResource('AWS::AutoScaling::LaunchConfiguration', { InstanceType: 'm2.xlarge' }); + + }); - 'defaultCapacity=0 will not allocate at all'(test: Test) { + test('defaultCapacity=0 will not allocate at all', () => { // GIVEN const { stack } = testFixtureNoVpc(); @@ -85,14 +84,14 @@ export = { const cluster = new eks.Cluster(stack, 'cluster', { defaultCapacity: 0 }); // THEN - test.ok(!cluster.defaultCapacity); - expect(stack).notTo(haveResource('AWS::AutoScaling::AutoScalingGroup')); - expect(stack).notTo(haveResource('AWS::AutoScaling::LaunchConfiguration')); - test.done(); - }, - }, - - 'creating a cluster tags the private VPC subnets'(test: Test) { + expect(cluster.defaultCapacity).toBeUndefined(); + expect(stack).not.toHaveResource('AWS::AutoScaling::AutoScalingGroup'); + expect(stack).not.toHaveResource('AWS::AutoScaling::LaunchConfiguration'); + + }); + }); + + test('creating a cluster tags the private VPC subnets', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -100,19 +99,19 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResource('AWS::EC2::Subnet', { + expect(stack).toHaveResource('AWS::EC2::Subnet', { Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Private' }, { Key: 'aws-cdk:subnet-type', Value: 'Private' }, { Key: 'kubernetes.io/role/internal-elb', Value: '1' }, { Key: 'Name', Value: 'Stack/VPC/PrivateSubnet1' }, ], - })); + }); + - test.done(); - }, + }); - 'creating a cluster tags the public VPC subnets'(test: Test) { + test('creating a cluster tags the public VPC subnets', () => { // GIVEN const { stack, vpc } = testFixture(); @@ -120,7 +119,7 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResource('AWS::EC2::Subnet', { + expect(stack).toHaveResource('AWS::EC2::Subnet', { MapPublicIpOnLaunch: true, Tags: [ { Key: 'aws-cdk:subnet-name', Value: 'Public' }, @@ -128,12 +127,12 @@ export = { { Key: 'kubernetes.io/role/elb', Value: '1' }, { Key: 'Name', Value: 'Stack/VPC/PublicSubnet1' }, ], - })); + }); + - test.done(); - }, + }); - 'adding capacity creates an ASG with tags'(test: Test) { + test('adding capacity creates an ASG with tags', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); @@ -144,7 +143,7 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::AutoScaling::AutoScalingGroup', { + expect(stack).toHaveResource('AWS::AutoScaling::AutoScalingGroup', { Tags: [ { Key: { 'Fn::Join': ['', ['kubernetes.io/cluster/', { Ref: 'ClusterEB0386A7' }]] }, @@ -157,12 +156,12 @@ export = { Value: 'Stack/Cluster/Default', }, ], - })); + }); - test.done(); - }, - 'exercise export/import'(test: Test) { + }); + + test('exercise export/import', () => { // GIVEN const { stack: stack1, vpc, app } = testFixture(); const stack2 = new cdk.Stack(app, 'stack2', { env: { region: 'us-east-1' } }); @@ -182,7 +181,7 @@ export = { new cdk.CfnOutput(stack2, 'ClusterARN', { value: imported.clusterArn }); // THEN - expect(stack2).toMatch({ + expect(stack2).toMatchTemplate({ Outputs: { ClusterARN: { Value: { @@ -191,23 +190,23 @@ export = { }, }, }); - test.done(); - }, - 'disabled features when kubectl is disabled'(test: Test) { + }); + + test('disabled features when kubectl is disabled', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); - test.throws(() => cluster.awsAuth, /Cannot define aws-auth mappings if kubectl is disabled/); - test.throws(() => cluster.addResource('foo', {}), /Cannot define a KubernetesManifest resource on a cluster with kubectl disabled/); - test.throws(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true }), + expect(() => cluster.awsAuth).toThrow(/Cannot define aws-auth mappings if kubectl is disabled/); + expect(() => cluster.addResource('foo', {})).toThrow(/Cannot define a KubernetesManifest resource on a cluster with kubectl disabled/); + expect(() => cluster.addCapacity('boo', { instanceType: new ec2.InstanceType('r5d.24xlarge'), mapRole: true })).toThrow( /Cannot map instance IAM role to RBAC if kubectl is disabled for the cluster/); - test.throws(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }), /Cannot define a Helm chart on a cluster with kubectl disabled/); - test.done(); - }, + expect(() => new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' })).toThrow(/Cannot define a Helm chart on a cluster with kubectl disabled/); - 'mastersRole can be used to map an IAM role to "system:masters" (required kubectl)'(test: Test) { + }); + + test('mastersRole can be used to map an IAM role to "system:masters" (required kubectl)', () => { // GIVEN const { stack, vpc } = testFixture(); const role = new iam.Role(stack, 'role', { assumedBy: new iam.AnyPrincipal() }); @@ -216,7 +215,7 @@ export = { new eks.Cluster(stack, 'Cluster', { vpc, mastersRole: role, defaultCapacity: 0 }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -232,12 +231,12 @@ export = { ], ], }, - })); + }); - test.done(); - }, - 'addResource can be used to apply k8s manifests on this cluster'(test: Test) { + }); + + test('addResource can be used to apply k8s manifests on this cluster', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0 }); @@ -247,18 +246,18 @@ export = { cluster.addResource('manifest2', { bar: 123 }, { boor: [1, 2, 3] }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"foo":123}]', - })); + }); - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: '[{"bar":123},{"boor":[1,2,3]}]', - })); + }); - test.done(); - }, - 'when kubectl is enabled (default) adding capacity will automatically map its IAM role'(test: Test) { + }); + + test('when kubectl is enabled (default) adding capacity will automatically map its IAM role', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0 }); @@ -269,7 +268,7 @@ export = { }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: { 'Fn::Join': [ '', @@ -285,12 +284,12 @@ export = { ], ], }, - })); + }); - test.done(); - }, - 'addCapacity will *not* map the IAM role if mapRole is false'(test: Test) { + }); + + test('addCapacity will *not* map the IAM role if mapRole is false', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, defaultCapacity: 0 }); @@ -302,11 +301,11 @@ export = { }); // THEN - expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); - test.done(); - }, + expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); + + }); - 'addCapacity will *not* map the IAM role if kubectl is disabled'(test: Test) { + test('addCapacity will *not* map the IAM role if kubectl is disabled', () => { // GIVEN const { stack, vpc } = testFixture(); const cluster = new eks.Cluster(stack, 'Cluster', { vpc, kubectlEnabled: false, defaultCapacity: 0 }); @@ -317,12 +316,12 @@ export = { }); // THEN - expect(stack).to(not(haveResource(eks.KubernetesResource.RESOURCE_TYPE))); - test.done(); - }, + expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); - 'outputs': { - 'aws eks update-kubeconfig is the only output synthesized by default'(test: Test) { + }); + + describe('outputs', () => { + test('aws eks update-kubeconfig is the only output synthesized by default', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -332,14 +331,14 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1']] } }, ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1']] } }, }); - test.done(); - }, - 'if masters role is defined, it should be included in the config command'(test: Test) { + }); + + test('if masters role is defined, it should be included in the config command', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -350,14 +349,14 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterConfigCommand43AAE40F: { Value: { 'Fn::Join': ['', ['aws eks update-kubeconfig --name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1 --role-arn ', { 'Fn::GetAtt': ['masters0D04F23D', 'Arn'] }]] } }, ClusterGetTokenCommand06AE992E: { Value: { 'Fn::Join': ['', ['aws eks get-token --cluster-name ', { Ref: 'Cluster9EE0221C' }, ' --region us-east-1 --role-arn ', { 'Fn::GetAtt': ['masters0D04F23D', 'Arn'] }]] } }, }); - test.done(); - }, - 'if `outputConfigCommand=false` will disabled the output'(test: Test) { + }); + + test('if `outputConfigCommand=false` will disabled the output', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -371,11 +370,11 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.ok(!template.Outputs); // no outputs - test.done(); - }, + expect(template.Outputs).toBeUndefined(); // no outputs + + }); - '`outputClusterName` can be used to synthesize an output with the cluster name'(test: Test) { + test('`outputClusterName` can be used to synthesize an output with the cluster name', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -388,13 +387,13 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterClusterNameEB26049E: { Value: { Ref: 'Cluster9EE0221C' } }, }); - test.done(); - }, - '`outputMastersRoleArn` can be used to synthesize an output with the arn of the masters role if defined'(test: Test) { + }); + + test('`outputMastersRoleArn` can be used to synthesize an output with the arn of the masters role if defined', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -408,13 +407,13 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterMastersRoleArnB15964B1: { Value: { 'Fn::GetAtt': ['masters0D04F23D', 'Arn'] } }, }); - test.done(); - }, - 'when adding capacity, instance role ARN will not be outputed only if we do not auto-map aws-auth'(test: Test) { + }); + + test('when adding capacity, instance role ARN will not be outputed only if we do not auto-map aws-auth', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -427,18 +426,18 @@ export = { // THEN const assembly = app.synth(); const template = assembly.getStackByName(stack.stackName).template; - test.deepEqual(template.Outputs, { + expect(template.Outputs).toEqual({ ClusterDefaultCapacityInstanceRoleARN7DADF219: { Value: { 'Fn::GetAtt': ['ClusterDefaultCapacityInstanceRole3E209969', 'Arn'] }, }, }); - test.done(); - }, - }, - 'boostrap user-data': { + }); + }); + + describe('boostrap user-data', () => { - 'rendered by default for ASGs'(test: Test) { + test('rendered by default for ASGs', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -449,11 +448,11 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(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 Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, + expect(userData).toEqual({ '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 Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - 'not rendered if bootstrap is disabled'(test: Test) { + }); + + test('not rendered if bootstrap is disabled', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -467,12 +466,12 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': '#!/bin/bash' }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': '#!/bin/bash' }); + + }); // cursory test for options: see test.user-data.ts for full suite - 'bootstrap options'(test: Test) { + test('bootstrap options', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -488,13 +487,13 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand --node-labels FOO=42" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=OnDemand --node-labels FOO=42" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - 'spot instances': { + }); + + describe('spot instances', () => { - 'nodes labeled an tainted accordingly'(test: Test) { + test('nodes labeled an tainted accordingly', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -508,11 +507,11 @@ export = { // THEN const template = app.synth().getStackByName(stack.stackName).template; const userData = template.Resources.ClusterMyCapcityLaunchConfig58583345.Properties.UserData; - test.deepEqual(userData, { 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - test.done(); - }, + expect(userData).toEqual({ 'Fn::Base64': { 'Fn::Join': ['', ['#!/bin/bash\nset -o xtrace\n/etc/eks/bootstrap.sh ', { Ref: 'Cluster9EE0221C' }, ' --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule" --use-max-pods true\n/opt/aws/bin/cfn-signal --exit-code $? --stack Stack --resource ClusterMyCapcityASGD4CD8B97 --region us-east-1']] } }); - 'if kubectl is enabled, the interrupt handler is added'(test: Test) { + }); + + test('if kubectl is enabled, the interrupt handler is added', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); @@ -524,11 +523,11 @@ export = { }); // THEN - expect(stack).to(haveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) })); - test.done(); - }, + expect(stack).toHaveResource(eks.KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(spotInterruptHandler()) }); + + }); - 'if kubectl is disabled, interrupt handler is not added'(test: Test) { + test('if kubectl is disabled, interrupt handler is not added', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0, kubectlEnabled: false }); @@ -540,29 +539,29 @@ export = { }); // THEN - expect(stack).notTo(haveResource(eks.KubernetesResource.RESOURCE_TYPE)); - test.done(); - }, + expect(stack).not.toHaveResource(eks.KubernetesResource.RESOURCE_TYPE); - }, + }); - }, + }); - 'if bootstrap is disabled cannot specify options'(test: Test) { + }); + + test('if bootstrap is disabled cannot specify options', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new eks.Cluster(stack, 'Cluster', { defaultCapacity: 0 }); // THEN - test.throws(() => cluster.addCapacity('MyCapcity', { + expect(() => cluster.addCapacity('MyCapcity', { instanceType: new ec2.InstanceType('m3.xlargs'), bootstrapEnabled: false, bootstrapOptions: { awsApiRetryAttempts: 10 }, - }), /Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false/); - test.done(); - }, + })).toThrow(/Cannot specify "bootstrapOptions" if "bootstrapEnabled" is false/); + + }); - 'EKS-Optimized AMI with GPU support'(test: Test) { + test('EKS-Optimized AMI with GPU support', () => { // GIVEN const { app, stack } = testFixtureNoVpc(); @@ -575,9 +574,9 @@ export = { // THEN const assembly = app.synth(); const parameters = assembly.getStackByName(stack.stackName).template.Parameters; - test.ok(Object.entries(parameters).some( + expect(Object.entries(parameters).some( ([k, v]) => k.startsWith('SsmParameterValueawsserviceeksoptimizedami') && (v as any).Default.includes('amazon-linux2-gpu'), - ), 'EKS AMI with GPU should be in ssm parameters'); - test.done(); - }, -}; + )).toEqual(true); + + }); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts new file mode 100644 index 0000000000000..4101ce02168d9 --- /dev/null +++ b/packages/@aws-cdk/aws-eks-legacy/test/helm-chart.test.ts @@ -0,0 +1,54 @@ +import '@aws-cdk/assert-internal/jest'; +import * as eks from '../lib'; +import { testFixtureCluster } from './util'; + +/* eslint-disable max-len */ + +describe('helm chart', () => { + describe('add Helm chart', () => { + test('should have default namespace', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' }); + + }); + test('should have a lowercase default release name', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' }); + + }); + test('should trim the last 63 of the default release name', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChartNameWhichISMostProbablyLongerThenSixtyThreeCharacters', { cluster, chart: 'chart' }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' }); + + }); + test('with values', () => { + // GIVEN + const { stack, cluster } = testFixtureCluster(); + + // WHEN + new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); + + // THEN + expect(stack).toHaveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' }); + + }); + }); +}); diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.manifest.ts b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts similarity index 86% rename from packages/@aws-cdk/aws-eks-legacy/test/test.manifest.ts rename to packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts index 32e418352f808..962e0c0129821 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.manifest.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/manifest.test.ts @@ -1,12 +1,11 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import { Test } from 'nodeunit'; +import '@aws-cdk/assert-internal/jest'; import { Cluster, KubernetesResource } from '../lib'; import { testFixtureNoVpc } from './util'; /* eslint-disable max-len */ -export = { - 'basic usage'(test: Test) { +describe('manifest', () => { + test('basic usage', () => { // GIVEN const { stack } = testFixtureNoVpc(); const cluster = new Cluster(stack, 'cluster'); @@ -69,9 +68,9 @@ export = { manifest, }); - expect(stack).to(haveResource(KubernetesResource.RESOURCE_TYPE, { + expect(stack).toHaveResource(KubernetesResource.RESOURCE_TYPE, { Manifest: JSON.stringify(manifest), - })); - test.done(); - }, -}; \ No newline at end of file + }); + + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts b/packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts deleted file mode 100644 index 90bddc141b05e..0000000000000 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.cluster-resource.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Test } from 'nodeunit'; - -export = { - create: { - 'defaults'(test: Test) { - test.done(); - }, - 'with no specific version'(test: Test) { - test.done(); - }, - }, - - update: { - 'requires replacement': { - 'change of "name" creates a new cluster with the new name and deletes the old cluster'(test: Test) { - test.done(); - }, - 'change of "resourcesVpcConfig"'(test: Test) { - test.done(); - }, - 'change of "roleArn"'(test: Test) { - test.done(); - }, - 'change of "roleArn" and "version"'(test: Test) { - test.done(); - }, - }, - - 'in-place': { - 'version change': { - 'from undefined to a specific value'(test: Test) { - test.done(); - }, - - 'from a specific value to another value'(test: Test) { - test.done(); - }, - - 'fails from specific value to undefined'(test: Test) { - test.done(); - }, - }, - }, - - 'update failure returns the previous physical name': { - - 'for "version" updates'(test: Test) { - test.done(); - }, - - 'for "name" updates'(test: Test) { - test.done(); - }, - - 'for "roleArn" updates'(test: Test) { - test.done(); - }, - - }, - }, - - delete: { - 'delete failure': { - 'returns correct physical name'(test: Test) { - test.done(); - }, - }, - }, -}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts b/packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts deleted file mode 100644 index e595e3d00a5ac..0000000000000 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.helm-chart.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; -import { Test } from 'nodeunit'; -import * as eks from '../lib'; -import { testFixtureCluster } from './util'; - -/* eslint-disable max-len */ - -export = { - 'add Helm chart': { - 'should have default namespace'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Namespace: 'default' })); - test.done(); - }, - 'should have a lowercase default release name'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart' }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'stackmychartff398361' })); - test.done(); - }, - 'should trim the last 63 of the default release name'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChartNameWhichISMostProbablyLongerThenSixtyThreeCharacters', { cluster, chart: 'chart' }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Release: 'rtnamewhichismostprobablylongerthensixtythreecharactersb800614d' })); - test.done(); - }, - 'with values'(test: Test) { - // GIVEN - const { stack, cluster } = testFixtureCluster(); - - // WHEN - new eks.HelmChart(stack, 'MyChart', { cluster, chart: 'chart', values: { foo: 123 } }); - - // THEN - expect(stack).to(haveResource(eks.HelmChart.RESOURCE_TYPE, { Values: '{\"foo\":123}' })); - test.done(); - }, - }, -}; diff --git a/packages/@aws-cdk/aws-eks-legacy/test/test.user-data.ts b/packages/@aws-cdk/aws-eks-legacy/test/user-data.test.ts similarity index 73% rename from packages/@aws-cdk/aws-eks-legacy/test/test.user-data.ts rename to packages/@aws-cdk/aws-eks-legacy/test/user-data.test.ts index be8341bf6834b..4189e720d2f97 100644 --- a/packages/@aws-cdk/aws-eks-legacy/test/test.user-data.ts +++ b/packages/@aws-cdk/aws-eks-legacy/test/user-data.test.ts @@ -1,13 +1,12 @@ import * as autoscaling from '@aws-cdk/aws-autoscaling'; import * as ec2 from '@aws-cdk/aws-ec2'; import { App, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { renderUserData } from '../lib/user-data'; /* eslint-disable max-len */ -export = { - 'default user data'(test: Test) { +describe('user data', () => { + test('default user data', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -15,16 +14,16 @@ export = { const userData = stack.resolve(renderUserData('my-cluster-name', asg)); // THEN - test.deepEqual(userData, [ + expect(userData).toEqual([ 'set -o xtrace', '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true', '/opt/aws/bin/cfn-signal --exit-code $? --stack my-stack --resource ASG46ED3070 --region us-west-33', ]); - test.done(); - }, - '--use-max-pods=true'(test: Test) { + }); + + test('--use-max-pods=true', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -34,11 +33,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - '--use-max-pods=false'(test: Test) { + }); + + test('--use-max-pods=false', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -48,11 +47,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods false'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods false'); + + }); - '--aws-api-retry-attempts'(test: Test) { + test('--aws-api-retry-attempts', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -62,11 +61,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --aws-api-retry-attempts 123'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --aws-api-retry-attempts 123'); + + }); - '--docker-config-json'(test: Test) { + test('--docker-config-json', () => { // GIVEN const { asg } = newFixtures(); @@ -76,11 +75,11 @@ export = { }); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --docker-config-json \'{"docker":123}\''); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --docker-config-json \'{"docker":123}\''); + + }); - '--enable-docker-bridge=true'(test: Test) { + test('--enable-docker-bridge=true', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -90,11 +89,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --enable-docker-bridge'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --enable-docker-bridge'); - '--enable-docker-bridge=false'(test: Test) { + }); + + test('--enable-docker-bridge=false', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -104,11 +103,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true'); - '--kubelet-extra-args'(test: Test) { + }); + + test('--kubelet-extra-args', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -118,11 +117,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand --extra-args-for --kubelet" --use-max-pods true'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand --extra-args-for --kubelet" --use-max-pods true'); + + }); - 'arbitrary additional bootstrap arguments can be passed through "additionalArgs"'(test: Test) { + test('arbitrary additional bootstrap arguments can be passed through "additionalArgs"', () => { // GIVEN const { asg, stack } = newFixtures(); @@ -132,11 +131,11 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --apiserver-endpoint 1111 --foo-bar'); - test.done(); - }, + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=OnDemand" --use-max-pods true --apiserver-endpoint 1111 --foo-bar'); + + }); - 'if asg has spot instances, the correct label and taint is used'(test: Test) { + test('if asg has spot instances, the correct label and taint is used', () => { // GIVEN const { asg, stack } = newFixtures(true); @@ -146,10 +145,10 @@ export = { })); // THEN - test.deepEqual(userData[1], '/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule --node-labels X=y" --use-max-pods true'); - test.done(); - }, -}; + expect(userData[1]).toEqual('/etc/eks/bootstrap.sh my-cluster-name --kubelet-extra-args "--node-labels lifecycle=Ec2Spot --register-with-taints=spotInstance=true:PreferNoSchedule --node-labels X=y" --use-max-pods true'); + + }); +}); function newFixtures(spot = false) { const app = new App(); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts index 4cf3f2fabe710..d63d01f90e7ea 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-target-group.ts @@ -295,6 +295,11 @@ export abstract class TargetGroupBase extends Construct implements ITargetGroup * Set/replace the target group's health check */ public configureHealthCheck(healthCheck: HealthCheck) { + if (healthCheck.interval && healthCheck.timeout) { + if (healthCheck.interval.toMilliseconds() <= healthCheck.timeout.toMilliseconds()) { + throw new Error(`Healthcheck interval ${healthCheck.interval.toHumanString()} must be greater than the timeout ${healthCheck.timeout.toHumanString()}`); + } + } this.healthCheck = healthCheck; } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index a6b252f8fe48f..6634a7a3906c7 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -437,17 +437,17 @@ describe('tests', () => { }); group.configureHealthCheck({ unhealthyThresholdCount: 3, - timeout: cdk.Duration.hours(1), - interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(30), + interval: cdk.Duration.seconds(60), path: '/test', }); // THEN expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::TargetGroup', { UnhealthyThresholdCount: 3, - HealthCheckIntervalSeconds: 30, + HealthCheckIntervalSeconds: 60, HealthCheckPath: '/test', - HealthCheckTimeoutSeconds: 3600, + HealthCheckTimeoutSeconds: 30, }); }); @@ -466,8 +466,8 @@ describe('tests', () => { group.configureHealthCheck({ unhealthyThresholdCount: 3, - timeout: cdk.Duration.hours(1), - interval: cdk.Duration.seconds(30), + timeout: cdk.Duration.seconds(30), + interval: cdk.Duration.seconds(60), path: '/test', protocol: elbv2.Protocol.TCP, }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts index 97c36c33ce237..f1a3db5eb9508 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/target-group.test.ts @@ -281,4 +281,45 @@ describe('tests', () => { }).toThrow(/Slow start duration value must be between 30 and 900 seconds./); }); }); + + test('Interval equal to timeout', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + const tg = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + }); + + // THEN + expect(() => { + tg.configureHealthCheck({ + interval: cdk.Duration.seconds(60), + timeout: cdk.Duration.seconds(60), + }); + }).toThrow(/Healthcheck interval 1 minute must be greater than the timeout 1 minute/); + }); + + test('Interval smaller than timeout', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'Stack'); + const vpc = new ec2.Vpc(stack, 'VPC', {}); + + // WHEN + const tg = new elbv2.ApplicationTargetGroup(stack, 'TargetGroup', { + vpc, + }); + + // THEN + expect(() => { + tg.configureHealthCheck({ + interval: cdk.Duration.seconds(60), + timeout: cdk.Duration.seconds(120), + }); + }).toThrow(/Healthcheck interval 1 minute must be greater than the timeout 2 minutes/); + }); + }); diff --git a/packages/@aws-cdk/aws-events/.gitignore b/packages/@aws-cdk/aws-events/.gitignore index dcc1dc41e477f..17a41566f0002 100644 --- a/packages/@aws-cdk/aws-events/.gitignore +++ b/packages/@aws-cdk/aws-events/.gitignore @@ -15,4 +15,5 @@ nyc.config.js *.snk !.eslintrc.js -junit.xml \ No newline at end of file +junit.xml +!jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/.npmignore b/packages/@aws-cdk/aws-events/.npmignore index 9a032ae80868c..e8acf10a468a1 100644 --- a/packages/@aws-cdk/aws-events/.npmignore +++ b/packages/@aws-cdk/aws-events/.npmignore @@ -24,4 +24,5 @@ tsconfig.json **/cdk.out junit.xml test/ -!*.lit.ts \ No newline at end of file +!*.lit.ts +jest.config.js \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events/jest.config.js b/packages/@aws-cdk/aws-events/jest.config.js new file mode 100644 index 0000000000000..cd664e1d069e5 --- /dev/null +++ b/packages/@aws-cdk/aws-events/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('../../../tools/cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-events/package.json b/packages/@aws-cdk/aws-events/package.json index 9170b76961835..e676b57b6573b 100644 --- a/packages/@aws-cdk/aws-events/package.json +++ b/packages/@aws-cdk/aws-events/package.json @@ -56,6 +56,7 @@ }, "cdk-build": { "cloudformation": "AWS::Events", + "jest": true, "env": { "AWSLINT_BASE_CONSTRUCT": "true" } @@ -75,10 +76,11 @@ }, "license": "Apache-2.0", "devDependencies": { + "@types/jest": "^26.0.24", "@types/nodeunit": "^0.0.32", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", + "jest": "^26.6.3", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-events/test/test.archive.ts b/packages/@aws-cdk/aws-events/test/archive.test.ts similarity index 69% rename from packages/@aws-cdk/aws-events/test/test.archive.ts rename to packages/@aws-cdk/aws-events/test/archive.test.ts index 84c9d12222e42..0c37a0ec4ae39 100644 --- a/packages/@aws-cdk/aws-events/test/test.archive.ts +++ b/packages/@aws-cdk/aws-events/test/archive.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Duration, Stack } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventBus } from '../lib'; import { Archive } from '../lib/archive'; -export = { - 'creates an archive for an EventBus'(test: Test) { +describe('archive', () => { + test('creates an archive for an EventBus', () => { // GIVEN const stack = new Stack(); @@ -21,11 +20,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { EventPattern: { account: [{ Ref: 'AWS::AccountId', @@ -38,11 +37,11 @@ export = { 'Arn', ], }, - })); + }); + - test.done(); - }, - 'creates an archive for an EventBus with a pattern including a detailType property'(test: Test) { + }); + test('creates an archive for an EventBus with a pattern including a detailType property', () => { // GIVEN const stack = new Stack(); @@ -59,11 +58,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { EventPattern: { 'account': [{ Ref: 'AWS::AccountId', @@ -77,8 +76,8 @@ export = { 'Arn', ], }, - })); + }); + - test.done(); - }, -} + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/test.event-bus.ts b/packages/@aws-cdk/aws-events/test/event-bus.test.ts similarity index 71% rename from packages/@aws-cdk/aws-events/test/test.event-bus.ts rename to packages/@aws-cdk/aws-events/test/event-bus.test.ts index 9dea3faab34f6..b4384255ea7b4 100644 --- a/packages/@aws-cdk/aws-events/test/test.event-bus.ts +++ b/packages/@aws-cdk/aws-events/test/event-bus.test.ts @@ -1,11 +1,10 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import { Aws, CfnResource, Stack, Arn } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventBus } from '../lib'; -export = { - 'default event bus'(test: Test) { +describe('event bus', () => { + test('default event bus', () => { // GIVEN const stack = new Stack(); @@ -13,14 +12,14 @@ export = { new EventBus(stack, 'Bus'); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); + - test.done(); - }, + }); - 'named event bus'(test: Test) { + test('named event bus', () => { // GIVEN const stack = new Stack(); @@ -30,14 +29,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'myEventBus', - })); + }); - test.done(); - }, - 'partner event bus'(test: Test) { + }); + + test('partner event bus', () => { // GIVEN const stack = new Stack(); @@ -47,15 +46,15 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'aws.partner/PartnerName/acct1/repo1', EventSourceName: 'aws.partner/PartnerName/acct1/repo1', - })); + }); - test.done(); - }, - 'imported event bus'(test: Test) { + }); + + test('imported event bus', () => { const stack = new Stack(); const eventBus = new EventBus(stack, 'Bus'); @@ -71,15 +70,15 @@ export = { }, }); - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { EventBusArn1: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, EventBusArn2: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, - })); + }); - test.done(); - }, - 'imported event bus from name'(test: Test) { + }); + + test('imported event bus from name', () => { const stack = new Stack(); const eventBus = new EventBus(stack, 'Bus', { eventBusName: 'test-bus-to-import-by-name' }); @@ -87,12 +86,12 @@ export = { const importEB = EventBus.fromEventBusName(stack, 'ImportBus', eventBus.eventBusName); // WHEN - test.deepEqual(stack.resolve(eventBus.eventBusName), stack.resolve(importEB.eventBusName)); + expect(stack.resolve(eventBus.eventBusName)).toEqual(stack.resolve(importEB.eventBusName)); + - test.done(); - }, + }); - 'same account imported event bus has right resource env'(test: Test) { + test('same account imported event bus has right resource env', () => { const stack = new Stack(); const eventBus = new EventBus(stack, 'Bus'); @@ -100,13 +99,13 @@ export = { const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', eventBus.eventBusArn); // WHEN - test.deepEqual(stack.resolve(importEB.env.account), { 'Fn::Select': [4, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); - test.deepEqual(stack.resolve(importEB.env.region), { 'Fn::Select': [3, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + expect(stack.resolve(importEB.env.account)).toEqual({ 'Fn::Select': [4, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); + expect(stack.resolve(importEB.env.region)).toEqual({ 'Fn::Select': [3, { 'Fn::Split': [':', { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }] }] }); - test.done(); - }, - 'cross account imported event bus has right resource env'(test: Test) { + }); + + test('cross account imported event bus has right resource env', () => { const stack = new Stack(); const arnParts = { @@ -121,13 +120,13 @@ export = { const importEB = EventBus.fromEventBusArn(stack, 'ImportBus', arn); // WHEN - test.deepEqual(importEB.env.account, arnParts.account); - test.deepEqual(importEB.env.region, arnParts.region); + expect(importEB.env.account).toEqual(arnParts.account); + expect(importEB.env.region).toEqual(arnParts.region); + - test.done(); - }, + }); - 'can get bus name'(test: Test) { + test('can get bus name', () => { // GIVEN const stack = new Stack(); const bus = new EventBus(stack, 'Bus', { @@ -143,14 +142,14 @@ export = { }); // THEN - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { EventBusName: { Ref: 'BusEA82B648' }, - })); + }); + - test.done(); - }, + }); - 'can get bus arn'(test: Test) { + test('can get bus arn', () => { // GIVEN const stack = new Stack(); const bus = new EventBus(stack, 'Bus', { @@ -166,14 +165,14 @@ export = { }); // THEN - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { EventBusArn: { 'Fn::GetAtt': ['BusEA82B648', 'Arn'] }, - })); + }); + - test.done(); - }, + }); - 'event bus name cannot be default'(test: Test) { + test('event bus name cannot be default', () => { // GIVEN const stack = new Stack(); @@ -183,14 +182,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' must not be 'default'/); + }).toThrow(/'eventBusName' must not be 'default'/); - test.done(); - }, - 'event bus name cannot contain slash'(test: Test) { + }); + + test('event bus name cannot contain slash', () => { // GIVEN const stack = new Stack(); @@ -200,14 +199,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' must not contain '\/'/); + }).toThrow(/'eventBusName' must not contain '\/'/); + - test.done(); - }, + }); - 'event bus cannot have name and source name'(test: Test) { + test('event bus cannot have name and source name', () => { // GIVEN const stack = new Stack(); @@ -218,14 +217,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' and 'eventSourceName' cannot both be provided/); + }).toThrow(/'eventBusName' and 'eventSourceName' cannot both be provided/); - test.done(); - }, - 'event bus name cannot be empty string'(test: Test) { + }); + + test('event bus name cannot be empty string', () => { // GIVEN const stack = new Stack(); @@ -235,26 +234,26 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventBusName' must satisfy: /); + }).toThrow(/'eventBusName' must satisfy: /); + - test.done(); - }, + }); - 'does not throw if eventBusName is a token'(test: Test) { + test('does not throw if eventBusName is a token', () => { // GIVEN const stack = new Stack(); // WHEN / THEN - test.doesNotThrow(() => new EventBus(stack, 'EventBus', { + expect(() => new EventBus(stack, 'EventBus', { eventBusName: Aws.STACK_NAME, - })); + })).not.toThrow(); + - test.done(); - }, + }); - 'event bus source name must follow pattern'(test: Test) { + test('event bus source name must follow pattern', () => { // GIVEN const stack = new Stack(); @@ -264,14 +263,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventSourceName' must satisfy: \/\^aws/); + }).toThrow(/'eventSourceName' must satisfy: \/\^aws/); - test.done(); - }, - 'event bus source name cannot be empty string'(test: Test) { + }); + + test('event bus source name cannot be empty string', () => { // GIVEN const stack = new Stack(); @@ -281,14 +280,14 @@ export = { }); // THEN - test.throws(() => { + expect(() => { createInvalidBus(); - }, /'eventSourceName' must satisfy: /); + }).toThrow(/'eventSourceName' must satisfy: /); + - test.done(); - }, + }); - 'can grant PutEvents'(test: Test) { + test('can grant PutEvents', () => { // GIVEN const stack = new Stack(); const role = new iam.Role(stack, 'Role', { @@ -299,7 +298,7 @@ export = { EventBus.grantPutEvents(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -315,12 +314,12 @@ export = { Ref: 'Role1ABCC5F0', }, ], - })); + }); + - test.done(); - }, + }); - 'can grant PutEvents using grantAllPutEvents'(test: Test) { + test('can grant PutEvents using grantAllPutEvents', () => { // GIVEN const stack = new Stack(); const role = new iam.Role(stack, 'Role', { @@ -331,7 +330,7 @@ export = { EventBus.grantAllPutEvents(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -347,11 +346,11 @@ export = { Ref: 'Role1ABCC5F0', }, ], - })); + }); + - test.done(); - }, - 'can grant PutEvents to a specific event bus'(test: Test) { + }); + test('can grant PutEvents to a specific event bus', () => { // GIVEN const stack = new Stack(); const role = new iam.Role(stack, 'Role', { @@ -364,7 +363,7 @@ export = { eventBus.grantPutEventsTo(role); // THEN - expect(stack).to(haveResource('AWS::IAM::Policy', { + expect(stack).toHaveResource('AWS::IAM::Policy', { PolicyDocument: { Statement: [ { @@ -385,11 +384,11 @@ export = { Ref: 'Role1ABCC5F0', }, ], - })); + }); + - test.done(); - }, - 'can archive events'(test: Test) { + }); + test('can archive events', () => { // GIVEN const stack = new Stack(); @@ -404,11 +403,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -436,11 +435,11 @@ export = { }, RetentionDays: 0, ArchiveName: 'MyArchive', - })); + }); - test.done(); - }, - 'can archive events from an imported EventBus'(test: Test) { + + }); + test('can archive events from an imported EventBus', () => { // GIVEN const stack = new Stack(); @@ -457,11 +456,11 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::EventBus', { + expect(stack).toHaveResource('AWS::Events::EventBus', { Name: 'Bus', - })); + }); - expect(stack).to(haveResource('AWS::Events::Archive', { + expect(stack).toHaveResource('AWS::Events::Archive', { SourceArn: { 'Fn::GetAtt': [ 'BusEA82B648', @@ -512,8 +511,8 @@ export = { }, RetentionDays: 0, ArchiveName: 'MyArchive', - })); + }); + - test.done(); - }, -}; + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/test.input.ts b/packages/@aws-cdk/aws-events/test/input.test.ts similarity index 78% rename from packages/@aws-cdk/aws-events/test/test.input.ts rename to packages/@aws-cdk/aws-events/test/input.test.ts index 43c763ef35f74..76ec2cd2d2bd9 100644 --- a/packages/@aws-cdk/aws-events/test/test.input.ts +++ b/packages/@aws-cdk/aws-events/test/input.test.ts @@ -1,13 +1,12 @@ -import { expect, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { User } from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { Test } from 'nodeunit'; import { EventField, IRuleTarget, RuleTargetInput, Schedule } from '../lib'; import { Rule } from '../lib/rule'; -export = { - 'json template': { - 'can just be a JSON object'(test: Test) { +describe('input', () => { + describe('json template', () => { + test('can just be a JSON object', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -18,17 +17,17 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ SomeObject: 'withAValue' }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '{"SomeObject":"withAValue"}', }, ], - })); - test.done(); - }, + }); + + }); - 'can use joined JSON containing refs in JSON object'(test: Test) { + test('can use joined JSON containing refs in JSON object', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -42,7 +41,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -62,12 +61,12 @@ export = { }, }, ], - })); + }); - test.done(); - }, - 'can use joined JSON containing refs in JSON object with tricky inputs'(test: Test) { + }); + + test('can use joined JSON containing refs in JSON object with tricky inputs', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -81,7 +80,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -101,12 +100,12 @@ export = { }, }, ], - })); + }); - test.done(); - }, - 'can use joined JSON containing refs in JSON object and concat'(test: Test) { + }); + + test('can use joined JSON containing refs in JSON object and concat', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -120,7 +119,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -140,12 +139,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'can use joined JSON containing refs in JSON object and quotes'(test: Test) { + test('can use joined JSON containing refs in JSON object and quotes', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -159,7 +158,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -179,12 +178,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'can use joined JSON containing refs in JSON object and multiple keys'(test: Test) { + test('can use joined JSON containing refs in JSON object and multiple keys', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -198,7 +197,7 @@ export = { }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { InputTransformer: { @@ -218,12 +217,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'can use token'(test: Test) { + test('can use token', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -235,7 +234,7 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromObject({ userArn: user.userArn }))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: { @@ -255,13 +254,13 @@ export = { }, }, ], - })); - test.done(); - }, - }, + }); - 'text templates': { - 'strings with newlines are serialized to a newline-delimited list of JSON strings'(test: Test) { + }); + }); + + describe('text templates', () => { + test('strings with newlines are serialized to a newline-delimited list of JSON strings', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -272,18 +271,18 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('I have\nmultiple lines'))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '"I have"\n"multiple lines"', }, ], - })); + }); - test.done(); - }, - 'escaped newlines are not interpreted as newlines'(test: Test) { + }); + + test('escaped newlines are not interpreted as newlines', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -294,18 +293,18 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromMultilineText('this is not\\na real newline'))), // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '"this is not\\\\na real newline"', }, ], - })); + }); + - test.done(); - }, + }); - 'can use Tokens in text templates'(test: Test) { + test('can use Tokens in text templates', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -318,18 +317,18 @@ export = { rule.addTarget(new SomeTarget(RuleTargetInput.fromText(`hello ${world}`))); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Input: '"hello world"', }, ], - })); + }); + - test.done(); - }, - }, -}; + }); + }); +}); class SomeTarget implements IRuleTarget { public constructor(private readonly input: RuleTargetInput) { diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/rule.test.ts similarity index 79% rename from packages/@aws-cdk/aws-events/test/test.rule.ts rename to packages/@aws-cdk/aws-events/test/rule.test.ts index 1ac347d3d1bbf..e7b940734d0e4 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/rule.test.ts @@ -1,23 +1,22 @@ /* eslint-disable object-curly-newline */ -import { expect, haveResource, haveResourceLike } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import { Construct, IConstruct } from 'constructs'; -import { Test } from 'nodeunit'; import { EventBus, EventField, IRule, IRuleTarget, RuleTargetConfig, RuleTargetInput, Schedule } from '../lib'; import { Rule } from '../lib/rule'; /* eslint-disable quote-props */ -export = { - 'default rule'(test: Test) { +describe('rule', () => { + test('default rule', () => { const stack = new cdk.Stack(); new Rule(stack, 'MyRule', { schedule: Schedule.rate(cdk.Duration.minutes(10)), }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -28,10 +27,10 @@ export = { }, }, }); - test.done(); - }, - 'can get rule name'(test: Test) { + }); + + test('can get rule name', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'MyRule', { schedule: Schedule.rate(cdk.Duration.minutes(10)), @@ -44,14 +43,14 @@ export = { }, }); - expect(stack).to(haveResource('Test::Resource', { + expect(stack).toHaveResource('Test::Resource', { RuleName: { Ref: 'MyRuleA44AB831' }, - })); + }); - test.done(); - }, - 'get rate as token'(test: Test) { + }); + + test('get rate as token', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyScheduledStack'); const lazyDuration = cdk.Duration.minutes(cdk.Lazy.number({ produce: () => 5 })); @@ -62,29 +61,29 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { 'Name': 'rateInMinutes', 'ScheduleExpression': 'rate(5 minutes)', - })); + }); - test.done(); - }, - 'Seconds is not an allowed value for Schedule rate'(test: Test) { + }); + + test('Seconds is not an allowed value for Schedule rate', () => { const lazyDuration = cdk.Duration.seconds(cdk.Lazy.number({ produce: () => 5 })); - test.throws(() => Schedule.rate(lazyDuration), /Allowed units for scheduling/i); - test.done(); - }, + expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); + + }); - 'Millis is not an allowed value for Schedule rate'(test: Test) { + test('Millis is not an allowed value for Schedule rate', () => { const lazyDuration = cdk.Duration.millis(cdk.Lazy.number({ produce: () => 5 })); // THEN - test.throws(() => Schedule.rate(lazyDuration), /Allowed units for scheduling/i); - test.done(); - }, + expect(() => Schedule.rate(lazyDuration)).toThrow(/Allowed units for scheduling/i); - 'rule with physical name'(test: Test) { + }); + + test('rule with physical name', () => { // GIVEN const stack = new cdk.Stack(); @@ -95,14 +94,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { Name: 'PhysicalName', - })); + }); - test.done(); - }, - 'eventPattern is rendered properly'(test: Test) { + }); + + test('eventPattern is rendered properly', () => { const stack = new cdk.Stack(); new Rule(stack, 'MyRule', { @@ -121,7 +120,7 @@ export = { }, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -143,18 +142,18 @@ export = { }, }); - test.done(); - }, - 'fails synthesis if neither eventPattern nor scheudleExpression are specified'(test: Test) { + }); + + test('fails synthesis if neither eventPattern nor scheudleExpression are specified', () => { const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyStack'); new Rule(stack, 'Rule'); - test.throws(() => app.synth(), /Either 'eventPattern' or 'schedule' must be defined/); - test.done(); - }, + expect(() => app.synth()).toThrow(/Either 'eventPattern' or 'schedule' must be defined/); - 'addEventPattern can be used to add filters'(test: Test) { + }); + + test('addEventPattern can be used to add filters', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'MyRule'); @@ -175,7 +174,7 @@ export = { }, }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -204,10 +203,10 @@ export = { }, }, }); - test.done(); - }, - 'addEventPattern can de-duplicate filters and keep the order'(test: Test) { + }); + + test('addEventPattern can de-duplicate filters and keep the order', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'MyRule'); @@ -219,7 +218,7 @@ export = { detailType: ['EC2 Instance State-change Notification', 'AWS API Call via CloudTrail'], }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyRuleA44AB831': { 'Type': 'AWS::Events::Rule', @@ -235,10 +234,10 @@ export = { }, }, }); - test.done(); - }, - 'targets can be added via props or addTarget with input transformer'(test: Test) { + }); + + test('targets can be added via props or addTarget with input transformer', () => { const stack = new cdk.Stack(); const t1: IRuleTarget = { bind: () => ({ @@ -263,7 +262,7 @@ export = { rule.addTarget(t2); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -293,10 +292,10 @@ export = { }, }, }); - test.done(); - }, - 'input template can contain tokens'(test: Test) { + }); + + test('input template can contain tokens', () => { const stack = new cdk.Stack(); const rule = new Rule(stack, 'EventRule', { @@ -339,7 +338,7 @@ export = { }), }); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'EventRule5A491D2C': { 'Type': 'AWS::Events::Rule', @@ -381,10 +380,10 @@ export = { }, }); - test.done(); - }, - 'target can declare role which will be used'(test: Test) { + }); + + test('target can declare role which will be used', () => { // GIVEN const stack = new cdk.Stack(); @@ -406,7 +405,7 @@ export = { }); // THEN - expect(stack).to(haveResourceLike('AWS::Events::Rule', { + expect(stack).toHaveResourceLike('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -414,12 +413,12 @@ export = { 'RoleArn': { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] }, }, ], - })); + }); - test.done(); - }, - 'in cross-account scenario, target role is only used in target account'(test: Test) { + }); + + test('in cross-account scenario, target role is only used in target account', () => { // GIVEN const app = new cdk.App(); const ruleStack = new cdk.Stack(app, 'RuleStack', { env: { account: '1234', region: 'us-east-1' } }); @@ -444,7 +443,7 @@ export = { }); // THEN - expect(ruleStack).to(haveResourceLike('AWS::Events::Rule', { + expect(ruleStack).toHaveResourceLike('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::Join': ['', [ @@ -454,8 +453,8 @@ export = { ]] }, }, ], - })); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + }); + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'Targets': [ { 'Arn': 'ARN2', @@ -463,12 +462,12 @@ export = { 'RoleArn': { 'Fn::GetAtt': ['SomeRole6DDC54DD', 'Arn'] }, }, ], - })); + }); + - test.done(); - }, + }); - 'asEventRuleTarget can use the ruleArn and a uniqueId of the rule'(test: Test) { + test('asEventRuleTarget can use the ruleArn and a uniqueId of the rule', () => { const stack = new cdk.Stack(); let receivedRuleArn = 'FAIL'; @@ -490,12 +489,12 @@ export = { const rule = new Rule(stack, 'EventRule'); rule.addTarget(t1); - test.deepEqual(stack.resolve(receivedRuleArn), stack.resolve(rule.ruleArn)); - test.deepEqual(receivedRuleId, cdk.Names.uniqueId(rule)); - test.done(); - }, + expect(stack.resolve(receivedRuleArn)).toEqual(stack.resolve(rule.ruleArn)); + expect(receivedRuleId).toEqual(cdk.Names.uniqueId(rule)); - 'fromEventRuleArn'(test: Test) { + }); + + test('fromEventRuleArn', () => { // GIVEN const stack = new cdk.Stack(); @@ -503,12 +502,12 @@ export = { const importedRule = Rule.fromEventRuleArn(stack, 'ImportedRule', 'arn:aws:events:us-east-2:123456789012:rule/example'); // THEN - test.deepEqual(importedRule.ruleArn, 'arn:aws:events:us-east-2:123456789012:rule/example'); - test.deepEqual(importedRule.ruleName, 'example'); - test.done(); - }, + expect(importedRule.ruleArn).toEqual('arn:aws:events:us-east-2:123456789012:rule/example'); + expect(importedRule.ruleName).toEqual('example'); + + }); - 'rule can be disabled'(test: Test) { + test('rule can be disabled', () => { // GIVEN const stack = new cdk.Stack(); @@ -519,14 +518,14 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { 'State': 'DISABLED', - })); + }); + - test.done(); - }, + }); - 'can add multiple targets with the same id'(test: Test) { + test('can add multiple targets with the same id', () => { // GIVEN const stack = new cdk.Stack(); const rule = new Rule(stack, 'Rule', { @@ -537,7 +536,7 @@ export = { rule.addTarget(new SomeTarget()); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -554,12 +553,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'sqsParameters are generated when they are specified in target props'(test: Test) { + test('sqsParameters are generated when they are specified in target props', () => { const stack = new cdk.Stack(); const t1: IRuleTarget = { bind: () => ({ @@ -574,7 +573,7 @@ export = { targets: [t1], }); - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -584,11 +583,11 @@ export = { }, }, ], - })); - test.done(); - }, + }); + + }); - 'associate rule with event bus'(test: Test) { + test('associate rule with event bus', () => { // GIVEN const stack = new cdk.Stack(); const eventBus = new EventBus(stack, 'EventBus'); @@ -602,30 +601,30 @@ export = { }); // THEN - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toHaveResource('AWS::Events::Rule', { EventBusName: { Ref: 'EventBus7B8748AA', }, - })); + }); + - test.done(); - }, + }); - 'throws with eventBus and schedule'(test: Test) { + test('throws with eventBus and schedule', () => { // GIVEN const app = new cdk.App(); const stack = new cdk.Stack(app, 'MyStack'); const eventBus = new EventBus(stack, 'EventBus'); // THEN - test.throws(() => new Rule(stack, 'MyRule', { + expect(() => new Rule(stack, 'MyRule', { schedule: Schedule.rate(cdk.Duration.minutes(10)), eventBus, - }), /Cannot associate rule with 'eventBus' when using 'schedule'/); - test.done(); - }, + })).toThrow(/Cannot associate rule with 'eventBus' when using 'schedule'/); - 'allow an imported target if is in the same account and region'(test: Test) { + }); + + test('allow an imported target if is in the same account and region', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -641,7 +640,7 @@ export = { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).to(haveResource('AWS::Events::Rule', { + expect(sourceStack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -651,13 +650,13 @@ export = { }, }, ], - })); + }); - test.done(); - }, - 'for cross-account and/or cross-region targets': { - 'requires that the source stack specify a concrete account'(test: Test) { + }); + + describe('for cross-account and/or cross-region targets', () => { + test('requires that the source stack specify a concrete account', () => { const app = new cdk.App(); const sourceStack = new cdk.Stack(app, 'SourceStack'); @@ -667,14 +666,14 @@ export = { const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount } }); const resource = new Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete region/); + }).toThrow(/You need to provide a concrete region/); + - test.done(); - }, + }); - 'requires that the target stack specify a concrete account'(test: Test) { + test('requires that the target stack specify a concrete account', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -684,14 +683,14 @@ export = { const targetStack = new cdk.Stack(app, 'TargetStack'); const resource = new Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete account for the target stack when using cross-account or cross-region events/); + }).toThrow(/You need to provide a concrete account for the target stack when using cross-account or cross-region events/); + - test.done(); - }, + }); - 'requires that the target stack specify a concrete region'(test: Test) { + test('requires that the target stack specify a concrete region', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -702,14 +701,14 @@ export = { const targetStack = new cdk.Stack(app, 'TargetStack', { env: { account: targetAccount } }); const resource = new Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /You need to provide a concrete region for the target stack when using cross-account or cross-region events/); + }).toThrow(/You need to provide a concrete region for the target stack when using cross-account or cross-region events/); + - test.done(); - }, + }); - 'creates cross-account targets if in the same region'(test: Test) { + test('creates cross-account targets if in the same region', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -728,7 +727,7 @@ export = { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -745,9 +744,9 @@ export = { }, }, ], - })); + }); - expect(targetStack).to(haveResource('AWS::Events::Rule', { + expect(targetStack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -757,12 +756,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'creates cross-region targets'(test: Test) { + test('creates cross-region targets', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -781,7 +780,7 @@ export = { rule.addTarget(new SomeTarget('T', resource)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -798,9 +797,9 @@ export = { }, }, ], - })); + }); - expect(targetStack).to(haveResource('AWS::Events::Rule', { + expect(targetStack).toHaveResource('AWS::Events::Rule', { Targets: [ { 'Arn': 'ARN1', @@ -810,12 +809,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'do not create duplicated targets'(test: Test) { + test('do not create duplicated targets', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -836,7 +835,7 @@ export = { // same target should be skipped rule.addTarget(new SomeTarget('T1', resource)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -853,9 +852,9 @@ export = { }, }, ], - })); + }); - expect(sourceStack).notTo(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).not.toHaveResourceLike('AWS::Events::Rule', { 'State': 'ENABLED', 'Targets': [ { @@ -872,12 +871,12 @@ export = { }, }, ], - })); + }); + - test.done(); - }, + }); - 'requires that the target is not imported'(test: Test) { + test('requires that the target is not imported', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -892,14 +891,14 @@ export = { const targetAccount = '123456789012'; const targetRegion = 'us-west-1'; const resource = EventBus.fromEventBusArn(sourceStack, 'TargetEventBus', `arn:aws:events:${targetRegion}:${targetAccount}:event-bus/default`); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /Cannot create a cross-account or cross-region rule for an imported resource/); + }).toThrow(/Cannot create a cross-account or cross-region rule for an imported resource/); + - test.done(); - }, + }); - 'requires that the source and target stacks be part of the same App'(test: Test) { + test('requires that the source and target stacks be part of the same App', () => { const sourceApp = new cdk.App(); const sourceAccount = '123456789012'; const sourceStack = new cdk.Stack(sourceApp, 'SourceStack', { env: { account: sourceAccount, region: 'us-west-2' } }); @@ -910,14 +909,14 @@ export = { const targetStack = new cdk.Stack(targetApp, 'TargetStack', { env: { account: targetAccount, region: 'us-west-2' } }); const resource = new Construct(targetStack, 'Resource'); - test.throws(() => { + expect(() => { rule.addTarget(new SomeTarget('T', resource)); - }, /Event stack and target stack must belong to the same CDK app/); + }).toThrow(/Event stack and target stack must belong to the same CDK app/); + - test.done(); - }, + }); - 'generates the correct rules in the source and target stacks when eventPattern is passed in the constructor'(test: Test) { + test('generates the correct rules in the source and target stacks when eventPattern is passed in the constructor', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -946,7 +945,7 @@ export = { rule.addTarget(new SomeTarget('T1', resource1)); rule.addTarget(new SomeTarget('T2', resource2)); - expect(sourceStack).to(haveResourceLike('AWS::Events::Rule', { + expect(sourceStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -968,9 +967,9 @@ export = { }, }, ], - })); + }); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -983,8 +982,8 @@ export = { 'Arn': 'ARN1', }, ], - })); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + }); + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -997,19 +996,19 @@ export = { 'Arn': 'ARN1', }, ], - })); + }); const eventBusPolicyStack = app.node.findChild(`EventBusPolicy-${sourceAccount}-us-west-2-${targetAccount}`) as cdk.Stack; - expect(eventBusPolicyStack).to(haveResourceLike('AWS::Events::EventBusPolicy', { + expect(eventBusPolicyStack).toHaveResourceLike('AWS::Events::EventBusPolicy', { 'Action': 'events:PutEvents', 'StatementId': `Allow-account-${sourceAccount}`, 'Principal': sourceAccount, - })); + }); + - test.done(); - }, + }); - 'generates the correct rule in the target stack when addEventPattern in the source rule is used'(test: Test) { + test('generates the correct rule in the target stack when addEventPattern in the source rule is used', () => { const app = new cdk.App(); const sourceAccount = '123456789012'; @@ -1036,7 +1035,7 @@ export = { source: ['some-event'], }); - expect(targetStack).to(haveResourceLike('AWS::Events::Rule', { + expect(targetStack).toHaveResourceLike('AWS::Events::Rule', { 'EventPattern': { 'source': [ 'some-event', @@ -1049,12 +1048,12 @@ export = { 'Arn': 'ARN1', }, ], - })); + }); - test.done(); - }, - }, -}; + + }); + }); +}); class SomeTarget implements IRuleTarget { // eslint-disable-next-line cdk/no-core-construct diff --git a/packages/@aws-cdk/aws-events/test/schedule.test.ts b/packages/@aws-cdk/aws-events/test/schedule.test.ts new file mode 100644 index 0000000000000..d853da9ba6c30 --- /dev/null +++ b/packages/@aws-cdk/aws-events/test/schedule.test.ts @@ -0,0 +1,96 @@ +import { Duration, Stack, Lazy } from '@aws-cdk/core'; +import * as events from '../lib'; + +describe('schedule', () => { + test('cron expressions day and dow are mutex: given weekday', () => { + // Run every 10 minutes Monday through Friday + expect('cron(0/10 * ? * MON-FRI *)').toEqual(events.Schedule.cron({ + minute: '0/10', + weekDay: 'MON-FRI', + }).expressionString); + + }); + + test('cron expressions day and dow are mutex: given month day', () => { + // Run at 8:00 am (UTC) every 1st day of the month + expect('cron(0 8 1 * ? *)').toEqual(events.Schedule.cron({ + minute: '0', + hour: '8', + day: '1', + }).expressionString); + + }); + + test('cron expressions day and dow are mutex: given neither', () => { + // Run at 10:00 am (UTC) every day + expect('cron(0 10 * * ? *)').toEqual(events.Schedule.cron({ + minute: '0', + hour: '10', + }).expressionString); + + }); + + test('rate must be whole number of minutes', () => { + expect(() => { + events.Schedule.rate(Duration.minutes(0.13456)); + }).toThrow(/'0.13456 minutes' cannot be converted into a whole number of seconds/); + + }); + + test('rate must be whole number', () => { + expect(() => { + events.Schedule.rate(Duration.minutes(1/8)); + }).toThrow(/'0.125 minutes' cannot be converted into a whole number of seconds/); + + }); + + test('rate cannot be 0', () => { + expect(() => { + events.Schedule.rate(Duration.days(0)); + }).toThrow(/Duration cannot be 0/); + + }); + + test('rate can be from a token', () => { + const stack = new Stack(); + const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); + const rate = events.Schedule.rate(lazyDuration); + expect('rate(5 minutes)').toEqual(stack.resolve(rate).expressionString); + + }); + + test('rate can be in minutes', () => { + expect('rate(10 minutes)').toEqual( + events.Schedule.rate(Duration.minutes(10)) + .expressionString); + + }); + + test('rate can be in days', () => { + expect('rate(10 days)').toEqual( + events.Schedule.rate(Duration.days(10)) + .expressionString); + + }); + + test('rate can be in hours', () => { + expect('rate(10 hours)').toEqual( + events.Schedule.rate(Duration.hours(10)) + .expressionString); + + }); + + test('rate can be in seconds', () => { + expect('rate(2 minutes)').toEqual( + events.Schedule.rate(Duration.seconds(120)) + .expressionString); + + }); + + test('rate must not be in seconds when specified as a token', () => { + expect(() => { + events.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); + }).toThrow(/Allowed units for scheduling/); + + }); +}); diff --git a/packages/@aws-cdk/aws-events/test/test.schedule.ts b/packages/@aws-cdk/aws-events/test/test.schedule.ts deleted file mode 100644 index 2d9728e743202..0000000000000 --- a/packages/@aws-cdk/aws-events/test/test.schedule.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { Duration, Stack, Lazy } from '@aws-cdk/core'; -import { Test } from 'nodeunit'; -import * as events from '../lib'; - -export = { - 'cron expressions day and dow are mutex: given weekday'(test: Test) { - // Run every 10 minutes Monday through Friday - test.equal('cron(0/10 * ? * MON-FRI *)', events.Schedule.cron({ - minute: '0/10', - weekDay: 'MON-FRI', - }).expressionString); - test.done(); - }, - - 'cron expressions day and dow are mutex: given month day'(test: Test) { - // Run at 8:00 am (UTC) every 1st day of the month - test.equal('cron(0 8 1 * ? *)', events.Schedule.cron({ - minute: '0', - hour: '8', - day: '1', - }).expressionString); - test.done(); - }, - - 'cron expressions day and dow are mutex: given neither'(test: Test) { - // Run at 10:00 am (UTC) every day - test.equal('cron(0 10 * * ? *)', events.Schedule.cron({ - minute: '0', - hour: '10', - }).expressionString); - test.done(); - }, - - 'rate must be whole number of minutes'(test: Test) { - test.throws(() => { - events.Schedule.rate(Duration.minutes(0.13456)); - }, /'0.13456 minutes' cannot be converted into a whole number of seconds/); - test.done(); - }, - - 'rate must be whole number'(test: Test) { - test.throws(() => { - events.Schedule.rate(Duration.minutes(1/8)); - }, /'0.125 minutes' cannot be converted into a whole number of seconds/); - test.done(); - }, - - 'rate cannot be 0'(test: Test) { - test.throws(() => { - events.Schedule.rate(Duration.days(0)); - }, /Duration cannot be 0/); - test.done(); - }, - - 'rate can be from a token'(test: Test) { - const stack = new Stack(); - const lazyDuration = Duration.minutes(Lazy.number({ produce: () => 5 })); - const rate = events.Schedule.rate(lazyDuration); - test.equal('rate(5 minutes)', stack.resolve(rate).expressionString); - test.done(); - }, - - 'rate can be in minutes'(test: Test) { - test.equal('rate(10 minutes)', - events.Schedule.rate(Duration.minutes(10)) - .expressionString); - test.done(); - }, - - 'rate can be in days'(test: Test) { - test.equal('rate(10 days)', - events.Schedule.rate(Duration.days(10)) - .expressionString); - test.done(); - }, - - 'rate can be in hours'(test: Test) { - test.equal('rate(10 hours)', - events.Schedule.rate(Duration.hours(10)) - .expressionString); - test.done(); - }, - - 'rate can be in seconds'(test: Test) { - test.equal('rate(2 minutes)', - events.Schedule.rate(Duration.seconds(120)) - .expressionString); - test.done(); - }, - - 'rate must not be in seconds when specified as a token'(test: Test) { - test.throws(() => { - events.Schedule.rate(Duration.seconds(Lazy.number({ produce: () => 5 }))); - }, /Allowed units for scheduling/); - test.done(); - }, -}; diff --git a/packages/@aws-cdk/aws-events/test/test.util.ts b/packages/@aws-cdk/aws-events/test/util.test.ts similarity index 53% rename from packages/@aws-cdk/aws-events/test/test.util.ts rename to packages/@aws-cdk/aws-events/test/util.test.ts index 80cbe36ddbba4..eb354ca9c48a0 100644 --- a/packages/@aws-cdk/aws-events/test/test.util.ts +++ b/packages/@aws-cdk/aws-events/test/util.test.ts @@ -1,10 +1,9 @@ -import { Test } from 'nodeunit'; import { mergeEventPattern } from '../lib/util'; -export = { - mergeEventPattern: { - 'happy case'(test: Test) { - test.deepEqual(mergeEventPattern({ +describe('util', () => { + describe('mergeEventPattern', () => { + test('happy case', () => { + expect(mergeEventPattern({ bar: [1, 2], hey: ['happy'], hello: { @@ -16,7 +15,7 @@ export = { hello: { world: ['you'], }, - }), { + })).toEqual({ bar: [1, 2], hey: ['happy', 'day', 'today'], hello: { @@ -24,25 +23,25 @@ export = { case: [1], }, }); - test.done(); - }, - 'merge into an empty destination'(test: Test) { - test.deepEqual(mergeEventPattern(undefined, { foo: ['123'] }), { foo: [123] }); - test.deepEqual(mergeEventPattern(undefined, { foo: { bar: ['123'] } }), { foo: { bar: [123] } }); - test.deepEqual(mergeEventPattern({ }, { foo: { bar: ['123'] } }), { foo: { bar: [123] } }); - test.done(); - }, + }); - 'fails if a field is not an array'(test: Test) { - test.throws(() => mergeEventPattern(undefined, 123), /Invalid event pattern '123', expecting an object or an array/); - test.throws(() => mergeEventPattern(undefined, 'Hello'), /Invalid event pattern '"Hello"', expecting an object or an array/); - test.throws(() => mergeEventPattern(undefined, { foo: '123' }), /Invalid event pattern field { foo: "123" }. All fields must be arrays/); - test.done(); - }, + test('merge into an empty destination', () => { + expect(mergeEventPattern(undefined, { foo: ['123'] })).toEqual({ foo: ['123'] }); + expect(mergeEventPattern(undefined, { foo: { bar: ['123'] } })).toEqual({ foo: { bar: ['123'] } }); + expect(mergeEventPattern({ }, { foo: { bar: ['123'] } })).toEqual({ foo: { bar: ['123'] } }); - 'fails if mismatch between dest and src'(test: Test) { - test.throws(() => mergeEventPattern({ + }); + + test('fails if a field is not an array', () => { + expect(() => mergeEventPattern(undefined, 123)).toThrow(/Invalid event pattern '123', expecting an object or an array/); + expect(() => mergeEventPattern(undefined, 'Hello')).toThrow(/Invalid event pattern '"Hello"', expecting an object or an array/); + expect(() => mergeEventPattern(undefined, { foo: '123' })).toThrow(/Invalid event pattern field { foo: "123" }. All fields must be arrays/); + + }); + + test('fails if mismatch between dest and src', () => { + expect(() => mergeEventPattern({ obj: { array: [1], }, @@ -52,46 +51,46 @@ export = { value: ['hello'], }, }, - }), /Invalid event pattern field array. Type mismatch between existing pattern \[1\] and added pattern \{"value":\["hello"\]\}/); - test.done(); - }, + })).toThrow(/Invalid event pattern field array. Type mismatch between existing pattern \[1\] and added pattern \{"value":\["hello"\]\}/); - 'deduplicate match values in pattern array'(test: Test) { - test.deepEqual(mergeEventPattern({ + }); + + test('deduplicate match values in pattern array', () => { + expect(mergeEventPattern({ 'detail-type': ['AWS API Call via CloudTrail'], }, { 'detail-type': ['AWS API Call via CloudTrail'], - }), { + })).toEqual({ 'detail-type': ['AWS API Call via CloudTrail'], }); - test.deepEqual(mergeEventPattern({ + expect(mergeEventPattern({ time: [{ prefix: '2017-10-02' }], }, { time: [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], - }), { + })).toEqual({ time: [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - test.deepEqual(mergeEventPattern({ + expect(mergeEventPattern({ 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }], }, { 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], - }), { + })).toEqual({ 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - test.deepEqual(mergeEventPattern({ + expect(mergeEventPattern({ 'detail-type': ['AWS API Call via CloudTrail', 'AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }], }, { 'detail-type': ['AWS API Call via CloudTrail', 'AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }, { prefix: '2017-10-02' }], - }), { + })).toEqual({ 'detail-type': ['AWS API Call via CloudTrail'], 'time': [{ prefix: '2017-10-02' }, { prefix: '2017-10-03' }], }); - test.done(); - }, - }, -}; + + }); + }); +}); diff --git a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json index 2a85e28a6984d..ba6e2aecacda8 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json +++ b/packages/@aws-cdk/aws-kinesisfirehose-destinations/test/integ.s3-bucket.lit.expected.json @@ -102,7 +102,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0" }, "S3Key": { "Fn::Join": [ @@ -115,7 +115,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -128,7 +128,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -751,17 +751,17 @@ } }, "Parameters": { - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0": { "Type": "String", - "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 bucket for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C": { "Type": "String", - "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 key for asset version \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9ArtifactHash9AE3702B": { "Type": "String", - "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "Artifact hash for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, "AssetParameters5ee078f2a1957fe672d6cfd84faf49e07b8460758b5cd2669b3df1212a14cd19S3BucketFEDDFB43": { "Type": "String", diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index c604a2ad5bce8..054a660f3529e 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -359,11 +359,12 @@ You can read more about loading data to (or from) S3 here: * Aurora MySQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.LoadFromS3.html) and [export](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Integrating.SaveIntoS3.html). -* Aurora PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Migrating.html) +* Aurora PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraPostgreSQL.Migrating.html#USER_PostgreSQL.S3Import) and [export](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/postgresql-s3-export.html). -* Microsoft SQL Server - [import & export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html) +* Microsoft SQL Server - [import and export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/SQLServer.Procedural.Importing.html) * PostgreSQL - [import](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/PostgreSQL.Procedural.Importing.html) -* Oracle - [import & export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html) + and [export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/postgresql-s3-export.html) +* Oracle - [import and export](https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/oracle-s3-integration.html) The following snippet sets up a database cluster with different S3 buckets where the data is imported and exported - diff --git a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts index e182733d40ede..754667e18bdbc 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster-engine.ts @@ -400,7 +400,7 @@ export interface AuroraPostgresEngineFeatures { readonly s3Import?: boolean; /** - * Whether this version of the Aurora Postgres cluster engine supports the S3 data import feature. + * Whether this version of the Aurora Postgres cluster engine supports the S3 data export feature. * * @default false */ diff --git a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts index 406d6953d63b8..89a63037316cd 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance-engine.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance-engine.ts @@ -244,6 +244,8 @@ export class MariaDbEngineVersion { public static readonly VER_10_2_32 = MariaDbEngineVersion.of('10.2.32', '10.2'); /** Version "10.2.37". */ public static readonly VER_10_2_37 = MariaDbEngineVersion.of('10.2.37', '10.2'); + /** Version "10.2.39". */ + public static readonly VER_10_2_39 = MariaDbEngineVersion.of('10.2.39', '10.2'); /** Version "10.3" (only a major version, without a specific minor version). */ public static readonly VER_10_3 = MariaDbEngineVersion.of('10.3', '10.3'); @@ -459,6 +461,8 @@ export class MysqlEngineVersion { public static readonly VER_5_7_31 = MysqlEngineVersion.of('5.7.31', '5.7'); /** Version "5.7.33". */ public static readonly VER_5_7_33 = MysqlEngineVersion.of('5.7.33', '5.7'); + /** Version "5.7.34". */ + public static readonly VER_5_7_34 = MysqlEngineVersion.of('5.7.34', '5.7'); /** Version "8.0" (only a major version, without a specific minor version). */ public static readonly VER_8_0 = MysqlEngineVersion.of('8.0', '8.0'); @@ -542,6 +546,13 @@ export interface PostgresEngineFeatures { * @default false */ readonly s3Import?: boolean; + + /** + * Whether this version of the Postgres engine supports the S3 data export feature. + * + * @default false + */ + readonly s3Export?: boolean; } /** @@ -776,13 +787,13 @@ export class PostgresEngineVersion { /** Version "10.13". */ public static readonly VER_10_13 = PostgresEngineVersion.of('10.13', '10', { s3Import: true }); /** Version "10.14". */ - public static readonly VER_10_14 = PostgresEngineVersion.of('10.14', '10', { s3Import: true }); + public static readonly VER_10_14 = PostgresEngineVersion.of('10.14', '10', { s3Import: true, s3Export: true }); /** Version "10.15". */ - public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true }); + public static readonly VER_10_15 = PostgresEngineVersion.of('10.15', '10', { s3Import: true, s3Export: true }); /** Version "10.16". */ - public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true }); + public static readonly VER_10_16 = PostgresEngineVersion.of('10.16', '10', { s3Import: true, s3Export: true }); /** Version "10.17". */ - public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true }); + public static readonly VER_10_17 = PostgresEngineVersion.of('10.17', '10', { s3Import: true, s3Export: true }); /** Version "11" (only a major version, without a specific minor version). */ public static readonly VER_11 = PostgresEngineVersion.of('11', '11', { s3Import: true }); @@ -801,13 +812,13 @@ export class PostgresEngineVersion { /** Version "11.8". */ public static readonly VER_11_8 = PostgresEngineVersion.of('11.8', '11', { s3Import: true }); /** Version "11.9". */ - public static readonly VER_11_9 = PostgresEngineVersion.of('11.9', '11', { s3Import: true }); + public static readonly VER_11_9 = PostgresEngineVersion.of('11.9', '11', { s3Import: true, s3Export: true }); /** Version "11.10". */ - public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true }); + public static readonly VER_11_10 = PostgresEngineVersion.of('11.10', '11', { s3Import: true, s3Export: true }); /** Version "11.11". */ - public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true }); + public static readonly VER_11_11 = PostgresEngineVersion.of('11.11', '11', { s3Import: true, s3Export: true }); /** Version "11.12". */ - public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true }); + public static readonly VER_11_12 = PostgresEngineVersion.of('11.12', '11', { s3Import: true, s3Export: true }); /** Version "12" (only a major version, without a specific minor version). */ public static readonly VER_12 = PostgresEngineVersion.of('12', '12', { s3Import: true }); @@ -816,22 +827,22 @@ export class PostgresEngineVersion { /** Version "12.3". */ public static readonly VER_12_3 = PostgresEngineVersion.of('12.3', '12', { s3Import: true }); /** Version "12.4". */ - public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true }); + public static readonly VER_12_4 = PostgresEngineVersion.of('12.4', '12', { s3Import: true, s3Export: true }); /** Version "12.5". */ - public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true }); + public static readonly VER_12_5 = PostgresEngineVersion.of('12.5', '12', { s3Import: true, s3Export: true }); /** Version "12.6". */ - public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true }); + public static readonly VER_12_6 = PostgresEngineVersion.of('12.6', '12', { s3Import: true, s3Export: true }); /** Version "12.7". */ - public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true }); + public static readonly VER_12_7 = PostgresEngineVersion.of('12.7', '12', { s3Import: true, s3Export: true }); /** Version "13" (only a major version, without a specific minor version). */ - public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true }); + public static readonly VER_13 = PostgresEngineVersion.of('13', '13', { s3Import: true, s3Export: true }); /** Version "13.1". */ - public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true }); + public static readonly VER_13_1 = PostgresEngineVersion.of('13.1', '13', { s3Import: true, s3Export: true }); /** Version "13.2". */ - public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true }); + public static readonly VER_13_2 = PostgresEngineVersion.of('13.2', '13', { s3Import: true, s3Export: true }); /** Version "13.3". */ - public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true }); + public static readonly VER_13_3 = PostgresEngineVersion.of('13.3', '13', { s3Import: true, s3Export: true }); /** * Create a new PostgresEngineVersion with an arbitrary version. @@ -861,6 +872,7 @@ export class PostgresEngineVersion { this.postgresMajorVersion = postgresMajorVersion; this._features = { s3Import: postgresFeatures?.s3Import ? 's3Import' : undefined, + s3Export: postgresFeatures?.s3Export ? 's3Export' : undefined, }; } } @@ -1319,6 +1331,10 @@ export class SqlServerEngineVersion { public static readonly VER_13_00_5598_27_V1 = SqlServerEngineVersion.of('13.00.5598.27.v1', '13.00'); /** Version "13.00.5820.21.v1". */ public static readonly VER_13_00_5820_21_V1 = SqlServerEngineVersion.of('13.00.5820.21.v1', '13.00'); + /** Version "13.00.5850.14.v1". */ + public static readonly VER_13_00_5850_14_V1 = SqlServerEngineVersion.of('13.00.5850.14.v1', '13.00'); + /** Version "13.00.5882.1.v1". */ + public static readonly VER_13_00_5882_1_V1 = SqlServerEngineVersion.of('13.00.5882.1.v1', '13.00'); /** Version "14.00" (only a major version, without a specific minor version). */ public static readonly VER_14 = SqlServerEngineVersion.of('14.00', '14.00'); @@ -1347,8 +1363,13 @@ export class SqlServerEngineVersion { public static readonly VER_15 = SqlServerEngineVersion.of('15.00', '15.00'); /** Version "15.00.4043.16.v1". */ public static readonly VER_15_00_4043_16_V1 = SqlServerEngineVersion.of('15.00.4043.16.v1', '15.00'); - /** Version "15.00.4043.23.v1". */ + /** + * Version "15.00.4043.23.v1". + * @deprecated This version is erroneous. You might be looking for {@link SqlServerEngineVersion.VER_15_00_4073_23_V1}, instead. + */ public static readonly VER_15_00_4043_23_V1 = SqlServerEngineVersion.of('15.00.4043.23.v1', '15.00'); + /** Version "15.00.4073.23.v1". */ + public static readonly VER_15_00_4073_23_V1 = SqlServerEngineVersion.of('15.00.4073.23.v1', '15.00'); /** * Create a new SqlServerEngineVersion with an arbitrary version. diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index 20073ad021b40..6ea2418ee1a97 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -5,7 +5,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as logs from '@aws-cdk/aws-logs'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { ArnComponents, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token } from '@aws-cdk/core'; +import { ArnComponents, Duration, FeatureFlags, IResource, Lazy, RemovalPolicy, Resource, Stack, Token, Tokenization } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { DatabaseSecret } from './database-secret'; @@ -124,7 +124,7 @@ export abstract class DatabaseInstanceBase extends Resource implements IDatabase }); public readonly instanceIdentifier = attrs.instanceIdentifier; public readonly dbInstanceEndpointAddress = attrs.instanceEndpointAddress; - public readonly dbInstanceEndpointPort = attrs.port.toString(); + public readonly dbInstanceEndpointPort = Tokenization.stringifyNumber(attrs.port); public readonly instanceEndpoint = new Endpoint(attrs.instanceEndpointAddress, attrs.port); public readonly engine = attrs.engine; protected enableIamAuthentication = true; diff --git a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts index 830ad0c97bd8e..e3b02c48770d4 100644 --- a/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance-engine.test.ts @@ -274,12 +274,12 @@ nodeunitShim({ test.done(); }, - 'returns s3 import feature if the version supports it'(test: Test) { - const engineNewerVersion = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }); + 'returns s3 import/export feature if the version supports it'(test: Test) { + const engineNewerVersion = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_13_3 }); const engineConfig = engineNewerVersion.bindToInstance(new cdk.Stack(), {}); test.equals(engineConfig.features?.s3Import, 's3Import'); - test.equals(engineConfig.features?.s3Export, undefined); + test.equals(engineConfig.features?.s3Export, 's3Export'); test.done(); }, diff --git a/packages/@aws-cdk/aws-rds/test/instance.test.ts b/packages/@aws-cdk/aws-rds/test/instance.test.ts index 4621a5bf6512d..f0591eb2469d1 100644 --- a/packages/@aws-cdk/aws-rds/test/instance.test.ts +++ b/packages/@aws-cdk/aws-rds/test/instance.test.ts @@ -401,6 +401,35 @@ describe('instance', () => { }); + + test('can create a new database instance with fromDatabaseInstanceAttributes using a token for the port', () => { + // GIVEN + const databasePort = new cdk.CfnParameter(stack, 'DatabasePort', { + type: 'Number', + default: 5432, + }).valueAsNumber; + + // WHEN + const instance = rds.DatabaseInstance.fromDatabaseInstanceAttributes(stack, 'DatabaseInstance', { + instanceIdentifier: '', + securityGroups: [], + instanceEndpointAddress: '', + port: databasePort, + }); + + new cdk.CfnOutput(stack, 'portOutput', { + exportName: 'databaseUrl', + value: `${instance.dbInstanceEndpointPort}`, + }); + + // THEN + expect(stack).toHaveOutput({ + exportName: 'databaseUrl', + outputValue: { + Ref: 'DatabasePort', + }, + }); + }); }); test('create a read replica in the same region - with the subnet group name', () => { diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index d939eea365b51..f7cc651b73f1b 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -82,7 +82,6 @@ "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "jest": "^26.6.3", - "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts index bf4fba67df048..0b66c9589cd50 100644 --- a/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone-provider.test.ts @@ -1,11 +1,11 @@ +import '@aws-cdk/assert-internal/jest'; import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; -nodeunitShim({ - 'Hosted Zone Provider': { - 'HostedZoneProvider will return context values if available'(test: Test) { +describe('hosted zone provider', () => { + describe('Hosted Zone Provider', () => { + test('HostedZoneProvider will return context values if available', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' }, @@ -15,7 +15,7 @@ nodeunitShim({ HostedZone.fromLookup(stack, 'Ref', filter); const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; - test.ok(missing && missing.length === 1); + expect(missing && missing.length === 1).toEqual(true); const fakeZoneId = '11111111111111'; const fakeZone = { @@ -38,12 +38,10 @@ nodeunitShim({ const zoneRef = HostedZone.fromLookup(stack2, 'MyZoneProvider', filter); // THEN - test.deepEqual(zoneRef.hostedZoneId, fakeZoneId); - test.done(); - }, - 'HostedZoneProvider will return context values if available when using plain hosted zone id'( - test: Test, - ) { + expect(zoneRef.hostedZoneId).toEqual(fakeZoneId); + + }); + test('HostedZoneProvider will return context values if available when using plain hosted zone id', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '12345', region: 'us-east-1' }, @@ -53,7 +51,7 @@ nodeunitShim({ HostedZone.fromLookup(stack, 'Ref', filter); const missing = SynthUtils.synthesize(stack).assembly.manifest.missing!; - test.ok(missing && missing.length === 1); + expect(missing && missing.length === 1).toEqual(true); const fakeZoneId = '11111111111111'; const fakeZone = { @@ -78,8 +76,8 @@ nodeunitShim({ const zoneId = zone.hostedZoneId; // THEN - test.deepEqual(fakeZoneId, zoneId); - test.done(); - }, - }, + expect(fakeZoneId).toEqual(zoneId); + + }); + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts index a11b3308641d2..7939452fd3421 100644 --- a/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts +++ b/packages/@aws-cdk/aws-route53/test/hosted-zone.test.ts @@ -1,12 +1,11 @@ -import { expect } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone, PublicHostedZone } from '../lib'; -nodeunitShim({ - 'Hosted Zone': { - 'Hosted Zone constructs the ARN'(test: Test) { +describe('hosted zone', () => { + describe('Hosted Zone', () => { + test('Hosted Zone constructs the ARN', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' }, @@ -16,7 +15,7 @@ nodeunitShim({ zoneName: 'testZone', }); - test.deepEqual(stack.resolve(testZone.hostedZoneArn), { + expect(stack.resolve(testZone.hostedZoneArn)).toEqual({ 'Fn::Join': [ '', [ @@ -28,11 +27,11 @@ nodeunitShim({ ], }); - test.done(); - }, - }, - 'Supports tags'(test: Test) { + }); + }); + + test('Supports tags', () => { // GIVEN const stack = new cdk.Stack(); @@ -43,7 +42,7 @@ nodeunitShim({ cdk.Tags.of(hostedZone).add('zoneTag', 'inMyZone'); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -60,10 +59,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'with crossAccountZoneDelegationPrincipal'(test: Test) { + }); + + test('with crossAccountZoneDelegationPrincipal', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' }, @@ -77,7 +76,7 @@ nodeunitShim({ }); // THEN - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -151,23 +150,23 @@ nodeunitShim({ }, }); - test.done(); - }, - 'with crossAccountZoneDelegationPrincipal, throws if name provided without principal'(test: Test) { + }); + + test('with crossAccountZoneDelegationPrincipal, throws if name provided without principal', () => { // GIVEN const stack = new cdk.Stack(undefined, 'TestStack', { env: { account: '123456789012', region: 'us-east-1' }, }); // THEN - test.throws(() => { + expect(() => { new PublicHostedZone(stack, 'HostedZone', { zoneName: 'testZone', crossAccountZoneDelegationRoleName: 'myrole', }); - }, /crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal/); + }).toThrow(/crossAccountZoneDelegationRoleName property is not supported without crossAccountZoneDelegationPrincipal/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/record-set.test.ts b/packages/@aws-cdk/aws-route53/test/record-set.test.ts index a071ad2a1aeea..8e380e3dfcd81 100644 --- a/packages/@aws-cdk/aws-route53/test/record-set.test.ts +++ b/packages/@aws-cdk/aws-route53/test/record-set.test.ts @@ -1,11 +1,11 @@ -import { expect, haveResource, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ResourcePart } from '@aws-cdk/assert-internal'; import * as iam from '@aws-cdk/aws-iam'; import { Duration, RemovalPolicy, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as route53 from '../lib'; -nodeunitShim({ - 'with default ttl'(test: Test) { +describe('record set', () => { + test('with default ttl', () => { // GIVEN const stack = new Stack(); @@ -22,7 +22,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CNAME', HostedZoneId: { @@ -32,11 +32,11 @@ nodeunitShim({ 'zzz', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'with custom ttl'(test: Test) { + test('with custom ttl', () => { // GIVEN const stack = new Stack(); @@ -54,7 +54,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'aa.myzone.', Type: 'CNAME', HostedZoneId: { @@ -64,11 +64,11 @@ nodeunitShim({ 'bbb', ], TTL: '6077', - })); - test.done(); - }, + }); - 'with ttl of 0'(test: Test) { + }); + + test('with ttl of 0', () => { // GIVEN const stack = new Stack(); @@ -86,13 +86,13 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { TTL: '0', - })); - test.done(); - }, + }); + + }); - 'defaults to zone root'(test: Test) { + test('defaults to zone root', () => { // GIVEN const stack = new Stack(); @@ -108,7 +108,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'myzone.', Type: 'A', HostedZoneId: { @@ -117,11 +117,11 @@ nodeunitShim({ ResourceRecords: [ '1.2.3.4', ], - })); - test.done(); - }, + }); + + }); - 'A record with ip addresses'(test: Test) { + test('A record with ip addresses', () => { // GIVEN const stack = new Stack(); @@ -137,7 +137,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'A', HostedZoneId: { @@ -148,11 +148,11 @@ nodeunitShim({ '5.6.7.8', ], TTL: '1800', - })); - test.done(); - }, + }); - 'A record with alias'(test: Test) { + }); + + test('A record with alias', () => { // GIVEN const stack = new Stack(); @@ -177,7 +177,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: '_foo.myzone.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -187,12 +187,12 @@ nodeunitShim({ HostedZoneId: 'Z2P70J7EXAMPLE', DNSName: 'foo.example.com', }, - })); + }); - test.done(); - }, - 'AAAA record with ip addresses'(test: Test) { + }); + + test('AAAA record with ip addresses', () => { // GIVEN const stack = new Stack(); @@ -208,7 +208,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'AAAA', HostedZoneId: { @@ -218,11 +218,11 @@ nodeunitShim({ '2001:0db8:85a3:0000:0000:8a2e:0370:7334', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'AAAA record with alias on zone root'(test: Test) { + test('AAAA record with alias on zone root', () => { // GIVEN const stack = new Stack(); const zone = new route53.HostedZone(stack, 'HostedZone', { @@ -245,7 +245,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'myzone.', HostedZoneId: { Ref: 'HostedZoneDB99F866', @@ -255,12 +255,12 @@ nodeunitShim({ HostedZoneId: 'Z2P70J7EXAMPLE', DNSName: 'foo.example.com', }, - })); + }); + - test.done(); - }, + }); - 'CNAME record'(test: Test) { + test('CNAME record', () => { // GIVEN const stack = new Stack(); @@ -276,7 +276,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CNAME', HostedZoneId: { @@ -286,11 +286,11 @@ nodeunitShim({ 'hello', ], TTL: '1800', - })); - test.done(); - }, + }); - 'TXT record'(test: Test) { + }); + + test('TXT record', () => { // GIVEN const stack = new Stack(); @@ -306,7 +306,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'TXT', HostedZoneId: { @@ -316,11 +316,11 @@ nodeunitShim({ '"should be enclosed with double quotes"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'TXT record with value longer than 255 chars'(test: Test) { + test('TXT record with value longer than 255 chars', () => { // GIVEN const stack = new Stack(); @@ -336,7 +336,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'TXT', HostedZoneId: { @@ -346,11 +346,11 @@ nodeunitShim({ '"hellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohellohello""hello"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'SRV record'(test: Test) { + test('SRV record', () => { // GIVEN const stack = new Stack(); @@ -371,7 +371,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'SRV', HostedZoneId: { @@ -381,11 +381,11 @@ nodeunitShim({ '10 5 8080 aws.com', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'CAA record'(test: Test) { + test('CAA record', () => { // GIVEN const stack = new Stack(); @@ -405,7 +405,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CAA', HostedZoneId: { @@ -415,11 +415,11 @@ nodeunitShim({ '0 issue "ssl.com"', ], TTL: '1800', - })); - test.done(); - }, + }); - 'CAA Amazon record'(test: Test) { + }); + + test('CAA Amazon record', () => { // GIVEN const stack = new Stack(); @@ -433,7 +433,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'myzone.', Type: 'CAA', HostedZoneId: { @@ -443,11 +443,11 @@ nodeunitShim({ '0 issue "amazon.com"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'CAA Amazon record with record name'(test: Test) { + test('CAA Amazon record with record name', () => { // GIVEN const stack = new Stack(); @@ -462,7 +462,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'CAA', HostedZoneId: { @@ -472,11 +472,11 @@ nodeunitShim({ '0 issue "amazon.com"', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'MX record'(test: Test) { + test('MX record', () => { // GIVEN const stack = new Stack(); @@ -495,7 +495,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'mail.myzone.', Type: 'MX', HostedZoneId: { @@ -505,11 +505,11 @@ nodeunitShim({ '10 workmail.aws', ], TTL: '1800', - })); - test.done(); - }, + }); - 'NS record'(test: Test) { + }); + + test('NS record', () => { // GIVEN const stack = new Stack(); @@ -525,7 +525,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'NS', HostedZoneId: { @@ -536,11 +536,11 @@ nodeunitShim({ 'ns-2.awsdns.com.', ], TTL: '1800', - })); - test.done(); - }, + }); - 'DS record'(test: Test) { + }); + + test('DS record', () => { // GIVEN const stack = new Stack(); @@ -556,7 +556,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'www.myzone.', Type: 'DS', HostedZoneId: { @@ -566,11 +566,11 @@ nodeunitShim({ '12345 3 1 123456789abcdef67890123456789abcdef67890', ], TTL: '1800', - })); - test.done(); - }, + }); + + }); - 'Zone delegation record'(test: Test) { + test('Zone delegation record', () => { // GIVEN const stack = new Stack(); @@ -586,7 +586,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Name: 'foo.myzone.', Type: 'NS', HostedZoneId: { @@ -596,11 +596,11 @@ nodeunitShim({ 'ns-1777.awsdns-30.co.uk.', ], TTL: '172800', - })); - test.done(); - }, + }); + + }); - 'Cross account zone delegation record with parentHostedZoneId'(test: Test) { + test('Cross account zone delegation record with parentHostedZoneId', () => { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -621,7 +621,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', @@ -645,15 +645,15 @@ nodeunitShim({ ], }, TTL: 60, - })); - expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + }); + expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { DeletionPolicy: 'Retain', UpdateReplacePolicy: 'Retain', - }, ResourcePart.CompleteDefinition)); - test.done(); - }, + }, ResourcePart.CompleteDefinition); + + }); - 'Cross account zone delegation record with parentHostedZoneName'(test: Test) { + test('Cross account zone delegation record with parentHostedZoneName', () => { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -673,7 +673,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('Custom::CrossAccountZoneDelegation', { + expect(stack).toHaveResource('Custom::CrossAccountZoneDelegation', { ServiceToken: { 'Fn::GetAtt': [ 'CustomCrossAccountZoneDelegationCustomResourceProviderHandler44A84265', @@ -695,11 +695,11 @@ nodeunitShim({ ], }, TTL: 60, - })); - test.done(); - }, + }); - 'Cross account zone delegation record throws when parent id and name both/nither are supplied'(test: Test) { + }); + + test('Cross account zone delegation record throws when parent id and name both/nither are supplied', () => { // GIVEN const stack = new Stack(); const parentZone = new route53.PublicHostedZone(stack, 'ParentHostedZone', { @@ -712,15 +712,15 @@ nodeunitShim({ zoneName: 'sub.myzone.com', }); - test.throws(() => { + expect(() => { new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation1', { delegatedZone: childZone, delegationRole: parentZone.crossAccountZoneDelegationRole!, ttl: Duration.seconds(60), }); - }, /At least one of parentHostedZoneName or parentHostedZoneId is required/); + }).toThrow(/At least one of parentHostedZoneName or parentHostedZoneId is required/); - test.throws(() => { + expect(() => { new route53.CrossAccountZoneDelegationRecord(stack, 'Delegation2', { delegatedZone: childZone, parentHostedZoneId: parentZone.hostedZoneId, @@ -728,8 +728,8 @@ nodeunitShim({ delegationRole: parentZone.crossAccountZoneDelegationRole!, ttl: Duration.seconds(60), }); - }, /Only one of parentHostedZoneName and parentHostedZoneId is supported/); + }).toThrow(/Only one of parentHostedZoneName and parentHostedZoneId is supported/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/route53.test.ts b/packages/@aws-cdk/aws-route53/test/route53.test.ts index e5dd624163a9b..f86b155592db7 100644 --- a/packages/@aws-cdk/aws-route53/test/route53.test.ts +++ b/packages/@aws-cdk/aws-route53/test/route53.test.ts @@ -1,15 +1,15 @@ -import { beASupersetOfTemplate, exactlyMatchTemplate, expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { MatchStyle } from '@aws-cdk/assert-internal'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone, PrivateHostedZone, PublicHostedZone, TxtRecord } from '../lib'; -nodeunitShim({ - 'default properties': { - 'public hosted zone'(test: Test) { +describe('route53', () => { + describe('default properties', () => { + test('public hosted zone', () => { const app = new TestApp(); new PublicHostedZone(app.stack, 'HostedZone', { zoneName: 'test.public' }); - expect(app.stack).to(exactlyMatchTemplate({ + expect(app.stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -18,14 +18,14 @@ nodeunitShim({ }, }, }, - })); - test.done(); - }, - 'private hosted zone'(test: Test) { + }); + + }); + test('private hosted zone', () => { const app = new TestApp(); const vpcNetwork = new ec2.Vpc(app.stack, 'VPC'); new PrivateHostedZone(app.stack, 'HostedZone', { zoneName: 'test.private', vpc: vpcNetwork }); - expect(app.stack).to(beASupersetOfTemplate({ + expect(app.stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -38,16 +38,16 @@ nodeunitShim({ }, }, }, - })); - test.done(); - }, - 'when specifying multiple VPCs'(test: Test) { + }, MatchStyle.SUPERSET); + + }); + test('when specifying multiple VPCs', () => { const app = new TestApp(); const vpcNetworkA = new ec2.Vpc(app.stack, 'VPC1'); const vpcNetworkB = new ec2.Vpc(app.stack, 'VPC2'); new PrivateHostedZone(app.stack, 'HostedZone', { zoneName: 'test.private', vpc: vpcNetworkA }) .addVpc(vpcNetworkB); - expect(app.stack).to(beASupersetOfTemplate({ + expect(app.stack).toMatchTemplate({ Resources: { HostedZoneDB99F866: { Type: 'AWS::Route53::HostedZone', @@ -64,12 +64,12 @@ nodeunitShim({ }, }, }, - })); - test.done(); - }, - }, + }, MatchStyle.SUPERSET); - 'exporting and importing works'(test: Test) { + }); + }); + + test('exporting and importing works', () => { const stack2 = new cdk.Stack(); const importedZone = HostedZone.fromHostedZoneAttributes(stack2, 'Imported', { @@ -83,17 +83,17 @@ nodeunitShim({ values: ['SeeThere'], }); - expect(stack2).to(haveResource('AWS::Route53::RecordSet', { + expect(stack2).toHaveResource('AWS::Route53::RecordSet', { HostedZoneId: 'hosted-zone-id', Name: 'lookHere.cdk.local.', ResourceRecords: ['"SeeThere"'], Type: 'TXT', - })); + }); - test.done(); - }, - 'adds period to name if not provided'(test: Test) { + }); + + test('adds period to name if not provided', () => { // GIVEN const stack = new cdk.Stack(); @@ -103,22 +103,22 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::HostedZone', { + expect(stack).toHaveResource('AWS::Route53::HostedZone', { Name: 'zonename.', - })); - test.done(); - }, + }); - 'fails if zone name ends with a trailing dot'(test: Test) { + }); + + test('fails if zone name ends with a trailing dot', () => { // GIVEN const stack = new cdk.Stack(); // THEN - test.throws(() => new HostedZone(stack, 'MyHostedZone', { zoneName: 'zonename.' }), /zone name must not end with a trailing dot/); - test.done(); - }, + expect(() => new HostedZone(stack, 'MyHostedZone', { zoneName: 'zonename.' })).toThrow(/zone name must not end with a trailing dot/); + + }); - 'a hosted zone can be assiciated with a VPC either upon creation or using "addVpc"'(test: Test) { + test('a hosted zone can be assiciated with a VPC either upon creation or using "addVpc"', () => { // GIVEN const stack = new cdk.Stack(); const vpc1 = new ec2.Vpc(stack, 'VPC1'); @@ -133,7 +133,7 @@ nodeunitShim({ zone.addVpc(vpc3); // THEN - expect(stack).to(haveResource('AWS::Route53::HostedZone', { + expect(stack).toHaveResource('AWS::Route53::HostedZone', { VPCs: [ { VPCId: { @@ -160,22 +160,22 @@ nodeunitShim({ }, }, ], - })); - test.done(); - }, + }); - 'public zone cannot be associated with a vpc (runtime error)'(test: Test) { + }); + + test('public zone cannot be associated with a vpc (runtime error)', () => { // GIVEN const stack = new cdk.Stack(); const zone = new PublicHostedZone(stack, 'MyHostedZone', { zoneName: 'zonename' }); const vpc = new ec2.Vpc(stack, 'VPC'); // THEN - test.throws(() => zone.addVpc(vpc), /Cannot associate public hosted zones with a VPC/); - test.done(); - }, + expect(() => zone.addVpc(vpc)).toThrow(/Cannot associate public hosted zones with a VPC/); + + }); - 'setting up zone delegation'(test: Test) { + test('setting up zone delegation', () => { // GIVEN const stack = new cdk.Stack(); const zone = new PublicHostedZone(stack, 'TopZone', { zoneName: 'top.test' }); @@ -185,17 +185,17 @@ nodeunitShim({ zone.addDelegation(delegate, { ttl: cdk.Duration.seconds(1337) }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Type: 'NS', Name: 'sub.top.test.', HostedZoneId: stack.resolve(zone.hostedZoneId), ResourceRecords: stack.resolve(delegate.hostedZoneNameServers), TTL: '1337', - })); - test.done(); - }, + }); + + }); - 'public hosted zone wiht caaAmazon set to true'(test: Test) { + test('public hosted zone wiht caaAmazon set to true', () => { // GIVEN const stack = new cdk.Stack(); @@ -206,15 +206,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::Route53::RecordSet', { + expect(stack).toHaveResource('AWS::Route53::RecordSet', { Type: 'CAA', Name: 'protected.com.', ResourceRecords: [ '0 issue "amazon.com"', ], - })); - test.done(); - }, + }); + + }); }); class TestApp { diff --git a/packages/@aws-cdk/aws-route53/test/util.test.ts b/packages/@aws-cdk/aws-route53/test/util.test.ts index c6ded4e74f7b4..f6ec2b7cd36a6 100644 --- a/packages/@aws-cdk/aws-route53/test/util.test.ts +++ b/packages/@aws-cdk/aws-route53/test/util.test.ts @@ -1,21 +1,20 @@ import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { HostedZone } from '../lib'; import * as util from '../lib/util'; -nodeunitShim({ - 'throws when zone name ending with a \'.\''(test: Test) { - test.throws(() => util.validateZoneName('zone.name.'), /trailing dot/); - test.done(); - }, +describe('util', () => { + test('throws when zone name ending with a \'.\'', () => { + expect(() => util.validateZoneName('zone.name.')).toThrow(/trailing dot/); - 'accepts a valid domain name'(test: Test) { + }); + + test('accepts a valid domain name', () => { const domainName = 'amazonaws.com'; util.validateZoneName(domainName); - test.done(); - }, - 'providedName ending with a dot returns the name'(test: Test) { + }); + + test('providedName ending with a dot returns the name', () => { // GIVEN const stack = new cdk.Stack(); @@ -27,11 +26,11 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); + + }); - 'providedName that matches zoneName returns providedName with a trailing dot'(test: Test) { + test('providedName that matches zoneName returns providedName with a trailing dot', () => { // GIVEN const stack = new cdk.Stack(); @@ -43,11 +42,11 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); - 'providedName that ends with zoneName returns providedName with a trailing dot'(test: Test) { + }); + + test('providedName that ends with zoneName returns providedName with a trailing dot', () => { // GIVEN const stack = new cdk.Stack(); @@ -59,11 +58,11 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); + + }); - 'providedName that does not match zoneName concatenates providedName and zoneName'(test: Test) { + test('providedName that does not match zoneName concatenates providedName and zoneName', () => { // GIVEN const stack = new cdk.Stack(); @@ -75,7 +74,7 @@ nodeunitShim({ })); // THEN - test.equal(qualified, 'test.domain.com.'); - test.done(); - }, + expect(qualified).toEqual('test.domain.com.'); + + }); }); diff --git a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts index 82706fe29d0ea..35b7ac9c67596 100644 --- a/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts +++ b/packages/@aws-cdk/aws-route53/test/vpc-endpoint-service-domain-name.test.ts @@ -1,4 +1,3 @@ -/* eslint-disable jest/no-disabled-tests */ import { expect as cdkExpect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; import '@aws-cdk/assert-internal/jest'; import { IVpcEndpointServiceLoadBalancer, VpcEndpointService } from '@aws-cdk/aws-ec2'; diff --git a/packages/@aws-cdk/aws-route53resolver/README.md b/packages/@aws-cdk/aws-route53resolver/README.md index 9cf4ab7748b3d..a3bf5946ead7d 100644 --- a/packages/@aws-cdk/aws-route53resolver/README.md +++ b/packages/@aws-cdk/aws-route53resolver/README.md @@ -9,10 +9,101 @@ > > [CFN Resources]: https://docs.aws.amazon.com/cdk/latest/guide/constructs.html#constructs_lib +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. +> They are subject to non-backward compatible changes or removal in any future version. These are +> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be +> announced in the release notes. This means that while you may use them, you may need to update +> your source code when upgrading to a newer version of this package. + --- +## DNS Firewall + +With Route 53 Resolver DNS Firewall, you can filter and regulate outbound DNS traffic for your +virtual private connections (VPCs). To do this, you create reusable collections of filtering rules +in DNS Firewall rule groups and associate the rule groups to your VPC. + +DNS Firewall provides protection for outbound DNS requests from your VPCs. These requests route +through Resolver for domain name resolution. A primary use of DNS Firewall protections is to help +prevent DNS exfiltration of your data. DNS exfiltration can happen when a bad actor compromises +an application instance in your VPC and then uses DNS lookup to send data out of the VPC to a domain +that they control. With DNS Firewall, you can monitor and control the domains that your applications +can query. You can deny access to the domains that you know to be bad and allow all other queries +to pass through. Alternately, you can deny access to all domains except for the ones that you +explicitly trust. + +### Domain lists + +Domain lists can be created using a list of strings, a text file stored in Amazon S3 or a local +text file: + +```ts +const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { + domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']), +}); + +const s3List = new route53resolver.FirewallDomainList(this, 'S3List', { + domains: route53resolver.FirewallDomains.fromS3Url('s3://bucket/prefix/object'), +}); + +const assetList = new route53resolver.FirewallDomainList(this, 'AssetList', { + domains: route53resolver.FirewallDomains.fromAsset('/path/to/domains.txt'), +}); +``` + +The file must be a text file and must contain a single domain per line. + +Use `FirewallDomainList.fromFirewallDomainListId()` to import an existing or [AWS managed domain list](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resolver-dns-firewall-managed-domain-lists.html): + +```ts +// AWSManagedDomainsMalwareDomainList in us-east-1 +const malwareList = route53resolver.FirewallDomainList.fromFirewallDomainListId(this, 'Malware', 'rslvr-fdl-2c46f2ecbfec4dcc'); +``` + +### Rule group + +Create a rule group: + +```ts +new route53resolver.FirewallRuleGroup(this, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList: myBlockList, + // block and reply with NODATA + action: route53resolver.FirewallRuleAction.block(), + }, + ], +}); +``` + +Rules can be added at construction time or using `addRule()`: + +```ts +ruleGroup.addRule({ + priority: 10, + firewallDomainList: blockList, + // block and reply with NXDOMAIN + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.nxDomain()), +}); + +ruleGroup.addRule({ + priority: 20, + firewallDomainList: blockList, + // block and override DNS response with a custom domain + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), +}); +``` + +Use `associate()` to associate a rule group with a VPC: + ```ts -import * as route53resolver from '@aws-cdk/aws-route53resolver'; +ruleGroup.associate({ + priority: 101, + vpc: myVpc, +}) ``` diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts new file mode 100644 index 0000000000000..a6303b2c78114 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-domain-list.ts @@ -0,0 +1,234 @@ +import * as path from 'path'; +import { IBucket } from '@aws-cdk/aws-s3'; +import { Asset } from '@aws-cdk/aws-s3-assets'; +import { IResource, Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnFirewallDomainList } from './route53resolver.generated'; + +/** + * A Firewall Domain List + */ +export interface IFirewallDomainList extends IResource { + /** + * The ID of the domain list + * + * @attribute + */ + readonly firewallDomainListId: string; +} + +/** + * Properties for a Firewall Domain List + */ +export interface FirewallDomainListProps { + /** + * A name for the domain list + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * A list of domains + */ + readonly domains: FirewallDomains; +} + +/** + * A list of domains + */ +export abstract class FirewallDomains { + /** + * Firewall domains created from a list of domains + * + * @param list the list of domains + */ + public static fromList(list: string[]): FirewallDomains { + for (const domain of list) { + if (!/^[\w-.]+$/.test(domain)) { + throw new Error(`Invalid domain: ${domain}. Valid characters: A-Z, a-z, 0-9, _, -, .`); + } + } + + return { + bind(_scope: Construct): DomainsConfig { + return { domains: list }; + }, + }; + } + + /** + * Firewall domains created from the URL of a file stored in Amazon S3. + * The file must be a text file and must contain a single domain per line. + * The content type of the S3 object must be `plain/text`. + * + * @param url S3 bucket url (s3://bucket/prefix/objet). + */ + public static fromS3Url(url: string): FirewallDomains { + if (!Token.isUnresolved(url) && !url.startsWith('s3://')) { + throw new Error(`The S3 URL must start with s3://, got ${url}`); + } + + return { + bind(_scope: Construct): DomainsConfig { + return { domainFileUrl: url }; + }, + }; + } + + /** + * Firewall domains created from a file stored in Amazon S3. + * The file must be a text file and must contain a single domain per line. + * The content type of the S3 object must be `plain/text`. + * + * @param bucket S3 bucket + * @param key S3 key + */ + public static fromS3(bucket: IBucket, key: string): FirewallDomains { + return this.fromS3Url(bucket.s3UrlForObject(key)); + } + + /** + * Firewall domains created from a local disk path to a text file. + * The file must be a text file (`.txt` extension) and must contain a single + * domain per line. It will be uploaded to S3. + * + * @param assetPath path to the text file + */ + public static fromAsset(assetPath: string): FirewallDomains { + // cdk-assets will correctly set the content type for the S3 object + // if the file has the correct extension + if (path.extname(assetPath) !== '.txt') { + throw new Error(`FirewallDomains.fromAsset() expects a file with the .txt extension, got ${assetPath}`); + } + + return { + bind(scope: Construct): DomainsConfig { + const asset = new Asset(scope, 'Domains', { path: assetPath }); + + if (!asset.isFile) { + throw new Error('FirewallDomains.fromAsset() expects a file'); + } + + return { domainFileUrl: asset.s3ObjectUrl }; + }, + }; + + } + + /** Binds the domains to a domain list */ + public abstract bind(scope: Construct): DomainsConfig; +} + +/** + * Domains configuration + */ +export interface DomainsConfig { + /** + * The fully qualified URL or URI of the file stored in Amazon S3 that contains + * the list of domains to import. The file must be a text file and must contain + * a single domain per line. The content type of the S3 object must be `plain/text`. + * + * @default - use `domains` + */ + readonly domainFileUrl?: string; + + /** + * A list of domains + * + * @default - use `domainFileUrl` + */ + readonly domains?: string[]; +} + +/** + * A Firewall Domain List + */ +export class FirewallDomainList extends Resource implements IFirewallDomainList { + /** + * Import an existing Firewall Rule Group + */ + public static fromFirewallDomainListId(scope: Construct, id: string, firewallDomainListId: string): IFirewallDomainList { + class Import extends Resource implements IFirewallDomainList { + public readonly firewallDomainListId = firewallDomainListId; + } + return new Import(scope, id); + } + + public readonly firewallDomainListId: string; + + /** + * The ARN (Amazon Resource Name) of the domain list + * @attribute + */ + public readonly firewallDomainListArn: string; + + /** + * The date and time that the domain list was created + * @attribute + */ + public readonly firewallDomainListCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallDomainListCreatorRequestId: string; + + /** + * The number of domains in the list + * @attribute + */ + public readonly firewallDomainListDomainCount: number; + + /** + * The owner of the list, used only for lists that are not managed by you. + * For example, the managed domain list `AWSManagedDomainsMalwareDomainList` + * has the managed owner name `Route 53 Resolver DNS Firewall`. + * @attribute + */ + public readonly firewallDomainListManagedOwnerName: string; + + /** + * The date and time that the domain list was last modified + * @attribute + */ + public readonly firewallDomainListModificationTime: string; + + /** + * The status of the domain list + * @attribute + */ + public readonly firewallDomainListStatus: string; + + /** + * Additional information about the status of the rule group + * @attribute + */ + public readonly firewallDomainListStatusMessage: string; + + constructor(scope: Construct, id: string, props: FirewallDomainListProps) { + super(scope, id); + + if (props.name && !Token.isUnresolved(props.name) && !/^[\w-.]{1,128}$/.test(props.name)) { + throw new Error(`Invalid domain list name: ${props.name}. The name must have 1-128 characters. Valid characters: A-Z, a-z, 0-9, _, -, .`); + } + + const domainsConfig = props.domains.bind(this); + const domainList = new CfnFirewallDomainList(this, 'Resource', { + name: props.name, + domainFileUrl: domainsConfig.domainFileUrl, + domains: domainsConfig.domains, + }); + + this.firewallDomainListId = domainList.attrId; + this.firewallDomainListArn = domainList.attrArn; + this.firewallDomainListCreationTime = domainList.attrCreationTime; + this.firewallDomainListCreatorRequestId = domainList.attrCreatorRequestId; + this.firewallDomainListDomainCount = domainList.attrDomainCount; + this.firewallDomainListManagedOwnerName = domainList.attrManagedOwnerName; + this.firewallDomainListModificationTime = domainList.attrModificationTime; + this.firewallDomainListStatus = domainList.attrStatus; + this.firewallDomainListStatusMessage = domainList.attrStatusMessage; + } +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts new file mode 100644 index 0000000000000..10281eba1dda1 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group-association.ts @@ -0,0 +1,129 @@ +import { IVpc } from '@aws-cdk/aws-ec2'; +import { Resource, Token } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IFirewallRuleGroup } from './firewall-rule-group'; +import { CfnFirewallRuleGroupAssociation } from './route53resolver.generated'; + +/** + * Options for a Firewall Rule Group Association + */ +export interface FirewallRuleGroupAssociationOptions { + /** + * If enabled, this setting disallows modification or removal of the + * association, to help prevent against accidentally altering DNS firewall + * protections. + * + * @default true + */ + readonly mutationProtection?: boolean; + + /** + * The name of the association + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * The setting that determines the processing order of the rule group among + * the rule groups that are associated with a single VPC. DNS Firewall filters VPC + * traffic starting from rule group with the lowest numeric priority setting. + * + * This value must be greater than 100 and less than 9,000 + */ + readonly priority: number; + + /** + * The VPC that to associate with the rule group. + */ + readonly vpc: IVpc; +} + +/** + * Properties for a Firewall Rule Group Association + */ +export interface FirewallRuleGroupAssociationProps extends FirewallRuleGroupAssociationOptions { + /** + * The firewall rule group which must be associated + */ + readonly firewallRuleGroup: IFirewallRuleGroup; +} + +/** + * A Firewall Rule Group Association + */ +export class FirewallRuleGroupAssociation extends Resource { + /** + * The ARN (Amazon Resource Name) of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationArn: string; + + /** + * The date and time that the association was created + * @attribute + */ + public readonly firewallRuleGroupAssociationCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallRuleGroupAssociationCreatorRequestId: string; + + /** + * The ID of the association + * + * @attribute + */ + public readonly firewallRuleGroupAssociationId: string; + + /** + * The owner of the association, used only for lists that are not managed by you. + * If you use AWS Firewall Manager to manage your firewallls from DNS Firewall, + * then this reports Firewall Manager as the managed owner. + * @attribute + */ + public readonly firewallRuleGroupAssociationManagedOwnerName: string; + + /** + * The date and time that the association was last modified + * @attribute + */ + public readonly firewallRuleGroupAssociationModificationTime: string; + + /** + * The status of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationStatus: string; + + /** + * Additional information about the status of the association + * @attribute + */ + public readonly firewallRuleGroupAssociationStatusMessage: string; + + constructor(scope: Construct, id: string, props: FirewallRuleGroupAssociationProps) { + super(scope, id); + + if (!Token.isUnresolved(props.priority) && (props.priority <= 100 || props.priority >= 9000)) { + throw new Error(`Priority must be greater than 100 and less than 9000, got ${props.priority}`); + } + + const association = new CfnFirewallRuleGroupAssociation(this, 'Resource', { + firewallRuleGroupId: props.firewallRuleGroup.firewallRuleGroupId, + priority: props.priority, + vpcId: props.vpc.vpcId, + }); + + this.firewallRuleGroupAssociationArn = association.attrArn; + this.firewallRuleGroupAssociationCreationTime = association.attrCreationTime; + this.firewallRuleGroupAssociationCreatorRequestId = association.attrCreatorRequestId; + this.firewallRuleGroupAssociationId = association.attrId; + this.firewallRuleGroupAssociationManagedOwnerName = association.attrManagedOwnerName; + this.firewallRuleGroupAssociationModificationTime = association.attrModificationTime; + this.firewallRuleGroupAssociationStatus = association.attrStatus; + this.firewallRuleGroupAssociationStatusMessage = association.attrStatusMessage; + } +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts new file mode 100644 index 0000000000000..4346e23e46435 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/lib/firewall-rule-group.ts @@ -0,0 +1,277 @@ +import { Duration, IResource, Lazy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { IFirewallDomainList } from './firewall-domain-list'; +import { FirewallRuleGroupAssociation, FirewallRuleGroupAssociationOptions } from './firewall-rule-group-association'; +import { CfnFirewallRuleGroup } from './route53resolver.generated'; + +/** + * A Firewall Rule Group + */ +export interface IFirewallRuleGroup extends IResource { + /** + * The ID of the rule group + * + * @attribute + */ + readonly firewallRuleGroupId: string; +} + +/** + * Properties for a Firewall Rule Group + */ +export interface FirewallRuleGroupProps { + /** + * The name of the rule group. + * + * @default - a CloudFormation generated name + */ + readonly name?: string; + + /** + * A list of rules for this group + * + * @default - no rules + */ + readonly rules?: FirewallRule[]; +} + +/** + * A Firewall Rule + */ +export interface FirewallRule { + /** + * The action for this rule + */ + readonly action: FirewallRuleAction; + + /** + * The domain list for this rule + */ + readonly firewallDomainList: IFirewallDomainList; + + /** + * The priority of the rule in the rule group. This value must be unique within + * the rule group. + */ + readonly priority: number; +} + +/** + * A Firewall Rule + */ +export abstract class FirewallRuleAction { + /** + * Permit the request to go through + */ + public static allow(): FirewallRuleAction { + return { action: 'ALLOW' }; + } + + /** + * Permit the request to go through but send an alert to the logs + */ + public static alert(): FirewallRuleAction { + return { action: 'ALERT' }; + } + + /** + * Disallow the request + * + * @param [response=DnsBlockResponse.noData()] The way that you want DNS Firewall to block the request + */ + public static block(response?: DnsBlockResponse): FirewallRuleAction { + return { + action: 'BLOCK', + blockResponse: response ?? DnsBlockResponse.noData(), + }; + } + + /** + * The action that DNS Firewall should take on a DNS query when it matches + * one of the domains in the rule's domain list + */ + public abstract readonly action: string; + + /** + * The way that you want DNS Firewall to block the request + */ + public abstract readonly blockResponse?: DnsBlockResponse; +} + +/** + * The way that you want DNS Firewall to block the request + */ +export abstract class DnsBlockResponse { + /** + * Respond indicating that the query was successful, but no + * response is available for it. + */ + public static noData(): DnsBlockResponse { + return { blockResponse: 'NODATA' }; + } + + /** + * Respond indicating that the domain name that's in the query + * doesn't exist. + */ + public static nxDomain(): DnsBlockResponse { + return { blockResponse: 'NXDOMAIN' }; + } + + /** + * Provides a custom override response to the query + * + * @param domain The custom DNS record to send back in response to the query + * @param [ttl=0] The recommended amount of time for the DNS resolver or + * web browser to cache the provided override record + */ + public static override(domain: string, ttl?: Duration): DnsBlockResponse { + return { + blockResponse: 'OVERRIDE', + blockOverrideDnsType: 'CNAME', + blockOverrideDomain: domain, + blockOverrideTtl: ttl ?? Duration.seconds(0), + }; + } + + /** The DNS record's type */ + public abstract readonly blockOverrideDnsType?: string; + + /** The custom DNS record to send back in response to the query */ + public abstract readonly blockOverrideDomain?: string; + + /** + * The recommended amount of time for the DNS resolver or + * web browser to cache the provided override record + */ + public abstract readonly blockOverrideTtl?: Duration; + + /** The way that you want DNS Firewall to block the request */ + public abstract readonly blockResponse?: string; +} + +/** + * A Firewall Rule Group + */ +export class FirewallRuleGroup extends Resource implements IFirewallRuleGroup { + /** + * Import an existing Firewall Rule Group + */ + public static fromFirewallRuleGroupId(scope: Construct, id: string, firewallRuleGroupId: string): IFirewallRuleGroup { + class Import extends Resource implements IFirewallRuleGroup { + public readonly firewallRuleGroupId = firewallRuleGroupId; + } + return new Import(scope, id); + } + + public readonly firewallRuleGroupId: string; + + /** + * The ARN (Amazon Resource Name) of the rule group + * @attribute + */ + public readonly firewallRuleGroupArn: string; + + /** + * The date and time that the rule group was created + * @attribute + */ + public readonly firewallRuleGroupCreationTime: string; + + /** + * The creator request ID + * @attribute + */ + public readonly firewallRuleGroupCreatorRequestId: string; + + /** + * The date and time that the rule group was last modified + * @attribute + */ + public readonly firewallRuleGroupModificationTime: string; + + /** + * The AWS account ID for the account that created the rule group + * @attribute + */ + public readonly firewallRuleGroupOwnerId: string; + + /** + * The number of rules in the rule group + * @attribute + */ + public readonly firewallRuleGroupRuleCount: number; + + /** + * Whether the rule group is shared with other AWS accounts, + * or was shared with the current account by another AWS account + * @attribute + */ + public readonly firewallRuleGroupShareStatus: string; + + /** + * The status of the rule group + * @attribute + */ + public readonly firewallRuleGroupStatus: string; + + /** + * Additional information about the status of the rule group + * @attribute + */ + public readonly firewallRuleGroupStatusMessage: string; + + private readonly rules: FirewallRule[]; + + constructor(scope: Construct, id: string, props: FirewallRuleGroupProps = {}) { + super(scope, id); + + this.rules = props.rules ?? []; + + const ruleGroup = new CfnFirewallRuleGroup(this, 'Resource', { + name: props.name, + firewallRules: Lazy.any({ produce: () => this.rules.map(renderRule) }), + }); + + this.firewallRuleGroupId = ruleGroup.attrId; + this.firewallRuleGroupArn = ruleGroup.attrArn; + this.firewallRuleGroupCreationTime = ruleGroup.attrCreationTime; + this.firewallRuleGroupCreatorRequestId = ruleGroup.attrCreatorRequestId; + this.firewallRuleGroupModificationTime = ruleGroup.attrModificationTime; + this.firewallRuleGroupOwnerId = ruleGroup.attrOwnerId; + this.firewallRuleGroupRuleCount = ruleGroup.attrRuleCount; + this.firewallRuleGroupShareStatus = ruleGroup.attrShareStatus; + this.firewallRuleGroupStatus = ruleGroup.attrStatus; + this.firewallRuleGroupStatusMessage = ruleGroup.attrStatusMessage; + } + + /** + * Adds a rule to this group + */ + public addRule(rule: FirewallRule): FirewallRuleGroup { + this.rules.push(rule); + return this; + } + + /** + * Associates this Firewall Rule Group with a VPC + */ + public associate(id: string, props: FirewallRuleGroupAssociationOptions): FirewallRuleGroupAssociation { + return new FirewallRuleGroupAssociation(this, id, { + ...props, + firewallRuleGroup: this, + }); + } +} + +function renderRule(rule: FirewallRule): CfnFirewallRuleGroup.FirewallRuleProperty { + return { + action: rule.action.action, + firewallDomainListId: rule.firewallDomainList.firewallDomainListId, + priority: rule.priority, + blockOverrideDnsType: rule.action.blockResponse?.blockOverrideDnsType, + blockOverrideDomain: rule.action.blockResponse?.blockOverrideDomain, + blockOverrideTtl: rule.action.blockResponse?.blockOverrideTtl?.toSeconds(), + blockResponse: rule.action.blockResponse?.blockResponse, + }; +} diff --git a/packages/@aws-cdk/aws-route53resolver/lib/index.ts b/packages/@aws-cdk/aws-route53resolver/lib/index.ts index 7a6d65433e61f..97baf0759d24a 100644 --- a/packages/@aws-cdk/aws-route53resolver/lib/index.ts +++ b/packages/@aws-cdk/aws-route53resolver/lib/index.ts @@ -1,2 +1,6 @@ +export * from './firewall-domain-list'; +export * from './firewall-rule-group'; +export * from './firewall-rule-group-association'; + // AWS::Route53Resolver CloudFormation Resources: export * from './route53resolver.generated'; diff --git a/packages/@aws-cdk/aws-route53resolver/package.json b/packages/@aws-cdk/aws-route53resolver/package.json index da7fe24cfb66d..6eaf9ecf192a6 100644 --- a/packages/@aws-cdk/aws-route53resolver/package.json +++ b/packages/@aws-cdk/aws-route53resolver/package.json @@ -77,15 +77,22 @@ "devDependencies": { "@types/jest": "^26.0.24", "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "pkglint": "0.0.0", "@aws-cdk/assertions": "0.0.0" }, "dependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", "constructs": "^10.0.0" }, @@ -93,7 +100,14 @@ "node": ">= 10.13.0 <13 || >=13.7.0" }, "stability": "experimental", - "maturity": "cfn-only", + "maturity": "experimental", + "awslint": { + "exclude": [ + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallDomainListProps", + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallRuleGroupProps", + "props-physical-name:@aws-cdk/aws-route53resolver.FirewallRuleGroupAssociationProps" + ] + }, "awscdkio": { "announce": false }, diff --git a/packages/@aws-cdk/aws-route53resolver/test/domains.txt b/packages/@aws-cdk/aws-route53resolver/test/domains.txt new file mode 100644 index 0000000000000..872337c768ca7 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/domains.txt @@ -0,0 +1,4 @@ +amazon.com +amazon.co.uk +amazon.fr +amazon.de diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts new file mode 100644 index 0000000000000..9fa768e173535 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-domain-list.test.ts @@ -0,0 +1,88 @@ +import * as path from 'path'; +import { Template } from '@aws-cdk/assertions'; +import { Bucket } from '@aws-cdk/aws-s3'; +import { Stack } from '@aws-cdk/core'; +import { FirewallDomainList, FirewallDomains } from '../lib'; + +let stack: Stack; +beforeEach(() => { + stack = new Stack(); +}); + +test('domain list from strings', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromList(['first-domain.com', 'second-domain.net']), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + Domains: [ + 'first-domain.com', + 'second-domain.net', + ], + }); +}); + +test('domain list from S3 URL', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3Url('s3://bucket/prefix/object'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: 's3://bucket/prefix/object', + }); +}); + +test('domain list from S3', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3(Bucket.fromBucketName(stack, 'Bucket', 'bucket'), 'prefix/object'), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: 's3://bucket/prefix/object', + }); +}); + +test('domain list from asset', () => { + // WHEN + new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromAsset(path.join(__dirname, 'domains.txt')), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallDomainList', { + DomainFileUrl: { + 'Fn::Sub': 's3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a.txt', + }, + }); +}); + +test('throws with invalid name', () => { + expect(() => new FirewallDomainList(stack, 'List', { + name: 'Inv@lid', + domains: FirewallDomains.fromList(['domain.com']), + })).toThrow(/Invalid domain list name/); +}); + +test('throws with invalid domain', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromList(['valid.fr', 'inv@lid.com']), + })).toThrow(/Invalid domain/); +}); + +test('throws with fromAsset and not .txt', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromAsset('image.jpg'), + })).toThrow(/expects a file with the .txt extension/); +}); + +test('throws with invalid S3 URL', () => { + expect(() => new FirewallDomainList(stack, 'List', { + domains: FirewallDomains.fromS3Url('https://invalid/bucket/url'), + })).toThrow(/The S3 URL must start with s3:\/\//); +}); diff --git a/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts new file mode 100644 index 0000000000000..f8868d2bb31d9 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/firewall-rule-group.test.ts @@ -0,0 +1,137 @@ +import { Template } from '@aws-cdk/assertions'; +import { Vpc } from '@aws-cdk/aws-ec2'; +import { Duration, Stack } from '@aws-cdk/core'; +import { DnsBlockResponse, FirewallDomainList, FirewallRuleAction, FirewallRuleGroup, IFirewallDomainList } from '../lib'; + +let stack: Stack; +let firewallDomainList: IFirewallDomainList; +beforeEach(() => { + stack = new Stack(); + firewallDomainList = FirewallDomainList.fromFirewallDomainListId(stack, 'List', 'domain-list-id'); +}); + +test('basic rule group', () => { + // WHEN + new FirewallRuleGroup(stack, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList, + action: FirewallRuleAction.block(), + }, + ], + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'BLOCK', + BlockResponse: 'NODATA', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + ], + }); +}); + +test('use addRule to add rules', () => { + // GIVEN + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup', { + rules: [ + { + priority: 10, + firewallDomainList, + action: FirewallRuleAction.allow(), + }, + ], + }); + + // WHEN + ruleGroup.addRule({ + priority: 20, + firewallDomainList: FirewallDomainList.fromFirewallDomainListId(stack, 'OtherList', 'other-list-id'), + action: FirewallRuleAction.allow(), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'ALLOW', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + { + Action: 'ALLOW', + FirewallDomainListId: 'other-list-id', + Priority: 20, + }, + ], + }); +}); + +test('rule with response override', () => { + // GIVEN + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // WHEN + ruleGroup.addRule({ + priority: 10, + firewallDomainList, + action: FirewallRuleAction.block(DnsBlockResponse.override('amazon.com', Duration.minutes(5))), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroup', { + FirewallRules: [ + { + Action: 'BLOCK', + BlockOverrideDnsType: 'CNAME', + BlockOverrideDomain: 'amazon.com', + BlockOverrideTtl: 300, + BlockResponse: 'OVERRIDE', + FirewallDomainListId: 'domain-list-id', + Priority: 10, + }, + ], + }); +}); + +test('associate rule group with a vpc', () => { + // GIVEN + const vpc = new Vpc(stack, 'Vpc'); + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // WHEN + ruleGroup.associate('Association', { + priority: 101, + vpc, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Route53Resolver::FirewallRuleGroupAssociation', { + FirewallRuleGroupId: { + 'Fn::GetAtt': [ + 'RuleGroup06BA8844', + 'Id', + ], + }, + Priority: 101, + VpcId: { + Ref: 'Vpc8378EB38', + }, + }); +}); + +test('throws when associating with a priority not between 100-9,000', () => { + // GIVEN + const vpc = new Vpc(stack, 'Vpc'); + const ruleGroup = new FirewallRuleGroup(stack, 'RuleGroup'); + + // THEN + expect(() => ruleGroup.associate('Association', { + priority: 100, + vpc, + })).toThrow(/Priority must be greater than 100 and less than 9000/); +}); diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json new file mode 100644 index 0000000000000..c6b43a9c3ccfd --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.expected.json @@ -0,0 +1,321 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/17", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "cdk-route53-resolver-firewall/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "BlockListC03D0423": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "Domains": [ + "bad-domain.com", + "bot-domain.net" + ] + } + }, + "OverrideListF573FB0F": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "Domains": [ + "override-domain.com" + ] + } + }, + "OtherListBA4427B5": { + "Type": "AWS::Route53Resolver::FirewallDomainList", + "Properties": { + "DomainFileUrl": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D" + } + ] + } + ] + } + ] + ] + } + } + }, + "RuleGroup06BA8844": { + "Type": "AWS::Route53Resolver::FirewallRuleGroup", + "Properties": { + "FirewallRules": [ + { + "Action": "BLOCK", + "BlockResponse": "NODATA", + "FirewallDomainListId": { + "Fn::GetAtt": [ + "BlockListC03D0423", + "Id" + ] + }, + "Priority": 10 + }, + { + "Action": "BLOCK", + "BlockOverrideDnsType": "CNAME", + "BlockOverrideDomain": "amazon.com", + "BlockOverrideTtl": 0, + "BlockResponse": "OVERRIDE", + "FirewallDomainListId": { + "Fn::GetAtt": [ + "OverrideListF573FB0F", + "Id" + ] + }, + "Priority": 20 + } + ] + } + }, + "RuleGroupAssociation5494BFB1": { + "Type": "AWS::Route53Resolver::FirewallRuleGroupAssociation", + "Properties": { + "FirewallRuleGroupId": { + "Fn::GetAtt": [ + "RuleGroup06BA8844", + "Id" + ] + }, + "Priority": 101, + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + } + }, + "Parameters": { + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3BucketD6778673": { + "Type": "String", + "Description": "S3 bucket for asset \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + }, + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aS3VersionKey1A69D23D": { + "Type": "String", + "Description": "S3 key for asset version \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + }, + "AssetParameterse820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35aArtifactHashFF61A347": { + "Type": "String", + "Description": "Artifact hash for asset \"e820b3f07bf66854be0dfd6f3ec357a10d644f2011069e5ad07d42f4f89ed35a\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts new file mode 100644 index 0000000000000..7c4b2498af6f5 --- /dev/null +++ b/packages/@aws-cdk/aws-route53resolver/test/integ.firewall.ts @@ -0,0 +1,46 @@ +import * as path from 'path'; +import { Vpc } from '@aws-cdk/aws-ec2'; +import { App, Stack, StackProps } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import * as route53resolver from '../lib'; + +class TestStack extends Stack { + constructor(scope: Construct, id: string, props?: StackProps) { + super(scope, id, props); + + const vpc = new Vpc(this, 'Vpc', { maxAzs: 1 }); + + const blockList = new route53resolver.FirewallDomainList(this, 'BlockList', { + domains: route53resolver.FirewallDomains.fromList(['bad-domain.com', 'bot-domain.net']), + }); + const overrideList = new route53resolver.FirewallDomainList(this, 'OverrideList', { + domains: route53resolver.FirewallDomains.fromList(['override-domain.com']), + }); + + new route53resolver.FirewallDomainList(this, 'OtherList', { + domains: route53resolver.FirewallDomains.fromAsset(path.join(__dirname, 'domains.txt')), + }); + + const ruleGroup = new route53resolver.FirewallRuleGroup(this, 'RuleGroup'); + + ruleGroup.addRule({ + priority: 10, + firewallDomainList: blockList, + action: route53resolver.FirewallRuleAction.block(), + }); + ruleGroup.addRule({ + priority: 20, + firewallDomainList: overrideList, + action: route53resolver.FirewallRuleAction.block(route53resolver.DnsBlockResponse.override('amazon.com')), + }); + + ruleGroup.associate('Association', { + priority: 101, + vpc, + }); + } +} + +const app = new App(); +new TestStack(app, 'cdk-route53-resolver-firewall'); +app.synth(); diff --git a/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts b/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts deleted file mode 100644 index 465c7bdea0693..0000000000000 --- a/packages/@aws-cdk/aws-route53resolver/test/route53resolver.test.ts +++ /dev/null @@ -1,6 +0,0 @@ -import '@aws-cdk/assertions'; -import {} from '../lib'; - -test('No tests are specified for this package', () => { - expect(true).toBe(true); -}); diff --git a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts index 5dd144b446e8e..f431aacf3fca2 100644 --- a/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts +++ b/packages/@aws-cdk/aws-s3/lib/auto-delete-objects-handler/index.ts @@ -6,10 +6,25 @@ const s3 = new S3(); export async function handler(event: AWSLambda.CloudFormationCustomResourceEvent) { switch (event.RequestType) { case 'Create': - case 'Update': return; + case 'Update': + return onUpdate(event); case 'Delete': - return onDelete(event); + return onDelete(event.ResourceProperties?.BucketName); + } +} + +async function onUpdate(event: AWSLambda.CloudFormationCustomResourceEvent) { + const updateEvent = event as AWSLambda.CloudFormationCustomResourceUpdateEvent; + const oldBucketName = updateEvent.OldResourceProperties?.BucketName; + const newBucketName = updateEvent.ResourceProperties?.BucketName; + const bucketNameHasChanged = newBucketName != null && oldBucketName != null && newBucketName !== oldBucketName; + + /* If the name of the bucket has changed, CloudFormation will try to delete the bucket + and create a new one with the new name. So we have to delete the contents of the + bucket so that this operation does not fail. */ + if (bucketNameHasChanged) { + return onDelete(oldBucketName); } } @@ -23,7 +38,7 @@ async function emptyBucket(bucketName: string) { const contents = [...listedObjects.Versions ?? [], ...listedObjects.DeleteMarkers ?? []]; if (contents.length === 0) { return; - }; + } const records = contents.map((record: any) => ({ Key: record.Key, VersionId: record.VersionId })); await s3.deleteObjects({ Bucket: bucketName, Delete: { Objects: records } }).promise(); @@ -33,8 +48,7 @@ async function emptyBucket(bucketName: string) { } } -async function onDelete(deleteEvent: AWSLambda.CloudFormationCustomResourceDeleteEvent) { - const bucketName = deleteEvent.ResourceProperties?.BucketName; +async function onDelete(bucketName?: string) { if (!bucketName) { throw new Error('No BucketName was provided.'); } diff --git a/packages/@aws-cdk/aws-s3/package.json b/packages/@aws-cdk/aws-s3/package.json index 8acecf7d37f2a..3364c56fbbcc8 100644 --- a/packages/@aws-cdk/aws-s3/package.json +++ b/packages/@aws-cdk/aws-s3/package.json @@ -79,8 +79,8 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", + "jest": "^26.6.3", "pkglint": "0.0.0", - "nodeunit-shim": "0.0.0", "@aws-cdk/assert-internal": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-s3/test/aspect.test.ts b/packages/@aws-cdk/aws-s3/test/aspect.test.ts index ef0394a4cf9cb..023ef826c18f8 100644 --- a/packages/@aws-cdk/aws-s3/test/aspect.test.ts +++ b/packages/@aws-cdk/aws-s3/test/aspect.test.ts @@ -1,12 +1,11 @@ -// import { expect, haveResource, haveResourceLike, SynthUtils } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { SynthUtils } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; import { IConstruct } from 'constructs'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; -nodeunitShim({ - 'bucket must have versioning: failure'(test: Test) { +describe('aspect', () => { + test('bucket must have versioning: failure', () => { // GIVEN const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket'); @@ -17,12 +16,12 @@ nodeunitShim({ // THEN const assembly = SynthUtils.synthesize(stack); const errorMessage = assembly.messages.find(m => m.entry.data === 'Bucket versioning is not enabled'); - test.ok(errorMessage, 'Error message not reported'); + expect(errorMessage).toBeDefined(); - test.done(); - }, - 'bucket must have versioning: success'(test: Test) { + }); + + test('bucket must have versioning: success', () => { // GIVEN const stack = new cdk.Stack(); new s3.Bucket(stack, 'MyBucket', { @@ -34,10 +33,10 @@ nodeunitShim({ // THEN const assembly = SynthUtils.synthesize(stack); - test.deepEqual(assembly.messages, []); + expect(assembly.messages.length).toEqual(0); + - test.done(); - }, + }); }); class BucketVersioningChecker implements cdk.IAspect { diff --git a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts index e5a7072441974..b09b9e5e4f5b3 100644 --- a/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts +++ b/packages/@aws-cdk/aws-s3/test/auto-delete-objects-handler.test.ts @@ -37,7 +37,51 @@ test('does nothing on create event', async () => { expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); }); -test('does nothing on update event', async () => { +test('does nothing on update event when everything remains the same', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the bucket name remains the same but the service token changes', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + OldResourceProperties: { + ServiceToken: 'Bar', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('does nothing on update event when the old resource properties are absent', async () => { // GIVEN const event: Partial = { RequestType: 'Update', @@ -55,6 +99,63 @@ test('does nothing on update event', async () => { expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); }); +test('does nothing on update event when the new resource properties are absent', async () => { + // GIVEN + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(0); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(0); +}); + +test('deletes all objects when the name changes on update event', async () => { + // GIVEN + mockS3Client.promise.mockResolvedValue({ // listObjectVersions() call + Versions: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }); + + const event: Partial = { + RequestType: 'Update', + OldResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket', + }, + ResourceProperties: { + ServiceToken: 'Foo', + BucketName: 'MyBucket-renamed', + }, + }; + + // WHEN + await invokeHandler(event); + + // THEN + expect(mockS3Client.listObjectVersions).toHaveBeenCalledTimes(1); + expect(mockS3Client.listObjectVersions).toHaveBeenCalledWith({ Bucket: 'MyBucket' }); + expect(mockS3Client.deleteObjects).toHaveBeenCalledTimes(1); + expect(mockS3Client.deleteObjects).toHaveBeenCalledWith({ + Bucket: 'MyBucket', + Delete: { + Objects: [ + { Key: 'Key1', VersionId: 'VersionId1' }, + { Key: 'Key2', VersionId: 'VersionId2' }, + ], + }, + }); +}); + test('deletes no objects on delete event when bucket has no objects', async () => { // GIVEN mockS3Client.promise.mockResolvedValue({ Versions: [] }); // listObjectVersions() call diff --git a/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts b/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts index b64ab3d99df43..13b4b798bbd09 100644 --- a/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts +++ b/packages/@aws-cdk/aws-s3/test/bucket-policy.test.ts @@ -1,14 +1,13 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { AnyPrincipal, PolicyStatement } from '@aws-cdk/aws-iam'; import { RemovalPolicy, Stack, App } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; // to make it easy to copy & paste from output: /* eslint-disable quote-props */ -nodeunitShim({ - 'default properties'(test: Test) { +describe('bucket policy', () => { + test('default properties', () => { const stack = new Stack(); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -21,7 +20,7 @@ nodeunitShim({ principals: [new AnyPrincipal()], })); - expect(stack).to(haveResource('AWS::S3::BucketPolicy', { + expect(stack).toHaveResource('AWS::S3::BucketPolicy', { Bucket: { 'Ref': 'MyBucketF68F3FF0', }, @@ -36,12 +35,12 @@ nodeunitShim({ }, ], }, - })); + }); - test.done(); - }, - 'when specifying a removalPolicy at creation'(test: Test) { + }); + + test('when specifying a removalPolicy at creation', () => { const stack = new Stack(); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -55,7 +54,7 @@ nodeunitShim({ principals: [new AnyPrincipal()], })); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -86,10 +85,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'when specifying a removalPolicy after creation'(test: Test) { + }); + + test('when specifying a removalPolicy after creation', () => { const stack = new Stack(); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -100,7 +99,7 @@ nodeunitShim({ })); myBucket.policy?.applyRemovalPolicy(RemovalPolicy.RETAIN); - expect(stack).toMatch({ + expect(stack).toMatchTemplate({ 'Resources': { 'MyBucketF68F3FF0': { 'Type': 'AWS::S3::Bucket', @@ -131,10 +130,10 @@ nodeunitShim({ }, }); - test.done(); - }, - 'fails if bucket policy has no actions'(test: Test) { + }); + + test('fails if bucket policy has no actions', () => { const app = new App(); const stack = new Stack(app, 'my-stack'); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -143,12 +142,12 @@ nodeunitShim({ principals: [new AnyPrincipal()], })); - test.throws(() => app.synth(), /A PolicyStatement must specify at least one \'action\' or \'notAction\'/); + expect(() => app.synth()).toThrow(/A PolicyStatement must specify at least one \'action\' or \'notAction\'/); + - test.done(); - }, + }); - 'fails if bucket policy has no IAM principals'(test: Test) { + test('fails if bucket policy has no IAM principals', () => { const app = new App(); const stack = new Stack(app, 'my-stack'); const myBucket = new s3.Bucket(stack, 'MyBucket'); @@ -157,8 +156,8 @@ nodeunitShim({ actions: ['s3:GetObject*'], })); - test.throws(() => app.synth(), /A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); + expect(() => app.synth()).toThrow(/A PolicyStatement used in a resource-based policy must specify at least one IAM principal/); + - test.done(); - }, + }); }); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-s3/test/cors.test.ts b/packages/@aws-cdk/aws-s3/test/cors.test.ts index 5769c1059640d..feddf0b159669 100644 --- a/packages/@aws-cdk/aws-s3/test/cors.test.ts +++ b/packages/@aws-cdk/aws-s3/test/cors.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { Bucket, HttpMethods } from '../lib'; -nodeunitShim({ - 'Can use addCors() to add a CORS configuration'(test: Test) { +describe('cors', () => { + test('Can use addCors() to add a CORS configuration', () => { // GIVEN const stack = new Stack(); @@ -16,19 +15,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { CorsConfiguration: { CorsRules: [{ AllowedMethods: ['GET', 'HEAD'], AllowedOrigins: ['https://example.com'], }], }, - })); + }); + - test.done(); - }, + }); - 'Bucket with multiple cors configurations'(test: Test) { + test('Bucket with multiple cors configurations', () => { // GIVEN const stack = new Stack(); @@ -74,7 +73,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { CorsConfiguration: { CorsRules: [ { @@ -114,8 +113,8 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json index 107132c1cd2dd..6623d9d3e7a8c 100644 --- a/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json +++ b/packages/@aws-cdk/aws-s3/test/integ.bucket-auto-delete-objects.expected.json @@ -102,7 +102,7 @@ "Properties": { "Code": { "S3Bucket": { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0" }, "S3Key": { "Fn::Join": [ @@ -115,7 +115,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -128,7 +128,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51" + "Ref": "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C" } ] } @@ -289,17 +289,17 @@ } }, "Parameters": { - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3BucketF01ADF6B": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3BucketD715D8B0": { "Type": "String", - "Description": "S3 bucket for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 bucket for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1S3VersionKey6FC34F51": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9S3VersionKey6E76822C": { "Type": "String", - "Description": "S3 key for asset version \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "S3 key for asset version \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, - "AssetParameters1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1ArtifactHash9ECACDFD": { + "AssetParametersfe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9ArtifactHash9AE3702B": { "Type": "String", - "Description": "Artifact hash for asset \"1a8becf42c48697a059094af1e94aa6bc6df0512d30433db8c22618ca02dfca1\"" + "Description": "Artifact hash for asset \"fe5df38824187483182e1459db47adfae2a515aa4caedd437fc4033a0c5b3de9\"" }, "AssetParametersf7ee44e9b6217d201200d9abd42c6493b0b11e86be8a7f36163c3ea049c54653S3BucketDB5FAF47": { "Type": "String", diff --git a/packages/@aws-cdk/aws-s3/test/metrics.test.ts b/packages/@aws-cdk/aws-s3/test/metrics.test.ts index bf7e57e04b557..9072c33a5fcb6 100644 --- a/packages/@aws-cdk/aws-s3/test/metrics.test.ts +++ b/packages/@aws-cdk/aws-s3/test/metrics.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { Bucket } from '../lib'; -nodeunitShim({ - 'Can use addMetrics() to add a metric configuration'(test: Test) { +describe('metrics', () => { + test('Can use addMetrics() to add a metric configuration', () => { // GIVEN const stack = new Stack(); @@ -15,16 +14,16 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', }], - })); + }); + - test.done(); - }, + }); - 'Bucket with metrics on prefix'(test: Test) { + test('Bucket with metrics on prefix', () => { // GIVEN const stack = new Stack(); @@ -37,17 +36,17 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', Prefix: 'prefix', }], - })); + }); + - test.done(); - }, + }); - 'Bucket with metrics on tag filter'(test: Test) { + test('Bucket with metrics on tag filter', () => { // GIVEN const stack = new Stack(); @@ -60,7 +59,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', TagFilters: [ @@ -68,12 +67,12 @@ nodeunitShim({ { Key: 'tagname2', Value: 'tagvalue2' }, ], }], - })); + }); + - test.done(); - }, + }); - 'Bucket with multiple metric configurations'(test: Test) { + test('Bucket with multiple metric configurations', () => { // GIVEN const stack = new Stack(); @@ -93,7 +92,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { MetricsConfigurations: [{ Id: 'test', TagFilters: [ @@ -105,8 +104,8 @@ nodeunitShim({ Id: 'test2', Prefix: 'prefix', }], - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/notification.test.ts b/packages/@aws-cdk/aws-s3/test/notification.test.ts index 75906826ddeff..2f3178fb42af0 100644 --- a/packages/@aws-cdk/aws-s3/test/notification.test.ts +++ b/packages/@aws-cdk/aws-s3/test/notification.test.ts @@ -1,10 +1,10 @@ -import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; +import { ResourcePart } from '@aws-cdk/assert-internal'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import * as s3 from '../lib'; -nodeunitShim({ - 'when notification is added a custom s3 bucket notification resource is provisioned'(test: Test) { +describe('notification', () => { + test('when notification is added a custom s3 bucket notification resource is provisioned', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -16,8 +16,8 @@ nodeunitShim({ }), }); - expect(stack).to(haveResource('AWS::S3::Bucket')); - expect(stack).to(haveResource('Custom::S3BucketNotifications', { + expect(stack).toHaveResource('AWS::S3::Bucket'); + expect(stack).toHaveResource('Custom::S3BucketNotifications', { NotificationConfiguration: { TopicConfigurations: [ { @@ -28,12 +28,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'can specify prefix and suffix filter rules'(test: Test) { + test('can specify prefix and suffix filter rules', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -45,7 +45,7 @@ nodeunitShim({ }), }, { prefix: 'images/', suffix: '.png' }); - expect(stack).to(haveResource('Custom::S3BucketNotifications', { + expect(stack).toHaveResource('Custom::S3BucketNotifications', { NotificationConfiguration: { TopicConfigurations: [ { @@ -70,12 +70,12 @@ nodeunitShim({ }, ], }, - })); + }); + - test.done(); - }, + }); - 'the notification lambda handler must depend on the role to prevent executing too early'(test: Test) { + test('the notification lambda handler must depend on the role to prevent executing too early', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); @@ -87,7 +87,7 @@ nodeunitShim({ }), }); - expect(stack).to(haveResourceLike('AWS::Lambda::Function', { + expect(stack).toHaveResourceLike('AWS::Lambda::Function', { Type: 'AWS::Lambda::Function', Properties: { Role: { @@ -99,38 +99,38 @@ nodeunitShim({ }, DependsOn: ['BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleDefaultPolicy2CF63D36', 'BucketNotificationsHandler050a0587b7544547bf325f094a3db834RoleB6FB88EC'], - }, ResourcePart.CompleteDefinition ) ); + }, ResourcePart.CompleteDefinition ); - test.done(); - }, - 'throws with multiple prefix rules in a filter'(test: Test) { + }); + + test('throws with multiple prefix rules in a filter', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); - test.throws(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { + expect(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { bind: () => ({ arn: 'ARN', type: s3.BucketNotificationDestinationType.TOPIC, }), - }, { prefix: 'images/' }, { prefix: 'archive/' }), /prefix rule/); + }, { prefix: 'images/' }, { prefix: 'archive/' })).toThrow(/prefix rule/); + - test.done(); - }, + }); - 'throws with multiple suffix rules in a filter'(test: Test) { + test('throws with multiple suffix rules in a filter', () => { const stack = new cdk.Stack(); const bucket = new s3.Bucket(stack, 'MyBucket'); - test.throws(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { + expect(() => bucket.addEventNotification(s3.EventType.OBJECT_CREATED, { bind: () => ({ arn: 'ARN', type: s3.BucketNotificationDestinationType.TOPIC, }), - }, { suffix: '.png' }, { suffix: '.zip' }), /suffix rule/); + }, { suffix: '.png' }, { suffix: '.zip' })).toThrow(/suffix rule/); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/rules.test.ts b/packages/@aws-cdk/aws-s3/test/rules.test.ts index fb613bcf109e8..818ae7d830439 100644 --- a/packages/@aws-cdk/aws-s3/test/rules.test.ts +++ b/packages/@aws-cdk/aws-s3/test/rules.test.ts @@ -1,10 +1,9 @@ -import { expect, haveResource } from '@aws-cdk/assert-internal'; +import '@aws-cdk/assert-internal/jest'; import { Duration, Stack } from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { Bucket, StorageClass } from '../lib'; -nodeunitShim({ - 'Bucket with expiration days'(test: Test) { +describe('rules', () => { + test('Bucket with expiration days', () => { // GIVEN const stack = new Stack(); @@ -16,19 +15,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpirationInDays: 30, Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); - 'Can use addLifecycleRule() to add a lifecycle rule'(test: Test) { + test('Can use addLifecycleRule() to add a lifecycle rule', () => { // GIVEN const stack = new Stack(); @@ -39,19 +38,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpirationInDays: 30, Status: 'Enabled', }], }, - })); + }); - test.done(); - }, - 'Bucket with expiration date'(test: Test) { + }); + + test('Bucket with expiration date', () => { // GIVEN const stack = new Stack(); @@ -63,19 +62,19 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpirationDate: '2018-01-01T00:00:00', Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); - 'Bucket with transition rule'(test: Test) { + test('Bucket with transition rule', () => { // GIVEN const stack = new Stack(); @@ -90,7 +89,7 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ Transitions: [{ @@ -100,23 +99,23 @@ nodeunitShim({ Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); - 'Noncurrent rule on nonversioned bucket fails'(test: Test) { + test('Noncurrent rule on nonversioned bucket fails', () => { // GIVEN const stack = new Stack(); // WHEN: Fail because of lack of versioning - test.throws(() => { + expect(() => { new Bucket(stack, 'Bucket1', { lifecycleRules: [{ noncurrentVersionExpiration: Duration.days(10), }], }); - }); + }).toThrow(); // WHEN: Succeeds because versioning is enabled new Bucket(stack, 'Bucket2', { @@ -126,10 +125,10 @@ nodeunitShim({ }], }); - test.done(); - }, - 'Bucket with expiredObjectDeleteMarker'(test: Test) { + }); + + test('Bucket with expiredObjectDeleteMarker', () => { // GIVEN const stack = new Stack(); @@ -141,15 +140,15 @@ nodeunitShim({ }); // THEN - expect(stack).to(haveResource('AWS::S3::Bucket', { + expect(stack).toHaveResource('AWS::S3::Bucket', { LifecycleConfiguration: { Rules: [{ ExpiredObjectDeleteMarker: true, Status: 'Enabled', }], }, - })); + }); + - test.done(); - }, + }); }); diff --git a/packages/@aws-cdk/aws-s3/test/util.test.ts b/packages/@aws-cdk/aws-s3/test/util.test.ts index 528b83b10404b..e688932a0f7eb 100644 --- a/packages/@aws-cdk/aws-s3/test/util.test.ts +++ b/packages/@aws-cdk/aws-s3/test/util.test.ts @@ -1,68 +1,69 @@ +import '@aws-cdk/assert-internal/jest'; import * as cdk from '@aws-cdk/core'; -import { nodeunitShim, Test } from 'nodeunit-shim'; import { parseBucketArn, parseBucketName } from '../lib/util'; -nodeunitShim({ - parseBucketArn: { - 'explicit arn'(test: Test) { +describe('utils', () => { + describe('parseBucketArn', () => { + test('explicit arn', () => { const stack = new cdk.Stack(); const bucketArn = 'my:bucket:arn'; - test.deepEqual(parseBucketArn(stack, { bucketArn }), bucketArn); - test.done(); - }, + expect(parseBucketArn(stack, { bucketArn })).toEqual(bucketArn); - 'produce arn from bucket name'(test: Test) { + }); + + test('produce arn from bucket name', () => { const stack = new cdk.Stack(); const bucketName = 'hello'; - test.deepEqual(stack.resolve(parseBucketArn(stack, { bucketName })), { + expect(stack.resolve(parseBucketArn(stack, { bucketName }))).toEqual({ 'Fn::Join': ['', ['arn:', { Ref: 'AWS::Partition' }, ':s3:::hello']], }); - test.done(); - }, - 'fails if neither arn nor name are provided'(test: Test) { + }); + + test('fails if neither arn nor name are provided', () => { const stack = new cdk.Stack(); - test.throws(() => parseBucketArn(stack, {}), /Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); - test.done(); - }, - }, + expect(() => parseBucketArn(stack, {})).toThrow(/Cannot determine bucket ARN. At least `bucketArn` or `bucketName` is needed/); - parseBucketName: { + }); + }); - 'explicit name'(test: Test) { + describe('parseBucketName', () => { + + test('explicit name', () => { const stack = new cdk.Stack(); const bucketName = 'foo'; - test.deepEqual(stack.resolve(parseBucketName(stack, { bucketName })), 'foo'); - test.done(); - }, + expect(stack.resolve(parseBucketName(stack, { bucketName }))).toEqual('foo'); + + }); - 'extract bucket name from string arn'(test: Test) { + test('extract bucket name from string arn', () => { const stack = new cdk.Stack(); const bucketArn = 'arn:aws:s3:::my-bucket'; - test.deepEqual(stack.resolve(parseBucketName(stack, { bucketArn })), 'my-bucket'); - test.done(); - }, + expect(stack.resolve(parseBucketName(stack, { bucketArn }))).toEqual('my-bucket'); - 'can parse bucket name even if it contains a token'(test: Test) { + }); + + test('can parse bucket name even if it contains a token', () => { const stack = new cdk.Stack(); const bucketArn = `arn:aws:s3:::${cdk.Token.asString({ Ref: 'my-bucket' })}`; - test.deepEqual( + expect( stack.resolve(parseBucketName(stack, { bucketArn })), + ).toEqual( { Ref: 'my-bucket' }, ); - test.done(); - }, - 'fails if ARN has invalid format'(test: Test) { + }); + + test('fails if ARN has invalid format', () => { const stack = new cdk.Stack(); const bucketArn = 'invalid-arn'; - test.throws(() => parseBucketName(stack, { bucketArn }), /ARNs must/); - test.done(); - }, - }, + expect(() => parseBucketName(stack, { bucketArn })).toThrow(/ARNs must/); + + }); + }); }); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts index 5e1e1d708d2d2..bfb3d94e28b5c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/emr-create-cluster.ts @@ -544,7 +544,7 @@ export namespace EmrCreateCluster { * */ export enum SpotTimeoutAction { - /**\ + /** * SWITCH_TO_ON_DEMAND */ SWITCH_TO_ON_DEMAND = 'SWITCH_TO_ON_DEMAND', @@ -554,6 +554,21 @@ export namespace EmrCreateCluster { TERMINATE_CLUSTER = 'TERMINATE_CLUSTER', } + /** + * Spot Allocation Strategies + * + * Specifies the strategy to use in launching Spot Instance fleets. For example, "capacity-optimized" launches instances from Spot Instance pools with optimal capacity for the number of instances that are launching. + * + * @see https://docs.aws.amazon.com/emr/latest/APIReference/API_SpotProvisioningSpecification.html + * + */ + export enum SpotAllocationStrategy { + /** + * Capacity-optimized, which launches instances from Spot Instance pools with optimal capacity for the number of instances that are launching. + */ + CAPACITY_OPTIMIZED = 'capacity-optimized', + } + /** * The launch specification for Spot instances in the instance fleet, which determines the defined duration and provisioning timeout behavior. * @@ -561,10 +576,16 @@ export namespace EmrCreateCluster { * */ export interface SpotProvisioningSpecificationProperty { + /** + * Specifies the strategy to use in launching Spot Instance fleets. + * + * @default - No allocation strategy, i.e. spot instance type will be chosen based on current price only + */ + readonly allocationStrategy?: SpotAllocationStrategy; /** * The defined duration for Spot instances (also known as Spot blocks) in minutes. * - * @default No blockDurationMinutes + * @default - No blockDurationMinutes */ readonly blockDurationMinutes?: number; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts index 37cf474c83ea0..c8ae8a50a360c 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/emr/private/cluster-utils.ts @@ -123,6 +123,7 @@ export function InstanceTypeConfigPropertyToJson(property: EmrCreateCluster.Inst export function InstanceFleetProvisioningSpecificationsPropertyToJson(property: EmrCreateCluster.InstanceFleetProvisioningSpecificationsProperty) { return { SpotSpecification: { + AllocationStrategy: cdk.stringToCloudFormation(property.spotSpecification.allocationStrategy), BlockDurationMinutes: cdk.numberToCloudFormation(property.spotSpecification.blockDurationMinutes), TimeoutAction: cdk.stringToCloudFormation(property.spotSpecification.timeoutAction?.valueOf()), TimeoutDurationMinutes: cdk.numberToCloudFormation(property.spotSpecification.timeoutDurationMinutes), 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 5f40b28be29ac..273a580a6a38e 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 @@ -641,6 +641,123 @@ test('Create Cluster with Instances configuration', () => { }); }); +test('Create Cluster with InstanceFleet with allocation strategy=capacity-optimized', () => { + // WHEN + const task = new EmrCreateCluster(stack, 'Task', { + instances: { + instanceFleets: [{ + instanceFleetType: EmrCreateCluster.InstanceRoleType.MASTER, + instanceTypeConfigs: [{ + bidPrice: '1', + bidPriceAsPercentageOfOnDemandPrice: 1, + configurations: [{ + classification: 'Classification', + properties: { + Key: 'Value', + }, + }], + ebsConfiguration: { + ebsBlockDeviceConfigs: [{ + volumeSpecification: { + iops: 1, + volumeSize: cdk.Size.gibibytes(1), + volumeType: EmrCreateCluster.EbsBlockDeviceVolumeType.STANDARD, + }, + volumesPerInstance: 1, + }], + ebsOptimized: true, + }, + instanceType: 'm5.xlarge', + weightedCapacity: 1, + }], + launchSpecifications: { + spotSpecification: { + allocationStrategy: EmrCreateCluster.SpotAllocationStrategy.CAPACITY_OPTIMIZED, + blockDurationMinutes: 1, + timeoutAction: EmrCreateCluster.SpotTimeoutAction.TERMINATE_CLUSTER, + timeoutDurationMinutes: 1, + }, + }, + name: 'Master', + targetOnDemandCapacity: 1, + targetSpotCapacity: 1, + }], + }, + clusterRole, + name: 'Cluster', + serviceRole, + integrationPattern: sfn.IntegrationPattern.REQUEST_RESPONSE, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::elasticmapreduce:createCluster', + ], + ], + }, + End: true, + Parameters: { + Name: 'Cluster', + Instances: { + KeepJobFlowAliveWhenNoSteps: true, + InstanceFleets: [{ + InstanceFleetType: 'MASTER', + InstanceTypeConfigs: [{ + BidPrice: '1', + BidPriceAsPercentageOfOnDemandPrice: 1, + Configurations: [{ + Classification: 'Classification', + Properties: { + Key: 'Value', + }, + }], + EbsConfiguration: { + EbsBlockDeviceConfigs: [{ + VolumeSpecification: { + Iops: 1, + SizeInGB: 1, + VolumeType: 'standard', + }, + VolumesPerInstance: 1, + }], + EbsOptimized: true, + }, + InstanceType: 'm5.xlarge', + WeightedCapacity: 1, + }], + LaunchSpecifications: { + SpotSpecification: { + AllocationStrategy: 'capacity-optimized', + BlockDurationMinutes: 1, + TimeoutAction: 'TERMINATE_CLUSTER', + TimeoutDurationMinutes: 1, + }, + }, + Name: 'Master', + TargetOnDemandCapacity: 1, + TargetSpotCapacity: 1, + }], + }, + VisibleToAllUsers: true, + JobFlowRole: { + Ref: 'ClusterRoleD9CA7471', + }, + ServiceRole: { + Ref: 'ServiceRole4288B192', + }, + }, + }); +}); + test('Create Cluster with InstanceFleet', () => { // WHEN const task = new EmrCreateCluster(stack, 'Task', { diff --git a/packages/@aws-cdk/aws-stepfunctions/README.md b/packages/@aws-cdk/aws-stepfunctions/README.md index 4faf512ad0509..6171bac75805a 100644 --- a/packages/@aws-cdk/aws-stepfunctions/README.md +++ b/packages/@aws-cdk/aws-stepfunctions/README.md @@ -593,8 +593,6 @@ new sfn.StateMachine(stack, 'MyStateMachine', { Enable X-Ray tracing for StateMachine: ```ts -const logGroup = new logs.LogGroup(stack, 'MyLogGroup'); - new sfn.StateMachine(stack, 'MyStateMachine', { definition: sfn.Chain.start(new sfn.Pass(stack, 'Pass')), tracingEnabled: true diff --git a/packages/@aws-cdk/aws-synthetics/README.md b/packages/@aws-cdk/aws-synthetics/README.md index 6dcd911f7da5e..bd4fcaede272c 100644 --- a/packages/@aws-cdk/aws-synthetics/README.md +++ b/packages/@aws-cdk/aws-synthetics/README.md @@ -130,7 +130,7 @@ new synthetics.Canary(this, 'Bucket Canary', { }); ``` -> **Note:** For `code.fromAsset()` and `code.fromBucket()`, the canary resource requires the following folder structure: +> **Note:** Synthetics have a specified folder structure for canaries. For Node scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: > > ```plaintext > canary/ @@ -139,6 +139,15 @@ new synthetics.Canary(this, 'Bucket Canary', { > ├── .js > ``` > +> +> For Python scripts supplied via `code.fromAsset()` or `code.fromBucket()`, the canary resource requires the following folder structure: +> +> ```plaintext +> canary/ +> ├── python/ +> ├── .py +> ``` +> > See Synthetics [docs](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html). ### Alarms diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index e5061c4c999c2..c512c4d636a78 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -5,6 +5,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code } from './code'; +import { Runtime } from './runtime'; import { Schedule } from './schedule'; import { CloudWatchSyntheticsMetrics } from './synthetics-canned-metrics.generated'; import { CfnCanary } from './synthetics.generated'; @@ -64,81 +65,6 @@ export interface CustomTestOptions { readonly handler: string, } -/** - * Runtime options for a canary - */ -export class Runtime { - /** - * `syn-1.0` includes the following: - * - * - Synthetics library 1.0 - * - Synthetics handler code 1.0 - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 1.14.0 - * - The Chromium version that matches Puppeteer-core 1.14.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 - */ - public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0'); - - /** - * `syn-nodejs-2.0` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 - */ - public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0'); - - - /** - * `syn-nodejs-2.1` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 - */ - public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1'); - - /** - * `syn-nodejs-2.2` includes the following: - * - Lambda runtime Node.js 10.x - * - Puppeteer-core version 3.3.0 - * - Chromium version 83.0.4103.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 - */ - public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2'); - - /** - * `syn-nodejs-puppeteer-3.0` includes the following: - * - Lambda runtime Node.js 12.x - * - Puppeteer-core version 5.5.0 - * - Chromium version 88.0.4298.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.0 - */ - public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_0 = new Runtime('syn-nodejs-puppeteer-3.0'); - - /** - * `syn-nodejs-puppeteer-3.1` includes the following: - * - Lambda runtime Node.js 12.x - * - Puppeteer-core version 5.5.0 - * - Chromium version 88.0.4298.0 - * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.1 - */ - public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_1 = new Runtime('syn-nodejs-puppeteer-3.1'); - - /** - * @param name The name of the runtime version - */ - public constructor(public readonly name: string) { - } -} - /** * Options for specifying the s3 location that stores the data of each canary run. The artifacts bucket location **cannot** * be updated once the canary is created. @@ -398,7 +324,7 @@ export class Canary extends cdk.Resource { private createCode(props: CanaryProps): CfnCanary.CodeProperty { const codeConfig = { handler: props.test.handler, - ...props.test.code.bind(this, props.test.handler), + ...props.test.code.bind(this, props.test.handler, props.runtime.family), }; return { handler: codeConfig.handler, diff --git a/packages/@aws-cdk/aws-synthetics/lib/code.ts b/packages/@aws-cdk/aws-synthetics/lib/code.ts index 0fa8501a842ab..9eef28d4674c6 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/code.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/code.ts @@ -3,6 +3,7 @@ import * as path from 'path'; import * as s3 from '@aws-cdk/aws-s3'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import { Construct } from 'constructs'; +import { RuntimeFamily } from './runtime'; /** * The code the canary should execute @@ -56,7 +57,7 @@ export abstract class Code { * * @returns a bound `CodeConfig`. */ - public abstract bind(scope: Construct, handler: string): CodeConfig; + public abstract bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig; } /** @@ -95,8 +96,8 @@ export class AssetCode extends Code { } } - public bind(scope: Construct, handler: string): CodeConfig { - this.validateCanaryAsset(handler); + public bind(scope: Construct, handler: string, family: RuntimeFamily): CodeConfig { + this.validateCanaryAsset(handler, family); // If the same AssetCode is used multiple times, retain only the first instantiation. if (!this.asset) { @@ -126,14 +127,19 @@ export class AssetCode extends Code { * * @param handler the canary handler */ - private validateCanaryAsset(handler: string) { + private validateCanaryAsset(handler: string, family: RuntimeFamily) { if (path.extname(this.assetPath) !== '.zip') { if (!fs.lstatSync(this.assetPath).isDirectory()) { throw new Error(`Asset must be a .zip file or a directory (${this.assetPath})`); } - const filename = `${handler.split('.')[0]}.js`; - if (!fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', filename))) { - throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${filename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + const filename = handler.split('.')[0]; + const nodeFilename = `${filename}.js`; + const pythonFilename = `${filename}.py`; + if (family === RuntimeFamily.NODEJS && !fs.existsSync(path.join(this.assetPath, 'nodejs', 'node_modules', nodeFilename))) { + throw new Error(`The canary resource requires that the handler is present at "nodejs/node_modules/${nodeFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + } + if (family === RuntimeFamily.PYTHON && !fs.existsSync(path.join(this.assetPath, 'python', pythonFilename))) { + throw new Error(`The canary resource requires that the handler is present at "python/${pythonFilename}" but not found at ${this.assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); } } } @@ -151,7 +157,7 @@ export class InlineCode extends Code { } } - public bind(_scope: Construct, handler: string): CodeConfig { + public bind(_scope: Construct, handler: string, _family: RuntimeFamily): CodeConfig { if (handler !== 'index.handler') { throw new Error(`The handler for inline code must be "index.handler" (got "${handler}")`); @@ -171,7 +177,7 @@ export class S3Code extends Code { super(); } - public bind(_scope: Construct, _handler: string): CodeConfig { + public bind(_scope: Construct, _handler: string, _family: RuntimeFamily): CodeConfig { return { s3Location: { bucketName: this.bucket.bucketName, diff --git a/packages/@aws-cdk/aws-synthetics/lib/index.ts b/packages/@aws-cdk/aws-synthetics/lib/index.ts index f769a0309352e..ee024834f2bf1 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/index.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/index.ts @@ -1,5 +1,6 @@ export * from './canary'; export * from './code'; +export * from './runtime'; export * from './schedule'; // AWS::Synthetics CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-synthetics/lib/runtime.ts b/packages/@aws-cdk/aws-synthetics/lib/runtime.ts new file mode 100644 index 0000000000000..c710d68a34e35 --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/lib/runtime.ts @@ -0,0 +1,123 @@ +/** + * All known Lambda runtime families. + */ +export enum RuntimeFamily { + /** + * All Lambda runtimes that depend on Node.js. + */ + NODEJS, + + /** + * All lambda runtimes that depend on Python. + */ + PYTHON, + + /** + * Any future runtime family. + */ + OTHER, +} + +/** + * Runtime options for a canary + */ +export class Runtime { + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-1.0` includes the following: + * + * - Synthetics library 1.0 + * - Synthetics handler code 1.0 + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 1.14.0 + * - The Chromium version that matches Puppeteer-core 1.14.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-1.0 + */ + public static readonly SYNTHETICS_1_0 = new Runtime('syn-1.0', RuntimeFamily.NODEJS); + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.0` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.0 + */ + public static readonly SYNTHETICS_NODEJS_2_0 = new Runtime('syn-nodejs-2.0', RuntimeFamily.NODEJS); + + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.1` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.1 + */ + public static readonly SYNTHETICS_NODEJS_2_1 = new Runtime('syn-nodejs-2.1', RuntimeFamily.NODEJS); + + /** + * **Deprecated by AWS Synthetics. You can't create canaries with deprecated runtimes.** + * + * `syn-nodejs-2.2` includes the following: + * - Lambda runtime Node.js 10.x + * - Puppeteer-core version 3.3.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-2.2 + */ + public static readonly SYNTHETICS_NODEJS_2_2 = new Runtime('syn-nodejs-2.2', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.0` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.0 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_0 = new Runtime('syn-nodejs-puppeteer-3.0', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.1` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.1 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_1 = new Runtime('syn-nodejs-puppeteer-3.1', RuntimeFamily.NODEJS); + + /** + * `syn-nodejs-puppeteer-3.2` includes the following: + * - Lambda runtime Node.js 12.x + * - Puppeteer-core version 5.5.0 + * - Chromium version 88.0.4298.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-3.2 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_3_2 = new Runtime('syn-nodejs-puppeteer-3.2', RuntimeFamily.NODEJS); + + /** + * `syn-python-selenium-1.0` includes the following: + * - Lambda runtime Python 3.8 + * - Selenium version 3.141.0 + * - Chromium version 83.0.4103.0 + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_python_selenium.html + */ + public static readonly SYNTHETICS_PYTHON_SELENIUM_1_0 = new Runtime('syn-python-selenium-1.0', RuntimeFamily.PYTHON); + + /** + * @param name The name of the runtime version + * @param family The Lambda runtime family + */ + public constructor(public readonly name: string, public readonly family: RuntimeFamily) { + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py b/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py new file mode 100644 index 0000000000000..2dbed4e312afe --- /dev/null +++ b/packages/@aws-cdk/aws-synthetics/test/canaries/python/canary.py @@ -0,0 +1,61 @@ +# This example comes from the AWS Synthetics service console "API canary" blueprint + +import json +import http.client +import urllib.parse +from aws_synthetics.selenium import synthetics_webdriver as syn_webdriver +from aws_synthetics.common import synthetics_logger as logger + + +def verify_request(method, url, post_data=None, headers={}): + parsed_url = urllib.parse.urlparse(url) + user_agent = str(syn_webdriver.get_canary_user_agent_string()) + if "User-Agent" in headers: + headers["User-Agent"] = " ".join([user_agent, headers["User-Agent"]]) + else: + headers["User-Agent"] = "{}".format(user_agent) + + logger.info("Making request with Method: '%s' URL: %s: Data: %s Headers: %s" % ( + method, url, json.dumps(post_data), json.dumps(headers))) + + if parsed_url.scheme == "https": + conn = http.client.HTTPSConnection(parsed_url.hostname, parsed_url.port) + else: + conn = http.client.HTTPConnection(parsed_url.hostname, parsed_url.port) + + conn.request(method, url, str(post_data), headers) + response = conn.getresponse() + logger.info("Status Code: %s " % response.status) + logger.info("Response Headers: %s" % json.dumps(response.headers.as_string())) + + if not response.status or response.status < 200 or response.status > 299: + try: + logger.error("Response: %s" % response.read().decode()) + finally: + if response.reason: + conn.close() + raise Exception("Failed: %s" % response.reason) + else: + conn.close() + raise Exception("Failed with status code: %s" % response.status) + + logger.info("Response: %s" % response.read().decode()) + logger.info("HTTP request successfully executed") + conn.close() + + +def main(): + + url = 'https://example.com/' + method = 'GET' + postData = "" + headers = {} + + verify_request(method, url, None, headers) + + logger.info("Canary successfully executed") + + +def handler(event, context): + logger.info("Selenium Python API canary") + main() \ No newline at end of file diff --git a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts index 8a0baa8cb0c29..c4583ef5494cf 100644 --- a/packages/@aws-cdk/aws-synthetics/test/canary.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/canary.test.ts @@ -169,6 +169,25 @@ test('Runtime can be specified', () => { }); }); +test('Python runtime can be specified', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('# Synthetics handler code'), + }), + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + RuntimeVersion: 'syn-python-selenium-1.0', + }); +}); + test('environment variables can be specified', () => { // GIVEN const stack = new Stack(); @@ -220,7 +239,7 @@ test('Runtime can be customized', () => { // WHEN new synthetics.Canary(stack, 'Canary', { - runtime: new synthetics.Runtime('fancy-future-runtime-1337.42'), + runtime: new synthetics.Runtime('fancy-future-runtime-1337.42', synthetics.RuntimeFamily.OTHER), test: synthetics.Test.custom({ handler: 'index.handler', code: synthetics.Code.fromInline('/* Synthetics handler code */'), diff --git a/packages/@aws-cdk/aws-synthetics/test/code.test.ts b/packages/@aws-cdk/aws-synthetics/test/code.test.ts index 74ee982240d47..6b93ad9b6cebd 100644 --- a/packages/@aws-cdk/aws-synthetics/test/code.test.ts +++ b/packages/@aws-cdk/aws-synthetics/test/code.test.ts @@ -4,6 +4,7 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cxapi from '@aws-cdk/cx-api'; import { App, Stack } from '@aws-cdk/core'; import * as synthetics from '../lib'; +import { RuntimeFamily } from '../lib'; describe(synthetics.Code.fromInline, () => { test('fromInline works', () => { @@ -17,7 +18,7 @@ describe(synthetics.Code.fromInline, () => { };`); // THEN - expect(inline.bind(stack, 'index.handler').inlineCode).toEqual(` + expect(inline.bind(stack, 'index.handler', RuntimeFamily.NODEJS).inlineCode).toEqual(` exports.handler = async () => { console.log(\'hello world\'); };`); @@ -33,13 +34,13 @@ describe(synthetics.Code.fromInline, () => { const stack = new Stack(new App(), 'canaries'); // THEN - expect(() => synthetics.Code.fromInline('code').bind(stack, 'canary.handler')) + expect(() => synthetics.Code.fromInline('code').bind(stack, 'canary.handler', RuntimeFamily.NODEJS)) .toThrowError('The handler for inline code must be "index.handler" (got "canary.handler")'); }); }); describe(synthetics.Code.fromAsset, () => { - test('fromAsset works', () => { + test('fromAsset works for node runtimes', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); @@ -57,8 +58,32 @@ describe(synthetics.Code.fromAsset, () => { Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { Code: { Handler: 'canary.handler', - S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler').s3Location?.bucketName), - S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler').s3Location?.objectKey), + S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.bucketName), + S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS).s3Location?.objectKey), + }, + }); + }); + + test('fromAsset works for python runtimes', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // WHEN + const directoryAsset = synthetics.Code.fromAsset(path.join(__dirname, 'canaries')); + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'canary.handler', + code: directoryAsset, + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + Code: { + Handler: 'canary.handler', + S3Bucket: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.bucketName), + S3Key: stack.resolve(directoryAsset.bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON).s3Location?.objectKey), }, }); }); @@ -109,18 +134,28 @@ describe(synthetics.Code.fromAsset, () => { // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules', 'canary.js'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler')) + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) .toThrowError(`Asset must be a .zip file or a directory (${assetPath})`); }); - test('fails if "nodejs/node_modules" folder structure not used', () => { + test('fails if node runtime and "nodejs/node_modules" folder structure not used', () => { // GIVEN const stack = new Stack(new App(), 'canaries'); // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler')) - .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/canary.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); + }); + + test('fails if python runtime and "python" folder structure not used', () => { + // GIVEN + const stack = new Stack(new App(), 'canaries'); + + // THEN + const assetPath = path.join(__dirname, 'canaries', 'python'); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'canary.handler', synthetics.RuntimeFamily.PYTHON)) + .toThrowError(`The canary resource requires that the handler is present at "python/canary.py" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Python.html)`); }); test('fails if handler is specified incorrectly', () => { @@ -129,8 +164,8 @@ describe(synthetics.Code.fromAsset, () => { // THEN const assetPath = path.join(__dirname, 'canaries', 'nodejs', 'node_modules'); - expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler')) - .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary.html#CloudWatch_Synthetics_Canaries_write_from_scratch)`); + expect(() => synthetics.Code.fromAsset(assetPath).bind(stack, 'incorrect.handler', synthetics.RuntimeFamily.NODEJS)) + .toThrowError(`The canary resource requires that the handler is present at "nodejs/node_modules/incorrect.js" but not found at ${assetPath} (https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Canaries_WritingCanary_Nodejs.html)`); }); }); @@ -143,7 +178,7 @@ describe(synthetics.Code.fromBucket, () => { // WHEN const code = synthetics.Code.fromBucket(bucket, 'code.js'); - const codeConfig = code.bind(stack, 'code.handler'); + const codeConfig = code.bind(stack, 'code.handler', RuntimeFamily.NODEJS); // THEN expect(codeConfig.s3Location?.bucketName).toEqual(bucket.bucketName); diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json index 58412fee9bfbb..70d3908aa07f6 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.expected.json @@ -114,7 +114,7 @@ ] }, "Name": "canary-integ", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(1 minute)" @@ -238,7 +238,7 @@ "Code": { "Handler": "canary.handler", "S3Bucket": { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3Bucket58589EB6" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2" }, "S3Key": { "Fn::Join": [ @@ -251,7 +251,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" } ] } @@ -264,7 +264,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90" + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" } ] } @@ -281,7 +281,7 @@ ] }, "Name": "assetcanary-one", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" @@ -448,7 +448,174 @@ ] }, "Name": "assetcanary-two", - "RuntimeVersion": "syn-nodejs-2.0", + "RuntimeVersion": "syn-nodejs-puppeteer-3.2", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(5 minutes)" + }, + "StartCanaryAfterCreation": true + } + }, + "MyPythonCanaryArtifactsBucket7AE88133": { + "Type": "AWS::S3::Bucket", + "Properties": { + "BucketEncryption": { + "ServerSideEncryptionConfiguration": [ + { + "ServerSideEncryptionByDefault": { + "SSEAlgorithm": "aws:kms" + } + } + ] + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "MyPythonCanaryServiceRole41A363E1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:PutObject", + "s3:GetBucketLocation" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyPythonCanaryArtifactsBucket7AE88133", + "Arn" + ] + }, + "/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogStream", + "logs:CreateLogGroup", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:::*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "MyPythonCanary9A3DE09E": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyPythonCanaryArtifactsBucket7AE88133" + } + ] + ] + }, + "Code": { + "Handler": "canary.handler", + "S3Bucket": { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2" + }, + "S3Key": { + "Fn::Join": [ + "", + [ + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8" + } + ] + } + ] + } + ] + ] + } + }, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "MyPythonCanaryServiceRole41A363E1", + "Arn" + ] + }, + "Name": "py-canary-integ", + "RuntimeVersion": "syn-python-selenium-1.0", "Schedule": { "DurationInSeconds": "0", "Expression": "rate(5 minutes)" @@ -458,17 +625,17 @@ } }, "Parameters": { - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3Bucket58589EB6": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3Bucket59F507C2": { "Type": "String", - "Description": "S3 bucket for asset \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "S3 bucket for asset \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bS3VersionKey8FF13E90": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbS3VersionKeyEFB5FFF8": { "Type": "String", - "Description": "S3 key for asset version \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "S3 key for asset version \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, - "AssetParameters5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529bArtifactHash74DCED3D": { + "AssetParameters9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bbArtifactHash0E2FE9D4": { "Type": "String", - "Description": "Artifact hash for asset \"5bf46c83158ab3b336aba1449c21b02cbac2ccea621f17d842593bb39e3e529b\"" + "Description": "Artifact hash for asset \"9d00e437db1f5f8788ce938a3f00a9a1b946820e78c9b4c36207c8475db882bb\"" }, "AssetParametersb1b777dcb79a2fa2790059927207d10bf5f4747d6dd1516e2780726d9d6fa820S3Bucket705C3761": { "Type": "String", diff --git a/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts b/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts index cbb3a505889ad..54822badf1c99 100644 --- a/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts +++ b/packages/@aws-cdk/aws-synthetics/test/integ.canary.ts @@ -29,7 +29,7 @@ new synthetics.Canary(stack, 'MyCanary', { }), schedule: synthetics.Schedule.rate(cdk.Duration.minutes(1)), artifactsBucketLocation: { bucket, prefix }, - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, }); new synthetics.Canary(stack, 'MyCanaryOne', { @@ -38,7 +38,7 @@ new synthetics.Canary(stack, 'MyCanaryOne', { handler: 'canary.handler', code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, }); new synthetics.Canary(stack, 'MyCanaryTwo', { @@ -47,7 +47,16 @@ new synthetics.Canary(stack, 'MyCanaryTwo', { handler: 'canary.handler', code: synthetics.Code.fromAsset(path.join(__dirname, 'canary.zip')), }), - runtime: synthetics.Runtime.SYNTHETICS_NODEJS_2_0, + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_3_2, +}); + +new synthetics.Canary(stack, 'MyPythonCanary', { + canaryName: 'py-canary-integ', + test: synthetics.Test.custom({ + handler: 'canary.handler', + code: synthetics.Code.fromAsset(path.join(__dirname, 'canaries')), + }), + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_1_0, }); app.synth(); diff --git a/packages/@aws-cdk/cfnspec/CHANGELOG.md b/packages/@aws-cdk/cfnspec/CHANGELOG.md index 85274a4543fbf..5def28ee631e9 100644 --- a/packages/@aws-cdk/cfnspec/CHANGELOG.md +++ b/packages/@aws-cdk/cfnspec/CHANGELOG.md @@ -1,3 +1,23 @@ +# CloudFormation Resource Specification v40.1.0 + +## New Resource Types + + +## Attribute Changes + + +## Property Changes + +* AWS::CE::CostCategory SplitChargeRules (__added__) + +## Property Type Changes + +* AWS::EFS::FileSystem.LifecyclePolicy TransitionToPrimaryStorageClass (__added__) +* AWS::EFS::FileSystem.LifecyclePolicy TransitionToIA.Required (__changed__) + * Old: true + * New: false + + # CloudFormation Resource Specification v40.0.0 ## New Resource Types diff --git a/packages/@aws-cdk/cfnspec/cfn.version b/packages/@aws-cdk/cfnspec/cfn.version index e9340fd6f7115..312a84c9209ab 100644 --- a/packages/@aws-cdk/cfnspec/cfn.version +++ b/packages/@aws-cdk/cfnspec/cfn.version @@ -1 +1 @@ -40.0.0 +40.1.0 diff --git a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json index 1bb4dde7c70f4..33d10e45ecb92 100644 --- a/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json +++ b/packages/@aws-cdk/cfnspec/spec-source/000_CloudFormationResourceSpecification.json @@ -23470,7 +23470,13 @@ "TransitionToIA": { "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-filesystem-lifecyclepolicy.html#cfn-efs-filesystem-lifecyclepolicy-transitiontoia", "PrimitiveType": "String", - "Required": true, + "Required": false, + "UpdateType": "Mutable" + }, + "TransitionToPrimaryStorageClass": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-efs-filesystem-lifecyclepolicy.html#cfn-efs-filesystem-lifecyclepolicy-transitiontoprimarystorageclass", + "PrimitiveType": "String", + "Required": false, "UpdateType": "Mutable" } } @@ -61896,7 +61902,7 @@ } } }, - "ResourceSpecificationVersion": "40.0.0", + "ResourceSpecificationVersion": "40.1.0", "ResourceTypes": { "AWS::ACMPCA::Certificate": { "Attributes": { @@ -67257,6 +67263,12 @@ "PrimitiveType": "String", "Required": true, "UpdateType": "Mutable" + }, + "SplitChargeRules": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-ce-costcategory.html#cfn-ce-costcategory-splitchargerules", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" } } }, diff --git a/packages/@aws-cdk/core/lib/fs/fingerprint.ts b/packages/@aws-cdk/core/lib/fs/fingerprint.ts index 50a81fd53982d..7ba7214109b2c 100644 --- a/packages/@aws-cdk/core/lib/fs/fingerprint.ts +++ b/packages/@aws-cdk/core/lib/fs/fingerprint.ts @@ -9,14 +9,18 @@ const BUFFER_SIZE = 8 * 1024; const CTRL_SOH = '\x01'; const CTRL_SOT = '\x02'; const CTRL_ETX = '\x03'; +const CR = '\r'; +const LF = '\n'; +const CRLF = `${CR}${LF}`; /** * Produces fingerprint based on the contents of a single file or an entire directory tree. * + * Line endings are converted from CRLF to LF. + * * The fingerprint will also include: * 1. An extra string if defined in `options.extra`. - * 2. The set of exclude patterns, if defined in `options.exclude` - * 3. The symlink follow mode value. + * 2. The symlink follow mode value. * * @param fileOrDirectory The directory or file to fingerprint * @param options Fingerprinting options @@ -60,7 +64,7 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions _hashField(hash, `link:${relativePath}`, linkTarget); } } else if (stat.isFile()) { - _hashField(hash, `file:${relativePath}`, _contentFingerprint(realPath, stat)); + _hashField(hash, `file:${relativePath}`, contentFingerprint(realPath)); } else if (stat.isDirectory()) { for (const item of fs.readdirSync(realPath).sort()) { _processFileOrDirectory(path.join(symbolicPath, item), false, path.join(realPath, item)); @@ -71,20 +75,54 @@ export function fingerprint(fileOrDirectory: string, options: FingerprintOptions } } -function _contentFingerprint(file: string, stat: fs.Stats): string { +export function contentFingerprint(file: string): string { const hash = crypto.createHash('sha256'); const buffer = Buffer.alloc(BUFFER_SIZE); // eslint-disable-next-line no-bitwise const fd = fs.openSync(file, fs.constants.O_DSYNC | fs.constants.O_RDONLY | fs.constants.O_SYNC); + let size = 0; + let isBinary = false; + let lastStr = ''; + let read = 0; try { - let read = 0; while ((read = fs.readSync(fd, buffer, 0, BUFFER_SIZE, null)) !== 0) { - hash.update(buffer.slice(0, read)); + const slicedBuffer = buffer.slice(0, read); + + // Detect if file is binary by checking the first 8k bytes for the + // null character (git like implementation) + if (size === 0) { + isBinary = slicedBuffer.indexOf(0) !== -1; + } + + let dataBuffer = slicedBuffer; + if (!isBinary) { // Line endings normalization (CRLF -> LF) + const str = buffer.slice(0, read).toString(); + + // We are going to normalize line endings to LF. So if the current + // buffer ends with CR, it could be that the next one starts with + // LF so we need to save it for later use. + if (new RegExp(`${CR}$`).test(str)) { + lastStr += str; + continue; + } + + const data = lastStr + str; + const normalizedData = data.replace(new RegExp(CRLF, 'g'), LF); + dataBuffer = Buffer.from(normalizedData); + lastStr = ''; + } + + size += dataBuffer.length; + hash.update(dataBuffer); + } + + if (lastStr) { + hash.update(Buffer.from(lastStr)); } } finally { fs.closeSync(fd); } - return `${stat.size}:${hash.digest('hex')}`; + return `${size}:${hash.digest('hex')}`; } function _hashField(hash: crypto.Hash, header: string, value: string | Buffer | DataView) { diff --git a/packages/@aws-cdk/core/lib/private/metadata-resource.ts b/packages/@aws-cdk/core/lib/private/metadata-resource.ts index 17818add1a3ee..6a8e07d7acb2a 100644 --- a/packages/@aws-cdk/core/lib/private/metadata-resource.ts +++ b/packages/@aws-cdk/core/lib/private/metadata-resource.ts @@ -74,8 +74,13 @@ export function formatAnalytics(infos: ConstructInfo[]) { infos.forEach(info => insertFqnInTrie(`${info.version}!${info.fqn}`, trie)); const plaintextEncodedConstructs = prefixEncodeTrie(trie); - const compressedConstructs = zlib.gzipSync(Buffer.from(plaintextEncodedConstructs)).toString('base64'); + const compressedConstructsBuffer = zlib.gzipSync(Buffer.from(plaintextEncodedConstructs)); + // set OS flag to "unknown" in order to ensure we get consistent results across operating systems + // see https://github.com/aws/aws-cdk/issues/15322 + setGzipOperatingSystemToUnknown(compressedConstructsBuffer); + + const compressedConstructs = compressedConstructsBuffer.toString('base64'); return `v2:deflate64:${compressedConstructs}`; } @@ -125,3 +130,47 @@ function prefixEncodeTrie(trie: Trie) { }); return prefixEncoded; } + +/** + * Sets the OS flag to "unknown" in order to ensure we get consistent results across operating systems. + * + * @see https://datatracker.ietf.org/doc/html/rfc1952#page-5 + * + * +---+---+---+---+---+---+---+---+---+---+ + * |ID1|ID2|CM |FLG| MTIME |XFL|OS | + * +---+---+---+---+---+---+---+---+---+---+ + * | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | + * +---+---+---+---+---+---+---+---+---+---+ + * + * OS (Operating System) + * ===================== + * This identifies the type of file system on which compression + * took place. This may be useful in determining end-of-line + * convention for text files. The currently defined values are + * as follows: + * 0 - FAT filesystem (MS-DOS, OS/2, NT/Win32) + * 1 - Amiga + * 2 - VMS (or OpenVMS) + * 3 - Unix + * 4 - VM/CMS + * 5 - Atari TOS + * 6 - HPFS filesystem (OS/2, NT) + * 7 - Macintosh + * 8 - Z-System + * 9 - CP/M + * 10 - TOPS-20 + * 11 - NTFS filesystem (NT) + * 12 - QDOS + * 13 - Acorn RISCOS + * 255 - unknown + * + * @param gzipBuffer A gzip buffer + */ +function setGzipOperatingSystemToUnknown(gzipBuffer: Buffer) { + // check that this is indeed a gzip buffer (https://datatracker.ietf.org/doc/html/rfc1952#page-6) + if (gzipBuffer[0] !== 0x1f || gzipBuffer[1] !== 0x8b) { + throw new Error('Expecting a gzip buffer (must start with 0x1f8b)'); + } + + gzipBuffer[9] = 255; +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/fs/eol/lf.txt b/packages/@aws-cdk/core/test/fs/eol/lf.txt new file mode 100644 index 0000000000000..f41bc690ba927 --- /dev/null +++ b/packages/@aws-cdk/core/test/fs/eol/lf.txt @@ -0,0 +1,2 @@ +hello word +this a new line! diff --git a/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts b/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts index 6a589b3ba159e..be093c32cbffc 100644 --- a/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts +++ b/packages/@aws-cdk/core/test/fs/fs-fingerprint.test.ts @@ -3,6 +3,7 @@ import * as os from 'os'; import * as path from 'path'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { FileSystem, SymlinkFollowMode } from '../../lib/fs'; +import { contentFingerprint } from '../../lib/fs/fingerprint'; nodeunitShim({ files: { @@ -155,4 +156,27 @@ nodeunitShim({ test.done(); }, }, + + eol: { + 'normalizes line endings'(test: Test) { + // GIVEN + const lf = path.join(__dirname, 'eol', 'lf.txt'); + const crlf = path.join(__dirname, 'eol', 'crlf.txt'); + fs.writeFileSync(crlf, fs.readFileSync(lf, 'utf8').replace(/\n/g, '\r\n')); + + const lfStat = fs.statSync(lf); + const crlfStat = fs.statSync(crlf); + + // WHEN + const crlfHash = contentFingerprint(crlf); + const lfHash = contentFingerprint(lf); + + // THEN + test.notEqual(crlfStat.size, lfStat.size); // Difference in size due to different line endings + test.deepEqual(crlfHash, lfHash); // Same hash + + fs.unlinkSync(crlf); + test.done(); + }, + }, }); diff --git a/packages/@aws-cdk/core/test/metadata-resource.test.ts b/packages/@aws-cdk/core/test/metadata-resource.test.ts index c3f8c99a2988f..1f5f676b5cdbf 100644 --- a/packages/@aws-cdk/core/test/metadata-resource.test.ts +++ b/packages/@aws-cdk/core/test/metadata-resource.test.ts @@ -123,6 +123,13 @@ describe('formatAnalytics', () => { expectAnalytics(constructInfo, '1.2.3!aws-cdk-lib.{Construct,CfnResource,Stack},0.1.2!aws-cdk-lib.{CoolResource,OtherResource}'); }); + test('ensure gzip is encoded with "unknown" operating system to maintain consistent output across systems', () => { + const constructInfo = [{ fqn: 'aws-cdk-lib.Construct', version: '1.2.3' }]; + const analytics = formatAnalytics(constructInfo); + const gzip = Buffer.from(analytics.split(':')[2], 'base64'); + expect(gzip[9]).toBe(255); + }); + // Compares the output of formatAnalytics with an expected (plaintext) output. // For ease of testing, the plaintext versions are compared rather than the encoded versions. function expectAnalytics(constructs: ConstructInfo[], expectedPlaintext: string) { diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index cc6bfe3bbd39a..729413ae6859d 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -140,7 +140,7 @@ $ cdk diff --app='node bin/main.js' MyStackName --template=path/to/template.yml ### `cdk deploy` -Deploys a stack of your CDK app to it's environment. During the deployment, the toolkit will output progress +Deploys a stack of your CDK app to its environment. During the deployment, the toolkit will output progress indications, similar to what can be observed in the AWS CloudFormation Console. If the environment was never bootstrapped (using `cdk bootstrap`), only stacks that are not using assets and synthesize to a template that is under 51,200 bytes will successfully deploy. @@ -154,6 +154,24 @@ currently deployed stack to the template and tags that are about to be deployed will skip deployment if they are identical. Use `--force` to override this behavior and always deploy the stack. +#### Disabling Rollback + +If a resource fails to be created or updated, the deployment will *roll back* before the CLI returns. All changes made +up to that point will be undone (resources that were created will be deleted, updates that were made will be changed +back) in order to leave the stack in a consistent state at the end of the operation. If you are using the CDK CLI +to iterate on a development stack in your personal account, you might not require CloudFormation to leave your +stack in a consistent state, but instead would prefer to update your CDK application and try again. + +To disable the rollback feature, specify `--no-rollback` (`-R` for short): + +```console +$ cdk deploy --no-rollback +$ cdk deploy -R +``` + +NOTE: you cannot use `--no-rollback` for any updates that would cause a resource replacement, only for updates +and creations of new resources. + #### Deploying multiple stacks You can have multiple stacks in a cdk app. An example can be found in [how to create multiple stacks](https://docs.aws.amazon.com/cdk/latest/guide/stack_how_to_create_multiple_stacks.html). diff --git a/packages/aws-cdk/bin/cdk.ts b/packages/aws-cdk/bin/cdk.ts index 29f217360355f..a21a350415223 100644 --- a/packages/aws-cdk/bin/cdk.ts +++ b/packages/aws-cdk/bin/cdk.ts @@ -104,7 +104,11 @@ async function parseCommandLineArguments() { .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }), + .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) + .option('rollback', { type: 'boolean', default: true, desc: 'Rollback stack to stable state on failure (iterate more rapidly with --no-rollback or -R)' }) + // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 + .option('R', { type: 'boolean', hidden: true }) + .middleware(yargsNegativeAlias('R', 'rollback'), true), ) .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', yargs => yargs .option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' }) @@ -319,6 +323,7 @@ async function initCommandLine() { outputsFile: configuration.settings.get(['outputsFile']), progress: configuration.settings.get(['progress']), ci: args.ci, + rollback: configuration.settings.get(['rollback']), }); case 'destroy': @@ -421,6 +426,15 @@ function arrayFromYargs(xs: string[]): string[] | undefined { return xs.filter(x => x !== ''); } +function yargsNegativeAlias(shortName: S, longName: L) { + return (argv: T) => { + if (shortName in argv && argv[shortName]) { + (argv as any)[longName] = false; + } + return argv; + }; +} + initCommandLine() .then(value => { if (value == null) { return; } diff --git a/packages/aws-cdk/lib/api/cloudformation-deployments.ts b/packages/aws-cdk/lib/api/cloudformation-deployments.ts index ed783e0826fd7..8cea2ae411212 100644 --- a/packages/aws-cdk/lib/api/cloudformation-deployments.ts +++ b/packages/aws-cdk/lib/api/cloudformation-deployments.ts @@ -129,6 +129,13 @@ export interface DeployStackOptions { * @default false */ readonly ci?: boolean; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; } export interface DestroyStackOptions { @@ -204,6 +211,7 @@ export class CloudFormationDeployments { usePreviousParameters: options.usePreviousParameters, progress: options.progress, ci: options.ci, + rollback: options.rollback, }); } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index df811e39587be..62c5815197fc2 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -175,6 +175,13 @@ export interface DeployStackOptions { * @default false */ readonly ci?: boolean; + + /** + * Rollback failed deployments + * + * @default true + */ + readonly rollback?: boolean; } const LARGE_TEMPLATE_SIZE_KB = 50; @@ -282,7 +289,12 @@ export async function deployStack(options: DeployStackOptions): Promise { + test('by default, we do not disable rollback (and also do not pass the flag)', async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + }); + + // THEN + expect(cfnMocks.executeChangeSet).toHaveBeenCalledTimes(1); + expect(cfnMocks.executeChangeSet).not.toHaveBeenCalledWith(expect.objectContaining({ + DisableRollback: expect.anything(), + })); + }); + + test('rollback can be disabled by setting rollback: false', async () => { + // WHEN + await deployStack({ + ...standardDeployStackArguments(), + rollback: false, + }); + + // THEN + expect(cfnMocks.executeChangeSet).toHaveBeenCalledWith(expect.objectContaining({ + DisableRollback: true, + })); + }); + +}); + /** * Set up the mocks so that it looks like the stack exists to start with * diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md new file mode 100644 index 0000000000000..5ca96b632f75a --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/NOTES.md @@ -0,0 +1,5 @@ +This [PR](https://github.com/aws/aws-cdk/pull/16205) added a node version check to our CLI courtesy of [`@jsii/check-node/run`](https://github.com/aws/jsii/tree/main/packages/%40jsii/check-node). + +This check now causes the CLI to print a deprecation warning that changes the output of the `synth` command. We don't consider this a breaking change since we have no guarantess for CLI output, but it did break some our integ tests (namely `cdk synth`) that used to rely on a specific output. + +This patch brings the [fix](https://github.com/aws/aws-cdk/pull/16216) into the regression suite. \ No newline at end of file diff --git a/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js new file mode 100644 index 0000000000000..b8009dcaab00e --- /dev/null +++ b/packages/aws-cdk/test/integ/cli-regression-patches/v1.119.0/cli.integtest.js @@ -0,0 +1,659 @@ +"use strict"; +Object.defineProperty(exports, "__esModule", { value: true }); +const fs_1 = require("fs"); +const os = require("os"); +const path = require("path"); +const aws_1 = require("../helpers/aws"); +const cdk_1 = require("../helpers/cdk"); +const test_helpers_1 = require("../helpers/test-helpers"); +jest.setTimeout(600 * 1000); +test_helpers_1.integTest('VPC Lookup', cdk_1.withDefaultFixture(async (fixture) => { + fixture.log('Making sure we are clean before starting.'); + await fixture.cdkDestroy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setting up: creating a VPC with known tags'); + await fixture.cdkDeploy('define-vpc', { modEnv: { ENABLE_VPC_TESTING: 'DEFINE' } }); + fixture.log('Setup complete!'); + fixture.log('Verifying we can now import that VPC'); + await fixture.cdkDeploy('import-vpc', { modEnv: { ENABLE_VPC_TESTING: 'IMPORT' } }); +})); +test_helpers_1.integTest('Two ways of shoing the version', cdk_1.withDefaultFixture(async (fixture) => { + const version1 = await fixture.cdk(['version'], { verbose: false }); + const version2 = await fixture.cdk(['--version'], { verbose: false }); + expect(version1).toEqual(version2); +})); +test_helpers_1.integTest('Termination protection', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'termination-protection'; + await fixture.cdkDeploy(stackName); + // Try a destroy that should fail + await expect(fixture.cdkDestroy(stackName)).rejects.toThrow('exited with error'); + // Can update termination protection even though the change set doesn't contain changes + await fixture.cdkDeploy(stackName, { modEnv: { TERMINATION_PROTECTION: 'FALSE' } }); + await fixture.cdkDestroy(stackName); +})); +test_helpers_1.integTest('cdk synth', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', fixture.fullStackName('test-1')]); + const template1 = await readTemplate(fixture, 'test-1'); + expect(template1).toEqual({ + Resources: { + topic69831491: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-1/topic/Resource`, + }, + }, + }, + }); + await fixture.cdk(['synth', fixture.fullStackName('test-2')], { verbose: false }); + const template2 = await readTemplate(fixture, 'test-2'); + expect(template2).toEqual({ + Resources: { + topic152D84A37: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic1/Resource`, + }, + }, + topic2A4FB547F: { + Type: 'AWS::SNS::Topic', + Metadata: { + 'aws:cdk:path': `${fixture.stackNamePrefix}-test-2/topic2/Resource`, + }, + }, + }, + }); +})); +test_helpers_1.integTest('ssm parameter provider error', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdk(['synth', + fixture.fullStackName('missing-ssm-parameter'), + '-c', 'test:ssm-parameter-name=/does/not/exist'], { + allowErrExit: true, + })).resolves.toContain('SSM parameter not available in account'); +})); +test_helpers_1.integTest('automatic ordering', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the consuming stack which will include the producing stack + await fixture.cdkDeploy('order-consuming'); + // Destroy the providing stack which will include the consuming stack + await fixture.cdkDestroy('order-providing'); +})); +test_helpers_1.integTest('context setting', cdk_1.withDefaultFixture(async (fixture) => { + await fs_1.promises.writeFile(path.join(fixture.integTestDir, 'cdk.context.json'), JSON.stringify({ + contextkey: 'this is the context value', + })); + try { + await expect(fixture.cdk(['context'])).resolves.toContain('this is the context value'); + // Test that deleting the contextkey works + await fixture.cdk(['context', '--reset', 'contextkey']); + await expect(fixture.cdk(['context'])).resolves.not.toContain('this is the context value'); + // Test that forced delete of the context key does not throw + await fixture.cdk(['context', '-f', '--reset', 'contextkey']); + } + finally { + await fs_1.promises.unlink(path.join(fixture.integTestDir, 'cdk.context.json')); + } +})); +test_helpers_1.integTest('context in stage propagates to top', cdk_1.withDefaultFixture(async (fixture) => { + await expect(fixture.cdkSynth({ + // This will make it error to prove that the context bubbles up, and also that we can fail on command + options: ['--no-lookups'], + modEnv: { + INTEG_STACK_SET: 'stage-using-context', + }, + allowErrExit: true, + })).resolves.toContain('Context lookups have been disabled'); +})); +test_helpers_1.integTest('deploy', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('test-2', { captureStderr: false }); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(2); +})); +test_helpers_1.integTest('deploy all', cdk_1.withDefaultFixture(async (fixture) => { + const arns = await fixture.cdkDeploy('test-*', { captureStderr: false }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(arns.split('\n').length).toEqual(2); +})); +test_helpers_1.integTest('nested stack with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + // STACK_NAME_PREFIX is used in MyTopicParam to allow multiple instances + // of this test to run in parallel, othewise they will attempt to create the same SNS topic. + const stackArn = await fixture.cdkDeploy('with-nested-stack-using-parameters', { + options: ['--parameters', `MyTopicParam=${fixture.stackNamePrefix}ThereIsNoSpoon`], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + // verify the number of resources in the stack + const response = await fixture.aws.cloudFormation('describeStackResources', { + StackName: stackArn, + }); + expect((_a = response.StackResources) === null || _a === void 0 ? void 0 : _a.length).toEqual(1); +})); +test_helpers_1.integTest('deploy without execute a named change set', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const changeSetName = 'custom-change-set-name'; + const stackArn = await fixture.cdkDeploy('test-2', { + options: ['--no-execute', '--change-set-name', changeSetName], + captureStderr: false, + }); + // verify that we only deployed a single stack (there's a single ARN in the output) + expect(stackArn.split('\n').length).toEqual(1); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('REVIEW_IN_PROGRESS'); + //verify a change set was created with the provided name + const changeSetResponse = await fixture.aws.cloudFormation('listChangeSets', { + StackName: stackArn, + }); + const changeSets = changeSetResponse.Summaries || []; + expect(changeSets.length).toEqual(1); + expect(changeSets[0].ChangeSetName).toEqual(changeSetName); + expect(changeSets[0].Status).toEqual('CREATE_COMPLETE'); +})); +test_helpers_1.integTest('security related changes without a CLI are expected to fail', cdk_1.withDefaultFixture(async (fixture) => { + // redirect /dev/null to stdin, which means there will not be tty attached + // since this stack includes security-related changes, the deployment should + // immediately fail because we can't confirm the changes + const stackName = 'iam-test'; + await expect(fixture.cdkDeploy(stackName, { + options: ['<', '/dev/null'], + neverRequireApproval: false, + })).rejects.toThrow('exited with error'); + // Ensure stack was not deployed + await expect(fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName(stackName), + })).rejects.toThrow('does not exist'); +})); +test_helpers_1.integTest('deploy wildcard with outputs', cdk_1.withDefaultFixture(async (fixture) => { + const outputsFile = path.join(fixture.integTestDir, 'outputs', 'outputs.json'); + await fs_1.promises.mkdir(path.dirname(outputsFile), { recursive: true }); + await fixture.cdkDeploy(['outputs-test-*'], { + options: ['--outputs-file', outputsFile], + }); + const outputs = JSON.parse((await fs_1.promises.readFile(outputsFile, { encoding: 'utf-8' })).toString()); + expect(outputs).toEqual({ + [`${fixture.stackNamePrefix}-outputs-test-1`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-1MyTopic`, + }, + [`${fixture.stackNamePrefix}-outputs-test-2`]: { + TopicName: `${fixture.stackNamePrefix}-outputs-test-2MyOtherTopic`, + }, + }); +})); +test_helpers_1.integTest('deploy with parameters', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}bazinga`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}bazinga`, + }, + ]); +})); +test_helpers_1.integTest('update to stack in ROLLBACK_COMPLETE state will delete stack and create a new one', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('param-test-1'), + }); + const stackArn = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackId; + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('ROLLBACK_COMPLETE'); + // WHEN + const newStackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + const newStackResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: newStackArn, + }); + // THEN + expect(stackArn).not.toEqual(newStackArn); // new stack was created + expect((_c = newStackResponse.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('CREATE_COMPLETE'); + expect((_d = newStackResponse.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('stack in UPDATE_ROLLBACK_COMPLETE state can be updated', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b, _c, _d; + // GIVEN + const stackArn = await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}nice`, + ], + captureStderr: false, + }); + let response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].StackStatus).toEqual('CREATE_COMPLETE'); + // bad parameter name with @ will put stack into UPDATE_ROLLBACK_COMPLETE + await expect(fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}@aww`, + ], + captureStderr: false, + })).rejects.toThrow('exited with error'); + ; + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].StackStatus).toEqual('UPDATE_ROLLBACK_COMPLETE'); + // WHEN + await fixture.cdkDeploy('param-test-1', { + options: [ + '--parameters', `TopicNameParam=${fixture.stackNamePrefix}allgood`, + ], + captureStderr: false, + }); + response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + // THEN + expect((_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0].StackStatus).toEqual('UPDATE_COMPLETE'); + expect((_d = response.Stacks) === null || _d === void 0 ? void 0 : _d[0].Parameters).toEqual([ + { + ParameterKey: 'TopicNameParam', + ParameterValue: `${fixture.stackNamePrefix}allgood`, + }, + ]); +})); +test_helpers_1.integTest('deploy with wildcard and parameters', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('param-test-*', { + options: [ + '--parameters', `${fixture.stackNamePrefix}-param-test-1:TopicNameParam=${fixture.stackNamePrefix}bazinga`, + '--parameters', `${fixture.stackNamePrefix}-param-test-2:OtherTopicNameParam=${fixture.stackNamePrefix}ThatsMySpot`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:DisplayNameParam=${fixture.stackNamePrefix}HeyThere`, + '--parameters', `${fixture.stackNamePrefix}-param-test-3:OtherDisplayNameParam=${fixture.stackNamePrefix}AnotherOne`, + ], + }); +})); +test_helpers_1.integTest('deploy with parameters multi', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const paramVal1 = `${fixture.stackNamePrefix}bazinga`; + const paramVal2 = `${fixture.stackNamePrefix}=jagshemash`; + const stackArn = await fixture.cdkDeploy('param-test-3', { + options: [ + '--parameters', `DisplayNameParam=${paramVal1}`, + '--parameters', `OtherDisplayNameParam=${paramVal2}`, + ], + captureStderr: false, + }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + expect((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Parameters).toEqual([ + { + ParameterKey: 'DisplayNameParam', + ParameterValue: paramVal1, + }, + { + ParameterKey: 'OtherDisplayNameParam', + ParameterValue: paramVal2, + }, + ]); +})); +test_helpers_1.integTest('deploy with notification ARN', cdk_1.withDefaultFixture(async (fixture) => { + var _a; + const topicName = `${fixture.stackNamePrefix}-test-topic`; + const response = await fixture.aws.sns('createTopic', { Name: topicName }); + const topicArn = response.TopicArn; + try { + await fixture.cdkDeploy('test-2', { + options: ['--notification-arns', topicArn], + }); + // verify that the stack we deployed has our notification ARN + const describeResponse = await fixture.aws.cloudFormation('describeStacks', { + StackName: fixture.fullStackName('test-2'), + }); + expect((_a = describeResponse.Stacks) === null || _a === void 0 ? void 0 : _a[0].NotificationARNs).toEqual([topicArn]); + } + finally { + await fixture.aws.sns('deleteTopic', { + TopicArn: topicArn, + }); + } +})); +test_helpers_1.integTest('deploy with role', cdk_1.withDefaultFixture(async (fixture) => { + const roleName = `${fixture.stackNamePrefix}-test-role`; + await deleteRole(); + const createResponse = await fixture.aws.iam('createRole', { + RoleName: roleName, + AssumeRolePolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: 'sts:AssumeRole', + Principal: { Service: 'cloudformation.amazonaws.com' }, + Effect: 'Allow', + }, { + Action: 'sts:AssumeRole', + Principal: { AWS: (await fixture.aws.sts('getCallerIdentity', {})).Arn }, + Effect: 'Allow', + }], + }), + }); + const roleArn = createResponse.Role.Arn; + try { + await fixture.aws.iam('putRolePolicy', { + RoleName: roleName, + PolicyName: 'DefaultPolicy', + PolicyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [{ + Action: '*', + Resource: '*', + Effect: 'Allow', + }], + }), + }); + await aws_1.retry(fixture.output, 'Trying to assume fresh role', aws_1.retry.forSeconds(300), async () => { + await fixture.aws.sts('assumeRole', { + RoleArn: roleArn, + RoleSessionName: 'testing', + }); + }); + // In principle, the role has replicated from 'us-east-1' to wherever we're testing. + // Give it a little more sleep to make sure CloudFormation is not hitting a box + // that doesn't have it yet. + await aws_1.sleep(5000); + await fixture.cdkDeploy('test-2', { + options: ['--role-arn', roleArn], + }); + // Immediately delete the stack again before we delete the role. + // + // Since roles are sticky, if we delete the role before the stack, subsequent DeleteStack + // operations will fail when CloudFormation tries to assume the role that's already gone. + await fixture.cdkDestroy('test-2'); + } + finally { + await deleteRole(); + } + async function deleteRole() { + try { + for (const policyName of (await fixture.aws.iam('listRolePolicies', { RoleName: roleName })).PolicyNames) { + await fixture.aws.iam('deleteRolePolicy', { + RoleName: roleName, + PolicyName: policyName, + }); + } + await fixture.aws.iam('deleteRole', { RoleName: roleName }); + } + catch (e) { + if (e.message.indexOf('cannot be found') > -1) { + return; + } + throw e; + } + } +})); +test_helpers_1.integTest('cdk diff', cdk_1.withDefaultFixture(async (fixture) => { + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // We can make it fail by passing --fail + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1')])) + .rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail on multiple stacks exits with error if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('AWS::SNS::Topic'); + await fixture.cdkDeploy('test-2'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('There were no differences'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --fail with multiple stack exits with if any of the stacks contains a diff', cdk_1.withDefaultFixture(async (fixture) => { + // GIVEN + await fixture.cdkDeploy('test-1'); + const diff1 = await fixture.cdk(['diff', fixture.fullStackName('test-1')]); + expect(diff1).toContain('There were no differences'); + const diff2 = await fixture.cdk(['diff', fixture.fullStackName('test-2')]); + expect(diff2).toContain('AWS::SNS::Topic'); + // WHEN / THEN + await expect(fixture.cdk(['diff', '--fail', fixture.fullStackName('test-1'), fixture.fullStackName('test-2')])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('cdk diff --security-only --fail exits when security changes are present', cdk_1.withDefaultFixture(async (fixture) => { + const stackName = 'iam-test'; + await expect(fixture.cdk(['diff', '--security-only', '--fail', fixture.fullStackName(stackName)])).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('deploy stack with docker asset', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdkDeploy('docker'); +})); +test_helpers_1.integTest('deploy and test stack with lambda asset', cdk_1.withDefaultFixture(async (fixture) => { + var _a, _b; + const stackArn = await fixture.cdkDeploy('lambda', { captureStderr: false }); + const response = await fixture.aws.cloudFormation('describeStacks', { + StackName: stackArn, + }); + const lambdaArn = (_b = (_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0].Outputs) === null || _b === void 0 ? void 0 : _b[0].OutputValue; + if (lambdaArn === undefined) { + throw new Error('Stack did not have expected Lambda ARN output'); + } + const output = await fixture.aws.lambda('invoke', { + FunctionName: lambdaArn, + }); + expect(JSON.stringify(output.Payload)).toContain('dear asset'); +})); +test_helpers_1.integTest('cdk ls', cdk_1.withDefaultFixture(async (fixture) => { + const listing = await fixture.cdk(['ls'], { captureStderr: false }); + const expectedStacks = [ + 'conditional-resource', + 'docker', + 'docker-with-custom-file', + 'failed', + 'iam-test', + 'lambda', + 'missing-ssm-parameter', + 'order-providing', + 'outputs-test-1', + 'outputs-test-2', + 'param-test-1', + 'param-test-2', + 'param-test-3', + 'termination-protection', + 'test-1', + 'test-2', + 'with-nested-stack', + 'with-nested-stack-using-parameters', + 'order-consuming', + ]; + for (const stack of expectedStacks) { + expect(listing).toContain(fixture.fullStackName(stack)); + } +})); +test_helpers_1.integTest('synthing a stage with errors leads to failure', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['synth'], { + allowErrExit: true, + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); + expect(output).toContain('This is an error'); +})); +test_helpers_1.integTest('synthing a stage with errors can be suppressed', cdk_1.withDefaultFixture(async (fixture) => { + await fixture.cdk(['synth', '--no-validation'], { + modEnv: { + INTEG_STACK_SET: 'stage-with-errors', + }, + }); +})); +test_helpers_1.integTest('deploy stack without resource', cdk_1.withDefaultFixture(async (fixture) => { + // Deploy the stack without resources + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + // This should have succeeded but not deployed the stack. + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); + // Deploy the stack with resources + await fixture.cdkDeploy('conditional-resource'); + // Then again WITHOUT resources (this should destroy the stack) + await fixture.cdkDeploy('conditional-resource', { modEnv: { NO_RESOURCE: 'TRUE' } }); + await expect(fixture.aws.cloudFormation('describeStacks', { StackName: fixture.fullStackName('conditional-resource') })) + .rejects.toThrow('conditional-resource does not exist'); +})); +test_helpers_1.integTest('IAM diff', cdk_1.withDefaultFixture(async (fixture) => { + const output = await fixture.cdk(['diff', fixture.fullStackName('iam-test')]); + // Roughly check for a table like this: + // + // ┌───┬─────────────────┬────────┬────────────────┬────────────────────────────-──┬───────────┐ + // │ │ Resource │ Effect │ Action │ Principal │ Condition │ + // ├───┼─────────────────┼────────┼────────────────┼───────────────────────────────┼───────────┤ + // │ + │ ${SomeRole.Arn} │ Allow │ sts:AssumeRole │ Service:ec2.amazonaws.com │ │ + // └───┴─────────────────┴────────┴────────────────┴───────────────────────────────┴───────────┘ + expect(output).toContain('${SomeRole.Arn}'); + expect(output).toContain('sts:AssumeRole'); + expect(output).toContain('ec2.amazonaws.com'); +})); +test_helpers_1.integTest('fast deploy', cdk_1.withDefaultFixture(async (fixture) => { + // we are using a stack with a nested stack because CFN will always attempt to + // update a nested stack, which will allow us to verify that updates are actually + // skipped unless --force is specified. + const stackArn = await fixture.cdkDeploy('with-nested-stack', { captureStderr: false }); + const changeSet1 = await getLatestChangeSet(); + // Deploy the same stack again, there should be no new change set created + await fixture.cdkDeploy('with-nested-stack'); + const changeSet2 = await getLatestChangeSet(); + expect(changeSet2.ChangeSetId).toEqual(changeSet1.ChangeSetId); + // Deploy the stack again with --force, now we should create a changeset + await fixture.cdkDeploy('with-nested-stack', { options: ['--force'] }); + const changeSet3 = await getLatestChangeSet(); + expect(changeSet3.ChangeSetId).not.toEqual(changeSet2.ChangeSetId); + // Deploy the stack again with tags, expected to create a new changeset + // even though the resources didn't change. + await fixture.cdkDeploy('with-nested-stack', { options: ['--tags', 'key=value'] }); + const changeSet4 = await getLatestChangeSet(); + expect(changeSet4.ChangeSetId).not.toEqual(changeSet3.ChangeSetId); + async function getLatestChangeSet() { + var _a, _b, _c; + const response = await fixture.aws.cloudFormation('describeStacks', { StackName: stackArn }); + if (!((_a = response.Stacks) === null || _a === void 0 ? void 0 : _a[0])) { + throw new Error('Did not get a ChangeSet at all'); + } + fixture.log(`Found Change Set ${(_b = response.Stacks) === null || _b === void 0 ? void 0 : _b[0].ChangeSetId}`); + return (_c = response.Stacks) === null || _c === void 0 ? void 0 : _c[0]; + } +})); +test_helpers_1.integTest('failed deploy does not hang', cdk_1.withDefaultFixture(async (fixture) => { + // this will hang if we introduce https://github.com/aws/aws-cdk/issues/6403 again. + await expect(fixture.cdkDeploy('failed')).rejects.toThrow('exited with error'); +})); +test_helpers_1.integTest('can still load old assemblies', cdk_1.withDefaultFixture(async (fixture) => { + const cxAsmDir = path.join(os.tmpdir(), 'cdk-integ-cx'); + const testAssembliesDirectory = path.join(__dirname, 'cloud-assemblies'); + for (const asmdir of await listChildDirs(testAssembliesDirectory)) { + fixture.log(`ASSEMBLY ${asmdir}`); + await cdk_1.cloneDirectory(asmdir, cxAsmDir); + // Some files in the asm directory that have a .js extension are + // actually treated as templates. Evaluate them using NodeJS. + const templates = await listChildren(cxAsmDir, fullPath => Promise.resolve(fullPath.endsWith('.js'))); + for (const template of templates) { + const targetName = template.replace(/.js$/, ''); + await cdk_1.shell([process.execPath, template, '>', targetName], { + cwd: cxAsmDir, + output: fixture.output, + modEnv: { + TEST_ACCOUNT: await fixture.aws.account(), + TEST_REGION: fixture.aws.region, + }, + }); + } + // Use this directory as a Cloud Assembly + const output = await fixture.cdk([ + '--app', cxAsmDir, + '-v', + 'synth', + ]); + // Assert that there was no providerError in CDK's stderr + // Because we rely on the app/framework to actually error in case the + // provider fails, we inspect the logs here. + expect(output).not.toContain('$providerError'); + } +})); +test_helpers_1.integTest('generating and loading assembly', cdk_1.withDefaultFixture(async (fixture) => { + const asmOutputDir = `${fixture.integTestDir}-cdk-integ-asm`; + await fixture.shell(['rm', '-rf', asmOutputDir]); + // Synthesize a Cloud Assembly tothe default directory (cdk.out) and a specific directory. + await fixture.cdk(['synth']); + await fixture.cdk(['synth', '--output', asmOutputDir]); + // cdk.out in the current directory and the indicated --output should be the same + await fixture.shell(['diff', 'cdk.out', asmOutputDir]); + // Check that we can 'ls' the synthesized asm. + // Change to some random directory to make sure we're not accidentally loading cdk.json + const list = await fixture.cdk(['--app', asmOutputDir, 'ls'], { cwd: os.tmpdir() }); + // Same stacks we know are in the app + expect(list).toContain(`${fixture.stackNamePrefix}-lambda`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-1`); + expect(list).toContain(`${fixture.stackNamePrefix}-test-2`); + // Check that we can use '.' and just synth ,the generated asm + const stackTemplate = await fixture.cdk(['--app', '.', 'synth', fixture.fullStackName('test-2')], { + cwd: asmOutputDir, + }); + expect(stackTemplate).toContain('topic152D84A37'); + // Deploy a Lambda from the copied asm + await fixture.cdkDeploy('lambda', { options: ['-a', '.'], cwd: asmOutputDir }); + // Remove (rename) the original custom docker file that was used during synth. + // this verifies that the assemly has a copy of it and that the manifest uses + // relative paths to reference to it. + const customDockerFile = path.join(fixture.integTestDir, 'docker', 'Dockerfile.Custom'); + await fs_1.promises.rename(customDockerFile, `${customDockerFile}~`); + try { + // deploy a docker image with custom file without synth (uses assets) + await fixture.cdkDeploy('docker-with-custom-file', { options: ['-a', '.'], cwd: asmOutputDir }); + } + finally { + // Rename back to restore fixture to original state + await fs_1.promises.rename(`${customDockerFile}~`, customDockerFile); + } +})); +test_helpers_1.integTest('templates on disk contain metadata resource, also in nested assemblies', cdk_1.withDefaultFixture(async (fixture) => { + // Synth first, and switch on version reporting because cdk.json is disabling it + await fixture.cdk(['synth', '--version-reporting=true']); + // Load template from disk from root assembly + const templateContents = await fixture.shell(['cat', 'cdk.out/*-lambda.template.json']); + expect(JSON.parse(templateContents).Resources.CDKMetadata).toBeTruthy(); + // Load template from nested assembly + const nestedTemplateContents = await fixture.shell(['cat', 'cdk.out/assembly-*-stage/*-stage-StackInStage.template.json']); + expect(JSON.parse(nestedTemplateContents).Resources.CDKMetadata).toBeTruthy(); +})); +async function listChildren(parent, pred) { + const ret = new Array(); + for (const child of await fs_1.promises.readdir(parent, { encoding: 'utf-8' })) { + const fullPath = path.join(parent, child.toString()); + if (await pred(fullPath)) { + ret.push(fullPath); + } + } + return ret; +} +async function listChildDirs(parent) { + return listChildren(parent, async (fullPath) => (await fs_1.promises.stat(fullPath)).isDirectory()); +} +async function readTemplate(fixture, stackName) { + const fullStackName = fixture.fullStackName(stackName); + const templatePath = path.join(fixture.integTestDir, 'cdk.out', `${fullStackName}.template.json`); + return JSON.parse((await fs_1.promises.readFile(templatePath, { encoding: 'utf-8' })).toString()); +} +//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiY2xpLmludGVndGVzdC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbImNsaS5pbnRlZ3Rlc3QudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSwyQkFBb0M7QUFDcEMseUJBQXlCO0FBQ3pCLDZCQUE2QjtBQUM3Qix3Q0FBOEM7QUFDOUMsd0NBQXdGO0FBQ3hGLDBEQUFvRDtBQUVwRCxJQUFJLENBQUMsVUFBVSxDQUFDLEdBQUcsR0FBRyxJQUFJLENBQUMsQ0FBQztBQUU1Qix3QkFBUyxDQUFDLFlBQVksRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDM0QsT0FBTyxDQUFDLEdBQUcsQ0FBQywyQ0FBMkMsQ0FBQyxDQUFDO0lBQ3pELE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsT0FBTyxDQUFDLEdBQUcsQ0FBQyw0Q0FBNEMsQ0FBQyxDQUFDO0lBQzFELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxZQUFZLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxrQkFBa0IsRUFBRSxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYsT0FBTyxDQUFDLEdBQUcsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRS9CLE9BQU8sQ0FBQyxHQUFHLENBQUMsc0NBQXNDLENBQUMsQ0FBQztJQUNwRCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsWUFBWSxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsa0JBQWtCLEVBQUUsUUFBUSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0FBQ3RGLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdDQUFnQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLENBQUMsRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ3BFLE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLFdBQVcsQ0FBQyxFQUFFLEVBQUUsT0FBTyxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFdEUsTUFBTSxDQUFDLFFBQVEsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNyQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyx3QkFBd0IsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDdkUsTUFBTSxTQUFTLEdBQUcsd0JBQXdCLENBQUM7SUFDM0MsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBRW5DLGlDQUFpQztJQUNqQyxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFNBQVMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBRWpGLHVGQUF1RjtJQUN2RixNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsU0FBUyxFQUFFLEVBQUUsTUFBTSxFQUFFLEVBQUUsc0JBQXNCLEVBQUUsT0FBTyxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBQ3BGLE1BQU0sT0FBTyxDQUFDLFVBQVUsQ0FBQyxTQUFTLENBQUMsQ0FBQztBQUN0QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxXQUFXLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzFELE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUM5RCxNQUFNLFNBQVMsR0FBRyxNQUFNLFlBQVksQ0FBQyxPQUFPLEVBQUUsUUFBUSxDQUFDLENBQUM7SUFDeEQsTUFBTSxDQUFDLFNBQVMsQ0FBQyxDQUFDLE9BQU8sQ0FBQztRQUN4QixTQUFTLEVBQUU7WUFDVCxhQUFhLEVBQUU7Z0JBQ2IsSUFBSSxFQUFFLGlCQUFpQjtnQkFDdkIsUUFBUSxFQUFFO29CQUNSLGNBQWMsRUFBRSxHQUFHLE9BQU8sQ0FBQyxlQUFlLHdCQUF3QjtpQkFDbkU7YUFDRjtTQUNGO0tBQ0YsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxFQUFFLE9BQU8sRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBQ2xGLE1BQU0sU0FBUyxHQUFHLE1BQU0sWUFBWSxDQUFDLE9BQU8sRUFBRSxRQUFRLENBQUMsQ0FBQztJQUN4RCxNQUFNLENBQUMsU0FBUyxDQUFDLENBQUMsT0FBTyxDQUFDO1FBQ3hCLFNBQVMsRUFBRTtZQUNULGNBQWMsRUFBRTtnQkFDZCxJQUFJLEVBQUUsaUJBQWlCO2dCQUN2QixRQUFRLEVBQUU7b0JBQ1IsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUseUJBQXlCO2lCQUNwRTthQUNGO1lBQ0QsY0FBYyxFQUFFO2dCQUNkLElBQUksRUFBRSxpQkFBaUI7Z0JBQ3ZCLFFBQVEsRUFBRTtvQkFDUixjQUFjLEVBQUUsR0FBRyxPQUFPLENBQUMsZUFBZSx5QkFBeUI7aUJBQ3BFO2FBQ0Y7U0FDRjtLQUNGLENBQUMsQ0FBQztBQUVMLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDhCQUE4QixFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUM3RSxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTztRQUMvQixPQUFPLENBQUMsYUFBYSxDQUFDLHVCQUF1QixDQUFDO1FBQzlDLElBQUksRUFBRSx5Q0FBeUMsQ0FBQyxFQUFFO1FBQ2xELFlBQVksRUFBRSxJQUFJO0tBQ25CLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsd0NBQXdDLENBQUMsQ0FBQztBQUNuRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxvQkFBb0IsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDbkUsb0VBQW9FO0lBQ3BFLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTNDLHFFQUFxRTtJQUNyRSxNQUFNLE9BQU8sQ0FBQyxVQUFVLENBQUMsaUJBQWlCLENBQUMsQ0FBQztBQUM5QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxpQkFBaUIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDaEUsTUFBTSxhQUFFLENBQUMsU0FBUyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxrQkFBa0IsQ0FBQyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUM7UUFDckYsVUFBVSxFQUFFLDJCQUEyQjtLQUN4QyxDQUFDLENBQUMsQ0FBQztJQUNKLElBQUk7UUFDRixNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsU0FBUyxDQUFDLENBQUMsQ0FBQyxDQUFDLFFBQVEsQ0FBQyxTQUFTLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUV2RiwwQ0FBMEM7UUFDMUMsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsU0FBUyxFQUFFLFNBQVMsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDO1FBQ3hELE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQyxTQUFTLENBQUMsMkJBQTJCLENBQUMsQ0FBQztRQUUzRiw0REFBNEQ7UUFDNUQsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsU0FBUyxFQUFFLElBQUksRUFBRSxTQUFTLEVBQUUsWUFBWSxDQUFDLENBQUMsQ0FBQztLQUUvRDtZQUFTO1FBQ1IsTUFBTSxhQUFFLENBQUMsTUFBTSxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxrQkFBa0IsQ0FBQyxDQUFDLENBQUM7S0FDdEU7QUFDSCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxvQ0FBb0MsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDbkYsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLFFBQVEsQ0FBQztRQUM1QixxR0FBcUc7UUFDckcsT0FBTyxFQUFFLENBQUMsY0FBYyxDQUFDO1FBQ3pCLE1BQU0sRUFBRTtZQUNOLGVBQWUsRUFBRSxxQkFBcUI7U0FDdkM7UUFDRCxZQUFZLEVBQUUsSUFBSTtLQUNuQixDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsU0FBUyxDQUFDLG9DQUFvQyxDQUFDLENBQUM7QUFDL0QsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsUUFBUSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDdkQsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFFBQVEsRUFBRSxFQUFFLGFBQWEsRUFBRSxLQUFLLEVBQUUsQ0FBQyxDQUFDO0lBRTdFLDhDQUE4QztJQUM5QyxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLHdCQUF3QixFQUFFO1FBQzFFLFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUNILE1BQU0sT0FBQyxRQUFRLENBQUMsY0FBYywwQ0FBRSxNQUFNLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFDckQsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsWUFBWSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMzRCxNQUFNLElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFekUsbUZBQW1GO0lBQ25GLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUM3QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw4QkFBOEIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQzdFLHdFQUF3RTtJQUN4RSw0RkFBNEY7SUFDNUYsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLG9DQUFvQyxFQUFFO1FBQzdFLE9BQU8sRUFBRSxDQUFDLGNBQWMsRUFBRSxnQkFBZ0IsT0FBTyxDQUFDLGVBQWUsZ0JBQWdCLENBQUM7UUFDbEYsYUFBYSxFQUFFLEtBQUs7S0FDckIsQ0FBQyxDQUFDO0lBRUgsbUZBQW1GO0lBQ25GLE1BQU0sQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUUvQyw4Q0FBOEM7SUFDOUMsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyx3QkFBd0IsRUFBRTtRQUMxRSxTQUFTLEVBQUUsUUFBUTtLQUNwQixDQUFDLENBQUM7SUFDSCxNQUFNLE9BQUMsUUFBUSxDQUFDLGNBQWMsMENBQUUsTUFBTSxDQUFDLENBQUMsT0FBTyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBQ3JELENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDJDQUEyQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDMUYsTUFBTSxhQUFhLEdBQUcsd0JBQXdCLENBQUM7SUFDL0MsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFFBQVEsRUFBRTtRQUNqRCxPQUFPLEVBQUUsQ0FBQyxjQUFjLEVBQUUsbUJBQW1CLEVBQUUsYUFBYSxDQUFDO1FBQzdELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUNILG1GQUFtRjtJQUNuRixNQUFNLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxJQUFJLENBQUMsQ0FBQyxNQUFNLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFL0MsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRSxTQUFTLEVBQUUsUUFBUTtLQUNwQixDQUFDLENBQUM7SUFDSCxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxvQkFBb0IsQ0FBQyxDQUFDO0lBRXZFLHdEQUF3RDtJQUN4RCxNQUFNLGlCQUFpQixHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDM0UsU0FBUyxFQUFFLFFBQVE7S0FDcEIsQ0FBQyxDQUFDO0lBQ0gsTUFBTSxVQUFVLEdBQUcsaUJBQWlCLENBQUMsU0FBUyxJQUFJLEVBQUUsQ0FBQztJQUNyRCxNQUFNLENBQUMsVUFBVSxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUNyQyxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLGFBQWEsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUMsQ0FBQztJQUMzRCxNQUFNLENBQUMsVUFBVSxDQUFDLENBQUMsQ0FBQyxDQUFDLE1BQU0sQ0FBQyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0FBQzFELENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDZEQUE2RCxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUM1RywwRUFBMEU7SUFDMUUsNEVBQTRFO0lBQzVFLHdEQUF3RDtJQUN4RCxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUM7SUFDN0IsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxTQUFTLEVBQUU7UUFDeEMsT0FBTyxFQUFFLENBQUMsR0FBRyxFQUFFLFdBQVcsQ0FBQztRQUMzQixvQkFBb0IsRUFBRSxLQUFLO0tBQzVCLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUV6QyxnQ0FBZ0M7SUFDaEMsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDeEQsU0FBUyxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsU0FBUyxDQUFDO0tBQzVDLENBQUMsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsZ0JBQWdCLENBQUMsQ0FBQztBQUN4QyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw4QkFBOEIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDN0UsTUFBTSxXQUFXLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLFNBQVMsRUFBRSxjQUFjLENBQUMsQ0FBQztJQUMvRSxNQUFNLGFBQUUsQ0FBQyxLQUFLLENBQUMsSUFBSSxDQUFDLE9BQU8sQ0FBQyxXQUFXLENBQUMsRUFBRSxFQUFFLFNBQVMsRUFBRSxJQUFJLEVBQUUsQ0FBQyxDQUFDO0lBRS9ELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxDQUFDLGdCQUFnQixDQUFDLEVBQUU7UUFDMUMsT0FBTyxFQUFFLENBQUMsZ0JBQWdCLEVBQUUsV0FBVyxDQUFDO0tBQ3pDLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBTyxHQUFHLElBQUksQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLGFBQUUsQ0FBQyxRQUFRLENBQUMsV0FBVyxFQUFFLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDO0lBQy9GLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDdEIsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxlQUFlLGlCQUFpQixDQUFDLEVBQUU7WUFDN0MsU0FBUyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsd0JBQXdCO1NBQzlEO1FBQ0QsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxlQUFlLGlCQUFpQixDQUFDLEVBQUU7WUFDN0MsU0FBUyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsNkJBQTZCO1NBQ25FO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsd0JBQXdCLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFOztJQUN2RSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQ3ZELE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsU0FBUztTQUNuRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDbEUsU0FBUyxFQUFFLFFBQVE7S0FDcEIsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFDLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxVQUFVLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDOUM7WUFDRSxZQUFZLEVBQUUsZ0JBQWdCO1lBQzlCLGNBQWMsRUFBRSxHQUFHLE9BQU8sQ0FBQyxlQUFlLFNBQVM7U0FDcEQ7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxtRkFBbUYsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQ2xJLFFBQVE7SUFDUixNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsU0FBUyxDQUFDLGNBQWMsRUFBRTtRQUM3QyxPQUFPLEVBQUU7WUFDUCxjQUFjLEVBQUUsa0JBQWtCLE9BQU8sQ0FBQyxlQUFlLE1BQU07U0FDaEU7UUFDRCxhQUFhLEVBQUUsS0FBSztLQUNyQixDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7SUFFekMsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUNsRSxTQUFTLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxjQUFjLENBQUM7S0FDakQsQ0FBQyxDQUFDO0lBRUgsTUFBTSxRQUFRLFNBQUcsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLE9BQU8sQ0FBQztJQUM5QyxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBRXRFLE9BQU87SUFDUCxNQUFNLFdBQVcsR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQzFELE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsU0FBUztTQUNuRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILE1BQU0sZ0JBQWdCLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUMxRSxTQUFTLEVBQUUsV0FBVztLQUN2QixDQUFDLENBQUM7SUFFSCxPQUFPO0lBQ1AsTUFBTSxDQUFFLFFBQVEsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsV0FBVyxDQUFDLENBQUMsQ0FBQyx3QkFBd0I7SUFDcEUsTUFBTSxPQUFDLGdCQUFnQixDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQzVFLE1BQU0sT0FBQyxnQkFBZ0IsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxVQUFVLENBQUMsQ0FBQyxPQUFPLENBQUM7UUFDdEQ7WUFDRSxZQUFZLEVBQUUsZ0JBQWdCO1lBQzlCLGNBQWMsRUFBRSxHQUFHLE9BQU8sQ0FBQyxlQUFlLFNBQVM7U0FDcEQ7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyx3REFBd0QsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQ3ZHLFFBQVE7SUFDUixNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQ3ZELE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsTUFBTTtTQUNoRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILElBQUksUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUU7UUFDaEUsU0FBUyxFQUFFLFFBQVE7S0FDcEIsQ0FBQyxDQUFDO0lBRUgsTUFBTSxPQUFDLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxXQUFXLENBQUMsQ0FBQyxPQUFPLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUVwRSx5RUFBeUU7SUFDekUsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUU7UUFDN0MsT0FBTyxFQUFFO1lBQ1AsY0FBYyxFQUFFLGtCQUFrQixPQUFPLENBQUMsZUFBZSxNQUFNO1NBQ2hFO1FBQ0QsYUFBYSxFQUFFLEtBQUs7S0FDckIsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0lBQUEsQ0FBQztJQUUxQyxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRTtRQUM1RCxTQUFTLEVBQUUsUUFBUTtLQUNwQixDQUFDLENBQUM7SUFFSCxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQywwQkFBMEIsQ0FBQyxDQUFDO0lBRTdFLE9BQU87SUFDUCxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsY0FBYyxFQUFFO1FBQ3RDLE9BQU8sRUFBRTtZQUNQLGNBQWMsRUFBRSxrQkFBa0IsT0FBTyxDQUFDLGVBQWUsU0FBUztTQUNuRTtRQUNELGFBQWEsRUFBRSxLQUFLO0tBQ3JCLENBQUMsQ0FBQztJQUVILFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1FBQzVELFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUVILE9BQU87SUFDUCxNQUFNLE9BQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFLFdBQVcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBQ3BFLE1BQU0sT0FBQyxRQUFRLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDO1FBQzlDO1lBQ0UsWUFBWSxFQUFFLGdCQUFnQjtZQUM5QixjQUFjLEVBQUUsR0FBRyxPQUFPLENBQUMsZUFBZSxTQUFTO1NBQ3BEO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMscUNBQXFDLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQ3BGLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUU7UUFDdEMsT0FBTyxFQUFFO1lBQ1AsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsZ0NBQWdDLE9BQU8sQ0FBQyxlQUFlLFNBQVM7WUFDMUcsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUscUNBQXFDLE9BQU8sQ0FBQyxlQUFlLGFBQWE7WUFDbkgsY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsa0NBQWtDLE9BQU8sQ0FBQyxlQUFlLFVBQVU7WUFDN0csY0FBYyxFQUFFLEdBQUcsT0FBTyxDQUFDLGVBQWUsdUNBQXVDLE9BQU8sQ0FBQyxlQUFlLFlBQVk7U0FDckg7S0FDRixDQUFDLENBQUM7QUFDTCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw4QkFBOEIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQzdFLE1BQU0sU0FBUyxHQUFHLEdBQUcsT0FBTyxDQUFDLGVBQWUsU0FBUyxDQUFDO0lBQ3RELE1BQU0sU0FBUyxHQUFHLEdBQUcsT0FBTyxDQUFDLGVBQWUsYUFBYSxDQUFDO0lBRTFELE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxjQUFjLEVBQUU7UUFDdkQsT0FBTyxFQUFFO1lBQ1AsY0FBYyxFQUFFLG9CQUFvQixTQUFTLEVBQUU7WUFDL0MsY0FBYyxFQUFFLHlCQUF5QixTQUFTLEVBQUU7U0FDckQ7UUFDRCxhQUFhLEVBQUUsS0FBSztLQUNyQixDQUFDLENBQUM7SUFFSCxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1FBQ2xFLFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUVILE1BQU0sT0FBQyxRQUFRLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsVUFBVSxDQUFDLENBQUMsT0FBTyxDQUFDO1FBQzlDO1lBQ0UsWUFBWSxFQUFFLGtCQUFrQjtZQUNoQyxjQUFjLEVBQUUsU0FBUztTQUMxQjtRQUNEO1lBQ0UsWUFBWSxFQUFFLHVCQUF1QjtZQUNyQyxjQUFjLEVBQUUsU0FBUztTQUMxQjtLQUNGLENBQUMsQ0FBQztBQUNMLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLDhCQUE4QixFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTs7SUFDN0UsTUFBTSxTQUFTLEdBQUcsR0FBRyxPQUFPLENBQUMsZUFBZSxhQUFhLENBQUM7SUFFMUQsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxhQUFhLEVBQUUsRUFBRSxJQUFJLEVBQUUsU0FBUyxFQUFFLENBQUMsQ0FBQztJQUMzRSxNQUFNLFFBQVEsR0FBRyxRQUFRLENBQUMsUUFBUyxDQUFDO0lBQ3BDLElBQUk7UUFDRixNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFO1lBQ2hDLE9BQU8sRUFBRSxDQUFDLHFCQUFxQixFQUFFLFFBQVEsQ0FBQztTQUMzQyxDQUFDLENBQUM7UUFFSCw2REFBNkQ7UUFDN0QsTUFBTSxnQkFBZ0IsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1lBQzFFLFNBQVMsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQztTQUMzQyxDQUFDLENBQUM7UUFDSCxNQUFNLE9BQUMsZ0JBQWdCLENBQUMsTUFBTSwwQ0FBRyxDQUFDLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQyxPQUFPLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDO0tBQzNFO1lBQVM7UUFDUixNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLGFBQWEsRUFBRTtZQUNuQyxRQUFRLEVBQUUsUUFBUTtTQUNuQixDQUFDLENBQUM7S0FDSjtBQUNILENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGtCQUFrQixFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUNqRSxNQUFNLFFBQVEsR0FBRyxHQUFHLE9BQU8sQ0FBQyxlQUFlLFlBQVksQ0FBQztJQUV4RCxNQUFNLFVBQVUsRUFBRSxDQUFDO0lBRW5CLE1BQU0sY0FBYyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsWUFBWSxFQUFFO1FBQ3pELFFBQVEsRUFBRSxRQUFRO1FBQ2xCLHdCQUF3QixFQUFFLElBQUksQ0FBQyxTQUFTLENBQUM7WUFDdkMsT0FBTyxFQUFFLFlBQVk7WUFDckIsU0FBUyxFQUFFLENBQUM7b0JBQ1YsTUFBTSxFQUFFLGdCQUFnQjtvQkFDeEIsU0FBUyxFQUFFLEVBQUUsT0FBTyxFQUFFLDhCQUE4QixFQUFFO29CQUN0RCxNQUFNLEVBQUUsT0FBTztpQkFDaEIsRUFBRTtvQkFDRCxNQUFNLEVBQUUsZ0JBQWdCO29CQUN4QixTQUFTLEVBQUUsRUFBRSxHQUFHLEVBQUUsQ0FBQyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLG1CQUFtQixFQUFFLEVBQUUsQ0FBQyxDQUFDLENBQUMsR0FBRyxFQUFFO29CQUN4RSxNQUFNLEVBQUUsT0FBTztpQkFDaEIsQ0FBQztTQUNILENBQUM7S0FDSCxDQUFDLENBQUM7SUFDSCxNQUFNLE9BQU8sR0FBRyxjQUFjLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztJQUN4QyxJQUFJO1FBQ0YsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxlQUFlLEVBQUU7WUFDckMsUUFBUSxFQUFFLFFBQVE7WUFDbEIsVUFBVSxFQUFFLGVBQWU7WUFDM0IsY0FBYyxFQUFFLElBQUksQ0FBQyxTQUFTLENBQUM7Z0JBQzdCLE9BQU8sRUFBRSxZQUFZO2dCQUNyQixTQUFTLEVBQUUsQ0FBQzt3QkFDVixNQUFNLEVBQUUsR0FBRzt3QkFDWCxRQUFRLEVBQUUsR0FBRzt3QkFDYixNQUFNLEVBQUUsT0FBTztxQkFDaEIsQ0FBQzthQUNILENBQUM7U0FDSCxDQUFDLENBQUM7UUFFSCxNQUFNLFdBQUssQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLDZCQUE2QixFQUFFLFdBQUssQ0FBQyxVQUFVLENBQUMsR0FBRyxDQUFDLEVBQUUsS0FBSyxJQUFJLEVBQUU7WUFDM0YsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLEdBQUcsQ0FBQyxZQUFZLEVBQUU7Z0JBQ2xDLE9BQU8sRUFBRSxPQUFPO2dCQUNoQixlQUFlLEVBQUUsU0FBUzthQUMzQixDQUFDLENBQUM7UUFDTCxDQUFDLENBQUMsQ0FBQztRQUVILG9GQUFvRjtRQUNwRiwrRUFBK0U7UUFDL0UsNEJBQTRCO1FBQzVCLE1BQU0sV0FBSyxDQUFDLElBQUksQ0FBQyxDQUFDO1FBRWxCLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLEVBQUU7WUFDaEMsT0FBTyxFQUFFLENBQUMsWUFBWSxFQUFFLE9BQU8sQ0FBQztTQUNqQyxDQUFDLENBQUM7UUFFSCxnRUFBZ0U7UUFDaEUsRUFBRTtRQUNGLHlGQUF5RjtRQUN6Rix5RkFBeUY7UUFDekYsTUFBTSxPQUFPLENBQUMsVUFBVSxDQUFDLFFBQVEsQ0FBQyxDQUFDO0tBRXBDO1lBQVM7UUFDUixNQUFNLFVBQVUsRUFBRSxDQUFDO0tBQ3BCO0lBRUQsS0FBSyxVQUFVLFVBQVU7UUFDdkIsSUFBSTtZQUNGLEtBQUssTUFBTSxVQUFVLElBQUksQ0FBQyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLGtCQUFrQixFQUFFLEVBQUUsUUFBUSxFQUFFLFFBQVEsRUFBRSxDQUFDLENBQUMsQ0FBQyxXQUFXLEVBQUU7Z0JBQ3hHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsa0JBQWtCLEVBQUU7b0JBQ3hDLFFBQVEsRUFBRSxRQUFRO29CQUNsQixVQUFVLEVBQUUsVUFBVTtpQkFDdkIsQ0FBQyxDQUFDO2FBQ0o7WUFDRCxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsR0FBRyxDQUFDLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxRQUFRLEVBQUUsQ0FBQyxDQUFDO1NBQzdEO1FBQUMsT0FBTyxDQUFDLEVBQUU7WUFDVixJQUFJLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLGlCQUFpQixDQUFDLEdBQUcsQ0FBQyxDQUFDLEVBQUU7Z0JBQUUsT0FBTzthQUFFO1lBQzFELE1BQU0sQ0FBQyxDQUFDO1NBQ1Q7SUFDSCxDQUFDO0FBQ0gsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsVUFBVSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN6RCxNQUFNLEtBQUssR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTNDLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMzRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFM0Msd0NBQXdDO0lBQ3hDLE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDO1NBQzNFLE9BQU8sQ0FBQyxPQUFPLENBQUMsbUJBQW1CLENBQUMsQ0FBQztBQUMxQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQywwRkFBMEYsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDekksUUFBUTtJQUNSLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMzRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLGlCQUFpQixDQUFDLENBQUM7SUFFM0MsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLFFBQVEsQ0FBQyxDQUFDO0lBQ2xDLE1BQU0sS0FBSyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLENBQUMsQ0FBQztJQUMzRSxNQUFNLENBQUMsS0FBSyxDQUFDLENBQUMsU0FBUyxDQUFDLDJCQUEyQixDQUFDLENBQUM7SUFFckQsY0FBYztJQUNkLE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsUUFBUSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDdkosQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMscUZBQXFGLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQ3BJLFFBQVE7SUFDUixNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLENBQUM7SUFDbEMsTUFBTSxLQUFLLEdBQUcsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDO0lBQzNFLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxTQUFTLENBQUMsMkJBQTJCLENBQUMsQ0FBQztJQUVyRCxNQUFNLEtBQUssR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFDM0UsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLFNBQVMsQ0FBQyxpQkFBaUIsQ0FBQyxDQUFDO0lBRTNDLGNBQWM7SUFDZCxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsTUFBTSxFQUFFLFFBQVEsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxFQUFFLE9BQU8sQ0FBQyxhQUFhLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsT0FBTyxDQUFDLE9BQU8sQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQ3ZKLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLHlFQUF5RSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN4SCxNQUFNLFNBQVMsR0FBRyxVQUFVLENBQUM7SUFDN0IsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE1BQU0sRUFBRSxpQkFBaUIsRUFBRSxRQUFRLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxTQUFTLENBQUMsQ0FBQyxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDMUksQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsZ0NBQWdDLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQy9FLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQUNwQyxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyx5Q0FBeUMsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7O0lBQ3hGLE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxRQUFRLEVBQUUsRUFBRSxhQUFhLEVBQUUsS0FBSyxFQUFFLENBQUMsQ0FBQztJQUU3RSxNQUFNLFFBQVEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsY0FBYyxDQUFDLGdCQUFnQixFQUFFO1FBQ2xFLFNBQVMsRUFBRSxRQUFRO0tBQ3BCLENBQUMsQ0FBQztJQUNILE1BQU0sU0FBUyxlQUFHLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxPQUFPLDBDQUFHLENBQUMsRUFBRSxXQUFXLENBQUM7SUFDaEUsSUFBSSxTQUFTLEtBQUssU0FBUyxFQUFFO1FBQzNCLE1BQU0sSUFBSSxLQUFLLENBQUMsK0NBQStDLENBQUMsQ0FBQztLQUNsRTtJQUVELE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxNQUFNLENBQUMsUUFBUSxFQUFFO1FBQ2hELFlBQVksRUFBRSxTQUFTO0tBQ3hCLENBQUMsQ0FBQztJQUVILE1BQU0sQ0FBQyxJQUFJLENBQUMsU0FBUyxDQUFDLE1BQU0sQ0FBQyxPQUFPLENBQUMsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxZQUFZLENBQUMsQ0FBQztBQUNqRSxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxRQUFRLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQ3ZELE1BQU0sT0FBTyxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFFcEUsTUFBTSxjQUFjLEdBQUc7UUFDckIsc0JBQXNCO1FBQ3RCLFFBQVE7UUFDUix5QkFBeUI7UUFDekIsUUFBUTtRQUNSLFVBQVU7UUFDVixRQUFRO1FBQ1IsdUJBQXVCO1FBQ3ZCLGlCQUFpQjtRQUNqQixnQkFBZ0I7UUFDaEIsZ0JBQWdCO1FBQ2hCLGNBQWM7UUFDZCxjQUFjO1FBQ2QsY0FBYztRQUNkLHdCQUF3QjtRQUN4QixRQUFRO1FBQ1IsUUFBUTtRQUNSLG1CQUFtQjtRQUNuQixvQ0FBb0M7UUFDcEMsaUJBQWlCO0tBQ2xCLENBQUM7SUFFRixLQUFLLE1BQU0sS0FBSyxJQUFJLGNBQWMsRUFBRTtRQUNsQyxNQUFNLENBQUMsT0FBTyxDQUFDLENBQUMsU0FBUyxDQUFDLE9BQU8sQ0FBQyxhQUFhLENBQUMsS0FBSyxDQUFDLENBQUMsQ0FBQztLQUN6RDtBQUNILENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLCtDQUErQyxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUM5RixNQUFNLE1BQU0sR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLENBQUMsRUFBRTtRQUMxQyxZQUFZLEVBQUUsSUFBSTtRQUNsQixNQUFNLEVBQUU7WUFDTixlQUFlLEVBQUUsbUJBQW1CO1NBQ3JDO0tBQ0YsQ0FBQyxDQUFDO0lBRUgsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFNBQVMsQ0FBQyxrQkFBa0IsQ0FBQyxDQUFDO0FBQy9DLENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGdEQUFnRCxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUMvRixNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsaUJBQWlCLENBQUMsRUFBRTtRQUM5QyxNQUFNLEVBQUU7WUFDTixlQUFlLEVBQUUsbUJBQW1CO1NBQ3JDO0tBQ0YsQ0FBQyxDQUFDO0FBQ0wsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsK0JBQStCLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzlFLHFDQUFxQztJQUNyQyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsc0JBQXNCLEVBQUUsRUFBRSxNQUFNLEVBQUUsRUFBRSxXQUFXLEVBQUUsTUFBTSxFQUFFLEVBQUUsQ0FBQyxDQUFDO0lBRXJGLHlEQUF5RDtJQUN6RCxNQUFNLE1BQU0sQ0FBQyxPQUFPLENBQUMsR0FBRyxDQUFDLGNBQWMsQ0FBQyxnQkFBZ0IsRUFBRSxFQUFFLFNBQVMsRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLHNCQUFzQixDQUFDLEVBQUUsQ0FBQyxDQUFDO1NBQ3JILE9BQU8sQ0FBQyxPQUFPLENBQUMscUNBQXFDLENBQUMsQ0FBQztJQUUxRCxrQ0FBa0M7SUFDbEMsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLHNCQUFzQixDQUFDLENBQUM7SUFFaEQsK0RBQStEO0lBQy9ELE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyxzQkFBc0IsRUFBRSxFQUFFLE1BQU0sRUFBRSxFQUFFLFdBQVcsRUFBRSxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFFckYsTUFBTSxNQUFNLENBQUMsT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUUsRUFBRSxTQUFTLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxzQkFBc0IsQ0FBQyxFQUFFLENBQUMsQ0FBQztTQUNySCxPQUFPLENBQUMsT0FBTyxDQUFDLHFDQUFxQyxDQUFDLENBQUM7QUFDNUQsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsVUFBVSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN6RCxNQUFNLE1BQU0sR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxNQUFNLEVBQUUsT0FBTyxDQUFDLGFBQWEsQ0FBQyxVQUFVLENBQUMsQ0FBQyxDQUFDLENBQUM7SUFFOUUsdUNBQXVDO0lBQ3ZDLEVBQUU7SUFDRixnR0FBZ0c7SUFDaEcsZ0dBQWdHO0lBQ2hHLGdHQUFnRztJQUNoRyxnR0FBZ0c7SUFDaEcsZ0dBQWdHO0lBRWhHLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxTQUFTLENBQUMsaUJBQWlCLENBQUMsQ0FBQztJQUM1QyxNQUFNLENBQUMsTUFBTSxDQUFDLENBQUMsU0FBUyxDQUFDLGdCQUFnQixDQUFDLENBQUM7SUFDM0MsTUFBTSxDQUFDLE1BQU0sQ0FBQyxDQUFDLFNBQVMsQ0FBQyxtQkFBbUIsQ0FBQyxDQUFDO0FBQ2hELENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLGFBQWEsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDNUQsOEVBQThFO0lBQzlFLGlGQUFpRjtJQUNqRix1Q0FBdUM7SUFDdkMsTUFBTSxRQUFRLEdBQUcsTUFBTSxPQUFPLENBQUMsU0FBUyxDQUFDLG1CQUFtQixFQUFFLEVBQUUsYUFBYSxFQUFFLEtBQUssRUFBRSxDQUFDLENBQUM7SUFDeEYsTUFBTSxVQUFVLEdBQUcsTUFBTSxrQkFBa0IsRUFBRSxDQUFDO0lBRTlDLHlFQUF5RTtJQUN6RSxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsbUJBQW1CLENBQUMsQ0FBQztJQUM3QyxNQUFNLFVBQVUsR0FBRyxNQUFNLGtCQUFrQixFQUFFLENBQUM7SUFDOUMsTUFBTSxDQUFDLFVBQVUsQ0FBQyxXQUFXLENBQUMsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRS9ELHdFQUF3RTtJQUN4RSxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsbUJBQW1CLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxTQUFTLENBQUMsRUFBRSxDQUFDLENBQUM7SUFDdkUsTUFBTSxVQUFVLEdBQUcsTUFBTSxrQkFBa0IsRUFBRSxDQUFDO0lBQzlDLE1BQU0sQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDLENBQUMsR0FBRyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDLENBQUM7SUFFbkUsdUVBQXVFO0lBQ3ZFLDJDQUEyQztJQUMzQyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsbUJBQW1CLEVBQUUsRUFBRSxPQUFPLEVBQUUsQ0FBQyxRQUFRLEVBQUUsV0FBVyxDQUFDLEVBQUUsQ0FBQyxDQUFDO0lBQ25GLE1BQU0sVUFBVSxHQUFHLE1BQU0sa0JBQWtCLEVBQUUsQ0FBQztJQUM5QyxNQUFNLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxPQUFPLENBQUMsVUFBVSxDQUFDLFdBQVcsQ0FBQyxDQUFDO0lBRW5FLEtBQUssVUFBVSxrQkFBa0I7O1FBQy9CLE1BQU0sUUFBUSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxjQUFjLENBQUMsZ0JBQWdCLEVBQUUsRUFBRSxTQUFTLEVBQUUsUUFBUSxFQUFFLENBQUMsQ0FBQztRQUM3RixJQUFJLFFBQUMsUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFDLEVBQUU7WUFBRSxNQUFNLElBQUksS0FBSyxDQUFDLGdDQUFnQyxDQUFDLENBQUM7U0FBRTtRQUNqRixPQUFPLENBQUMsR0FBRyxDQUFDLG9CQUFvQixNQUFBLFFBQVEsQ0FBQyxNQUFNLDBDQUFHLENBQUMsRUFBRSxXQUFXLEVBQUUsQ0FBQyxDQUFDO1FBQ3BFLGFBQU8sUUFBUSxDQUFDLE1BQU0sMENBQUcsQ0FBQyxFQUFFO0lBQzlCLENBQUM7QUFDSCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyw2QkFBNkIsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDNUUsbUZBQW1GO0lBQ25GLE1BQU0sTUFBTSxDQUFDLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxPQUFPLENBQUMsT0FBTyxDQUFDLG1CQUFtQixDQUFDLENBQUM7QUFDakYsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLHdCQUFTLENBQUMsK0JBQStCLEVBQUUsd0JBQWtCLENBQUMsS0FBSyxFQUFFLE9BQU8sRUFBRSxFQUFFO0lBQzlFLE1BQU0sUUFBUSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsRUFBRSxDQUFDLE1BQU0sRUFBRSxFQUFFLGNBQWMsQ0FBQyxDQUFDO0lBRXhELE1BQU0sdUJBQXVCLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxTQUFTLEVBQUUsa0JBQWtCLENBQUMsQ0FBQztJQUN6RSxLQUFLLE1BQU0sTUFBTSxJQUFJLE1BQU0sYUFBYSxDQUFDLHVCQUF1QixDQUFDLEVBQUU7UUFDakUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxZQUFZLE1BQU0sRUFBRSxDQUFDLENBQUM7UUFDbEMsTUFBTSxvQkFBYyxDQUFDLE1BQU0sRUFBRSxRQUFRLENBQUMsQ0FBQztRQUV2QyxnRUFBZ0U7UUFDaEUsNkRBQTZEO1FBQzdELE1BQU0sU0FBUyxHQUFHLE1BQU0sWUFBWSxDQUFDLFFBQVEsRUFBRSxRQUFRLENBQUMsRUFBRSxDQUFDLE9BQU8sQ0FBQyxPQUFPLENBQUMsUUFBUSxDQUFDLFFBQVEsQ0FBQyxLQUFLLENBQUMsQ0FBQyxDQUFDLENBQUM7UUFDdEcsS0FBSyxNQUFNLFFBQVEsSUFBSSxTQUFTLEVBQUU7WUFDaEMsTUFBTSxVQUFVLEdBQUcsUUFBUSxDQUFDLE9BQU8sQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7WUFDaEQsTUFBTSxXQUFLLENBQUMsQ0FBQyxPQUFPLENBQUMsUUFBUSxFQUFFLFFBQVEsRUFBRSxHQUFHLEVBQUUsVUFBVSxDQUFDLEVBQUU7Z0JBQ3pELEdBQUcsRUFBRSxRQUFRO2dCQUNiLE1BQU0sRUFBRSxPQUFPLENBQUMsTUFBTTtnQkFDdEIsTUFBTSxFQUFFO29CQUNOLFlBQVksRUFBRSxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsT0FBTyxFQUFFO29CQUN6QyxXQUFXLEVBQUUsT0FBTyxDQUFDLEdBQUcsQ0FBQyxNQUFNO2lCQUNoQzthQUNGLENBQUMsQ0FBQztTQUNKO1FBRUQseUNBQXlDO1FBQ3pDLE1BQU0sTUFBTSxHQUFHLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQztZQUMvQixPQUFPLEVBQUUsUUFBUTtZQUNqQixJQUFJO1lBQ0osT0FBTztTQUNSLENBQUMsQ0FBQztRQUVILHlEQUF5RDtRQUN6RCxxRUFBcUU7UUFDckUsNENBQTRDO1FBQzVDLE1BQU0sQ0FBQyxNQUFNLENBQUMsQ0FBQyxHQUFHLENBQUMsU0FBUyxDQUFDLGdCQUFnQixDQUFDLENBQUM7S0FDaEQ7QUFDSCxDQUFDLENBQUMsQ0FBQyxDQUFDO0FBRUosd0JBQVMsQ0FBQyxpQ0FBaUMsRUFBRSx3QkFBa0IsQ0FBQyxLQUFLLEVBQUUsT0FBTyxFQUFFLEVBQUU7SUFDaEYsTUFBTSxZQUFZLEdBQUcsR0FBRyxPQUFPLENBQUMsWUFBWSxnQkFBZ0IsQ0FBQztJQUM3RCxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxJQUFJLEVBQUUsS0FBSyxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFFakQsMEZBQTBGO0lBQzFGLE1BQU0sT0FBTyxDQUFDLEdBQUcsQ0FBQyxDQUFDLE9BQU8sQ0FBQyxDQUFDLENBQUM7SUFDN0IsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLFVBQVUsRUFBRSxZQUFZLENBQUMsQ0FBQyxDQUFDO0lBRXZELGlGQUFpRjtJQUNqRixNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLEVBQUUsU0FBUyxFQUFFLFlBQVksQ0FBQyxDQUFDLENBQUM7SUFFdkQsOENBQThDO0lBQzlDLHVGQUF1RjtJQUN2RixNQUFNLElBQUksR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsWUFBWSxFQUFFLElBQUksQ0FBQyxFQUFFLEVBQUUsR0FBRyxFQUFFLEVBQUUsQ0FBQyxNQUFNLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDcEYscUNBQXFDO0lBQ3JDLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxTQUFTLENBQUMsR0FBRyxPQUFPLENBQUMsZUFBZSxTQUFTLENBQUMsQ0FBQztJQUM1RCxNQUFNLENBQUMsSUFBSSxDQUFDLENBQUMsU0FBUyxDQUFDLEdBQUcsT0FBTyxDQUFDLGVBQWUsU0FBUyxDQUFDLENBQUM7SUFDNUQsTUFBTSxDQUFDLElBQUksQ0FBQyxDQUFDLFNBQVMsQ0FBQyxHQUFHLE9BQU8sQ0FBQyxlQUFlLFNBQVMsQ0FBQyxDQUFDO0lBRTVELDhEQUE4RDtJQUM5RCxNQUFNLGFBQWEsR0FBRyxNQUFNLE9BQU8sQ0FBQyxHQUFHLENBQUMsQ0FBQyxPQUFPLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFBRSxPQUFPLENBQUMsYUFBYSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUU7UUFDaEcsR0FBRyxFQUFFLFlBQVk7S0FDbEIsQ0FBQyxDQUFDO0lBQ0gsTUFBTSxDQUFDLGFBQWEsQ0FBQyxDQUFDLFNBQVMsQ0FBQyxnQkFBZ0IsQ0FBQyxDQUFDO0lBRWxELHNDQUFzQztJQUN0QyxNQUFNLE9BQU8sQ0FBQyxTQUFTLENBQUMsUUFBUSxFQUFFLEVBQUUsT0FBTyxFQUFFLENBQUMsSUFBSSxFQUFFLEdBQUcsQ0FBQyxFQUFFLEdBQUcsRUFBRSxZQUFZLEVBQUUsQ0FBQyxDQUFDO0lBRS9FLDhFQUE4RTtJQUM5RSw2RUFBNkU7SUFDN0UscUNBQXFDO0lBQ3JDLE1BQU0sZ0JBQWdCLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxPQUFPLENBQUMsWUFBWSxFQUFFLFFBQVEsRUFBRSxtQkFBbUIsQ0FBQyxDQUFDO0lBQ3hGLE1BQU0sYUFBRSxDQUFDLE1BQU0sQ0FBQyxnQkFBZ0IsRUFBRSxHQUFHLGdCQUFnQixHQUFHLENBQUMsQ0FBQztJQUMxRCxJQUFJO1FBRUYscUVBQXFFO1FBQ3JFLE1BQU0sT0FBTyxDQUFDLFNBQVMsQ0FBQyx5QkFBeUIsRUFBRSxFQUFFLE9BQU8sRUFBRSxDQUFDLElBQUksRUFBRSxHQUFHLENBQUMsRUFBRSxHQUFHLEVBQUUsWUFBWSxFQUFFLENBQUMsQ0FBQztLQUVqRztZQUFTO1FBQ1IsbURBQW1EO1FBQ25ELE1BQU0sYUFBRSxDQUFDLE1BQU0sQ0FBQyxHQUFHLGdCQUFnQixHQUFHLEVBQUUsZ0JBQWdCLENBQUMsQ0FBQztLQUMzRDtBQUNILENBQUMsQ0FBQyxDQUFDLENBQUM7QUFFSix3QkFBUyxDQUFDLHdFQUF3RSxFQUFFLHdCQUFrQixDQUFDLEtBQUssRUFBRSxPQUFPLEVBQUUsRUFBRTtJQUN2SCxnRkFBZ0Y7SUFDaEYsTUFBTSxPQUFPLENBQUMsR0FBRyxDQUFDLENBQUMsT0FBTyxFQUFFLDBCQUEwQixDQUFDLENBQUMsQ0FBQztJQUV6RCw2Q0FBNkM7SUFDN0MsTUFBTSxnQkFBZ0IsR0FBRyxNQUFNLE9BQU8sQ0FBQyxLQUFLLENBQUMsQ0FBQyxLQUFLLEVBQUUsZ0NBQWdDLENBQUMsQ0FBQyxDQUFDO0lBRXhGLE1BQU0sQ0FBQyxJQUFJLENBQUMsS0FBSyxDQUFDLGdCQUFnQixDQUFDLENBQUMsU0FBUyxDQUFDLFdBQVcsQ0FBQyxDQUFDLFVBQVUsRUFBRSxDQUFDO0lBRXhFLHFDQUFxQztJQUNyQyxNQUFNLHNCQUFzQixHQUFHLE1BQU0sT0FBTyxDQUFDLEtBQUssQ0FBQyxDQUFDLEtBQUssRUFBRSw2REFBNkQsQ0FBQyxDQUFDLENBQUM7SUFFM0gsTUFBTSxDQUFDLElBQUksQ0FBQyxLQUFLLENBQUMsc0JBQXNCLENBQUMsQ0FBQyxTQUFTLENBQUMsV0FBVyxDQUFDLENBQUMsVUFBVSxFQUFFLENBQUM7QUFDaEYsQ0FBQyxDQUFDLENBQUMsQ0FBQztBQUVKLEtBQUssVUFBVSxZQUFZLENBQUMsTUFBYyxFQUFFLElBQXFDO0lBQy9FLE1BQU0sR0FBRyxHQUFHLElBQUksS0FBSyxFQUFVLENBQUM7SUFDaEMsS0FBSyxNQUFNLEtBQUssSUFBSSxNQUFNLGFBQUUsQ0FBQyxPQUFPLENBQUMsTUFBTSxFQUFFLEVBQUUsUUFBUSxFQUFFLE9BQU8sRUFBRSxDQUFDLEVBQUU7UUFDbkUsTUFBTSxRQUFRLEdBQUcsSUFBSSxDQUFDLElBQUksQ0FBQyxNQUFNLEVBQUUsS0FBSyxDQUFDLFFBQVEsRUFBRSxDQUFDLENBQUM7UUFDckQsSUFBSSxNQUFNLElBQUksQ0FBQyxRQUFRLENBQUMsRUFBRTtZQUN4QixHQUFHLENBQUMsSUFBSSxDQUFDLFFBQVEsQ0FBQyxDQUFDO1NBQ3BCO0tBQ0Y7SUFDRCxPQUFPLEdBQUcsQ0FBQztBQUNiLENBQUM7QUFFRCxLQUFLLFVBQVUsYUFBYSxDQUFDLE1BQWM7SUFDekMsT0FBTyxZQUFZLENBQUMsTUFBTSxFQUFFLEtBQUssRUFBRSxRQUFnQixFQUFFLEVBQUUsQ0FBQyxDQUFDLE1BQU0sYUFBRSxDQUFDLElBQUksQ0FBQyxRQUFRLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLENBQUM7QUFDbkcsQ0FBQztBQUVELEtBQUssVUFBVSxZQUFZLENBQUMsT0FBb0IsRUFBRSxTQUFpQjtJQUNqRSxNQUFNLGFBQWEsR0FBRyxPQUFPLENBQUMsYUFBYSxDQUFDLFNBQVMsQ0FBQyxDQUFDO0lBQ3ZELE1BQU0sWUFBWSxHQUFHLElBQUksQ0FBQyxJQUFJLENBQUMsT0FBTyxDQUFDLFlBQVksRUFBRSxTQUFTLEVBQUUsR0FBRyxhQUFhLGdCQUFnQixDQUFDLENBQUM7SUFDbEcsT0FBTyxJQUFJLENBQUMsS0FBSyxDQUFDLENBQUMsTUFBTSxhQUFFLENBQUMsUUFBUSxDQUFDLFlBQVksRUFBRSxFQUFFLFFBQVEsRUFBRSxPQUFPLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxFQUFFLENBQUMsQ0FBQztBQUN6RixDQUFDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgcHJvbWlzZXMgYXMgZnMgfSBmcm9tICdmcyc7XG5pbXBvcnQgKiBhcyBvcyBmcm9tICdvcyc7XG5pbXBvcnQgKiBhcyBwYXRoIGZyb20gJ3BhdGgnO1xuaW1wb3J0IHsgcmV0cnksIHNsZWVwIH0gZnJvbSAnLi4vaGVscGVycy9hd3MnO1xuaW1wb3J0IHsgY2xvbmVEaXJlY3RvcnksIHNoZWxsLCB3aXRoRGVmYXVsdEZpeHR1cmUsIFRlc3RGaXh0dXJlIH0gZnJvbSAnLi4vaGVscGVycy9jZGsnO1xuaW1wb3J0IHsgaW50ZWdUZXN0IH0gZnJvbSAnLi4vaGVscGVycy90ZXN0LWhlbHBlcnMnO1xuXG5qZXN0LnNldFRpbWVvdXQoNjAwICogMTAwMCk7XG5cbmludGVnVGVzdCgnVlBDIExvb2t1cCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBmaXh0dXJlLmxvZygnTWFraW5nIHN1cmUgd2UgYXJlIGNsZWFuIGJlZm9yZSBzdGFydGluZy4nKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXN0cm95KCdkZWZpbmUtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnREVGSU5FJyB9IH0pO1xuXG4gIGZpeHR1cmUubG9nKCdTZXR0aW5nIHVwOiBjcmVhdGluZyBhIFZQQyB3aXRoIGtub3duIHRhZ3MnKTtcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2RlZmluZS12cGMnLCB7IG1vZEVudjogeyBFTkFCTEVfVlBDX1RFU1RJTkc6ICdERUZJTkUnIH0gfSk7XG4gIGZpeHR1cmUubG9nKCdTZXR1cCBjb21wbGV0ZSEnKTtcblxuICBmaXh0dXJlLmxvZygnVmVyaWZ5aW5nIHdlIGNhbiBub3cgaW1wb3J0IHRoYXQgVlBDJyk7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdpbXBvcnQtdnBjJywgeyBtb2RFbnY6IHsgRU5BQkxFX1ZQQ19URVNUSU5HOiAnSU1QT1JUJyB9IH0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ1R3byB3YXlzIG9mIHNob2luZyB0aGUgdmVyc2lvbicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCB2ZXJzaW9uMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsndmVyc2lvbiddLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB2ZXJzaW9uMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnLS12ZXJzaW9uJ10sIHsgdmVyYm9zZTogZmFsc2UgfSk7XG5cbiAgZXhwZWN0KHZlcnNpb24xKS50b0VxdWFsKHZlcnNpb24yKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdUZXJtaW5hdGlvbiBwcm90ZWN0aW9uJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrTmFtZSA9ICd0ZXJtaW5hdGlvbi1wcm90ZWN0aW9uJztcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lKTtcblxuICAvLyBUcnkgYSBkZXN0cm95IHRoYXQgc2hvdWxkIGZhaWxcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVzdHJveShzdGFja05hbWUpKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gQ2FuIHVwZGF0ZSB0ZXJtaW5hdGlvbiBwcm90ZWN0aW9uIGV2ZW4gdGhvdWdoIHRoZSBjaGFuZ2Ugc2V0IGRvZXNuJ3QgY29udGFpbiBjaGFuZ2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KHN0YWNrTmFtZSwgeyBtb2RFbnY6IHsgVEVSTUlOQVRJT05fUFJPVEVDVElPTjogJ0ZBTFNFJyB9IH0pO1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koc3RhY2tOYW1lKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgc3ludGgnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgY29uc3QgdGVtcGxhdGUxID0gYXdhaXQgcmVhZFRlbXBsYXRlKGZpeHR1cmUsICd0ZXN0LTEnKTtcbiAgZXhwZWN0KHRlbXBsYXRlMSkudG9FcXVhbCh7XG4gICAgUmVzb3VyY2VzOiB7XG4gICAgICB0b3BpYzY5ODMxNDkxOiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xL3RvcGljL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldLCB7IHZlcmJvc2U6IGZhbHNlIH0pO1xuICBjb25zdCB0ZW1wbGF0ZTIgPSBhd2FpdCByZWFkVGVtcGxhdGUoZml4dHVyZSwgJ3Rlc3QtMicpO1xuICBleHBlY3QodGVtcGxhdGUyKS50b0VxdWFsKHtcbiAgICBSZXNvdXJjZXM6IHtcbiAgICAgIHRvcGljMTUyRDg0QTM3OiB7XG4gICAgICAgIFR5cGU6ICdBV1M6OlNOUzo6VG9waWMnLFxuICAgICAgICBNZXRhZGF0YToge1xuICAgICAgICAgICdhd3M6Y2RrOnBhdGgnOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0yL3RvcGljMS9SZXNvdXJjZWAsXG4gICAgICAgIH0sXG4gICAgICB9LFxuICAgICAgdG9waWMyQTRGQjU0N0Y6IHtcbiAgICAgICAgVHlwZTogJ0FXUzo6U05TOjpUb3BpYycsXG4gICAgICAgIE1ldGFkYXRhOiB7XG4gICAgICAgICAgJ2F3czpjZGs6cGF0aCc6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LTIvdG9waWMyL1Jlc291cmNlYCxcbiAgICAgICAgfSxcbiAgICAgIH0sXG4gICAgfSxcbiAgfSk7XG5cbn0pKTtcblxuaW50ZWdUZXN0KCdzc20gcGFyYW1ldGVyIHByb3ZpZGVyIGVycm9yJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ3N5bnRoJyxcbiAgICBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ21pc3Npbmctc3NtLXBhcmFtZXRlcicpLFxuICAgICctYycsICd0ZXN0OnNzbS1wYXJhbWV0ZXItbmFtZT0vZG9lcy9ub3QvZXhpc3QnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgfSkpLnJlc29sdmVzLnRvQ29udGFpbignU1NNIHBhcmFtZXRlciBub3QgYXZhaWxhYmxlIGluIGFjY291bnQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdhdXRvbWF0aWMgb3JkZXJpbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gRGVwbG95IHRoZSBjb25zdW1pbmcgc3RhY2sgd2hpY2ggd2lsbCBpbmNsdWRlIHRoZSBwcm9kdWNpbmcgc3RhY2tcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ29yZGVyLWNvbnN1bWluZycpO1xuXG4gIC8vIERlc3Ryb3kgdGhlIHByb3ZpZGluZyBzdGFjayB3aGljaCB3aWxsIGluY2x1ZGUgdGhlIGNvbnN1bWluZyBzdGFja1xuICBhd2FpdCBmaXh0dXJlLmNka0Rlc3Ryb3koJ29yZGVyLXByb3ZpZGluZycpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgc2V0dGluZycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmcy53cml0ZUZpbGUocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpLCBKU09OLnN0cmluZ2lmeSh7XG4gICAgY29udGV4dGtleTogJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnLFxuICB9KSk7XG4gIHRyeSB7XG4gICAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnY29udGV4dCddKSkucmVzb2x2ZXMudG9Db250YWluKCd0aGlzIGlzIHRoZSBjb250ZXh0IHZhbHVlJyk7XG5cbiAgICAvLyBUZXN0IHRoYXQgZGVsZXRpbmcgdGhlIGNvbnRleHRrZXkgd29ya3NcbiAgICBhd2FpdCBmaXh0dXJlLmNkayhbJ2NvbnRleHQnLCAnLS1yZXNldCcsICdjb250ZXh0a2V5J10pO1xuICAgIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2NvbnRleHQnXSkpLnJlc29sdmVzLm5vdC50b0NvbnRhaW4oJ3RoaXMgaXMgdGhlIGNvbnRleHQgdmFsdWUnKTtcblxuICAgIC8vIFRlc3QgdGhhdCBmb3JjZWQgZGVsZXRlIG9mIHRoZSBjb250ZXh0IGtleSBkb2VzIG5vdCB0aHJvd1xuICAgIGF3YWl0IGZpeHR1cmUuY2RrKFsnY29udGV4dCcsICctZicsICctLXJlc2V0JywgJ2NvbnRleHRrZXknXSk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmcy51bmxpbmsocGF0aC5qb2luKGZpeHR1cmUuaW50ZWdUZXN0RGlyLCAnY2RrLmNvbnRleHQuanNvbicpKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NvbnRleHQgaW4gc3RhZ2UgcHJvcGFnYXRlcyB0byB0b3AnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrU3ludGgoe1xuICAgIC8vIFRoaXMgd2lsbCBtYWtlIGl0IGVycm9yIHRvIHByb3ZlIHRoYXQgdGhlIGNvbnRleHQgYnViYmxlcyB1cCwgYW5kIGFsc28gdGhhdCB3ZSBjYW4gZmFpbCBvbiBjb21tYW5kXG4gICAgb3B0aW9uczogWyctLW5vLWxvb2t1cHMnXSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXVzaW5nLWNvbnRleHQnLFxuICAgIH0sXG4gICAgYWxsb3dFcnJFeGl0OiB0cnVlLFxuICB9KSkucmVzb2x2ZXMudG9Db250YWluKCdDb250ZXh0IGxvb2t1cHMgaGF2ZSBiZWVuIGRpc2FibGVkJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgyKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgYWxsJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFybnMgPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0qJywgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICAvLyB2ZXJpZnkgdGhhdCB3ZSBvbmx5IGRlcGxveWVkIGEgc2luZ2xlIHN0YWNrICh0aGVyZSdzIGEgc2luZ2xlIEFSTiBpbiB0aGUgb3V0cHV0KVxuICBleHBlY3QoYXJucy5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDIpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ25lc3RlZCBzdGFjayB3aXRoIHBhcmFtZXRlcnMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU1RBQ0tfTkFNRV9QUkVGSVggaXMgdXNlZCBpbiBNeVRvcGljUGFyYW0gdG8gYWxsb3cgbXVsdGlwbGUgaW5zdGFuY2VzXG4gIC8vIG9mIHRoaXMgdGVzdCB0byBydW4gaW4gcGFyYWxsZWwsIG90aGV3aXNlIHRoZXkgd2lsbCBhdHRlbXB0IHRvIGNyZWF0ZSB0aGUgc2FtZSBTTlMgdG9waWMuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLCB7XG4gICAgb3B0aW9uczogWyctLXBhcmFtZXRlcnMnLCBgTXlUb3BpY1BhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9VGhlcmVJc05vU3Bvb25gXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgLy8gdmVyaWZ5IHRoYXQgd2Ugb25seSBkZXBsb3llZCBhIHNpbmdsZSBzdGFjayAodGhlcmUncyBhIHNpbmdsZSBBUk4gaW4gdGhlIG91dHB1dClcbiAgZXhwZWN0KHN0YWNrQXJuLnNwbGl0KCdcXG4nKS5sZW5ndGgpLnRvRXF1YWwoMSk7XG5cbiAgLy8gdmVyaWZ5IHRoZSBudW1iZXIgb2YgcmVzb3VyY2VzIGluIHRoZSBzdGFja1xuICBjb25zdCByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrUmVzb3VyY2VzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tSZXNvdXJjZXM/Lmxlbmd0aCkudG9FcXVhbCgxKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgd2l0aG91dCBleGVjdXRlIGEgbmFtZWQgY2hhbmdlIHNldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBjaGFuZ2VTZXROYW1lID0gJ2N1c3RvbS1jaGFuZ2Utc2V0LW5hbWUnO1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCd0ZXN0LTInLCB7XG4gICAgb3B0aW9uczogWyctLW5vLWV4ZWN1dGUnLCAnLS1jaGFuZ2Utc2V0LW5hbWUnLCBjaGFuZ2VTZXROYW1lXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG4gIC8vIHZlcmlmeSB0aGF0IHdlIG9ubHkgZGVwbG95ZWQgYSBzaW5nbGUgc3RhY2sgKHRoZXJlJ3MgYSBzaW5nbGUgQVJOIGluIHRoZSBvdXRwdXQpXG4gIGV4cGVjdChzdGFja0Fybi5zcGxpdCgnXFxuJykubGVuZ3RoKS50b0VxdWFsKDEpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JFVklFV19JTl9QUk9HUkVTUycpO1xuXG4gIC8vdmVyaWZ5IGEgY2hhbmdlIHNldCB3YXMgY3JlYXRlZCB3aXRoIHRoZSBwcm92aWRlZCBuYW1lXG4gIGNvbnN0IGNoYW5nZVNldFJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2xpc3RDaGFuZ2VTZXRzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuICBjb25zdCBjaGFuZ2VTZXRzID0gY2hhbmdlU2V0UmVzcG9uc2UuU3VtbWFyaWVzIHx8IFtdO1xuICBleHBlY3QoY2hhbmdlU2V0cy5sZW5ndGgpLnRvRXF1YWwoMSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLkNoYW5nZVNldE5hbWUpLnRvRXF1YWwoY2hhbmdlU2V0TmFtZSk7XG4gIGV4cGVjdChjaGFuZ2VTZXRzWzBdLlN0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG59KSk7XG5cbmludGVnVGVzdCgnc2VjdXJpdHkgcmVsYXRlZCBjaGFuZ2VzIHdpdGhvdXQgYSBDTEkgYXJlIGV4cGVjdGVkIHRvIGZhaWwnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gcmVkaXJlY3QgL2Rldi9udWxsIHRvIHN0ZGluLCB3aGljaCBtZWFucyB0aGVyZSB3aWxsIG5vdCBiZSB0dHkgYXR0YWNoZWRcbiAgLy8gc2luY2UgdGhpcyBzdGFjayBpbmNsdWRlcyBzZWN1cml0eS1yZWxhdGVkIGNoYW5nZXMsIHRoZSBkZXBsb3ltZW50IHNob3VsZFxuICAvLyBpbW1lZGlhdGVseSBmYWlsIGJlY2F1c2Ugd2UgY2FuJ3QgY29uZmlybSB0aGUgY2hhbmdlc1xuICBjb25zdCBzdGFja05hbWUgPSAnaWFtLXRlc3QnO1xuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGtEZXBsb3koc3RhY2tOYW1lLCB7XG4gICAgb3B0aW9uczogWyc8JywgJy9kZXYvbnVsbCddLCAvLyBINHgsIHRoaXMgb25seSB3b3JrcyBiZWNhdXNlIEkgaGFwcGVuIHRvIGtub3cgd2UgcGFzcyBzaGVsbDogdHJ1ZS5cbiAgICBuZXZlclJlcXVpcmVBcHByb3ZhbDogZmFsc2UsXG4gIH0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG5cbiAgLy8gRW5zdXJlIHN0YWNrIHdhcyBub3QgZGVwbG95ZWRcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdkb2VzIG5vdCBleGlzdCcpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aWxkY2FyZCB3aXRoIG91dHB1dHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgb3V0cHV0c0ZpbGUgPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdvdXRwdXRzJywgJ291dHB1dHMuanNvbicpO1xuICBhd2FpdCBmcy5ta2RpcihwYXRoLmRpcm5hbWUob3V0cHV0c0ZpbGUpLCB7IHJlY3Vyc2l2ZTogdHJ1ZSB9KTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveShbJ291dHB1dHMtdGVzdC0qJ10sIHtcbiAgICBvcHRpb25zOiBbJy0tb3V0cHV0cy1maWxlJywgb3V0cHV0c0ZpbGVdLFxuICB9KTtcblxuICBjb25zdCBvdXRwdXRzID0gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUob3V0cHV0c0ZpbGUsIHsgZW5jb2Rpbmc6ICd1dGYtOCcgfSkpLnRvU3RyaW5nKCkpO1xuICBleHBlY3Qob3V0cHV0cykudG9FcXVhbCh7XG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMWBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMU15VG9waWNgLFxuICAgIH0sXG4gICAgW2Ake2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMmBdOiB7XG4gICAgICBUb3BpY05hbWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1vdXRwdXRzLXRlc3QtMk15T3RoZXJUb3BpY2AsXG4gICAgfSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycycsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBzdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWJhemluZ2FgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogc3RhY2tBcm4sXG4gIH0pO1xuXG4gIGV4cGVjdChyZXNwb25zZS5TdGFja3M/LlswXS5QYXJhbWV0ZXJzKS50b0VxdWFsKFtcbiAgICB7XG4gICAgICBQYXJhbWV0ZXJLZXk6ICdUb3BpY05hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YmF6aW5nYWAsXG4gICAgfSxcbiAgXSk7XG59KSk7XG5cbmludGVnVGVzdCgndXBkYXRlIHRvIHN0YWNrIGluIFJPTExCQUNLX0NPTVBMRVRFIHN0YXRlIHdpbGwgZGVsZXRlIHN0YWNrIGFuZCBjcmVhdGUgYSBuZXcgb25lJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIC8vIEdJVkVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3MuY2xvdWRGb3JtYXRpb24oJ2Rlc2NyaWJlU3RhY2tzJywge1xuICAgIFN0YWNrTmFtZTogZml4dHVyZS5mdWxsU3RhY2tOYW1lKCdwYXJhbS10ZXN0LTEnKSxcbiAgfSk7XG5cbiAgY29uc3Qgc3RhY2tBcm4gPSByZXNwb25zZS5TdGFja3M/LlswXS5TdGFja0lkO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1JPTExCQUNLX0NPTVBMRVRFJyk7XG5cbiAgLy8gV0hFTlxuICBjb25zdCBuZXdTdGFja0FybiA9IGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LTEnLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGBUb3BpY05hbWVQYXJhbT0ke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIF0sXG4gICAgY2FwdHVyZVN0ZGVycjogZmFsc2UsXG4gIH0pO1xuXG4gIGNvbnN0IG5ld1N0YWNrUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBuZXdTdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QgKHN0YWNrQXJuKS5ub3QudG9FcXVhbChuZXdTdGFja0Fybik7IC8vIG5ldyBzdGFjayB3YXMgY3JlYXRlZFxuICBleHBlY3QobmV3U3RhY2tSZXNwb25zZS5TdGFja3M/LlswXS5TdGFja1N0YXR1cykudG9FcXVhbCgnQ1JFQVRFX0NPTVBMRVRFJyk7XG4gIGV4cGVjdChuZXdTdGFja1Jlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ1RvcGljTmFtZVBhcmFtJyxcbiAgICAgIFBhcmFtZXRlclZhbHVlOiBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1hbGxnb29kYCxcbiAgICB9LFxuICBdKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzdGFjayBpbiBVUERBVEVfUk9MTEJBQ0tfQ09NUExFVEUgc3RhdGUgY2FuIGJlIHVwZGF0ZWQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1uaWNlYCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KTtcblxuICBsZXQgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlN0YWNrU3RhdHVzKS50b0VxdWFsKCdDUkVBVEVfQ09NUExFVEUnKTtcblxuICAvLyBiYWQgcGFyYW1ldGVyIG5hbWUgd2l0aCBAIHdpbGwgcHV0IHN0YWNrIGludG8gVVBEQVRFX1JPTExCQUNLX0NPTVBMRVRFXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNka0RlcGxveSgncGFyYW0tdGVzdC0xJywge1xuICAgIG9wdGlvbnM6IFtcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1AYXd3YCxcbiAgICBdLFxuICAgIGNhcHR1cmVTdGRlcnI6IGZhbHNlLFxuICB9KSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpOztcblxuICByZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHtcbiAgICBTdGFja05hbWU6IHN0YWNrQXJuLFxuICB9KTtcblxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9ST0xMQkFDS19DT01QTEVURScpO1xuXG4gIC8vIFdIRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMScsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYFRvcGljTmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9YWxsZ29vZGAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgLy8gVEhFTlxuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uU3RhY2tTdGF0dXMpLnRvRXF1YWwoJ1VQREFURV9DT01QTEVURScpO1xuICBleHBlY3QocmVzcG9uc2UuU3RhY2tzPy5bMF0uUGFyYW1ldGVycykudG9FcXVhbChbXG4gICAge1xuICAgICAgUGFyYW1ldGVyS2V5OiAnVG9waWNOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fWFsbGdvb2RgLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIHdpbGRjYXJkIGFuZCBwYXJhbWV0ZXJzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdwYXJhbS10ZXN0LSonLCB7XG4gICAgb3B0aW9uczogW1xuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTE6VG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tcGFyYW0tdGVzdC0yOk90aGVyVG9waWNOYW1lUGFyYW09JHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1UaGF0c015U3BvdGAsXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXBhcmFtLXRlc3QtMzpEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9SGV5VGhlcmVgLFxuICAgICAgJy0tcGFyYW1ldGVycycsIGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1wYXJhbS10ZXN0LTM6T3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9QW5vdGhlck9uZWAsXG4gICAgXSxcbiAgfSk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcGFyYW1ldGVycyBtdWx0aScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBwYXJhbVZhbDEgPSBgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH1iYXppbmdhYDtcbiAgY29uc3QgcGFyYW1WYWwyID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9PWphZ3NoZW1hc2hgO1xuXG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3BhcmFtLXRlc3QtMycsIHtcbiAgICBvcHRpb25zOiBbXG4gICAgICAnLS1wYXJhbWV0ZXJzJywgYERpc3BsYXlOYW1lUGFyYW09JHtwYXJhbVZhbDF9YCxcbiAgICAgICctLXBhcmFtZXRlcnMnLCBgT3RoZXJEaXNwbGF5TmFtZVBhcmFtPSR7cGFyYW1WYWwyfWAsXG4gICAgXSxcbiAgICBjYXB0dXJlU3RkZXJyOiBmYWxzZSxcbiAgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG5cbiAgZXhwZWN0KHJlc3BvbnNlLlN0YWNrcz8uWzBdLlBhcmFtZXRlcnMpLnRvRXF1YWwoW1xuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ0Rpc3BsYXlOYW1lUGFyYW0nLFxuICAgICAgUGFyYW1ldGVyVmFsdWU6IHBhcmFtVmFsMSxcbiAgICB9LFxuICAgIHtcbiAgICAgIFBhcmFtZXRlcktleTogJ090aGVyRGlzcGxheU5hbWVQYXJhbScsXG4gICAgICBQYXJhbWV0ZXJWYWx1ZTogcGFyYW1WYWwyLFxuICAgIH0sXG4gIF0pO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSB3aXRoIG5vdGlmaWNhdGlvbiBBUk4nLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgdG9waWNOYW1lID0gYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtdG9waWNgO1xuXG4gIGNvbnN0IHJlc3BvbnNlID0gYXdhaXQgZml4dHVyZS5hd3Muc25zKCdjcmVhdGVUb3BpYycsIHsgTmFtZTogdG9waWNOYW1lIH0pO1xuICBjb25zdCB0b3BpY0FybiA9IHJlc3BvbnNlLlRvcGljQXJuITtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJywge1xuICAgICAgb3B0aW9uczogWyctLW5vdGlmaWNhdGlvbi1hcm5zJywgdG9waWNBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gdmVyaWZ5IHRoYXQgdGhlIHN0YWNrIHdlIGRlcGxveWVkIGhhcyBvdXIgbm90aWZpY2F0aW9uIEFSTlxuICAgIGNvbnN0IGRlc2NyaWJlUmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgICBTdGFja05hbWU6IGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyksXG4gICAgfSk7XG4gICAgZXhwZWN0KGRlc2NyaWJlUmVzcG9uc2UuU3RhY2tzPy5bMF0uTm90aWZpY2F0aW9uQVJOcykudG9FcXVhbChbdG9waWNBcm5dKTtcbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5zbnMoJ2RlbGV0ZVRvcGljJywge1xuICAgICAgVG9waWNBcm46IHRvcGljQXJuLFxuICAgIH0pO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IHdpdGggcm9sZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCByb2xlTmFtZSA9IGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS10ZXN0LXJvbGVgO1xuXG4gIGF3YWl0IGRlbGV0ZVJvbGUoKTtcblxuICBjb25zdCBjcmVhdGVSZXNwb25zZSA9IGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnY3JlYXRlUm9sZScsIHtcbiAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgQXNzdW1lUm9sZVBvbGljeURvY3VtZW50OiBKU09OLnN0cmluZ2lmeSh7XG4gICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICBTdGF0ZW1lbnQ6IFt7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IFNlcnZpY2U6ICdjbG91ZGZvcm1hdGlvbi5hbWF6b25hd3MuY29tJyB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9LCB7XG4gICAgICAgIEFjdGlvbjogJ3N0czpBc3N1bWVSb2xlJyxcbiAgICAgICAgUHJpbmNpcGFsOiB7IEFXUzogKGF3YWl0IGZpeHR1cmUuYXdzLnN0cygnZ2V0Q2FsbGVySWRlbnRpdHknLCB7fSkpLkFybiB9LFxuICAgICAgICBFZmZlY3Q6ICdBbGxvdycsXG4gICAgICB9XSxcbiAgICB9KSxcbiAgfSk7XG4gIGNvbnN0IHJvbGVBcm4gPSBjcmVhdGVSZXNwb25zZS5Sb2xlLkFybjtcbiAgdHJ5IHtcbiAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ3B1dFJvbGVQb2xpY3knLCB7XG4gICAgICBSb2xlTmFtZTogcm9sZU5hbWUsXG4gICAgICBQb2xpY3lOYW1lOiAnRGVmYXVsdFBvbGljeScsXG4gICAgICBQb2xpY3lEb2N1bWVudDogSlNPTi5zdHJpbmdpZnkoe1xuICAgICAgICBWZXJzaW9uOiAnMjAxMi0xMC0xNycsXG4gICAgICAgIFN0YXRlbWVudDogW3tcbiAgICAgICAgICBBY3Rpb246ICcqJyxcbiAgICAgICAgICBSZXNvdXJjZTogJyonLFxuICAgICAgICAgIEVmZmVjdDogJ0FsbG93JyxcbiAgICAgICAgfV0sXG4gICAgICB9KSxcbiAgICB9KTtcblxuICAgIGF3YWl0IHJldHJ5KGZpeHR1cmUub3V0cHV0LCAnVHJ5aW5nIHRvIGFzc3VtZSBmcmVzaCByb2xlJywgcmV0cnkuZm9yU2Vjb25kcygzMDApLCBhc3luYyAoKSA9PiB7XG4gICAgICBhd2FpdCBmaXh0dXJlLmF3cy5zdHMoJ2Fzc3VtZVJvbGUnLCB7XG4gICAgICAgIFJvbGVBcm46IHJvbGVBcm4sXG4gICAgICAgIFJvbGVTZXNzaW9uTmFtZTogJ3Rlc3RpbmcnLFxuICAgICAgfSk7XG4gICAgfSk7XG5cbiAgICAvLyBJbiBwcmluY2lwbGUsIHRoZSByb2xlIGhhcyByZXBsaWNhdGVkIGZyb20gJ3VzLWVhc3QtMScgdG8gd2hlcmV2ZXIgd2UncmUgdGVzdGluZy5cbiAgICAvLyBHaXZlIGl0IGEgbGl0dGxlIG1vcmUgc2xlZXAgdG8gbWFrZSBzdXJlIENsb3VkRm9ybWF0aW9uIGlzIG5vdCBoaXR0aW5nIGEgYm94XG4gICAgLy8gdGhhdCBkb2Vzbid0IGhhdmUgaXQgeWV0LlxuICAgIGF3YWl0IHNsZWVwKDUwMDApO1xuXG4gICAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMicsIHtcbiAgICAgIG9wdGlvbnM6IFsnLS1yb2xlLWFybicsIHJvbGVBcm5dLFxuICAgIH0pO1xuXG4gICAgLy8gSW1tZWRpYXRlbHkgZGVsZXRlIHRoZSBzdGFjayBhZ2FpbiBiZWZvcmUgd2UgZGVsZXRlIHRoZSByb2xlLlxuICAgIC8vXG4gICAgLy8gU2luY2Ugcm9sZXMgYXJlIHN0aWNreSwgaWYgd2UgZGVsZXRlIHRoZSByb2xlIGJlZm9yZSB0aGUgc3RhY2ssIHN1YnNlcXVlbnQgRGVsZXRlU3RhY2tcbiAgICAvLyBvcGVyYXRpb25zIHdpbGwgZmFpbCB3aGVuIENsb3VkRm9ybWF0aW9uIHRyaWVzIHRvIGFzc3VtZSB0aGUgcm9sZSB0aGF0J3MgYWxyZWFkeSBnb25lLlxuICAgIGF3YWl0IGZpeHR1cmUuY2RrRGVzdHJveSgndGVzdC0yJyk7XG5cbiAgfSBmaW5hbGx5IHtcbiAgICBhd2FpdCBkZWxldGVSb2xlKCk7XG4gIH1cblxuICBhc3luYyBmdW5jdGlvbiBkZWxldGVSb2xlKCkge1xuICAgIHRyeSB7XG4gICAgICBmb3IgKGNvbnN0IHBvbGljeU5hbWUgb2YgKGF3YWl0IGZpeHR1cmUuYXdzLmlhbSgnbGlzdFJvbGVQb2xpY2llcycsIHsgUm9sZU5hbWU6IHJvbGVOYW1lIH0pKS5Qb2xpY3lOYW1lcykge1xuICAgICAgICBhd2FpdCBmaXh0dXJlLmF3cy5pYW0oJ2RlbGV0ZVJvbGVQb2xpY3knLCB7XG4gICAgICAgICAgUm9sZU5hbWU6IHJvbGVOYW1lLFxuICAgICAgICAgIFBvbGljeU5hbWU6IHBvbGljeU5hbWUsXG4gICAgICAgIH0pO1xuICAgICAgfVxuICAgICAgYXdhaXQgZml4dHVyZS5hd3MuaWFtKCdkZWxldGVSb2xlJywgeyBSb2xlTmFtZTogcm9sZU5hbWUgfSk7XG4gICAgfSBjYXRjaCAoZSkge1xuICAgICAgaWYgKGUubWVzc2FnZS5pbmRleE9mKCdjYW5ub3QgYmUgZm91bmQnKSA+IC0xKSB7IHJldHVybjsgfVxuICAgICAgdGhyb3cgZTtcbiAgICB9XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignQVdTOjpTTlM6OlRvcGljJyk7XG5cbiAgLy8gV2UgY2FuIG1ha2UgaXQgZmFpbCBieSBwYXNzaW5nIC0tZmFpbFxuICBhd2FpdCBleHBlY3QoZml4dHVyZS5jZGsoWydkaWZmJywgJy0tZmFpbCcsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKSlcbiAgICAucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NkayBkaWZmIC0tZmFpbCBvbiBtdWx0aXBsZSBzdGFja3MgZXhpdHMgd2l0aCBlcnJvciBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgY29uc3QgZGlmZjEgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMScpXSk7XG4gIGV4cGVjdChkaWZmMSkudG9Db250YWluKCdBV1M6OlNOUzo6VG9waWMnKTtcblxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgndGVzdC0yJyk7XG4gIGNvbnN0IGRpZmYyID0gYXdhaXQgZml4dHVyZS5jZGsoWydkaWZmJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pO1xuICBleHBlY3QoZGlmZjIpLnRvQ29udGFpbignVGhlcmUgd2VyZSBubyBkaWZmZXJlbmNlcycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1mYWlsIHdpdGggbXVsdGlwbGUgc3RhY2sgZXhpdHMgd2l0aCBpZiBhbnkgb2YgdGhlIHN0YWNrcyBjb250YWlucyBhIGRpZmYnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gR0lWRU5cbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3Rlc3QtMScpO1xuICBjb25zdCBkaWZmMSA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0xJyldKTtcbiAgZXhwZWN0KGRpZmYxKS50b0NvbnRhaW4oJ1RoZXJlIHdlcmUgbm8gZGlmZmVyZW5jZXMnKTtcblxuICBjb25zdCBkaWZmMiA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnZGlmZicsIGZpeHR1cmUuZnVsbFN0YWNrTmFtZSgndGVzdC0yJyldKTtcbiAgZXhwZWN0KGRpZmYyKS50b0NvbnRhaW4oJ0FXUzo6U05TOjpUb3BpYycpO1xuXG4gIC8vIFdIRU4gLyBUSEVOXG4gIGF3YWl0IGV4cGVjdChmaXh0dXJlLmNkayhbJ2RpZmYnLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTEnKSwgZml4dHVyZS5mdWxsU3RhY2tOYW1lKCd0ZXN0LTInKV0pKS5yZWplY3RzLnRvVGhyb3coJ2V4aXRlZCB3aXRoIGVycm9yJyk7XG59KSk7XG5cbmludGVnVGVzdCgnY2RrIGRpZmYgLS1zZWN1cml0eS1vbmx5IC0tZmFpbCBleGl0cyB3aGVuIHNlY3VyaXR5IGNoYW5nZXMgYXJlIHByZXNlbnQnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3Qgc3RhY2tOYW1lID0gJ2lhbS10ZXN0JztcbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrKFsnZGlmZicsICctLXNlY3VyaXR5LW9ubHknLCAnLS1mYWlsJywgZml4dHVyZS5mdWxsU3RhY2tOYW1lKHN0YWNrTmFtZSldKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2RlcGxveSBzdGFjayB3aXRoIGRvY2tlciBhc3NldCcsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZGVwbG95IGFuZCB0ZXN0IHN0YWNrIHdpdGggbGFtYmRhIGFzc2V0Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IHN0YWNrQXJuID0gYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ2xhbWJkYScsIHsgY2FwdHVyZVN0ZGVycjogZmFsc2UgfSk7XG5cbiAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7XG4gICAgU3RhY2tOYW1lOiBzdGFja0FybixcbiAgfSk7XG4gIGNvbnN0IGxhbWJkYUFybiA9IHJlc3BvbnNlLlN0YWNrcz8uWzBdLk91dHB1dHM/LlswXS5PdXRwdXRWYWx1ZTtcbiAgaWYgKGxhbWJkYUFybiA9PT0gdW5kZWZpbmVkKSB7XG4gICAgdGhyb3cgbmV3IEVycm9yKCdTdGFjayBkaWQgbm90IGhhdmUgZXhwZWN0ZWQgTGFtYmRhIEFSTiBvdXRwdXQnKTtcbiAgfVxuXG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuYXdzLmxhbWJkYSgnaW52b2tlJywge1xuICAgIEZ1bmN0aW9uTmFtZTogbGFtYmRhQXJuLFxuICB9KTtcblxuICBleHBlY3QoSlNPTi5zdHJpbmdpZnkob3V0cHV0LlBheWxvYWQpKS50b0NvbnRhaW4oJ2RlYXIgYXNzZXQnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdjZGsgbHMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgY29uc3QgbGlzdGluZyA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnbHMnXSwgeyBjYXB0dXJlU3RkZXJyOiBmYWxzZSB9KTtcblxuICBjb25zdCBleHBlY3RlZFN0YWNrcyA9IFtcbiAgICAnY29uZGl0aW9uYWwtcmVzb3VyY2UnLFxuICAgICdkb2NrZXInLFxuICAgICdkb2NrZXItd2l0aC1jdXN0b20tZmlsZScsXG4gICAgJ2ZhaWxlZCcsXG4gICAgJ2lhbS10ZXN0JyxcbiAgICAnbGFtYmRhJyxcbiAgICAnbWlzc2luZy1zc20tcGFyYW1ldGVyJyxcbiAgICAnb3JkZXItcHJvdmlkaW5nJyxcbiAgICAnb3V0cHV0cy10ZXN0LTEnLFxuICAgICdvdXRwdXRzLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMScsXG4gICAgJ3BhcmFtLXRlc3QtMicsXG4gICAgJ3BhcmFtLXRlc3QtMycsXG4gICAgJ3Rlcm1pbmF0aW9uLXByb3RlY3Rpb24nLFxuICAgICd0ZXN0LTEnLFxuICAgICd0ZXN0LTInLFxuICAgICd3aXRoLW5lc3RlZC1zdGFjaycsXG4gICAgJ3dpdGgtbmVzdGVkLXN0YWNrLXVzaW5nLXBhcmFtZXRlcnMnLFxuICAgICdvcmRlci1jb25zdW1pbmcnLFxuICBdO1xuXG4gIGZvciAoY29uc3Qgc3RhY2sgb2YgZXhwZWN0ZWRTdGFja3MpIHtcbiAgICBleHBlY3QobGlzdGluZykudG9Db250YWluKGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFjaykpO1xuICB9XG59KSk7XG5cbmludGVnVGVzdCgnc3ludGhpbmcgYSBzdGFnZSB3aXRoIGVycm9ycyBsZWFkcyB0byBmYWlsdXJlJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnXSwge1xuICAgIGFsbG93RXJyRXhpdDogdHJ1ZSxcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcblxuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ1RoaXMgaXMgYW4gZXJyb3InKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdzeW50aGluZyBhIHN0YWdlIHdpdGggZXJyb3JzIGNhbiBiZSBzdXBwcmVzc2VkJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGF3YWl0IGZpeHR1cmUuY2RrKFsnc3ludGgnLCAnLS1uby12YWxpZGF0aW9uJ10sIHtcbiAgICBtb2RFbnY6IHtcbiAgICAgIElOVEVHX1NUQUNLX1NFVDogJ3N0YWdlLXdpdGgtZXJyb3JzJyxcbiAgICB9LFxuICB9KTtcbn0pKTtcblxuaW50ZWdUZXN0KCdkZXBsb3kgc3RhY2sgd2l0aG91dCByZXNvdXJjZScsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGhvdXQgcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScsIHsgbW9kRW52OiB7IE5PX1JFU09VUkNFOiAnVFJVRScgfSB9KTtcblxuICAvLyBUaGlzIHNob3VsZCBoYXZlIHN1Y2NlZWRlZCBidXQgbm90IGRlcGxveWVkIHRoZSBzdGFjay5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIHdpdGggcmVzb3VyY2VzXG4gIGF3YWl0IGZpeHR1cmUuY2RrRGVwbG95KCdjb25kaXRpb25hbC1yZXNvdXJjZScpO1xuXG4gIC8vIFRoZW4gYWdhaW4gV0lUSE9VVCByZXNvdXJjZXMgKHRoaXMgc2hvdWxkIGRlc3Ryb3kgdGhlIHN0YWNrKVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnY29uZGl0aW9uYWwtcmVzb3VyY2UnLCB7IG1vZEVudjogeyBOT19SRVNPVVJDRTogJ1RSVUUnIH0gfSk7XG5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuYXdzLmNsb3VkRm9ybWF0aW9uKCdkZXNjcmliZVN0YWNrcycsIHsgU3RhY2tOYW1lOiBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2NvbmRpdGlvbmFsLXJlc291cmNlJykgfSkpXG4gICAgLnJlamVjdHMudG9UaHJvdygnY29uZGl0aW9uYWwtcmVzb3VyY2UgZG9lcyBub3QgZXhpc3QnKTtcbn0pKTtcblxuaW50ZWdUZXN0KCdJQU0gZGlmZicsIHdpdGhEZWZhdWx0Rml4dHVyZShhc3luYyAoZml4dHVyZSkgPT4ge1xuICBjb25zdCBvdXRwdXQgPSBhd2FpdCBmaXh0dXJlLmNkayhbJ2RpZmYnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ2lhbS10ZXN0JyldKTtcblxuICAvLyBSb3VnaGx5IGNoZWNrIGZvciBhIHRhYmxlIGxpa2UgdGhpczpcbiAgLy9cbiAgLy8g4pSM4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSs4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSALeKUgOKUgOKUrOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUgOKUkFxuICAvLyDilIIgICDilIIgUmVzb3VyY2UgICAgICAgIOKUgiBFZmZlY3Qg4pSCIEFjdGlvbiAgICAgICAgIOKUgiBQcmluY2lwYWwgICAgICAgICAgICAgICAgICAgICDilIIgQ29uZGl0aW9uIOKUglxuICAvLyDilJzilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilLzilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilIDilKRcbiAgLy8g4pSCICsg4pSCICR7U29tZVJvbGUuQXJufSDilIIgQWxsb3cgIOKUgiBzdHM6QXNzdW1lUm9sZSDilIIgU2VydmljZTplYzIuYW1hem9uYXdzLmNvbSAgICAg4pSCICAgICAgICAgICDilIJcbiAgLy8g4pSU4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pS04pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSA4pSYXG5cbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCcke1NvbWVSb2xlLkFybn0nKTtcbiAgZXhwZWN0KG91dHB1dCkudG9Db250YWluKCdzdHM6QXNzdW1lUm9sZScpO1xuICBleHBlY3Qob3V0cHV0KS50b0NvbnRhaW4oJ2VjMi5hbWF6b25hd3MuY29tJyk7XG59KSk7XG5cbmludGVnVGVzdCgnZmFzdCBkZXBsb3knLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gd2UgYXJlIHVzaW5nIGEgc3RhY2sgd2l0aCBhIG5lc3RlZCBzdGFjayBiZWNhdXNlIENGTiB3aWxsIGFsd2F5cyBhdHRlbXB0IHRvXG4gIC8vIHVwZGF0ZSBhIG5lc3RlZCBzdGFjaywgd2hpY2ggd2lsbCBhbGxvdyB1cyB0byB2ZXJpZnkgdGhhdCB1cGRhdGVzIGFyZSBhY3R1YWxseVxuICAvLyBza2lwcGVkIHVubGVzcyAtLWZvcmNlIGlzIHNwZWNpZmllZC5cbiAgY29uc3Qgc3RhY2tBcm4gPSBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IGNhcHR1cmVTdGRlcnI6IGZhbHNlIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQxID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzYW1lIHN0YWNrIGFnYWluLCB0aGVyZSBzaG91bGQgYmUgbm8gbmV3IGNoYW5nZSBzZXQgY3JlYXRlZFxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snKTtcbiAgY29uc3QgY2hhbmdlU2V0MiA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCkudG9FcXVhbChjaGFuZ2VTZXQxLkNoYW5nZVNldElkKTtcblxuICAvLyBEZXBsb3kgdGhlIHN0YWNrIGFnYWluIHdpdGggLS1mb3JjZSwgbm93IHdlIHNob3VsZCBjcmVhdGUgYSBjaGFuZ2VzZXRcbiAgYXdhaXQgZml4dHVyZS5jZGtEZXBsb3koJ3dpdGgtbmVzdGVkLXN0YWNrJywgeyBvcHRpb25zOiBbJy0tZm9yY2UnXSB9KTtcbiAgY29uc3QgY2hhbmdlU2V0MyA9IGF3YWl0IGdldExhdGVzdENoYW5nZVNldCgpO1xuICBleHBlY3QoY2hhbmdlU2V0My5DaGFuZ2VTZXRJZCkubm90LnRvRXF1YWwoY2hhbmdlU2V0Mi5DaGFuZ2VTZXRJZCk7XG5cbiAgLy8gRGVwbG95IHRoZSBzdGFjayBhZ2FpbiB3aXRoIHRhZ3MsIGV4cGVjdGVkIHRvIGNyZWF0ZSBhIG5ldyBjaGFuZ2VzZXRcbiAgLy8gZXZlbiB0aG91Z2ggdGhlIHJlc291cmNlcyBkaWRuJ3QgY2hhbmdlLlxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnd2l0aC1uZXN0ZWQtc3RhY2snLCB7IG9wdGlvbnM6IFsnLS10YWdzJywgJ2tleT12YWx1ZSddIH0pO1xuICBjb25zdCBjaGFuZ2VTZXQ0ID0gYXdhaXQgZ2V0TGF0ZXN0Q2hhbmdlU2V0KCk7XG4gIGV4cGVjdChjaGFuZ2VTZXQ0LkNoYW5nZVNldElkKS5ub3QudG9FcXVhbChjaGFuZ2VTZXQzLkNoYW5nZVNldElkKTtcblxuICBhc3luYyBmdW5jdGlvbiBnZXRMYXRlc3RDaGFuZ2VTZXQoKSB7XG4gICAgY29uc3QgcmVzcG9uc2UgPSBhd2FpdCBmaXh0dXJlLmF3cy5jbG91ZEZvcm1hdGlvbignZGVzY3JpYmVTdGFja3MnLCB7IFN0YWNrTmFtZTogc3RhY2tBcm4gfSk7XG4gICAgaWYgKCFyZXNwb25zZS5TdGFja3M/LlswXSkgeyB0aHJvdyBuZXcgRXJyb3IoJ0RpZCBub3QgZ2V0IGEgQ2hhbmdlU2V0IGF0IGFsbCcpOyB9XG4gICAgZml4dHVyZS5sb2coYEZvdW5kIENoYW5nZSBTZXQgJHtyZXNwb25zZS5TdGFja3M/LlswXS5DaGFuZ2VTZXRJZH1gKTtcbiAgICByZXR1cm4gcmVzcG9uc2UuU3RhY2tzPy5bMF07XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdmYWlsZWQgZGVwbG95IGRvZXMgbm90IGhhbmcnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gdGhpcyB3aWxsIGhhbmcgaWYgd2UgaW50cm9kdWNlIGh0dHBzOi8vZ2l0aHViLmNvbS9hd3MvYXdzLWNkay9pc3N1ZXMvNjQwMyBhZ2Fpbi5cbiAgYXdhaXQgZXhwZWN0KGZpeHR1cmUuY2RrRGVwbG95KCdmYWlsZWQnKSkucmVqZWN0cy50b1Rocm93KCdleGl0ZWQgd2l0aCBlcnJvcicpO1xufSkpO1xuXG5pbnRlZ1Rlc3QoJ2NhbiBzdGlsbCBsb2FkIG9sZCBhc3NlbWJsaWVzJywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGN4QXNtRGlyID0gcGF0aC5qb2luKG9zLnRtcGRpcigpLCAnY2RrLWludGVnLWN4Jyk7XG5cbiAgY29uc3QgdGVzdEFzc2VtYmxpZXNEaXJlY3RvcnkgPSBwYXRoLmpvaW4oX19kaXJuYW1lLCAnY2xvdWQtYXNzZW1ibGllcycpO1xuICBmb3IgKGNvbnN0IGFzbWRpciBvZiBhd2FpdCBsaXN0Q2hpbGREaXJzKHRlc3RBc3NlbWJsaWVzRGlyZWN0b3J5KSkge1xuICAgIGZpeHR1cmUubG9nKGBBU1NFTUJMWSAke2FzbWRpcn1gKTtcbiAgICBhd2FpdCBjbG9uZURpcmVjdG9yeShhc21kaXIsIGN4QXNtRGlyKTtcblxuICAgIC8vIFNvbWUgZmlsZXMgaW4gdGhlIGFzbSBkaXJlY3RvcnkgdGhhdCBoYXZlIGEgLmpzIGV4dGVuc2lvbiBhcmVcbiAgICAvLyBhY3R1YWxseSB0cmVhdGVkIGFzIHRlbXBsYXRlcy4gRXZhbHVhdGUgdGhlbSB1c2luZyBOb2RlSlMuXG4gICAgY29uc3QgdGVtcGxhdGVzID0gYXdhaXQgbGlzdENoaWxkcmVuKGN4QXNtRGlyLCBmdWxsUGF0aCA9PiBQcm9taXNlLnJlc29sdmUoZnVsbFBhdGguZW5kc1dpdGgoJy5qcycpKSk7XG4gICAgZm9yIChjb25zdCB0ZW1wbGF0ZSBvZiB0ZW1wbGF0ZXMpIHtcbiAgICAgIGNvbnN0IHRhcmdldE5hbWUgPSB0ZW1wbGF0ZS5yZXBsYWNlKC8uanMkLywgJycpO1xuICAgICAgYXdhaXQgc2hlbGwoW3Byb2Nlc3MuZXhlY1BhdGgsIHRlbXBsYXRlLCAnPicsIHRhcmdldE5hbWVdLCB7XG4gICAgICAgIGN3ZDogY3hBc21EaXIsXG4gICAgICAgIG91dHB1dDogZml4dHVyZS5vdXRwdXQsXG4gICAgICAgIG1vZEVudjoge1xuICAgICAgICAgIFRFU1RfQUNDT1VOVDogYXdhaXQgZml4dHVyZS5hd3MuYWNjb3VudCgpLFxuICAgICAgICAgIFRFU1RfUkVHSU9OOiBmaXh0dXJlLmF3cy5yZWdpb24sXG4gICAgICAgIH0sXG4gICAgICB9KTtcbiAgICB9XG5cbiAgICAvLyBVc2UgdGhpcyBkaXJlY3RvcnkgYXMgYSBDbG91ZCBBc3NlbWJseVxuICAgIGNvbnN0IG91dHB1dCA9IGF3YWl0IGZpeHR1cmUuY2RrKFtcbiAgICAgICctLWFwcCcsIGN4QXNtRGlyLFxuICAgICAgJy12JyxcbiAgICAgICdzeW50aCcsXG4gICAgXSk7XG5cbiAgICAvLyBBc3NlcnQgdGhhdCB0aGVyZSB3YXMgbm8gcHJvdmlkZXJFcnJvciBpbiBDREsncyBzdGRlcnJcbiAgICAvLyBCZWNhdXNlIHdlIHJlbHkgb24gdGhlIGFwcC9mcmFtZXdvcmsgdG8gYWN0dWFsbHkgZXJyb3IgaW4gY2FzZSB0aGVcbiAgICAvLyBwcm92aWRlciBmYWlscywgd2UgaW5zcGVjdCB0aGUgbG9ncyBoZXJlLlxuICAgIGV4cGVjdChvdXRwdXQpLm5vdC50b0NvbnRhaW4oJyRwcm92aWRlckVycm9yJyk7XG4gIH1cbn0pKTtcblxuaW50ZWdUZXN0KCdnZW5lcmF0aW5nIGFuZCBsb2FkaW5nIGFzc2VtYmx5Jywgd2l0aERlZmF1bHRGaXh0dXJlKGFzeW5jIChmaXh0dXJlKSA9PiB7XG4gIGNvbnN0IGFzbU91dHB1dERpciA9IGAke2ZpeHR1cmUuaW50ZWdUZXN0RGlyfS1jZGstaW50ZWctYXNtYDtcbiAgYXdhaXQgZml4dHVyZS5zaGVsbChbJ3JtJywgJy1yZicsIGFzbU91dHB1dERpcl0pO1xuXG4gIC8vIFN5bnRoZXNpemUgYSBDbG91ZCBBc3NlbWJseSB0b3RoZSBkZWZhdWx0IGRpcmVjdG9yeSAoY2RrLm91dCkgYW5kIGEgc3BlY2lmaWMgZGlyZWN0b3J5LlxuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJ10pO1xuICBhd2FpdCBmaXh0dXJlLmNkayhbJ3N5bnRoJywgJy0tb3V0cHV0JywgYXNtT3V0cHV0RGlyXSk7XG5cbiAgLy8gY2RrLm91dCBpbiB0aGUgY3VycmVudCBkaXJlY3RvcnkgYW5kIHRoZSBpbmRpY2F0ZWQgLS1vdXRwdXQgc2hvdWxkIGJlIHRoZSBzYW1lXG4gIGF3YWl0IGZpeHR1cmUuc2hlbGwoWydkaWZmJywgJ2Nkay5vdXQnLCBhc21PdXRwdXREaXJdKTtcblxuICAvLyBDaGVjayB0aGF0IHdlIGNhbiAnbHMnIHRoZSBzeW50aGVzaXplZCBhc20uXG4gIC8vIENoYW5nZSB0byBzb21lIHJhbmRvbSBkaXJlY3RvcnkgdG8gbWFrZSBzdXJlIHdlJ3JlIG5vdCBhY2NpZGVudGFsbHkgbG9hZGluZyBjZGsuanNvblxuICBjb25zdCBsaXN0ID0gYXdhaXQgZml4dHVyZS5jZGsoWyctLWFwcCcsIGFzbU91dHB1dERpciwgJ2xzJ10sIHsgY3dkOiBvcy50bXBkaXIoKSB9KTtcbiAgLy8gU2FtZSBzdGFja3Mgd2Uga25vdyBhcmUgaW4gdGhlIGFwcFxuICBleHBlY3QobGlzdCkudG9Db250YWluKGAke2ZpeHR1cmUuc3RhY2tOYW1lUHJlZml4fS1sYW1iZGFgKTtcbiAgZXhwZWN0KGxpc3QpLnRvQ29udGFpbihgJHtmaXh0dXJlLnN0YWNrTmFtZVByZWZpeH0tdGVzdC0xYCk7XG4gIGV4cGVjdChsaXN0KS50b0NvbnRhaW4oYCR7Zml4dHVyZS5zdGFja05hbWVQcmVmaXh9LXRlc3QtMmApO1xuXG4gIC8vIENoZWNrIHRoYXQgd2UgY2FuIHVzZSAnLicgYW5kIGp1c3Qgc3ludGggLHRoZSBnZW5lcmF0ZWQgYXNtXG4gIGNvbnN0IHN0YWNrVGVtcGxhdGUgPSBhd2FpdCBmaXh0dXJlLmNkayhbJy0tYXBwJywgJy4nLCAnc3ludGgnLCBmaXh0dXJlLmZ1bGxTdGFja05hbWUoJ3Rlc3QtMicpXSwge1xuICAgIGN3ZDogYXNtT3V0cHV0RGlyLFxuICB9KTtcbiAgZXhwZWN0KHN0YWNrVGVtcGxhdGUpLnRvQ29udGFpbigndG9waWMxNTJEODRBMzcnKTtcblxuICAvLyBEZXBsb3kgYSBMYW1iZGEgZnJvbSB0aGUgY29waWVkIGFzbVxuICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnbGFtYmRhJywgeyBvcHRpb25zOiBbJy1hJywgJy4nXSwgY3dkOiBhc21PdXRwdXREaXIgfSk7XG5cbiAgLy8gUmVtb3ZlIChyZW5hbWUpIHRoZSBvcmlnaW5hbCBjdXN0b20gZG9ja2VyIGZpbGUgdGhhdCB3YXMgdXNlZCBkdXJpbmcgc3ludGguXG4gIC8vIHRoaXMgdmVyaWZpZXMgdGhhdCB0aGUgYXNzZW1seSBoYXMgYSBjb3B5IG9mIGl0IGFuZCB0aGF0IHRoZSBtYW5pZmVzdCB1c2VzXG4gIC8vIHJlbGF0aXZlIHBhdGhzIHRvIHJlZmVyZW5jZSB0byBpdC5cbiAgY29uc3QgY3VzdG9tRG9ja2VyRmlsZSA9IHBhdGguam9pbihmaXh0dXJlLmludGVnVGVzdERpciwgJ2RvY2tlcicsICdEb2NrZXJmaWxlLkN1c3RvbScpO1xuICBhd2FpdCBmcy5yZW5hbWUoY3VzdG9tRG9ja2VyRmlsZSwgYCR7Y3VzdG9tRG9ja2VyRmlsZX1+YCk7XG4gIHRyeSB7XG5cbiAgICAvLyBkZXBsb3kgYSBkb2NrZXIgaW1hZ2Ugd2l0aCBjdXN0b20gZmlsZSB3aXRob3V0IHN5bnRoICh1c2VzIGFzc2V0cylcbiAgICBhd2FpdCBmaXh0dXJlLmNka0RlcGxveSgnZG9ja2VyLXdpdGgtY3VzdG9tLWZpbGUnLCB7IG9wdGlvbnM6IFsnLWEnLCAnLiddLCBjd2Q6IGFzbU91dHB1dERpciB9KTtcblxuICB9IGZpbmFsbHkge1xuICAgIC8vIFJlbmFtZSBiYWNrIHRvIHJlc3RvcmUgZml4dHVyZSB0byBvcmlnaW5hbCBzdGF0ZVxuICAgIGF3YWl0IGZzLnJlbmFtZShgJHtjdXN0b21Eb2NrZXJGaWxlfX5gLCBjdXN0b21Eb2NrZXJGaWxlKTtcbiAgfVxufSkpO1xuXG5pbnRlZ1Rlc3QoJ3RlbXBsYXRlcyBvbiBkaXNrIGNvbnRhaW4gbWV0YWRhdGEgcmVzb3VyY2UsIGFsc28gaW4gbmVzdGVkIGFzc2VtYmxpZXMnLCB3aXRoRGVmYXVsdEZpeHR1cmUoYXN5bmMgKGZpeHR1cmUpID0+IHtcbiAgLy8gU3ludGggZmlyc3QsIGFuZCBzd2l0Y2ggb24gdmVyc2lvbiByZXBvcnRpbmcgYmVjYXVzZSBjZGsuanNvbiBpcyBkaXNhYmxpbmcgaXRcbiAgYXdhaXQgZml4dHVyZS5jZGsoWydzeW50aCcsICctLXZlcnNpb24tcmVwb3J0aW5nPXRydWUnXSk7XG5cbiAgLy8gTG9hZCB0ZW1wbGF0ZSBmcm9tIGRpc2sgZnJvbSByb290IGFzc2VtYmx5XG4gIGNvbnN0IHRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvKi1sYW1iZGEudGVtcGxhdGUuanNvbiddKTtcblxuICBleHBlY3QoSlNPTi5wYXJzZSh0ZW1wbGF0ZUNvbnRlbnRzKS5SZXNvdXJjZXMuQ0RLTWV0YWRhdGEpLnRvQmVUcnV0aHkoKTtcblxuICAvLyBMb2FkIHRlbXBsYXRlIGZyb20gbmVzdGVkIGFzc2VtYmx5XG4gIGNvbnN0IG5lc3RlZFRlbXBsYXRlQ29udGVudHMgPSBhd2FpdCBmaXh0dXJlLnNoZWxsKFsnY2F0JywgJ2Nkay5vdXQvYXNzZW1ibHktKi1zdGFnZS8qLXN0YWdlLVN0YWNrSW5TdGFnZS50ZW1wbGF0ZS5qc29uJ10pO1xuXG4gIGV4cGVjdChKU09OLnBhcnNlKG5lc3RlZFRlbXBsYXRlQ29udGVudHMpLlJlc291cmNlcy5DREtNZXRhZGF0YSkudG9CZVRydXRoeSgpO1xufSkpO1xuXG5hc3luYyBmdW5jdGlvbiBsaXN0Q2hpbGRyZW4ocGFyZW50OiBzdHJpbmcsIHByZWQ6ICh4OiBzdHJpbmcpID0+IFByb21pc2U8Ym9vbGVhbj4pIHtcbiAgY29uc3QgcmV0ID0gbmV3IEFycmF5PHN0cmluZz4oKTtcbiAgZm9yIChjb25zdCBjaGlsZCBvZiBhd2FpdCBmcy5yZWFkZGlyKHBhcmVudCwgeyBlbmNvZGluZzogJ3V0Zi04JyB9KSkge1xuICAgIGNvbnN0IGZ1bGxQYXRoID0gcGF0aC5qb2luKHBhcmVudCwgY2hpbGQudG9TdHJpbmcoKSk7XG4gICAgaWYgKGF3YWl0IHByZWQoZnVsbFBhdGgpKSB7XG4gICAgICByZXQucHVzaChmdWxsUGF0aCk7XG4gICAgfVxuICB9XG4gIHJldHVybiByZXQ7XG59XG5cbmFzeW5jIGZ1bmN0aW9uIGxpc3RDaGlsZERpcnMocGFyZW50OiBzdHJpbmcpIHtcbiAgcmV0dXJuIGxpc3RDaGlsZHJlbihwYXJlbnQsIGFzeW5jIChmdWxsUGF0aDogc3RyaW5nKSA9PiAoYXdhaXQgZnMuc3RhdChmdWxsUGF0aCkpLmlzRGlyZWN0b3J5KCkpO1xufVxuXG5hc3luYyBmdW5jdGlvbiByZWFkVGVtcGxhdGUoZml4dHVyZTogVGVzdEZpeHR1cmUsIHN0YWNrTmFtZTogc3RyaW5nKTogUHJvbWlzZTxhbnk+IHtcbiAgY29uc3QgZnVsbFN0YWNrTmFtZSA9IGZpeHR1cmUuZnVsbFN0YWNrTmFtZShzdGFja05hbWUpO1xuICBjb25zdCB0ZW1wbGF0ZVBhdGggPSBwYXRoLmpvaW4oZml4dHVyZS5pbnRlZ1Rlc3REaXIsICdjZGsub3V0JywgYCR7ZnVsbFN0YWNrTmFtZX0udGVtcGxhdGUuanNvbmApO1xuICByZXR1cm4gSlNPTi5wYXJzZSgoYXdhaXQgZnMucmVhZEZpbGUodGVtcGxhdGVQYXRoLCB7IGVuY29kaW5nOiAndXRmLTgnIH0pKS50b1N0cmluZygpKTtcbn1cbiJdfQ== \ No newline at end of file diff --git a/scripts/bump.js b/scripts/bump.js index e307304c8bb68..626f6038d079a 100755 --- a/scripts/bump.js +++ b/scripts/bump.js @@ -29,11 +29,6 @@ async function main() { } }; - const majorVersion = semver.major(ver.version); - if (majorVersion > 1) { - opts.stripExperimentalChanges = true; - } - const forTesting = process.env.BUMP_CANDIDATE || false; if (forTesting) { opts.skip.commit = true; @@ -46,6 +41,8 @@ async function main() { console.error(`BUMP_CANDIDATE is set, so bumping version for testing (with the "${opts.prerelease}" prerelease tag)`); } + const majorVersion = semver.major(ver.version); + const useLegacyBump = process.env.LEGACY_BUMP || false; if (useLegacyBump) { console.error("ℹ️ Using the third-party 'standard-version' package to perform the bump"); @@ -67,9 +64,14 @@ async function main() { // this is incredible, but passing this option to standard-version actually makes it crash! // good thing we're getting rid of it... opts.verbose = !!process.env.VERBOSE; + if (majorVersion > 1) { + // NOTE - Once we start publishing alpha modules independently, this needs to be flipped to 'separate' + opts.experimentalChangesTreatment = 'strip'; + } // Rename some options to match cdk-release inputs (replaces bumpFiles, packageFiles, and infile) opts.versionFile = ver.versionFile; opts.changelogFile = ver.changelogFile; + opts.alphaChangelogFile = ver.alphaChangelogFile; console.error("🎉 Calling our 'cdk-release' package to make the bump"); console.error("ℹ️ Set the LEGACY_BUMP env variable to use the old 'standard-version' bump instead"); const cdkRelease = require('cdk-release'); diff --git a/scripts/resolve-version-lib.js b/scripts/resolve-version-lib.js index 21a13c0eb4ab2..ec1f7b1b83822 100755 --- a/scripts/resolve-version-lib.js +++ b/scripts/resolve-version-lib.js @@ -51,21 +51,26 @@ function resolveVersion(rootdir) { } // - // determine changelog file name + // determine changelog file names // const changelogFile = majorVersion === 1 ? 'CHANGELOG.md' : `CHANGELOG.v${majorVersion}.md`; + const alphaChangelogFile = majorVersion === 1 + ? undefined + : `CHANGELOG.v${majorVersion}.alpha.md`; + // // export all of it // return { version: currentVersion, - versionFile: versionFile, - changelogFile: changelogFile, + versionFile, + changelogFile, + alphaChangelogFile, prerelease: releaseType !== 'stable' ? releaseType : undefined, marker: '0.0.0', }; diff --git a/scripts/script-tests/resolve-version.test.js b/scripts/script-tests/resolve-version.test.js index 442e97e45b3cf..693e29234f023 100644 --- a/scripts/script-tests/resolve-version.test.js +++ b/scripts/script-tests/resolve-version.test.js @@ -13,6 +13,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: undefined, version: '2.1.0', @@ -28,6 +29,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: 'alpha', version: '2.1.0-alpha.0', @@ -43,6 +45,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: 'rc', version: '2.1.0-rc.0', @@ -58,6 +61,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.md', + alphaChangelogFile: undefined, marker: '0.0.0', prerelease: undefined, version: '1.72.0', @@ -73,6 +77,7 @@ happy({ }, expected: { changelogFile: 'CHANGELOG.v2.md', + alphaChangelogFile: 'CHANGELOG.v2.alpha.md', marker: '0.0.0', prerelease: undefined, version: '2.0.0-rc.0', diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index c4c3df70fc154..91fad0c39faf3 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -55,9 +55,9 @@ "fs-extra": "^9.1.0", "jest": "^26.6.3", "jest-junit": "^11.1.0", - "jsii": "^1.31.0", - "jsii-pacmak": "^1.31.0", - "jsii-reflect": "^1.31.0", + "jsii": "^1.34.0", + "jsii-pacmak": "^1.34.0", + "jsii-reflect": "^1.34.0", "markdownlint-cli": "^0.27.1", "nodeunit": "^0.11.3", "nyc": "^15.1.0", diff --git a/tools/cdk-integ-tools/lib/integ-helpers.ts b/tools/cdk-integ-tools/lib/integ-helpers.ts index ff07a799165f1..4f40f5aa3c79d 100644 --- a/tools/cdk-integ-tools/lib/integ-helpers.ts +++ b/tools/cdk-integ-tools/lib/integ-helpers.ts @@ -41,7 +41,7 @@ export class IntegrationTests { public async discover(): Promise { const files = await this.readTree(); const integs = files.filter(fileName => path.basename(fileName).startsWith('integ.') && path.basename(fileName).endsWith('.js')); - return await this.request(integs); + return this.request(integs); } public async request(files: string[]): Promise { diff --git a/tools/cdk-release/lib/conventional-commits.ts b/tools/cdk-release/lib/conventional-commits.ts index ed42884bc8b08..56355edc7ac62 100644 --- a/tools/cdk-release/lib/conventional-commits.ts +++ b/tools/cdk-release/lib/conventional-commits.ts @@ -1,8 +1,5 @@ -import * as fs from 'fs-extra'; import { ReleaseOptions } from './types'; // eslint-disable-next-line @typescript-eslint/no-require-imports -const lerna_project = require('@lerna/project'); -// eslint-disable-next-line @typescript-eslint/no-require-imports const conventionalCommitsParser = require('conventional-commits-parser'); // eslint-disable-next-line @typescript-eslint/no-require-imports const gitRawCommits = require('git-raw-commits'); @@ -95,31 +92,42 @@ export async function getConventionalCommitsFromGitHistory(args: ReleaseOptions, } /** - * Filters commits based on the criteria in `args` - * (right now, the only criteria is whether to remove commits that relate to experimental packages). + * Options for filterCommits + */ +export interface FilterCommitsOptions { + /** + * Scopes matching these package names (and variants) will be excluded from the commits returned. + * @default - No packages are excluded. + **/ + excludePackages?: string[]; + + /** + * If provided, scopes matching these package names (and variants) will be the *only commits* considered. + * @default - All packages are included. + **/ + includePackages?: string[]; +} + +/** + * Filters commits based on package scopes and inclusion/exclusion criteria. + * If `opts.includePackages` is provided, commits without scopes will not be included. * - * @param args configuration * @param commits the array of Conventional Commits to filter - * @returns an array of ConventionalCommit objects which is a subset of `commits` - * (possibly exactly equal to `commits`) + * @param opts filtering options; if none are provided, all commits are returned. */ -export function filterCommits(args: ReleaseOptions, commits: ConventionalCommit[]): ConventionalCommit[] { - if (!args.stripExperimentalChanges) { - return commits; - } +export function filterCommits(commits: ConventionalCommit[], opts: FilterCommitsOptions = {}): ConventionalCommit[] { + const excludeScopes = createScopeVariations(opts.excludePackages ?? []); + const includeScopes = createScopeVariations(opts.includePackages ?? []); + + return commits + .filter(commit => includeScopes.length === 0 || (commit.scope && includeScopes.includes(commit.scope))) + .filter(commit => excludeScopes.length === 0 || !commit.scope || !excludeScopes.includes(commit.scope)); +} + +function createScopeVariations(names: string[]) { + const simplifiedNames = names.map(n => n.replace(/^@aws-cdk\//, '')); - // a get a list of packages from our monorepo - const project = new lerna_project.Project(); - const packages = project.getPackagesSync(); - const experimentalPackageNames: string[] = packages - .filter((pkg: any) => { - const pkgJson = fs.readJsonSync(pkg.manifestLocation); - return pkgJson.name.startsWith('@aws-cdk/') - && (pkgJson.maturity === 'experimental' || pkgJson.maturity === 'developer-preview'); - }) - .map((pkg: any) => pkg.name.substr('@aws-cdk/'.length)); - - const experimentalScopes = flatMap(experimentalPackageNames, (pkgName) => [ + return flatMap(simplifiedNames, (pkgName) => [ pkgName, ...(pkgName.startsWith('aws-') ? [ @@ -133,8 +141,6 @@ export function filterCommits(args: ReleaseOptions, commits: ConventionalCommit[ : [] ), ]); - - return commits.filter(commit => !commit.scope || !experimentalScopes.includes(commit.scope)); } function flatMap(xs: T[], fn: (x: T) => U[]): U[] { diff --git a/tools/cdk-release/lib/index.ts b/tools/cdk-release/lib/index.ts index 04159170ffd81..b908e4cccdc7d 100644 --- a/tools/cdk-release/lib/index.ts +++ b/tools/cdk-release/lib/index.ts @@ -1,12 +1,14 @@ -import * as fs from 'fs'; import * as path from 'path'; -import { filterCommits, getConventionalCommitsFromGitHistory } from './conventional-commits'; +import * as fs from 'fs-extra'; +import { getConventionalCommitsFromGitHistory } from './conventional-commits'; import { defaults } from './defaults'; import { bump } from './lifecycles/bump'; -import { changelog } from './lifecycles/changelog'; +import { writeChangelogs } from './lifecycles/changelog'; import { commit } from './lifecycles/commit'; import { debug, debugObject } from './private/print'; -import { ReleaseOptions, Versions } from './types'; +import { PackageInfo, ReleaseOptions, Versions } from './types'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const lerna_project = require('@lerna/project'); module.exports = async function main(opts: ReleaseOptions): Promise { // handle the default options @@ -19,21 +21,20 @@ module.exports = async function main(opts: ReleaseOptions): Promise { const currentVersion = readVersion(args.versionFile); debugObject(args, 'Current version info', currentVersion); - const commits = await getConventionalCommitsFromGitHistory(args, `v${currentVersion.stableVersion}`); - const filteredCommits = filterCommits(args, commits); - debugObject(args, 'Found and filtered commits', filteredCommits); - const newVersion = await bump(args, currentVersion); debugObject(args, 'New version is', newVersion); + debug(args, 'Reading Git commits'); + const commits = await getConventionalCommitsFromGitHistory(args, `v${currentVersion.stableVersion}`); + debug(args, 'Writing Changelog'); - await changelog(args, currentVersion.stableVersion, newVersion.stableVersion, filteredCommits); + const changelogResults = await writeChangelogs({ ...args, currentVersion, newVersion, commits, packages: getProjectPackageInfos() }); debug(args, 'Committing result'); - await commit(args, newVersion.stableVersion, [args.versionFile, args.changelogFile]); + await commit(args, newVersion.stableVersion, [args.versionFile, ...changelogResults.map(r => r.filePath)]); }; -export function readVersion(versionFile: string): Versions { +function readVersion(versionFile: string): Versions { const versionPath = path.resolve(process.cwd(), versionFile); const contents = JSON.parse(fs.readFileSync(versionPath, { encoding: 'utf-8' })); return { @@ -41,3 +42,19 @@ export function readVersion(versionFile: string): Versions { alphaVersion: contents.alphaVersion, }; } + +function getProjectPackageInfos(): PackageInfo[] { + const packages = lerna_project.Project.getPackagesSync(); + + return packages.map((pkg: any) => { + const maturity = pkg.get('maturity'); + const alpha = pkg.name.startsWith('@aws-cdk/') + && (maturity === 'experimental' || maturity === 'developer-preview'); + + return { + name: pkg.name, + location: pkg.location, + alpha, + }; + }); +} diff --git a/tools/cdk-release/lib/lifecycles/changelog.ts b/tools/cdk-release/lib/lifecycles/changelog.ts index 7e1052068ffd9..3da1b8e10de3a 100644 --- a/tools/cdk-release/lib/lifecycles/changelog.ts +++ b/tools/cdk-release/lib/lifecycles/changelog.ts @@ -1,9 +1,9 @@ import * as stream from 'stream'; import * as fs from 'fs-extra'; -import { ConventionalCommit } from '../conventional-commits'; +import { ConventionalCommit, filterCommits } from '../conventional-commits'; import { writeFile } from '../private/files'; -import { notify, debug, debugObject } from '../private/print'; -import { LifecyclesSkip } from '../types'; +import { notify, debug } from '../private/print'; +import { ExperimentalChangesTreatment, LifecyclesSkip, PackageInfo, Versions } from '../types'; // eslint-disable-next-line @typescript-eslint/no-require-imports const conventionalChangelogPresetLoader = require('conventional-changelog-preset-loader'); // eslint-disable-next-line @typescript-eslint/no-require-imports @@ -11,8 +11,18 @@ const conventionalChangelogWriter = require('conventional-changelog-writer'); const START_OF_LAST_RELEASE_PATTERN = /(^#+ \[?[0-9]+\.[0-9]+\.[0-9]+| { + if (opts.skip?.changelog) { + return []; + } + + const experimentalChangesTreatment = opts.experimentalChangesTreatment ?? ExperimentalChangesTreatment.INCLUDE; + const alphaPackages = opts.packages.filter(p => p.alpha); + const stableCommits = filterCommits(opts.commits, { excludePackages: alphaPackages.map(p => p.name) }); + + switch (experimentalChangesTreatment) { + case ExperimentalChangesTreatment.INCLUDE: + const allContents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, opts.commits); + return [{ filePath: opts.changelogFile, fileContents: allContents }]; + + case ExperimentalChangesTreatment.STRIP: + const strippedContents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, stableCommits); + return [{ filePath: opts.changelogFile, fileContents: strippedContents }]; + + case ExperimentalChangesTreatment.SEPARATE: + if (!opts.currentVersion.alphaVersion || !opts.newVersion.alphaVersion) { + throw new Error('unable to create separate alpha Changelog without alpha package versions'); + } + if (!opts.alphaChangelogFile) { + throw new Error('alphaChangelogFile must be specified if experimentalChangesTreatment is SEPARATE'); + } + + const changelogResults: ChangelogResult[] = []; + const contents = await changelog(opts, opts.currentVersion.stableVersion, opts.newVersion.stableVersion, stableCommits); + changelogResults.push({ filePath: opts.changelogFile, fileContents: contents }); + + const alphaCommits = filterCommits(opts.commits, { includePackages: alphaPackages.map(p => p.name) }); + const alphaContents = await changelog( + { ...opts, changelogFile: opts.alphaChangelogFile }, + opts.currentVersion.alphaVersion, opts.newVersion.alphaVersion, alphaCommits); + changelogResults.push({ filePath: opts.alphaChangelogFile, fileContents: alphaContents }); + + return changelogResults; + + default: + throw new Error(`unsupported experimentalChanges type: ${opts.experimentalChangesTreatment}`); + } } export async function changelog( args: ChangelogOptions, currentVersion: string, newVersion: string, commits: ConventionalCommit[], -): Promise { - if (args.skip?.changelog) { - return { - contents: '', - changedFiles: [], - }; - } +): Promise { + createChangelogIfMissing(args); // find the position of the last release and remove header @@ -50,7 +97,6 @@ export async function changelog( const presetConfig = await conventionalChangelogPresetLoader({ name: 'conventional-changelog-conventionalcommits', }); - debugObject(args, 'conventionalChangelogPresetLoader returned', presetConfig); return new Promise((resolve, reject) => { // convert an array of commits into a Stream, @@ -118,10 +164,7 @@ export async function changelog( } else { writeFile(args, args.changelogFile, args.changeLogHeader + '\n' + (content + oldContent).replace(/\n+$/, '\n')); } - return resolve({ - contents: content, - changedFiles: [args.changelogFile], - }); + return resolve(content); }); }); } diff --git a/tools/cdk-release/lib/types.ts b/tools/cdk-release/lib/types.ts index 822a5beba84d2..7dcbb12956f51 100644 --- a/tools/cdk-release/lib/types.ts +++ b/tools/cdk-release/lib/types.ts @@ -20,22 +20,41 @@ export interface Versions { export type ReleaseType = 'major' | 'minor' | 'patch'; -/* ****** main options ******** */ +/** How to handle experimental changes in the changelog. */ +export enum ExperimentalChangesTreatment { + /** Experimental changes are included in the main changelog (this is the default) */ + INCLUDE = 'include', + /** Remove all experimental changes from the changelog */ + STRIP = 'strip', + /** Write experimental changes to a separate changelog */ + SEPARATE = 'separate' +}; export interface ReleaseOptions { releaseAs: ReleaseType; skip?: LifecyclesSkip; versionFile: string; changelogFile: string; + alphaChangelogFile?: string; prerelease?: string; scripts?: Lifecycles; dryRun?: boolean; verbose?: boolean; silent?: boolean; sign?: boolean; - stripExperimentalChanges?: boolean; + /** + * How to handle experimental changes in the changelog. + * @default ExperimentalChangesTreatment.INCLUDE + */ + experimentalChangesTreatment?: ExperimentalChangesTreatment; changeLogHeader?: string; includeDateInChangelog?: boolean; releaseCommitMessageFormat?: string; } + +export interface PackageInfo { + name: string; + location: string; + alpha: boolean; +} diff --git a/tools/cdk-release/test/changelog.test.ts b/tools/cdk-release/test/changelog.test.ts index 67e01477e4ced..6e618d00a1389 100644 --- a/tools/cdk-release/test/changelog.test.ts +++ b/tools/cdk-release/test/changelog.test.ts @@ -1,14 +1,140 @@ import { ConventionalCommit } from '../lib/conventional-commits'; -import { changelog, ChangelogOptions } from '../lib/lifecycles/changelog'; - -describe('Changelog generation', () => { - const args: ChangelogOptions = { - changelogFile: 'CHANGELOG.md', - dryRun: true, - silent: true, - includeDateInChangelog: false, +import { changelog, ChangelogOptions, writeChangelogs } from '../lib/lifecycles/changelog'; +import { ExperimentalChangesTreatment, PackageInfo, Versions } from '../lib/types'; + +const args: ChangelogOptions = { + changelogFile: 'CHANGELOG.md', + dryRun: true, + silent: true, + includeDateInChangelog: false, +}; + +describe('writeChangelogs', () => { + + const currentVersion: Versions = { stableVersion: '1.23.0' }; + const newVersion: Versions = { stableVersion: '1.24.0' }; + + const commits: ConventionalCommit[] = [ + buildCommit({ type: 'feat', scope: 'aws-stable', subject: 'new stable feat' }), + buildCommit({ type: 'feat', scope: 'aws-experimental', subject: 'new experimental feat' }), + ]; + const packages: PackageInfo[] = [ + { name: 'aws-stable', alpha: false, location: 'aws-stable' }, + { name: 'aws-experimental', alpha: true, location: 'aws-experimental' }, + ]; + + const defaultWriteChangelogOpts = { + ...args, + currentVersion, + newVersion, + commits, + packages, }; + test('does nothing if skip.changelog is set', async () => { + const changelogResult = await writeChangelogs({ ...defaultWriteChangelogOpts, skip: { changelog: true } }); + + expect(changelogResult).toEqual([]); + }); + + test('defaults experimentalChangesTreatment to "include"', async () => { + const changelogResultDefault = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: undefined, + }); + const changelogResultInclude = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: ExperimentalChangesTreatment.INCLUDE, + }); + + expect(changelogResultDefault).toEqual(changelogResultInclude); + }); + + test('if experimentalChangesTreatment is "include", includes experimental changes', async () => { + const changelogResult = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: ExperimentalChangesTreatment.INCLUDE, + }); + + expect(changelogResult.length).toEqual(1); + expect(changelogResult[0].filePath).toEqual('CHANGELOG.md'); + expect(changelogResult[0].fileContents.trim()).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **aws-experimental:** new experimental feat +* **aws-stable:** new stable feat`); + }); + + test('if changelogExperimentalChanges is "strip", excludes experimental changes', async () => { + const changelogResult = await writeChangelogs({ + ...defaultWriteChangelogOpts, experimentalChangesTreatment: ExperimentalChangesTreatment.STRIP, + }); + + expect(changelogResult.length).toEqual(1); + expect(changelogResult[0].filePath).toEqual('CHANGELOG.md'); + expect(changelogResult[0].fileContents.trim()).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **aws-stable:** new stable feat`); + }); + + describe('experimentalChangesTreatment is SEPARATE', () => { + + const defaultSeparateChangelogOpts = { + ...defaultWriteChangelogOpts, + experimentalChangesTreatment: ExperimentalChangesTreatment.SEPARATE, + currentVersion: { stableVersion: '1.23.0', alphaVersion: '1.23.0-alpha.0' }, + newVersion: { stableVersion: '1.24.0', alphaVersion: '1.24.0-alpha.0' }, + alphaChangelogFile: 'CHANGELOG.alpha.md', + }; + + test('throws if alpha versions are not present', async () => { + await expect(writeChangelogs({ + ...defaultSeparateChangelogOpts, + currentVersion: { stableVersion: '1.23.0' }, + newVersion: { stableVersion: '1.24.0' }, + })) + .rejects + .toThrow(/without alpha package versions/); + + await expect(writeChangelogs({ ...defaultSeparateChangelogOpts, newVersion: { stableVersion: '1.24.0' } })) + .rejects + .toThrow(/without alpha package versions/); + + await expect(writeChangelogs({ ...defaultSeparateChangelogOpts, currentVersion: { stableVersion: '1.23.0' } })) + .rejects + .toThrow(/without alpha package versions/); + }); + + test('throws if alpha changelog file is not present', async () => { + await expect(writeChangelogs({ ...defaultSeparateChangelogOpts, alphaChangelogFile: undefined })) + .rejects + .toThrow(/alphaChangelogFile must be specified/); + }); + + test('excludes experimental changes and writes to the alpha changelog', async () => { + const changelogResult = await writeChangelogs(defaultSeparateChangelogOpts); + + const mainResult = changelogResult.find(r => r.filePath === 'CHANGELOG.md'); + const alphaResult = changelogResult.find(r => r.filePath === 'CHANGELOG.alpha.md'); + expect(mainResult?.fileContents.trim()).toBe( + `## [1.24.0](https://github.com/aws/aws-cdk/compare/v1.23.0...v1.24.0) + +### Features + +* **aws-stable:** new stable feat`); + expect(alphaResult?.fileContents.trim()).toBe( + `## [1.24.0-alpha.0](https://github.com/aws/aws-cdk/compare/v1.23.0-alpha.0...v1.24.0-alpha.0) + +### Features + +* **aws-experimental:** new experimental feat`); + }); + }); +}); + +describe('changelog', () => { test("correctly handles 'BREAKING CHANGES'", async () => { const commits: ConventionalCommit[] = [ buildCommit({ @@ -68,6 +194,6 @@ function buildCommit(commit: PartialCommit): ConventionalCommit { }; } -async function invokeChangelogFrom1_23_0to1_24_0(args: ChangelogOptions, commits: ConventionalCommit[]): Promise { - return (await changelog(args, '1.23.0', '1.24.0', commits)).contents; +async function invokeChangelogFrom1_23_0to1_24_0(changelogArgs: ChangelogOptions, commits: ConventionalCommit[]): Promise { + return changelog(changelogArgs, '1.23.0', '1.24.0', commits); } diff --git a/tools/cdk-release/test/conventional-commits.test.ts b/tools/cdk-release/test/conventional-commits.test.ts index 773f229849e5d..000031f142074 100644 --- a/tools/cdk-release/test/conventional-commits.test.ts +++ b/tools/cdk-release/test/conventional-commits.test.ts @@ -39,38 +39,69 @@ describe('getConventionalCommitsFromGitHistory', () => { }); }); -// NOTE - These test currently use real package.json data to determine package's stability. describe('filterCommits', () => { const commits: ConventionalCommit[] = [ - buildCommit({ - type: 'feat', - scope: 'scope', - subject: 'super important feature', - }), - buildCommit({ - type: 'fix', - scope: 'example-construct-library', // really hope we don't stabilize this one - subject: 'hairy bugfix', - notes: [ - { - title: 'BREAKING CHANGE', - text: 'this is a breaking change', - }, - ], - }), + commitWithScope('aws-stable'), + commitWithScope('aws-experimental'), + commitWithScope(), ]; - test('if stripExperimental is not set, returns original commits', async () => { - const filteredCommits = filterCommits(args, commits); + test('if no options are provided, returns all commits', () => { + const filteredCommits = filterCommits(commits); + + expect(filteredCommits).toEqual(commits); + }); + + test('excludePackages removes commits matching scope', () => { + const filteredCommits = filterCommits(commits, { excludePackages: ['@aws-cdk/aws-experimental'] }); expect(filteredCommits.length).toEqual(2); + expect(filteredCommits.map(c => c.scope)).not.toContain('aws-experimental'); + }); + + test('excludePackages removes commits matching specific variants of the scope', () => { + const experimentalCommits = [ + commitWithScope('aws-experimental'), + commitWithScope('awsexperimental'), + commitWithScope('experimental'), + commitWithScope('aws.experimental'), + ]; + + const filteredCommits = filterCommits(experimentalCommits, { excludePackages: ['@aws-cdk/aws-experimental'] }); + + expect(filteredCommits.length).toEqual(1); + expect(filteredCommits[0].scope).toEqual('aws.experimental'); + }); + + test('includePackages only includes commits matching scope', () => { + const filteredCommits = filterCommits(commits, { includePackages: ['@aws-cdk/aws-stable'] }); + + expect(filteredCommits.length).toEqual(1); + expect(filteredCommits[0].scope).toEqual('aws-stable'); }); - test("skips experimental modules if requested, even with 'BREAKING CHANGES'", async () => { - const filteredCommits = filterCommits({ ...args, stripExperimentalChanges: true }, commits); + test('includePackages includes commits matching variants of the scope', () => { + const stableCommits = [ + commitWithScope('aws-stable'), + commitWithScope('awsstable'), + commitWithScope('stable'), + commitWithScope('notstable'), + ]; + + const filteredCommits = filterCommits(stableCommits, { includePackages: ['@aws-cdk/aws-stable'] }); + + expect(filteredCommits.length).toEqual(3); + expect(filteredCommits.map(c => c.scope)).not.toContain('notstable'); + }); + + test('excludes criteria are run after includes', () => { + const filteredCommits = filterCommits(commits, { + includePackages: ['@aws-cdk/aws-stable', '@aws-cdk/aws-experimental'], + excludePackages: ['@aws-cdk/aws-experimental'], + }); expect(filteredCommits.length).toEqual(1); - expect(filteredCommits[0].subject).toEqual('super important feature'); + expect(filteredCommits[0].scope).toEqual('aws-stable'); }); }); @@ -85,16 +116,13 @@ function mockGitCommits(messages: string[]) { return rStream; } -interface PartialCommit extends Partial { - readonly type: string; - readonly subject: string; -} - -function buildCommit(commit: PartialCommit): ConventionalCommit { +function commitWithScope(scope?: string): ConventionalCommit { return { notes: [], references: [], - header: `${commit.type}${commit.scope ? '(' + commit.scope + ')' : ''}: ${commit.subject}`, - ...commit, + header: `feat${scope ? '(' + scope + ')' : ''}: some commit message`, + type: 'feat', + subject: 'some commit message', + scope, }; } diff --git a/version.v1.json b/version.v1.json index e02425c0f7b3d..7ff9a577d56ba 100644 --- a/version.v1.json +++ b/version.v1.json @@ -1,3 +1,3 @@ { - "version": "1.119.0" + "version": "1.120.0" } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index ffbbedd9e2552..16ea150357e58 100644 --- a/yarn.lock +++ b/yarn.lock @@ -565,6 +565,14 @@ chalk "^4.1.2" semver "^7.3.5" +"@jsii/check-node@1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@jsii/check-node/-/check-node-1.34.0.tgz#24da38da36e18639c84787dabd464b8474ddee22" + integrity sha512-Z+eGyIoV6B6RNFCR+Z/p0ANnZA++bmCXhoU1RIwGh9RG39PAT38KkZZNr9ZHNTTQbVoTJMSatoX/9WQ33pQxAw== + dependencies: + chalk "^4.1.2" + semver "^7.3.5" + "@jsii/spec@^1.31.0": version "1.31.0" resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.31.0.tgz#9298dc163fdae0bab4006b817592235a29922871" @@ -572,6 +580,13 @@ dependencies: jsonschema "^1.4.0" +"@jsii/spec@^1.34.0": + version "1.34.0" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.34.0.tgz#8b78adf07518f567d2c44bf6c98c426e5c18e0cf" + integrity sha512-yAK8FrTRrZ3lQ+DmdyAFZuHmsTJ1ej0719+sVgjr5ahE9i64huStaraX/jJM+PniuUQwE7N+B49ue6X9qj7vJA== + dependencies: + jsonschema "^1.4.0" + "@lerna/add@4.0.0": version "4.0.0" resolved "https://registry.yarnpkg.com/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" @@ -1886,6 +1901,11 @@ "@typescript-eslint/types" "4.28.4" eslint-visitor-keys "^2.0.0" +"@xmldom/xmldom@^0.7.0": + version "0.7.3" + resolved "https://registry.yarnpkg.com/@xmldom/xmldom/-/xmldom-0.7.3.tgz#55de695f77afd3cc0e5bee0aa900040bc63c0f63" + integrity sha512-8XmJdPut2XGtfFcsNsqEsvMUmAwk7xLq7m+E/GcsU9b5qyFFIsiX4Fvnb5UoQ4wo12Wlm07YFJERoyWUYdbIpw== + "@yarnpkg/lockfile@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" @@ -2299,6 +2319,21 @@ aws-sdk@^2.848.0, aws-sdk@^2.928.0: uuid "3.3.2" xml2js "0.4.19" +aws-sdk@^2.979.0: + version "2.979.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.979.0.tgz#d0104fec763cc3eafb123e709f94866790109da4" + integrity sha512-pKKhpYZwmihCvuH3757WHY8JQI9g2wvtF3s0aiyH2xCUmX/6uekhExz/utD4uqZP3m3PwKZPGQkQkH30DtHrPw== + dependencies: + buffer "4.9.2" + events "1.1.1" + ieee754 "1.1.13" + jmespath "0.15.0" + querystring "0.2.0" + sax "1.2.1" + url "0.10.3" + uuid "3.3.2" + xml2js "0.4.19" + aws-sign2@~0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/aws-sign2/-/aws-sign2-0.7.0.tgz#b46e890934a9591f2d2f6f86d7e6a9f1b3fe76a8" @@ -2843,6 +2878,15 @@ codemaker@^1.31.0: decamelize "^5.0.0" fs-extra "^9.1.0" +codemaker@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.34.0.tgz#dba5dbd9ca6d1d9d9af64d64f17bde45882b0aa0" + integrity sha512-NHwy6TxMh21ygch7+K/OwtdN3BjxhAMoP5QXqzkkR0TDP2kEdKCNc31EChz3Xcmxk1qkdJN5CpXMnLjo7f07sQ== + dependencies: + camelcase "^6.2.0" + decamelize "^5.0.0" + fs-extra "^9.1.0" + collect-v8-coverage@^1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz#cc2c8e94fc18bbdffe64d6534570c8a673b27f59" @@ -2920,10 +2964,10 @@ commondir@^1.0.1: resolved "https://registry.yarnpkg.com/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -commonmark@^0.29.3: - version "0.29.3" - resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.29.3.tgz#bb1d5733bfe3ea213b412f33f16439cc12999c2c" - integrity sha512-fvt/NdOFKaL2gyhltSy6BC4LxbbxbnPxBMl923ittqO/JBM0wQHaoYZliE4tp26cRxX/ZZtRsJlZzQrVdUkXAA== +commonmark@^0.30.0: + version "0.30.0" + resolved "https://registry.yarnpkg.com/commonmark/-/commonmark-0.30.0.tgz#38811dc7bbf0f59d277ae09054d4d73a332f2e45" + integrity sha512-j1yoUo4gxPND1JWV9xj5ELih0yMv1iCWDG6eEQIPLSWLxzCXiFoyS7kvB+WwU+tZMf4snwJMMtaubV0laFpiBA== dependencies: entities "~2.0" mdurl "~1.0.1" @@ -6007,31 +6051,33 @@ jsesc@^2.5.1: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" integrity sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA== -jsii-diff@^1.31.0: - version "1.31.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.31.0.tgz#7f32b340cf340cc1929f4d534bdfa6495fc09bed" - integrity sha512-eEKFfZXGXxlWFg7E0F4h2UGOnpVCzHclM586SE4KnMwHzSlpRrdYrXa2KhFQSLs/gpZofDV4rPLZ9UDLvNu75Q== +jsii-diff@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.34.0.tgz#86742f21f5ab809df2ce8b6902f390c6379a9a39" + integrity sha512-fPVBoixNjo3kQs8WgjNeE7M5HxcK3jqfgM/IC6fpzrsGDjjqnH7emtVH3gvS18uQxUfYKKkXEKbkHsywQe6EkA== dependencies: - "@jsii/spec" "^1.31.0" + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" fs-extra "^9.1.0" - jsii-reflect "^1.31.0" + jsii-reflect "^1.34.0" log4js "^6.3.0" - typescript "~3.9.9" + typescript "~3.9.10" yargs "^16.2.0" -jsii-pacmak@^1.31.0: - version "1.31.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.31.0.tgz#7e4fa67f1de582be04263904aa45966d84210996" - integrity sha512-fGiAoooRPMadwTWU0vfHJdcNzeYdESnkU/8LmlI4k6yF1iIlFMIbWPulBxP6fV7SqV3CZQKGpUbcPD/Uzf1glg== +jsii-pacmak@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.34.0.tgz#7fc6a79fb72bd791a0cac5877339253fd26e14e3" + integrity sha512-OngbNHieb5g7B1VkRSZkZq1vgoflhjX4heTJnQJZYbG59j2qVgD7E/o/Dl2OTBLrGRms8e2oCsYc7XROt2htSA== dependencies: - "@jsii/spec" "^1.31.0" + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" clone "^2.1.2" - codemaker "^1.31.0" - commonmark "^0.29.3" + codemaker "^1.34.0" + commonmark "^0.30.0" escape-string-regexp "^4.0.0" fs-extra "^9.1.0" - jsii-reflect "^1.31.0" - jsii-rosetta "^1.31.0" + jsii-reflect "^1.34.0" + jsii-rosetta "^1.34.0" semver "^7.3.5" spdx-license-list "^6.4.0" xmlbuilder "^15.1.1" @@ -6048,16 +6094,29 @@ jsii-reflect@^1.31.0: oo-ascii-tree "^1.31.0" yargs "^16.2.0" -jsii-rosetta@^1.31.0: - version "1.31.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.31.0.tgz#f5174b532b4c3a79eadd9ed059aa33bee21e3225" - integrity sha512-Heu6D+yI5mmUklLQdX3PdDvHUQm14618Fj4PQM9seKa4cohxzJ7EHopfRObKYHMko9awopx4Qr7Gtu6u/QPqfw== +jsii-reflect@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.34.0.tgz#3d5c8f7c2e8310df2c8dea3aad5bef487fe1d0d9" + integrity sha512-IOEdwgeDCOq821PM3OfRro1Pgu0QzHFW7zQy3aN7/w5Fcb/tSYGxI9+Ykr6JCdg681LFzcMEgwJpCUHnfi/shw== dependencies: - "@jsii/spec" "^1.31.0" - commonmark "^0.29.3" + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + colors "^1.4.0" fs-extra "^9.1.0" - typescript "~3.9.9" - xmldom "^0.6.0" + oo-ascii-tree "^1.34.0" + yargs "^16.2.0" + +jsii-rosetta@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.34.0.tgz#64b1233726a98a992be5cffd1d0f4b824346dbef" + integrity sha512-GOGAy5b+zCGeyYziBoNVXgamL2CEZKMj5moeemkyN4AUHUqugNk3fSul2Zdbxs2S13Suk0D9iYAgChDxew0bOw== + dependencies: + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + "@xmldom/xmldom" "^0.7.0" + commonmark "^0.30.0" + fs-extra "^9.1.0" + typescript "~3.9.10" yargs "^16.2.0" jsii@^1.31.0: @@ -6078,6 +6137,25 @@ jsii@^1.31.0: typescript "~3.9.9" yargs "^16.2.0" +jsii@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.34.0.tgz#d009d7ae36f069819f97dcade4203722a5ec524f" + integrity sha512-z/p8cuWdRntQzdZ1Fq/hvXHPjq/HjZhQzTF/GmYrH3s7Wsb14LphHGAENTZwICBaSovoqSRIboOb2FbPLsCjoA== + dependencies: + "@jsii/check-node" "1.34.0" + "@jsii/spec" "^1.34.0" + case "^1.6.3" + colors "^1.4.0" + deep-equal "^2.0.5" + fs-extra "^9.1.0" + log4js "^6.3.0" + semver "^7.3.5" + semver-intersect "^1.4.0" + sort-json "^2.0.0" + spdx-license-list "^6.4.0" + typescript "~3.9.10" + yargs "^16.2.0" + json-diff@^0.5.4: version "0.5.4" resolved "https://registry.yarnpkg.com/json-diff/-/json-diff-0.5.4.tgz#7bc8198c441756632aab66c7d9189d365a7a035a" @@ -7481,6 +7559,11 @@ oo-ascii-tree@^1.31.0: resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.31.0.tgz#36e10dcad35ba767db41c2d2050ff2174f3d5e6f" integrity sha512-gNb2MyP1ZcF7cX0WgsAjYe4gZcx7BMLBWKE2TJZZbQ9/j4D8gbJh5Aq6RlXBgev74ODlgAVVcPr2wKU4Dufhqg== +oo-ascii-tree@^1.34.0: + version "1.34.0" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.34.0.tgz#5528a52d92ef18b3860d0e784383007e476c18b3" + integrity sha512-gAY+yfKCskAk7mkfI8nOhkP12iTGE7b8UxnQuscN80vghrozt/E/2rLeKKMJFagJlm/NnnUmBA0tBQZ3oPHEKg== + open@^7.4.2: version "7.4.2" resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321" @@ -10120,11 +10203,6 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xmldom@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/xmldom/-/xmldom-0.6.0.tgz#43a96ecb8beece991cef382c08397d82d4d0c46f" - integrity sha512-iAcin401y58LckRZ0TkI4k0VSM1Qg0KGSc3i8rU+xrxe19A/BN1zHyVSJY7uoutVlaTSzYyk/v5AmkewAP7jtg== - xregexp@2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/xregexp/-/xregexp-2.0.0.tgz#52a63e56ca0b84a7f3a5f3d61872f126ad7a5943"